diff options
52 files changed, 1063 insertions, 521 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 6a62c752338..51877a85a99 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -784,6 +784,10 @@ Style/RegexpLiteralMixedPreserve: - mixed_preserve EnforcedStyle: mixed_preserve +# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94317#note_1139610896 +Style/Lambda: + EnforcedStyle: literal + RSpec/TopLevelDescribePath: Exclude: - 'spec/fixtures/**/*.rb' diff --git a/.rubocop_todo/style/lambda.yml b/.rubocop_todo/style/lambda.yml index f733af601ec..9da29f7bb59 100644 --- a/.rubocop_todo/style/lambda.yml +++ b/.rubocop_todo/style/lambda.yml @@ -1,273 +1,81 @@ --- -# Cop supports --auto-correct. +# Cop supports --autocorrect. Style/Lambda: - # Offense count: 653 - # Temporarily disabled due to too many offenses - Enabled: false + Details: grace period Exclude: - - 'app/controllers/concerns/notes_actions.rb' - - 'app/controllers/concerns/spammable_actions/captcha_check/rest_api_actions_support.rb' - - 'app/controllers/projects/issues_controller.rb' - - 'app/controllers/search_controller.rb' - - 'app/graphql/mutations/container_repositories/destroy_tags.rb' - - 'app/graphql/mutations/design_management/delete.rb' - - 'app/graphql/types/permission_types/base_permission_type.rb' - - 'app/models/analytics/cycle_analytics/issue_stage_event.rb' - - 'app/models/analytics/cycle_analytics/merge_request_stage_event.rb' - - 'app/models/bulk_imports/tracker.rb' - - 'app/models/ci/build.rb' - - 'app/models/ci/deleted_object.rb' - - 'app/models/ci/instance_variable.rb' - - 'app/models/ci/job_artifact.rb' - - 'app/models/ci/namespace_mirror.rb' - - 'app/models/ci/pending_build.rb' - - 'app/models/ci/pipeline.rb' - - 'app/models/ci/processable.rb' - - 'app/models/ci/runner.rb' - - 'app/models/clusters/cluster.rb' - - 'app/models/clusters/concerns/application_status.rb' - - 'app/models/commit_status.rb' - - 'app/models/concerns/analytics/cycle_analytics/stage_event_model.rb' - - 'app/models/concerns/approvable_base.rb' - - 'app/models/concerns/atomic_internal_id.rb' - - 'app/models/concerns/ci/has_status.rb' - - 'app/models/concerns/clusters/agents/authorization_config_scopes.rb' - - 'app/models/concerns/has_environment_scope.rb' - - 'app/models/concerns/has_wiki_page_meta_attributes.rb' - - 'app/models/concerns/id_in_ordered.rb' - - 'app/models/concerns/integrations/has_issue_tracker_fields.rb' - - 'app/models/concerns/issuable.rb' - - 'app/models/concerns/issue_resource_event.rb' - - 'app/models/concerns/milestoneable.rb' - - 'app/models/concerns/mirror_authentication.rb' - - 'app/models/concerns/packages/debian/component_file.rb' - - 'app/models/concerns/reactive_caching.rb' - - 'app/models/concerns/timebox.rb' - - 'app/models/container_repository.rb' - - 'app/models/custom_emoji.rb' - - 'app/models/deployment.rb' - - 'app/models/design_management/action.rb' - - 'app/models/design_management/design.rb' - - 'app/models/design_management/version.rb' - - 'app/models/environment.rb' - - 'app/models/event.rb' - - 'app/models/group.rb' - - 'app/models/group_deploy_key.rb' - - 'app/models/group_group_link.rb' - - 'app/models/hooks/web_hook.rb' - - 'app/models/identity.rb' - - 'app/models/import_failure.rb' - - 'app/models/integrations/zentao_tracker_data.rb' - - 'app/models/internal_id.rb' - - 'app/models/issue.rb' - - 'app/models/issue/metrics.rb' - - 'app/models/jira_connect_installation.rb' - - 'app/models/label.rb' - - 'app/models/label_link.rb' - - 'app/models/loose_foreign_keys/deleted_record.rb' - - 'app/models/member.rb' - - 'app/models/members/project_member.rb' - - 'app/models/merge_request.rb' - - 'app/models/merge_request/cleanup_schedule.rb' - - 'app/models/merge_request_diff.rb' - - 'app/models/merge_request_diff_file.rb' - - 'app/models/merge_requests_closing_issues.rb' - - 'app/models/milestone.rb' - - 'app/models/namespace.rb' - - 'app/models/note.rb' - - 'app/models/note_diff_file.rb' - - 'app/models/notification_setting.rb' - - 'app/models/onboarding/progress.rb' - - 'app/models/operations/feature_flags/user_list.rb' - - 'app/models/packages/package.rb' - - 'app/models/packages/package_file.rb' - - 'app/models/pages_domain.rb' - - 'app/models/product_analytics_event.rb' - - 'app/models/programming_language.rb' - - 'app/models/project.rb' + - 'app/controllers/concerns/project_unauthorized.rb' + - 'app/controllers/profiles/two_factor_auths_controller.rb' + - 'app/models/concerns/featurable.rb' - 'app/models/project_feature.rb' - - 'app/models/project_feature_usage.rb' - - 'app/models/projects/topic.rb' - - 'app/models/prometheus_alert_event.rb' - - 'app/models/raw_usage_data.rb' - - 'app/models/redirect_route.rb' - - 'app/models/release.rb' - - 'app/models/remote_mirror.rb' - - 'app/models/repository_language.rb' - - 'app/models/snippet.rb' - - 'app/models/timelog.rb' - - 'app/models/todo.rb' - - 'app/models/user.rb' - - 'app/models/users/in_product_marketing_email.rb' - 'app/serializers/ci/daily_build_group_report_result_entity.rb' - 'app/serializers/group_child_entity.rb' - 'app/serializers/issuable_sidebar_basic_entity.rb' - 'app/serializers/merge_request_sidebar_basic_entity.rb' - - 'app/services/issues/referenced_merge_requests_service.rb' - - 'config/initializers/deprecations.rb' - - 'config/initializers/rspec_profiling.rb' + - 'config/application.rb' + - 'config/initializers/0_license.rb' + - 'config/initializers/0_log_deprecations.rb' + - 'config/initializers/action_cable.rb' + - 'config/initializers/gitlab_experiment.rb' + - 'config/initializers/lograge.rb' + - 'config/routes.rb' - 'config/routes/dashboard.rb' - 'config/routes/group.rb' - 'config/routes/issues.rb' - - 'db/post_migrate/20210303121224_update_gitlab_subscriptions_start_at_post_eoa.rb' - - 'db/post_migrate/20210513155546_backfill_nuget_temporary_packages_to_processing_status.rb' - - 'db/post_migrate/20210823132600_remove_duplicate_dast_site_tokens.rb' - - 'db/post_migrate/20220425121435_backfill_integrations_enable_ssl_verification.rb' - - 'ee/app/controllers/groups/analytics/productivity_analytics_controller.rb' - - 'ee/app/models/analytics/devops_adoption/enabled_namespace.rb' - - 'ee/app/models/analytics/devops_adoption/snapshot.rb' - - 'ee/app/models/app_sec/fuzzing/coverage/corpus.rb' - - 'ee/app/models/approval_merge_request_rule.rb' - - 'ee/app/models/boards/epic_board_position.rb' - - 'ee/app/models/boards/epic_user_preference.rb' - - 'ee/app/models/ci/minutes/project_monthly_usage.rb' - - 'ee/app/models/concerns/ee/protected_ref.rb' - - 'ee/app/models/concerns/geo/replicable_model.rb' - - 'ee/app/models/concerns/issue_widgets/acts_like_requirement.rb' - - 'ee/app/models/dast/profile.rb' - - 'ee/app/models/dast_site_validation.rb' - - 'ee/app/models/dora/daily_metrics.rb' - - 'ee/app/models/ee/ci/build.rb' - - 'ee/app/models/ee/ci/daily_build_group_report_result.rb' - - 'ee/app/models/ee/ci/job_artifact.rb' - - 'ee/app/models/ee/ci/pipeline.rb' - - 'ee/app/models/ee/environment.rb' - - 'ee/app/models/ee/epic.rb' - - 'ee/app/models/ee/group.rb' - - 'ee/app/models/ee/group_member.rb' - - 'ee/app/models/ee/identity.rb' - - 'ee/app/models/ee/issue.rb' - - 'ee/app/models/ee/iteration.rb' - - 'ee/app/models/ee/label.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_ci_cd_setting.rb' - - 'ee/app/models/ee/note.rb' - - 'ee/app/models/ee/project.rb' - - 'ee/app/models/ee/user.rb' - - 'ee/app/models/ee/vulnerability.rb' - - 'ee/app/models/gitlab_subscription.rb' - - 'ee/app/models/incident_management/oncall_rotation.rb' - - 'ee/app/models/incident_management/oncall_shift.rb' - - 'ee/app/models/iterations/cadence.rb' - - 'ee/app/models/merge_request_block.rb' - - 'ee/app/models/merge_requests/external_status_check.rb' - - 'ee/app/models/merge_train.rb' - - 'ee/app/models/protected_environment.rb' - - 'ee/app/models/requirements_management/requirement.rb' - - 'ee/app/models/security/finding.rb' - - 'ee/app/models/security/orchestration_policy_configuration.rb' - - 'ee/app/models/security/orchestration_policy_rule_schedule.rb' - - 'ee/app/models/security/scan.rb' - - 'ee/app/models/security/training_provider.rb' - - 'ee/app/models/software_license_policy.rb' - - 'ee/app/models/vulnerabilities/feedback.rb' - - 'ee/app/models/vulnerabilities/finding.rb' - - 'ee/app/models/vulnerabilities/historical_statistic.rb' - - 'ee/app/models/vulnerabilities/read.rb' - - 'ee/app/models/vulnerabilities/scanner.rb' + - 'ee/app/controllers/concerns/ee/routable_actions/sso_enforcement_redirect.rb' - 'ee/app/serializers/ee/group_child_entity.rb' - - 'ee/lib/ee/api/entities/application_setting.rb' - - 'ee/lib/ee/api/entities/geo_node_status.rb' - - 'ee/lib/ee/api/entities/group.rb' + - 'ee/app/services/ee/issues/export_csv_service.rb' + - 'ee/lib/ee/api/entities/group_detail.rb' - 'ee/lib/ee/api/entities/group_push_rule.rb' - - 'ee/lib/ee/api/entities/project.rb' - - 'ee/lib/ee/api/entities/vulnerability_issue_link.rb' - - 'ee/lib/ee/gitlab/background_migration/populate_resolved_on_default_branch_column.rb' + - 'ee/lib/ee/banzai/filter/sanitization_filter.rb' + - 'ee/lib/ee/gitlab/checks/diff_check.rb' + - 'ee/lib/elastic/latest/application_class_proxy.rb' - 'ee/lib/gem_extensions/elasticsearch/model/adapter/active_record/importing.rb' - - 'ee/spec/migrations/backfill_delayed_group_deletion_spec.rb' - - 'ee/spec/migrations/remove_schedule_and_status_null_constraints_from_pending_escalations_alert_spec.rb' + - 'ee/spec/elastic_integration/global_search_spec.rb' + - 'ee/spec/lib/gitlab/geo/event_gap_tracking_spec.rb' - 'ee/spec/services/ee/groups/autocomplete_service_spec.rb' - - 'ee/spec/services/ee/notes/create_service_spec.rb' + - 'ee/spec/support/helpers/elasticsearch_helpers.rb' - 'ee/spec/support/shared_examples/lib/gitlab/middleware/maintenance_mode_gitlab_ee_instance_shared_examples.rb' - 'ee/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_ee_instance_shared_examples.rb' - - 'lib/api/ci/jobs.rb' - - 'lib/api/ci/pipelines.rb' - - 'lib/api/entities/group_detail.rb' - - 'lib/api/entities/issue.rb' - 'lib/api/entities/label.rb' - - 'lib/api/entities/merge_request.rb' - 'lib/api/entities/project.rb' - 'lib/api/entities/project_export_status.rb' - - 'lib/api/feature_flags_user_lists.rb' - - 'lib/container_registry/base_client.rb' - - 'lib/container_registry/client.rb' - - 'lib/csv_builder.rb' - - 'lib/event_filter.rb' - - 'lib/gitlab/background_migration/backfill_ci_namespace_mirrors.rb' - - 'lib/gitlab/background_migration/backfill_ci_project_mirrors.rb' - - 'lib/gitlab/background_migration/backfill_ci_queuing_tables.rb' - - 'lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb' - - 'lib/gitlab/background_migration/populate_latest_pipeline_ids.rb' - - 'lib/gitlab/ci/config/entry/includes.rb' - - 'lib/gitlab/ci/config/entry/trigger.rb' - - 'lib/gitlab/config/entry/validatable.rb' - - 'lib/gitlab/database/background_migration/batched_migration.rb' - - 'lib/gitlab/database/background_migration_job.rb' - - 'lib/gitlab/database/postgres_foreign_key.rb' - - 'lib/gitlab/database/postgres_index.rb' - - 'lib/gitlab/database/postgres_partition.rb' - - 'lib/gitlab/database/postgres_partitioned_table.rb' - - 'lib/gitlab/gl_repository.rb' - - 'lib/gitlab/import_export/import_failure_service.rb' - - 'lib/gitlab/merge_requests/commit_message_generator.rb' - - 'lib/gitlab/seeder.rb' + - 'lib/api/validations/types/comma_separated_to_array.rb' + - 'lib/api/validations/types/comma_separated_to_integer_array.rb' + - 'lib/api/validations/types/hash_of_integer_values.rb' + - 'lib/api/validations/validators/check_assignees_count.rb' + - 'lib/banzai/filter/ascii_doc_sanitization_filter.rb' + - 'lib/banzai/filter/base_sanitization_filter.rb' + - 'lib/banzai/filter/sanitization_filter.rb' + - 'lib/gitlab/action_cable/request_store_callbacks.rb' + - 'lib/gitlab/checks/diff_check.rb' + - '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_signals.rb' - - 'lib/gitlab/utils/measuring.rb' - - 'lib/gitlab/visibility_level.rb' - - 'rubocop/cop/rspec/modify_sidekiq_middleware.rb' - - 'rubocop/cop/rspec/timecop_freeze.rb' - - 'rubocop/cop/rspec/timecop_travel.rb' + - 'lib/gitlab/sidekiq_middleware.rb' + - 'lib/gitlab/utils/usage_data.rb' + - 'qa/qa/page/base.rb' + - 'qa/qa/runtime/allure_report.rb' + - 'qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb' + - 'qa/qa/support/api.rb' + - 'rubocop/cop/inject_enterprise_edition_module.rb' + - 'rubocop/cop/rspec/have_gitlab_http_status.rb' - 'spec/controllers/concerns/routable_actions_spec.rb' - - 'spec/deprecation_toolkit_env.rb' - - 'spec/factories/design_management/designs.rb' - - 'spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb' - - 'spec/graphql/resolvers/concerns/resolves_groups_spec.rb' + - 'spec/features/groups/dependency_proxy_for_containers_spec.rb' + - 'spec/graphql/types/base_object_spec.rb' + - 'spec/lib/gitlab/action_cable/request_store_callbacks_spec.rb' - 'spec/lib/gitlab/cross_project_access/class_methods_spec.rb' - - 'spec/lib/gitlab/database/consistency_spec.rb' - - 'spec/lib/gitlab/database/dynamic_model_helpers_spec.rb' - 'spec/lib/gitlab/database/load_balancing/action_cable_callbacks_spec.rb' - - 'spec/lib/gitlab/database/load_balancing_spec.rb' - - 'spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb' - - 'spec/lib/gitlab/database/migration_helpers_spec.rb' - - 'spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb' + - 'spec/lib/gitlab/graphql/tracers/application_context_tracer_spec.rb' - 'spec/lib/gitlab/graphql/tracers/timer_tracer_spec.rb' - - 'spec/lib/gitlab/import_export/members_mapper_spec.rb' - - 'spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb' - - 'spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb' - - 'spec/migrations/20210804150320_create_base_work_item_types_spec.rb' - - 'spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb' - - 'spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb' - - 'spec/migrations/20210902144144_drop_temporary_columns_and_triggers_for_ci_build_needs_spec.rb' - - 'spec/migrations/20210906100316_drop_temporary_columns_and_triggers_for_ci_build_trace_chunks_spec.rb' - - 'spec/migrations/20210906130643_drop_temporary_columns_and_triggers_for_taggings_spec.rb' - - 'spec/migrations/20210907013944_cleanup_bigint_conversion_for_ci_builds_metadata_spec.rb' - - 'spec/migrations/20210915022415_cleanup_bigint_conversion_for_ci_builds_spec.rb' - - 'spec/migrations/20210922021816_drop_int4_columns_for_ci_job_artifacts_spec.rb' - - 'spec/migrations/20210922025631_drop_int4_column_for_ci_sources_pipelines_spec.rb' - - 'spec/migrations/20210922082019_drop_int4_column_for_events_spec.rb' - - 'spec/migrations/20210922091402_drop_int4_column_for_push_event_payloads_spec.rb' - - 'spec/migrations/20211126115449_encrypt_static_objects_external_storage_auth_token_spec.rb' - - 'spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb' - - 'spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb' - - 'spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb' - - 'spec/migrations/20220305223212_add_security_training_providers_spec.rb' - - 'spec/migrations/20220505174658_update_index_on_alerts_to_exclude_null_fingerprints_spec.rb' - - 'spec/migrations/generate_customers_dot_jwt_signing_key_spec.rb' - - 'spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb' - - 'spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_features_spec.rb' - - 'spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb' - - 'spec/migrations/remove_schedule_and_status_from_pending_alert_escalations_spec.rb' - - 'spec/models/ability_spec.rb' - - 'spec/models/broadcast_message_spec.rb' - - 'spec/models/concerns/participable_spec.rb' + - 'spec/lib/gitlab/middleware/rack_multipart_tempfile_factory_spec.rb' + - 'spec/lib/gitlab/path_regex_spec.rb' - 'spec/services/groups/autocomplete_service_spec.rb' - - 'spec/services/notes/create_service_spec.rb' + - 'spec/services/issues/referenced_merge_requests_service_spec.rb' - 'spec/services/projects/autocomplete_service_spec.rb' - 'spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb' - 'spec/support/helpers/email_helpers.rb' + - 'spec/support/helpers/reference_parser_helpers.rb' - 'spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb' - - 'spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb' - - 'spec/workers/process_commit_worker_spec.rb' diff --git a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js index c11cf1a7882..4e0a59d0a38 100644 --- a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js @@ -271,6 +271,25 @@ export const secondsToMilliseconds = (seconds) => seconds * 1000; export const secondsToDays = (seconds) => Math.round(seconds / 86400); /** + * Returns the date `n` seconds after the date provided + * + * @param {Date} date the initial date + * @param {Number} numberOfSeconds number of seconds after + * @return {Date} A `Date` object `n` seconds after the provided `Date` + */ +export const nSecondsAfter = (date, numberOfSeconds) => + new Date(date.getTime() + numberOfSeconds * 1000); + +/** + * Returns the date `n` seconds before the date provided + * + * @param {Date} date the initial date + * @param {Number} numberOfSeconds number of seconds before + * @return {Date} A `Date` object `n` seconds before the provided `Date` + */ +export const nSecondsBefore = (date, numberOfSeconds) => nSecondsAfter(date, -numberOfSeconds); + +/** * Returns the date `n` days after the date provided * * @param {Date} date the initial date diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js index 9887bda973c..5fbad124acb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js @@ -11,13 +11,10 @@ export default { render(h) { const { extensions } = registeredExtensions; - if (extensions.length === 0) return null; - return h( 'section', { attrs: { - class: 'mr-section-container mr-widget-workflow', role: 'region', 'aria-label': __('Merge request reports'), }, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue new file mode 100644 index 00000000000..2683956e603 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue @@ -0,0 +1,18 @@ +<script> +export default { + data() { + return { + hasChildren: false, + }; + }, + updated() { + this.hasChildren = this.$scopedSlots.default?.()?.some((c) => c.tag); + }, +}; +</script> + +<template> + <div v-show="hasChildren" class="mr-section-container mr-widget-workflow"> + <slot></slot> + </div> +</template> 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 c658d3a38f4..9b64ba3f191 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,7 +1,10 @@ <script> import { GlSafeHtmlDirective } from '@gitlab/ui'; import { isEmpty } from 'lodash'; -import { registerExtension } from '~/vue_merge_request_widget/components/extensions'; +import { + registerExtension, + registeredExtensions, +} from '~/vue_merge_request_widget/components/extensions'; 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'; import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store'; @@ -47,6 +50,7 @@ import terraformExtension from './extensions/terraform'; import accessibilityExtension from './extensions/accessibility'; import codeQualityExtension from './extensions/code_quality'; import testReportExtension from './extensions/test_report'; +import ReportWidgetContainer from './components/report_widget_container.vue'; export default { // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25 @@ -86,6 +90,7 @@ export default { SecurityReportsApp: () => import('~/vue_shared/security_reports/security_reports_app.vue'), MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'), ReadyToMerge: ReadyToMergeState, + ReportWidgetContainer, }, apollo: { state: { @@ -216,6 +221,9 @@ export default { return !this.mr.mergeDetailsCollapsed; }, + hasExtensions() { + return registeredExtensions.extensions.length; + }, }, watch: { 'mr.machineValue': { @@ -553,7 +561,17 @@ export default { :mr="mr" :service="service" /> - <extensions-container :mr="mr" /> + <report-widget-container> + <extensions-container v-if="hasExtensions" :mr="mr" /> + <security-reports-app + v-if="shouldRenderSecurityReport && !shouldShowSecurityExtension" + :pipeline-id="mr.pipeline.id" + :project-id="mr.sourceProjectId" + :security-reports-docs-path="mr.securityReportsDocsPath" + :target-project-full-path="mr.targetProjectFullPath" + :mr-iid="mr.iid" + /> + </report-widget-container> <div class="mr-section-container mr-widget-workflow"> <div v-if="hasAlerts" class="gl-overflow-hidden mr-widget-alert-container"> <mr-widget-alert-message @@ -582,15 +600,6 @@ export default { <widget-container v-if="mr" :mr="mr" /> - <security-reports-app - v-if="shouldRenderSecurityReport && !shouldShowSecurityExtension" - :pipeline-id="mr.pipeline.id" - :project-id="mr.sourceProjectId" - :security-reports-docs-path="mr.securityReportsDocsPath" - :target-project-full-path="mr.targetProjectFullPath" - :mr-iid="mr.iid" - /> - <div class="mr-widget-section" data-qa-selector="mr_widget_content"> <component :is="componentName" :mr="mr" :service="service" /> <ready-to-merge diff --git a/app/components/pajamas/spinner_component.html.haml b/app/components/pajamas/spinner_component.html.haml index aab9c5fdbf7..b319f3b1632 100644 --- a/app/components/pajamas/spinner_component.html.haml +++ b/app/components/pajamas/spinner_component.html.haml @@ -1,5 +1,2 @@ -.gl-spinner-container{ class: @class } - - if @inline - %span{ class: spinner_class, aria: {label: @label} } - - else - %div{ class: spinner_class, aria: {label: @label} } += content_tag (@inline ? :span : :div), **html_options do + %span{ class: spinner_class, aria: {label: @label} }> diff --git a/app/components/pajamas/spinner_component.rb b/app/components/pajamas/spinner_component.rb index c7ffc1ec3da..b8e095bb73e 100644 --- a/app/components/pajamas/spinner_component.rb +++ b/app/components/pajamas/spinner_component.rb @@ -2,26 +2,31 @@ module Pajamas class SpinnerComponent < Pajamas::Component - # @param [String] class # @param [Symbol] color # @param [Boolean] inline # @param [String] label # @param [Symbol] size - def initialize(class: '', color: :dark, inline: false, label: _("Loading"), size: :sm) - @class = binding.local_variable_get(:class) + def initialize(color: :dark, inline: false, label: _("Loading"), size: :sm, **html_options) @color = filter_attribute(color.to_sym, COLOR_OPTIONS) @inline = inline @label = label.presence @size = filter_attribute(size.to_sym, SIZE_OPTIONS) + @html_options = html_options end private def spinner_class - ["gl-spinner", "gl-spinner-#{@size}", "gl-spinner-#{@color}"] + ["gl-spinner", "gl-spinner-#{@size}", "gl-spinner-#{@color} gl-vertical-align-text-bottom!"] end COLOR_OPTIONS = [:light, :dark].freeze SIZE_OPTIONS = [:sm, :md, :lg, :xl].freeze + + def html_options + options = format_options(options: @html_options, css_classes: "gl-spinner-container") + options[:role] = "status" + options + end end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index aba786da3f2..c81041c2d9c 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -71,18 +71,13 @@ module IconsHelper # # See also https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/base-loading-icon--default def gl_loading_icon(inline: false, color: 'dark', size: 'sm', css_class: nil, data: nil) - spinner = content_tag(:span, "", { - class: %[gl-spinner gl-spinner-#{color} gl-spinner-#{size} gl-vertical-align-text-bottom!], - aria: { label: _('Loading') }, + render Pajamas::SpinnerComponent.new( + inline: inline, + color: color, + size: size, + class: css_class, data: data - }) - - container_classes = ['gl-spinner-container'] - container_classes << css_class unless css_class.blank? - content_tag(inline ? :span : :div, spinner, { - class: container_classes, - role: 'status' - }) + ) end def external_snippet_icon(name) diff --git a/app/mailers/emails/releases.rb b/app/mailers/emails/releases.rb index 8fe93f59662..468a8624319 100644 --- a/app/mailers/emails/releases.rb +++ b/app/mailers/emails/releases.rb @@ -11,6 +11,8 @@ module Emails ) @recipient = User.find(user_id) + add_project_headers + mail_with_locale( to: @recipient.notification_email_for(@project.group), subject: subject(release_email_subject) diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index 76469b34832..ae730629b02 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -54,3 +54,4 @@ .col-12 .issuable-form-select-holder = form.gitlab_ui_datepicker :due_date, placeholder: _('Select due date'), autocomplete: 'off', id: "issuable-due-date" + = render_if_exists "shared/issuable/form/iteration", form: form, group: project.group diff --git a/config/application.rb b/config/application.rb index 02a57ad447c..249db9c6a67 100644 --- a/config/application.rb +++ b/config/application.rb @@ -566,7 +566,8 @@ module Gitlab # Used in app/services/web_hooks/log_execution_service.rb: log_execution ActiveSupport::TimeWithZone, ActiveSupport::TimeZone, - Gitlab::Color # https://gitlab.com/gitlab-org/gitlab/-/issues/368844 + Gitlab::Color, # https://gitlab.com/gitlab-org/gitlab/-/issues/368844, + Hashie::Array # https://gitlab.com/gitlab-org/gitlab/-/issues/378089 ] # on_master_start yields immediately in unclustered environments and runs diff --git a/config/feature_flags/development/ci_variable_expansion_in_rules_exists.yml b/config/feature_flags/development/ci_variable_expansion_in_rules_exists.yml new file mode 100644 index 00000000000..dec187db4ab --- /dev/null +++ b/config/feature_flags/development/ci_variable_expansion_in_rules_exists.yml @@ -0,0 +1,8 @@ +--- +name: ci_variable_expansion_in_rules_exists +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101639 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/381046 +milestone: '15.6' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/initializers/memory_watchdog.rb b/config/initializers/memory_watchdog.rb index ce8e5029e7a..8a540414378 100644 --- a/config/initializers/memory_watchdog.rb +++ b/config/initializers/memory_watchdog.rb @@ -5,31 +5,10 @@ return unless Gitlab::Utils.to_boolean(ENV['GITLAB_MEMORY_WATCHDOG_ENABLED']) Gitlab::Cluster::LifecycleEvents.on_worker_start do watchdog = Gitlab::Memory::Watchdog.new - max_strikes = ENV.fetch('GITLAB_MEMWD_MAX_STRIKES', 5).to_i - sleep_time_seconds = ENV.fetch('GITLAB_MEMWD_SLEEP_TIME_SEC', 60).to_i - max_mem_growth = ENV.fetch('GITLAB_MEMWD_MAX_MEM_GROWTH', 3.0).to_f - max_heap_frag = ENV.fetch('GITLAB_MEMWD_MAX_HEAP_FRAG', 0.5).to_f - - watchdog.configure do |config| - config.handler = - if Gitlab::Runtime.puma? - Gitlab::Memory::Watchdog::PumaHandler.new - elsif Gitlab::Runtime.sidekiq? - Gitlab::Memory::Watchdog::TermProcessHandler.new - else - Gitlab::Memory::Watchdog::NullHandler.instance - end - - config.logger = Gitlab::AppLogger - config.sleep_time_seconds = sleep_time_seconds - # config.monitor.use MonitorClass, args*, max_strikes:, kwargs**, &block - config.monitors.use Gitlab::Memory::Watchdog::Monitor::HeapFragmentation, - max_heap_fragmentation: max_heap_frag, - max_strikes: max_strikes - - config.monitors.use Gitlab::Memory::Watchdog::Monitor::UniqueMemoryGrowth, - max_mem_growth: max_mem_growth, - max_strikes: max_strikes + if Gitlab::Runtime.puma? + watchdog.configure(&Gitlab::Memory::Watchdog::Configurator.configure_for_puma) + elsif Gitlab::Runtime.sidekiq? + watchdog.configure(&Gitlab::Memory::Watchdog::Configurator.configure_for_sidekiq) end Gitlab::BackgroundTask.new(watchdog).start diff --git a/config/open_api.yml b/config/open_api.yml index 5502577e201..785bf43509f 100644 --- a/config/open_api.yml +++ b/config/open_api.yml @@ -17,6 +17,8 @@ metadata: # Keep in alphabetical order - name: access_requests description: Operations related to access requests + - name: ci_variables + description: Operations related to CI/CD variables - name: cluster_agents description: Operations related to the GitLab agent for Kubernetes - name: ci_resource_groups @@ -51,6 +53,8 @@ metadata: description: Operations related to import BitBucket projects - name: project_import_github description: Operations related to import GitHub projects + - name: protected environments + description: Operations related to protected environments - name: release_links description: Operations related to release assets (links) - name: releases @@ -60,4 +64,4 @@ metadata: - name: system_hooks description: Operations related to system hooks - name: unleash_api - description: Operations related to Unleash API
\ No newline at end of file + description: Operations related to Unleash API diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 6ef1666147c..89c3680ec7c 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -10333,7 +10333,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="boardepicchildreniidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. | | <a id="boardepicchildreniids"></a>`iids` | [`[ID!]`](#id) | List of IIDs of epics, e.g., `[1, 2]`. | | <a id="boardepicchildrenin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. | -| <a id="boardepicchildrenincludeancestorgroups"></a>`includeAncestorGroups` | [`Boolean`](#boolean) | Include epics from ancestor groups. | +| <a id="boardepicchildrenincludeancestorgroups"></a>`includeAncestorGroups` | [`Boolean`](#boolean) | Include child epics from ancestor groups. | | <a id="boardepicchildrenincludedescendantgroups"></a>`includeDescendantGroups` | [`Boolean`](#boolean) | Include epics from descendant groups. | | <a id="boardepicchildrenlabelname"></a>`labelName` | [`[String!]`](#string) | Filter epics by labels. | | <a id="boardepicchildrenmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Filter epics by milestone title, computed from epic's issues. | @@ -12247,7 +12247,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="epicchildreniidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. | | <a id="epicchildreniids"></a>`iids` | [`[ID!]`](#id) | List of IIDs of epics, e.g., `[1, 2]`. | | <a id="epicchildrenin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. | -| <a id="epicchildrenincludeancestorgroups"></a>`includeAncestorGroups` | [`Boolean`](#boolean) | Include epics from ancestor groups. | +| <a id="epicchildrenincludeancestorgroups"></a>`includeAncestorGroups` | [`Boolean`](#boolean) | Include child epics from ancestor groups. | | <a id="epicchildrenincludedescendantgroups"></a>`includeDescendantGroups` | [`Boolean`](#boolean) | Include epics from descendant groups. | | <a id="epicchildrenlabelname"></a>`labelName` | [`[String!]`](#string) | Filter epics by labels. | | <a id="epicchildrenmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Filter epics by milestone title, computed from epic's issues. | @@ -17693,7 +17693,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="projectworkitemssearch"></a>`search` | [`String`](#string) | Search query for title or description. | | <a id="projectworkitemssort"></a>`sort` | [`WorkItemSort`](#workitemsort) | Sort work items by this criteria. | | <a id="projectworkitemsstate"></a>`state` | [`IssuableState`](#issuablestate) | Current state of this work item. | -| <a id="projectworkitemsstatuswidget"></a>`statusWidget` | [`StatusFilterInput`](#statusfilterinput) | Input for status widget filter. | +| <a id="projectworkitemsstatuswidget"></a>`statusWidget` | [`StatusFilterInput`](#statusfilterinput) | Input for status widget filter. Ignored if `work_items_mvc_2` is disabled. | | <a id="projectworkitemstypes"></a>`types` | [`[IssueType!]`](#issuetype) | Filter work items by the given work item types. | ### `ProjectCiCdSetting` diff --git a/doc/api/group_protected_environments.md b/doc/api/group_protected_environments.md index 4cf87cb4305..ecd433bf321 100644 --- a/doc/api/group_protected_environments.md +++ b/doc/api/group_protected_environments.md @@ -95,7 +95,7 @@ Example response: } ``` -## Protect an environment +## Protect a single environment Protects a single environment. @@ -136,7 +136,7 @@ Example response: } ``` -## Update an environment +## Update a protected environment > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351854) in GitLab 15.4. @@ -304,7 +304,7 @@ Example response: } ``` -## Unprotect environment +## Unprotect a single environment Unprotects the given protected environment. diff --git a/doc/api/protected_environments.md b/doc/api/protected_environments.md index 9ff3c34d2d7..ec0657148da 100644 --- a/doc/api/protected_environments.md +++ b/doc/api/protected_environments.md @@ -103,7 +103,7 @@ Example response: } ``` -## Protect repository environments +## Protect a single environment Protects a single environment: @@ -343,7 +343,7 @@ Example response: } ``` -## Unprotect environment +## Unprotect a single environment Unprotects the given protected environment: diff --git a/doc/ci/environments/deployment_approvals.md b/doc/ci/environments/deployment_approvals.md index 5f5ec027827..0ab5b6f823b 100644 --- a/doc/ci/environments/deployment_approvals.md +++ b/doc/ci/environments/deployment_approvals.md @@ -66,7 +66,7 @@ There are two ways to configure approvals for a protected environment: 1. Using the [UI](protected_environments.md#protecting-environments) 1. Set the **Required approvals** field to 1 or more. -1. Using the [REST API](../../api/protected_environments.md#protect-repository-environments) +1. Using the [REST API](../../api/protected_environments.md#protect-a-single-environment) 2. 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. @@ -89,7 +89,7 @@ Maintainer role. > - [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. -1. Using the [REST API](../../api/group_protected_environments.md#protect-an-environment). +1. Using the [REST API](../../api/group_protected_environments.md#protect-a-single-environment). 1. `deploy_access_levels` represents which entity can execute the deployment job. 1. `approval_rules` represents which entity can approve the deployment job. diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md index beb35645492..c8436fc044d 100644 --- a/doc/ci/variables/where_variables_can_be_used.md +++ b/doc/ci/variables/where_variables_can_be_used.md @@ -36,6 +36,7 @@ There are two places defined variables can be used. On the: | [`include`](../yaml/index.md#include) | yes | GitLab | The variable expansion is made by the [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism) in GitLab. <br/><br/>See [Use variables with include](../yaml/includes.md#use-variables-with-include) for more information on supported variables. | | [`only:variables`](../yaml/index.md#onlyvariables--exceptvariables) | no | Not applicable | The variable must be in the form of `$variable`. Not supported are the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). | | [`resource_group`](../yaml/index.md#resource_group) | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support the following:<br/>- `CI_ENVIRONMENT_URL`<br/>- [Persisted variables](#persisted-variables). | +| [`rules:exists`](../yaml/index.md#rulesexists) | yes | GitLab | The variable expansion is made by the [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism) in GitLab. | | [`rules:if`](../yaml/index.md#rulesif) | no | Not applicable | The variable must be in the form of `$variable`. Not supported are the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). | | [`script`](../yaml/index.md#script) | yes | Script execution shell | The variable expansion is made by the [execution shell environment](#execution-shell-environment). | | [`services:name`](../yaml/index.md#services) | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism). | diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index e8dd24cf7e0..f511a74b244 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3437,7 +3437,8 @@ relative to `refs/heads/branch1` and the pipeline source is a merge request even #### `rules:exists` -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24021) in GitLab 12.4. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24021) in GitLab 12.4. +> - CI/CD variable support [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/283881) in GitLab 15.6 [with a flag](../../administration/feature_flags.md) named `ci_variable_expansion_in_rules_exists`. Disabled by default. Use `exists` to run a job when certain files exist in the repository. @@ -3445,8 +3446,7 @@ Use `exists` to run a job when certain files exist in the repository. **Possible inputs**: -- An array of file paths. Paths are relative to the project directory (`$CI_PROJECT_DIR`) - and can't directly link outside it. File paths can use glob patterns. +- An array of file paths. Paths are relative to the project directory (`$CI_PROJECT_DIR`) and can't directly link outside it. File paths can use glob patterns and [CI/CD variables](../variables/where_variables_can_be_used.md#gitlab-ciyml-file). **Example of `rules:exists`**: diff --git a/doc/development/fe_guide/vue3_migration.md b/doc/development/fe_guide/vue3_migration.md index c3ce42a80a5..aae7674d190 100644 --- a/doc/development/fe_guide/vue3_migration.md +++ b/doc/development/fe_guide/vue3_migration.md @@ -159,3 +159,232 @@ export default { [In Vue 3](https://v3-migration.vuejs.org/breaking-changes/props-default-this.html), the props default value factory is passed the raw props as an argument, and can also access injections. + +## Handling libraries that do not work with `@vue/compat` + +**Problem** + +Some libraries rely on Vue.js 2 internals. They might not work with `@vue/compat`, so we need a strategy to use an updated version with Vue.js 3 while maintaining compatibility with the current codebase. + +**Goals** + +- We should add as few changes as possible to existing code to support new libraries. Instead, we should **add*- new code, which will act as **facade**, making the new version compatible with the old one +- Switching between new and old versions should be hidden inside tooling (webpack / jest) and should not be exposed to the code +- All facades specific to migration should live in the same directory to simplify future migration steps + +### Step-by-step migration + +In the step-by-step guide, we will be migrating [VueApollo Demo](https://gitlab.com/gitlab-org/frontend/vue3-migration-vue-apollo/-/tree/main/src/vue3compat) project. It will allow us to focus on migration specifics while avoiding nuances of complex tooling setup in the GitLab project. The project intentionally uses the same tooling as GitLab: + +- webpack +- yarn +- Vue.js + VueApollo + +#### Initial state + +Right after cloning, you could run [VueApollo Demo](https://gitlab.com/gitlab-org/frontend/vue3-migration-vue-apollo/-/tree/main/src/vue3compat) with Vue.js 2 using `yarn serve` or with Vue.js 3 (compat build) using `yarn serve:vue3`. However latter immediately crashes: + +```javascript +Uncaught TypeError: Cannot read properties of undefined (reading 'loading') +``` + +VueApollo v3 (used for Vue.js 2) fails to initialize in Vue.js compat + +NOTE: +While stubbing `Vue.version` will solve VueApollo-related issues in the demo project, it will still lose reactivity on specific scenarios, so an upgrade is still needed + +#### Step 1. Perform upgrade according to library docs + +According to [VueApollo v4 installation guide](https://v4.apollo.vuejs.org/guide/installation.html), we need to install `@vue/apollo-option` (this package provides VueApollo support for Options API) and make changes to our application: + +```diff +--- a/src/index.js ++++ b/src/index.js +@@ -1,19 +1,17 @@ +-import Vue from "vue"; +-import VueApollo from "vue-apollo"; ++import { createApp, h } from "vue"; ++import { createApolloProvider } from "@vue/apollo-option"; + + import Demo from "./components/Demo.vue"; + import createDefaultClient from "./lib/graphql"; + +-Vue.use(VueApollo); +- +-const apolloProvider = new VueApollo({ ++const apolloProvider = createApolloProvider({ + defaultClient: createDefaultClient(), + }); + +-new Vue({ +- el: "#app", +- apolloProvider, +- render(h) { ++const app = createApp({ ++ render() { + return h(Demo); + }, + }); ++app.use(apolloProvider); ++app.mount("#app"); +``` + +You can view these changes in [01-upgrade-vue-apollo](https://gitlab.com/gitlab-org/frontend/vue3-migration-vue-apollo/-/compare/main...01-upgrade-vue-apollo) branch of demo project + +#### Step 2. Addressing differences in augmenting applications in Vue.js 2 and 3 + +In Vue.js 2 tooling like `VueApollo` is initialized in a "lazy" fashion: + +```javascript +// We are registering VueApollo "handler" to handle some data LATER +Vue.use(VueApollo) +// ... +// apolloProvider is provided at app instantiation, +// previously registered VueApollo will handle that +new Vue({ /- ... */, apolloProvider }) +``` + +In Vue.js 3 both steps were merged in one - we are immediately registering the handler and passing configuration: + +```javascript +app.use(apolloProvider) +``` + +In order to backport this behavior, we need the following knowledge: + +- We can access extra options provided to Vue instance via `$options`, so extra `apolloProvider` will be visible as `this.$options.apolloProvider` +- We can access the current `app` (in Vue.js 3 meaning) on the Vue instance via `this.$.appContext.app` + +NOTE: +We're relying on non-public Vue.js 3 API in this case. However, since `@vue/compat` builds are expected to be available only for 3.2.x branch, we have reduced risks that this API will be changed + +With this knowledge, we can move the initialization of our tooling as early as possible in Vue2 - in the `beforeCreate()` lifecycle hook: + +```diff +--- a/src/index.js ++++ b/src/index.js +@@ -1,4 +1,4 @@ +-import { createApp, h } from "vue"; ++import Vue from "vue"; + import { createApolloProvider } from "@vue/apollo-option"; + + import Demo from "./components/Demo.vue"; +@@ -8,10 +8,13 @@ const apolloProvider = createApolloProvider({ + defaultClient: createDefaultClient(), + }); + +-const app = createApp({ +- render() { ++new Vue({ ++ el: "#app", ++ apolloProvider, ++ render(h) { + return h(Demo); + }, ++ beforeCreate() { ++ this.$.appContext.app.use(this.$options.apolloProvider); ++ }, + }); +-app.use(apolloProvider); +-app.mount("#app"); +``` + +You can view these changes in [02-bring-back-new-vue](https://gitlab.com/gitlab-org/frontend/vue3-migration-vue-apollo/-/compare/01-upgrade-vue-apollo...02-bring-back-new-vue) branch of demo project + +#### Step 3. Recreating `VueApollo` class + +Vue.js 3 libraries (and Vue.js itself) have a preference for using factories like `createApp` instead of classes (previously `new Vue`) + +`VueApollo` class served two purposes: + +- constructor for creating `apolloProvider` +- installation of apollo-related logic in components + +We can utilize `Vue.use(VueApollo)` code, which existed in our codebase, to hide there our mixin and avoid modification of our app code: + +```diff +--- a/src/index.js ++++ b/src/index.js +@@ -4,7 +4,26 @@ import { createApolloProvider } from "@vue/apollo-option"; + import Demo from "./components/Demo.vue"; + import createDefaultClient from "./lib/graphql"; + +-const apolloProvider = createApolloProvider({ ++class VueApollo { ++ constructor(...args) { ++ return createApolloProvider(...args); ++ } ++ ++ // called by Vue.use ++ static install() { ++ Vue.mixin({ ++ beforeCreate() { ++ if (this.$options.apolloProvider) { ++ this.$.appContext.app.use(this.$options.apolloProvider); ++ } ++ }, ++ }); ++ } ++} ++ ++Vue.use(VueApollo); ++ ++const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + +@@ -14,7 +33,4 @@ new Vue({ + render(h) { + return h(Demo); + }, +- beforeCreate() { +- this.$.appContext.app.use(this.$options.apolloProvider); +- }, + }); +``` + +You can view these changes in [03-recreate-vue-apollo](https://gitlab.com/gitlab-org/frontend/vue3-migration-vue-apollo/-/compare/02-bring-back-new-vue...03-recreate-vue-apollo) branch of demo project + +#### Step 4. Moving `VueApollo` class to a separate file and setting up an alias + +Now, we have almost the same code (excluding import) as in Vue.js 2 version. +We will move our facade to the separate file and set up `webpack` conditionally execute it if `vue-apollo` is imported when using Vue.js 3: + +```diff +--- a/src/index.js ++++ b/src/index.js +@@ -1,5 +1,5 @@ + import Vue from "vue"; +-import { createApolloProvider } from "@vue/apollo-option"; ++import VueApollo from "vue-apollo"; + + import Demo from "./components/Demo.vue"; + import createDefaultClient from "./lib/graphql"; +diff --git a/webpack.config.js b/webpack.config.js +index 6160d3f..b8b955f 100644 +--- a/webpack.config.js ++++ b/webpack.config.js +@@ -12,6 +12,7 @@ if (USE_VUE3) { + + VUE3_ALIASES = { + vue: "@vue/compat", ++ "vue-apollo": path.resolve("src/vue3compat/vue-apollo"), + }; + } +``` + +(moving `VueApollo` class from `index.js` to `vue3compat/vue-apollo.js` as default export is omitted for clarity) + +You can view these changes in [04-add-webpack-alias](https://gitlab.com/gitlab-org/frontend/vue3-migration-vue-apollo/-/compare/03-recreate-vue-apollo...04-add-webpack-alias) branch of demo project + +#### Step 5. Observe the results + +At this point, you should be able again to run **both*- Vue.js 2 version with `yarn serve` and Vue.js 3 one with `yarn serve:vue3` +[Final MR](https://gitlab.com/gitlab-org/frontend/vue3-migration-vue-apollo/-/merge_requests/1/diffs) with all changes from previous steps displays no changes to `index.js` (application code), which was our goal + +### Applying this approach in the GitLab project + +In [commit adding VueApollo v4 support](https://gitlab.com/gitlab-org/gitlab/-/commit/e0af7e6479695a28a4fe85a88f90815aa3ce2814) we can see additional nuances not covered by step-by-step guide: + +- We might need to add additional imports to our facades (our code in GitLab uses `ApolloMutation` component) +- We need to update aliases not only for webpack but also for jest so our tests could also consume our facade diff --git a/doc/operations/incident_management/alerts.md b/doc/operations/incident_management/alerts.md index cde43e8c077..d6293cf1479 100644 --- a/doc/operations/incident_management/alerts.md +++ b/doc/operations/incident_management/alerts.md @@ -103,31 +103,6 @@ When you upload an image, you can add text to the image and link it to the origi If you add a link, it is shown above the uploaded image. -#### View an alert's logs - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/201846) in GitLab Ultimate 12.8. -> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/217768) in GitLab 13.3. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25455) from GitLab Ultimate to GitLab Free in 12.9. - -Viewing logs from a metrics panel can be useful if you're triaging an -application incident and need to [explore logs](../metrics/dashboards/index.md#chart-context-menu) -from across your application. These logs help you understand what's affecting -your application's performance and how to resolve any problems. - -Prerequisite: - -- You must have at least the Developer role. - -To view the logs for an alert: - -1. On the top bar, select **Main menu > Projects** and find your project. -1. On the left sidebar, select **Monitor > Alerts**. -1. Select the alert you want to view. -1. Below the title of the alert, select the **Metrics** tab. -1. Select the [menu](../metrics/dashboards/index.md#chart-context-menu) of - the metric chart to view options. -1. Select **View logs**. - ### Activity feed tab > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3066) in GitLab 13.1. @@ -177,6 +152,16 @@ To change an alert's status: To stop email notifications for alert reoccurrences in projects with [email notifications enabled](paging.md#email-notifications-for-alerts), change the alert's status away from **Triggered**. +#### Resolve an alert by closing the linked incident + +Prerequisites: + +- You must have at least the Reporter role. + +When you close an [incident](incidents.md) that is linked to an alert, +the linked alert's status changes to **Resolved**. +You are then credited with the alert's status change. + #### As an on-call responder **(PREMIUM)** On-call responders can respond to [alert pages](paging.md#escalating-an-alert) diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md index c24c8980197..9baf6d99b61 100644 --- a/doc/user/project/issues/managing_issues.md +++ b/doc/user/project/issues/managing_issues.md @@ -207,7 +207,8 @@ the appropriate project and followed up from there. ### Fields in the new issue form -> Adding the new issue to an epic [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13847) in GitLab 13.1. +> - Adding the new issue to an epic [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13847) in GitLab 13.1. +> - Iteration field [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233517) in GitLab 15.6. When you're creating a new issue, you can complete the following fields: @@ -222,6 +223,7 @@ When you're creating a new issue, you can complete the following fields: - [Due date](due_dates.md) - [Milestone](../milestones/index.md) - [Labels](../labels.md) +- [Iteration](../../group/iterations/index.md) ## Edit an issue diff --git a/lib/api/admin/ci/variables.rb b/lib/api/admin/ci/variables.rb index be73fd496c5..0d959d31a65 100644 --- a/lib/api/admin/ci/variables.rb +++ b/lib/api/admin/ci/variables.rb @@ -13,8 +13,9 @@ module API namespace 'admin' do namespace 'ci' do namespace 'variables' do - desc 'Get instance-level variables' do + desc 'List all instance-level variables' do success Entities::Ci::Variable + tags %w[ci_variables] end params do use :pagination @@ -25,11 +26,13 @@ module API present paginate(variables), with: Entities::Ci::Variable end - desc 'Get a specific variable from a group' do + desc 'Get the details of a specific instance-level variable' do success Entities::Ci::Variable + failure [{ code: 404, message: 'Instance Variable Not Found' }] + tags %w[ci_variables] end params do - requires :key, type: String, desc: 'The key of the variable' + requires :key, type: String, desc: 'The key of a variable' end get ':key' do key = params[:key] @@ -42,16 +45,18 @@ module API desc 'Create a new instance-level variable' do success Entities::Ci::Variable + failure [{ code: 400, message: '400 Bad Request' }] + tags %w[ci_variables] end route_setting :log_safety, { safe: %w[key], unsafe: %w[value] } params do requires :key, type: String, - desc: 'The key of the variable' + desc: 'The key of the variable. Max 255 characters' requires :value, type: String, - desc: 'The value of the variable' + desc: 'The value of a variable' optional :protected, type: String, @@ -64,7 +69,7 @@ module API optional :variable_type, type: String, values: ::Ci::InstanceVariable.variable_types.keys, - desc: 'The type of variable, must be one of env_var or file. Defaults to env_var' + desc: 'The type of a variable. Available types are: env_var (default) and file' end post '/' do variable_params = declared_params(include_missing: false) @@ -78,18 +83,20 @@ module API end end - desc 'Update an existing instance-variable' do + desc 'Update an instance-level variable' do success Entities::Ci::Variable + failure [{ code: 404, message: 'Instance Variable Not Found' }] + tags %w[ci_variables] end route_setting :log_safety, { safe: %w[key], unsafe: %w[value] } params do optional :key, type: String, - desc: 'The key of the variable' + desc: 'The key of a variable' optional :value, type: String, - desc: 'The value of the variable' + desc: 'The value of a variable' optional :protected, type: String, @@ -102,7 +109,7 @@ module API optional :variable_type, type: String, values: ::Ci::InstanceVariable.variable_types.keys, - desc: 'The type of variable, must be one of env_var or file' + desc: 'The type of a variable. Available types are: env_var (default) and file' end put ':key' do variable = ::Ci::InstanceVariable.find_by_key(params[:key]) @@ -120,9 +127,11 @@ module API desc 'Delete an existing instance-level variable' do success Entities::Ci::Variable + failure [{ code: 404, message: 'Instance Variable Not Found' }] + tags %w[ci_variables] end params do - requires :key, type: String, desc: 'The key of the variable' + requires :key, type: String, desc: 'The key of a variable' end delete ':key' do variable = ::Ci::InstanceVariable.find_by_key(params[:key]) diff --git a/lib/api/api.rb b/lib/api/api.rb index 97b4c08bc2a..bc0828209fa 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -171,6 +171,7 @@ module API namespace do # Keep in alphabetical order mount ::API::AccessRequests + mount ::API::Admin::Ci::Variables mount ::API::Appearance mount ::API::Applications mount ::API::BroadcastMessages @@ -180,6 +181,7 @@ module API mount ::API::Ci::Runners mount ::API::Clusters::AgentTokens mount ::API::Clusters::Agents + mount ::API::CommitStatuses mount ::API::DeployKeys mount ::API::DeployTokens mount ::API::Deployments @@ -217,7 +219,6 @@ module API # Keep in alphabetical order mount ::API::Admin::BatchedBackgroundMigrations - mount ::API::Admin::Ci::Variables mount ::API::Admin::InstanceClusters mount ::API::Admin::PlanLimits mount ::API::Admin::Sidekiq diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 859cbd1ce60..e299f49042d 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -16,14 +16,20 @@ module API before { authenticate! } desc "Get a commit's statuses" do - success Entities::CommitStatus + success code: 200, model: Entities::CommitStatus + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' } + ] + is_array true end params do - requires :sha, type: String, desc: 'The commit hash' - optional :ref, type: String, desc: 'The ref' - optional :stage, type: String, desc: 'The stage' - optional :name, type: String, desc: 'The name' - optional :all, type: String, desc: 'Show all statuses, default: false' + requires :sha, type: String, desc: 'The commit hash', documentation: { example: '18f3e63d05582537db6d183d9d557be09e1f90c8' } + optional :ref, type: String, desc: 'The ref', documentation: { example: 'develop' } + optional :stage, type: String, desc: 'The stage', documentation: { example: 'test' } + optional :name, type: String, desc: 'The name', documentation: { example: 'bundler:audit' } + optional :all, type: Boolean, desc: 'Show all statuses', documentation: { default: false } use :pagination end # rubocop: disable CodeReuse/ActiveRecord @@ -43,19 +49,32 @@ module API # rubocop: enable CodeReuse/ActiveRecord desc 'Post status to a commit' do - success Entities::CommitStatus + success code: 200, model: Entities::CommitStatus + failure [ + { code: 400, message: 'Bad request' }, + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' } + ] end params do - requires :sha, type: String, desc: 'The commit hash' - requires :state, type: String, desc: 'The state of the status', - values: %w(pending running success failed canceled) - optional :ref, type: String, desc: 'The ref' - optional :target_url, type: String, desc: 'The target URL to associate with this status' - optional :description, type: String, desc: 'A short description of the status' - optional :name, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"', documentation: { default: 'default' } - optional :context, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"', documentation: { default: 'default' } - optional :coverage, type: Float, desc: 'The total code coverage' - optional :pipeline_id, type: Integer, desc: 'An existing pipeline ID, when multiple pipelines on the same commit SHA have been triggered' + requires :sha, type: String, desc: 'The commit hash', + documentation: { example: '18f3e63d05582537db6d183d9d557be09e1f90c8' } + requires :state, type: String, desc: 'The state of the status', + values: %w(pending running success failed canceled), + documentation: { example: 'pending' } + optional :ref, type: String, desc: 'The ref', + documentation: { example: 'develop' } + optional :target_url, type: String, desc: 'The target URL to associate with this status', + documentation: { example: 'https://gitlab.example.com/thedude/gitlab-foss/builds/91' } + optional :description, type: String, desc: 'A short description of the status' + optional :name, type: String, desc: 'A string label to differentiate this status from the status of other systems', + documentation: { example: 'coverage', default: 'default' } + optional :context, type: String, desc: 'A string label to differentiate this status from the status of other systems', + documentation: { example: 'coverage', default: 'default' } + optional :coverage, type: Float, desc: 'The total code coverage', + documentation: { example: 100.0 } + optional :pipeline_id, type: Integer, desc: 'An existing pipeline ID, when multiple pipelines on the same commit SHA have been triggered' end # rubocop: disable CodeReuse/ActiveRecord post ':id/statuses/:sha' do diff --git a/lib/api/entities/ci/variable.rb b/lib/api/entities/ci/variable.rb index f4d5248245a..da5f5cc5cb1 100644 --- a/lib/api/entities/ci/variable.rb +++ b/lib/api/entities/ci/variable.rb @@ -4,10 +4,15 @@ module API module Entities module Ci class Variable < Grape::Entity - expose :variable_type, :key, :value - expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) } - expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) } - expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) } + expose :variable_type, documentation: { type: 'string', example: 'env_var' } + expose :key, documentation: { type: 'string', example: 'TEST_VARIABLE_1' } + expose :value, documentation: { type: 'string', example: 'TEST_1' } + expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) }, + documentation: { type: 'boolean' } + expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) }, + documentation: { type: 'boolean' } + expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) }, + documentation: { type: 'string', example: '*' } end end end diff --git a/lib/api/entities/commit_status.rb b/lib/api/entities/commit_status.rb index 61b8bf89cfe..21c1512edfd 100644 --- a/lib/api/entities/commit_status.rb +++ b/lib/api/entities/commit_status.rb @@ -3,8 +3,22 @@ module API module Entities class CommitStatus < Grape::Entity - expose :id, :sha, :ref, :status, :name, :target_url, :description, - :created_at, :started_at, :finished_at, :allow_failure, :coverage + expose :id, documentation: { type: 'integer', example: 93 } + expose :sha, documentation: { type: 'string', example: '18f3e63d05582537db6d183d9d557be09e1f90c8' } + expose :ref, documentation: { type: 'string', example: 'develop' } + expose :status, documentation: { type: 'string', example: 'success' } + expose :name, documentation: { type: 'string', example: 'default' } + expose :target_url, documentation: { + type: 'string', + example: 'https://gitlab.example.com/thedude/gitlab-foss/builds/91' + } + expose :description, documentation: { type: 'string' } + expose :created_at, documentation: { type: 'dateTime', example: '2016-01-19T09:05:50.355Z' } + expose :started_at, documentation: { type: 'dateTime', example: '2016-01-20T08:40:25.832Z' } + expose :finished_at, documentation: { type: 'dateTime', example: '2016-01-21T08:40:25.832Z' } + expose :allow_failure, documentation: { type: 'boolean', example: false } + expose :coverage, documentation: { type: 'number', format: 'float', example: 98.29 } + expose :author, using: Entities::UserBasic end end diff --git a/lib/api/entities/custom_attribute.rb b/lib/api/entities/custom_attribute.rb index f949b709517..883b572ac75 100644 --- a/lib/api/entities/custom_attribute.rb +++ b/lib/api/entities/custom_attribute.rb @@ -3,8 +3,8 @@ module API module Entities class CustomAttribute < Grape::Entity - expose :key - expose :value + expose :key, documentation: { type: 'string', example: 'foo' } + expose :value, documentation: { type: 'string', example: 'bar' } end end end diff --git a/lib/api/entities/user_basic.rb b/lib/api/entities/user_basic.rb index 9f20e0caa72..32e066b9f7e 100644 --- a/lib/api/entities/user_basic.rb +++ b/lib/api/entities/user_basic.rb @@ -9,8 +9,17 @@ module API user.avatar_url(only_path: false) end - expose :avatar_path, if: ->(user, options) { options.fetch(:only_path, false) && user.avatar_path } - expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes + expose( + :avatar_path, + documentation: { + type: 'string', + example: '/user/avatar/28/The-Big-Lebowski-400-400.png' + }, + if: ->(user, options) { options.fetch(:only_path, false) && user.avatar_path } + ) + + expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes, + documentation: { is_array: true } expose :web_url, documentation: { type: 'string', example: 'https://gitlab.example.com/root' } do |user, options| Gitlab::Routing.url_helpers.user_url(user) diff --git a/lib/gitlab/ci/build/rules/rule/clause/exists.rb b/lib/gitlab/ci/build/rules/rule/clause/exists.rb index aebd81e7b07..5617d153bc8 100644 --- a/lib/gitlab/ci/build/rules/rule/clause/exists.rb +++ b/lib/gitlab/ci/build/rules/rule/clause/exists.rb @@ -9,20 +9,34 @@ module Gitlab MAX_PATTERN_COMPARISONS = 10_000 def initialize(globs) - globs = Array(globs) - - @top_level_only = globs.all?(&method(:top_level_glob?)) - @exact_globs, @pattern_globs = globs.partition(&method(:exact_glob?)) + @globs = Array(globs) + @top_level_only = @globs.all?(&method(:top_level_glob?)) end def satisfied_by?(_pipeline, context) paths = worktree_paths(context) + exact_globs, pattern_globs = separate_globs(context) - exact_matches?(paths) || pattern_matches?(paths) + exact_matches?(paths, exact_globs) || pattern_matches?(paths, pattern_globs) end private + def separate_globs(context) + if ::Feature.enabled?(:ci_variable_expansion_in_rules_exists, context.project) + expanded_globs = expand_globs(context) + expanded_globs.partition(&method(:exact_glob?)) + else + @globs.partition(&method(:exact_glob?)) + end + end + + def expand_globs(context) + @globs.map do |glob| + ExpandVariables.expand_existing(glob, -> { context.variables_hash }) + end + end + def worktree_paths(context) return [] unless context.project @@ -33,13 +47,16 @@ module Gitlab end end - def exact_matches?(paths) - @exact_globs.any? { |glob| paths.bsearch { |path| glob <=> path } } + def exact_matches?(paths, exact_globs) + exact_globs.any? do |glob| + paths.bsearch { |path| glob <=> path } + end end - def pattern_matches?(paths) + def pattern_matches?(paths, pattern_globs) comparisons = 0 - @pattern_globs.any? do |glob| + + pattern_globs.any? do |glob| paths.any? do |path| comparisons += 1 comparisons > MAX_PATTERN_COMPARISONS || pattern_match?(glob, path) diff --git a/lib/gitlab/memory/watchdog.rb b/lib/gitlab/memory/watchdog.rb index a36442aca43..19dfc640b5d 100644 --- a/lib/gitlab/memory/watchdog.rb +++ b/lib/gitlab/memory/watchdog.rb @@ -54,6 +54,17 @@ module Gitlab init_prometheus_metrics end + ## + # Configuration for Watchdog, use like: + # + # watchdog.configure do |config| + # config.handler = Gitlab::Memory::Watchdog::TermProcessHandler + # config.sleep_time_seconds = 60 + # config.logger = Gitlab::AppLogger + # config.monitors do |stack| + # stack.push MyMonitorClass, args*, max_strikes:, kwargs**, &block + # end + # end def configure yield @configuration end diff --git a/lib/gitlab/memory/watchdog/configuration.rb b/lib/gitlab/memory/watchdog/configuration.rb index 2d84b083f55..793f75adf59 100644 --- a/lib/gitlab/memory/watchdog/configuration.rb +++ b/lib/gitlab/memory/watchdog/configuration.rb @@ -9,7 +9,7 @@ module Gitlab @monitors = [] end - def use(monitor_class, *args, **kwargs, &block) + def push(monitor_class, *args, **kwargs, &block) remove(monitor_class) @monitors.push(build_monitor_state(monitor_class, *args, **kwargs, &block)) end @@ -39,11 +39,12 @@ module Gitlab DEFAULT_SLEEP_TIME_SECONDS = 60 - attr_reader :monitors attr_writer :logger, :handler, :sleep_time_seconds - def initialize - @monitors = MonitorStack.new + def monitors + @monitor_stack ||= MonitorStack.new + yield @monitor_stack if block_given? + @monitor_stack end def handler diff --git a/lib/gitlab/memory/watchdog/configurator.rb b/lib/gitlab/memory/watchdog/configurator.rb new file mode 100644 index 00000000000..6d6f97dc8ba --- /dev/null +++ b/lib/gitlab/memory/watchdog/configurator.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Gitlab + module Memory + class Watchdog + class Configurator + class << self + def configure_for_puma + lambda do |config| + sleep_time_seconds = ENV.fetch('GITLAB_MEMWD_SLEEP_TIME_SEC', 60).to_i + config.logger = Gitlab::AppLogger + config.handler = Gitlab::Memory::Watchdog::PumaHandler.new + config.sleep_time_seconds = sleep_time_seconds + config.monitors(&configure_monitors_for_puma) + end + end + + def configure_for_sidekiq + lambda do |config| + sleep_time_seconds = [ENV.fetch('SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL', 3).to_i, 2].max + config.logger = Sidekiq.logger + config.handler = Gitlab::Memory::Watchdog::TermProcessHandler.new + config.sleep_time_seconds = sleep_time_seconds + config.monitors(&configure_monitors_for_sidekiq) + end + end + + private + + def configure_monitors_for_puma + lambda do |stack| + max_strikes = ENV.fetch('GITLAB_MEMWD_MAX_STRIKES', 5).to_i + + if Gitlab::Utils.to_boolean(ENV['DISABLE_PUMA_WORKER_KILLER']) + max_heap_frag = ENV.fetch('GITLAB_MEMWD_MAX_HEAP_FRAG', 0.5).to_f + max_mem_growth = ENV.fetch('GITLAB_MEMWD_MAX_MEM_GROWTH', 3.0).to_f + + # stack.push MonitorClass, args*, max_strikes:, kwargs**, &block + stack.push Gitlab::Memory::Watchdog::Monitor::HeapFragmentation, + max_heap_fragmentation: max_heap_frag, + max_strikes: max_strikes + + stack.push Gitlab::Memory::Watchdog::Monitor::UniqueMemoryGrowth, + max_mem_growth: max_mem_growth, + max_strikes: max_strikes + else + memory_limit = ENV.fetch('PUMA_WORKER_MAX_MEMORY', 1200).to_i + + stack.push Gitlab::Memory::Watchdog::Monitor::RssMemoryLimit, + memory_limit: memory_limit, + max_strikes: max_strikes + end + end + end + + def configure_monitors_for_sidekiq + # NOP - At the moment we don't run watchdog for Sidekiq + end + end + end + end + end +end diff --git a/lib/gitlab/memory/watchdog/monitor/heap_fragmentation.rb b/lib/gitlab/memory/watchdog/monitor/heap_fragmentation.rb index 7748c19c6d8..8f230980eac 100644 --- a/lib/gitlab/memory/watchdog/monitor/heap_fragmentation.rb +++ b/lib/gitlab/memory/watchdog/monitor/heap_fragmentation.rb @@ -22,7 +22,7 @@ module Gitlab def call heap_fragmentation = Gitlab::Metrics::Memory.gc_heap_fragmentation - return { threshold_violated: false, payload: {} } unless heap_fragmentation > max_heap_fragmentation + return { threshold_violated: false, payload: {} } if heap_fragmentation <= max_heap_fragmentation { threshold_violated: true, payload: payload(heap_fragmentation) } end diff --git a/lib/gitlab/memory/watchdog/monitor/rss_memory_limit.rb b/lib/gitlab/memory/watchdog/monitor/rss_memory_limit.rb new file mode 100644 index 00000000000..3e7de024630 --- /dev/null +++ b/lib/gitlab/memory/watchdog/monitor/rss_memory_limit.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Gitlab + module Memory + class Watchdog + module Monitor + class RssMemoryLimit + attr_reader :memory_limit + + def initialize(memory_limit:) + @memory_limit = memory_limit + end + + def call + worker_rss = Gitlab::Metrics::System.memory_usage_rss[:total] + + return { threshold_violated: false, payload: {} } if worker_rss <= memory_limit + + { threshold_violated: true, payload: payload(worker_rss, memory_limit) } + end + + private + + def payload(worker_rss, memory_limit) + { + message: 'rss memory limit exceeded', + memwd_rss_bytes: worker_rss, + memwd_max_rss_bytes: memory_limit + } + end + end + end + end + end +end diff --git a/lib/gitlab/memory/watchdog/monitor/unique_memory_growth.rb b/lib/gitlab/memory/watchdog/monitor/unique_memory_growth.rb index 2a1512c4cff..ce3477e6227 100644 --- a/lib/gitlab/memory/watchdog/monitor/unique_memory_growth.rb +++ b/lib/gitlab/memory/watchdog/monitor/unique_memory_growth.rb @@ -16,7 +16,7 @@ module Gitlab reference_uss = reference_mem[:uss] memory_limit = max_mem_growth * reference_uss - return { threshold_violated: false, payload: {} } unless worker_uss > memory_limit + return { threshold_violated: false, payload: {} } if worker_uss <= memory_limit { threshold_violated: true, payload: payload(worker_uss, reference_uss, memory_limit) } end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index cc70d4443ab..023663f4435 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7787,9 +7787,6 @@ msgstr "" msgid "Certificate Subject" msgstr "" -msgid "Change" -msgstr "" - msgid "Change Failure Rate" msgstr "" @@ -12008,9 +12005,6 @@ msgstr "" msgid "DORA4Metrics|%{startDate} - %{endDate}" msgstr "" -msgid "DORA4Metrics|30 days before that" -msgstr "" - msgid "DORA4Metrics|Average (last %{days}d)" msgstr "" @@ -12035,9 +12029,6 @@ msgstr "" msgid "DORA4Metrics|Deployment frequency" msgstr "" -msgid "DORA4Metrics|Last 30 days" -msgstr "" - msgid "DORA4Metrics|Lead time for changes" msgstr "" @@ -12053,6 +12044,9 @@ msgstr "" msgid "DORA4Metrics|Median time an incident was open in a production environment over the given time period." msgstr "" +msgid "DORA4Metrics|Month to date" +msgstr "" + msgid "DORA4Metrics|No incidents during this period" msgstr "" @@ -48511,9 +48505,6 @@ msgstr "" msgid "mrWidget|%{boldHeaderStart}Looks like there's no pipeline here.%{boldHeaderEnd}" msgstr "" -msgid "mrWidget|%{linkStart}Set up now%{linkEnd} to analyze your source code for known security vulnerabilities." -msgstr "" - msgid "mrWidget|%{mergeError}." msgstr "" @@ -48758,9 +48749,6 @@ msgstr "" msgid "mrWidget|Revoke approval" msgstr "" -msgid "mrWidget|SAST and Secret Detection is not enabled." -msgstr "" - msgid "mrWidget|Set by %{merge_author} to be added to the merge train when the pipeline succeeds" msgstr "" diff --git a/scripts/security-harness b/scripts/security-harness index df499be23f5..db397a6c1b1 100755 --- a/scripts/security-harness +++ b/scripts/security-harness @@ -75,6 +75,7 @@ end def delete_hook FileUtils.rm(HOOK_PATH) + system("git checkout master") puts "#{SHELL_YELLOW}Security harness removed -- you can now push to all remotes.#{SHELL_CLEAR}" end diff --git a/spec/components/pajamas/spinner_component_spec.rb b/spec/components/pajamas/spinner_component_spec.rb index 9aac9a0085c..f03d8c9561b 100644 --- a/spec/components/pajamas/spinner_component_spec.rb +++ b/spec/components/pajamas/spinner_component_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Pajamas::SpinnerComponent, type: :component do describe 'inline' do context 'by default' do it 'renders a div' do - expect(page).to have_css 'div.gl-spinner' + expect(page).to have_css 'div.gl-spinner-container' end end @@ -43,7 +43,7 @@ RSpec.describe Pajamas::SpinnerComponent, type: :component do let(:options) { { inline: true } } it 'renders a span' do - expect(page).to have_css 'span.gl-spinner' + expect(page).to have_css 'span.gl-spinner-container' end end end diff --git a/spec/components/previews/pajamas/spinner_component_preview.rb b/spec/components/previews/pajamas/spinner_component_preview.rb index 149bfddcfc2..3ff87786768 100644 --- a/spec/components/previews/pajamas/spinner_component_preview.rb +++ b/spec/components/previews/pajamas/spinner_component_preview.rb @@ -9,14 +9,28 @@ module Pajamas # @param label text # @param size select [[small, sm], [medium, md], [large, lg], [extra large, xl]] def default(inline: false, label: "Loading", size: :md) - render(Pajamas::SpinnerComponent.new(inline: inline, label: label, size: size)) + render Pajamas::SpinnerComponent.new( + inline: inline, + label: label, + size: size + ) end - # Use a light spinner on dark backgrounds + # Use a light spinner on dark backgrounds. # # @display bg_color "#222" def light render(Pajamas::SpinnerComponent.new(color: :light)) end + + # Any extra HTML attributes like `class`, `data` or `id` get automatically applied to the spinner container element. + # + def extra_attributes + render Pajamas::SpinnerComponent.new( + class: "js-do-something", + data: { foo: "bar" }, + id: "my-special-spinner" + ) + end end end diff --git a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js index 59b3b4c02df..055d57d6ada 100644 --- a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js +++ b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js @@ -1,4 +1,9 @@ -import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility'; +import { + getDateWithUTC, + newDateAsLocaleTime, + nSecondsAfter, + nSecondsBefore, +} from '~/lib/utils/datetime/date_calculation_utility'; describe('newDateAsLocaleTime', () => { it.each` @@ -31,3 +36,33 @@ describe('getDateWithUTC', () => { expect(getDateWithUTC(date)).toEqual(expected); }); }); + +describe('nSecondsAfter', () => { + const start = new Date('2022-03-22T01:23:45.678Z'); + it.each` + date | seconds | expected + ${start} | ${0} | ${start} + ${start} | ${1} | ${new Date('2022-03-22T01:23:46.678Z')} + ${start} | ${5} | ${new Date('2022-03-22T01:23:50.678Z')} + ${start} | ${60} | ${new Date('2022-03-22T01:24:45.678Z')} + ${start} | ${3600} | ${new Date('2022-03-22T02:23:45.678Z')} + ${start} | ${86400} | ${new Date('2022-03-23T01:23:45.678Z')} + `('returns $expected given $string', ({ date, seconds, expected }) => { + expect(nSecondsAfter(date, seconds)).toEqual(expected); + }); +}); + +describe('nSecondsBefore', () => { + const start = new Date('2022-03-22T01:23:45.678Z'); + it.each` + date | seconds | expected + ${start} | ${0} | ${start} + ${start} | ${1} | ${new Date('2022-03-22T01:23:44.678Z')} + ${start} | ${5} | ${new Date('2022-03-22T01:23:40.678Z')} + ${start} | ${60} | ${new Date('2022-03-22T01:22:45.678Z')} + ${start} | ${3600} | ${new Date('2022-03-22T00:23:45.678Z')} + ${start} | ${86400} | ${new Date('2022-03-21T01:23:45.678Z')} + `('returns $expected given $string', ({ date, seconds, expected }) => { + expect(nSecondsBefore(date, seconds)).toEqual(expected); + }); +}); diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb index 139e8be33d5..2f1682e9194 100644 --- a/spec/helpers/icons_helper_spec.rb +++ b/spec/helpers/icons_helper_spec.rb @@ -234,7 +234,7 @@ RSpec.describe IconsHelper do describe 'gl_loading_icon' do it 'returns the default spinner markup' do expect(gl_loading_icon.to_s) - .to eq '<div class="gl-spinner-container" role="status"><span class="gl-spinner gl-spinner-dark gl-spinner-sm gl-vertical-align-text-bottom!" aria-label="Loading"></span></div>' + .to eq '<div class="gl-spinner-container" role="status"><span aria-label="Loading" class="gl-spinner gl-spinner-sm gl-spinner-dark gl-vertical-align-text-bottom!"></span></div>' end context 'when css_class is provided' do diff --git a/spec/initializers/memory_watchdog_spec.rb b/spec/initializers/memory_watchdog_spec.rb index 36f96131c3d..5c3d020016a 100644 --- a/spec/initializers/memory_watchdog_spec.rb +++ b/spec/initializers/memory_watchdog_spec.rb @@ -29,28 +29,10 @@ RSpec.describe 'memory watchdog' do run_initializer end - shared_examples 'starts configured watchdog' do |handler_class| - let(:configuration) { Gitlab::Memory::Watchdog::Configuration.new } - let(:watchdog_monitors_params) do - { - Gitlab::Memory::Watchdog::Monitor::HeapFragmentation => { - max_heap_fragmentation: max_heap_fragmentation, - max_strikes: max_strikes - }, - Gitlab::Memory::Watchdog::Monitor::UniqueMemoryGrowth => { - max_mem_growth: max_mem_growth, - max_strikes: max_strikes - } - } - end - + 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 - expect(watchdog).to receive(:configure).and_yield(configuration) - - watchdog_monitors_params.each do |monitor_class, params| - expect(configuration.monitors).to receive(:use).with(monitor_class, **params) - end + expect(Gitlab::Memory::Watchdog::Configurator).to receive(configure_monitor_method) expect(Gitlab::Memory::Watchdog).to receive(:new).and_return(watchdog) expect(Gitlab::BackgroundTask).to receive(:new).with(watchdog).and_return(background_task) @@ -58,71 +40,24 @@ RSpec.describe 'memory watchdog' do expect(Gitlab::Cluster::LifecycleEvents).to receive(:on_worker_start).and_yield run_initializer - - expect(configuration.handler).to be_an_instance_of(handler_class) - expect(configuration.logger).to eq(logger) - expect(configuration.sleep_time_seconds).to eq(sleep_time_seconds) end end - - context 'when settings are not passed through the environment' do - let(:max_strikes) { 5 } - let(:max_heap_fragmentation) { 0.5 } - let(:max_mem_growth) { 3.0 } - let(:sleep_time_seconds) { 60 } - - include_examples 'configures and starts watchdog' - end - - context 'when settings are passed through the environment' do - let(:max_strikes) { 6 } - let(:max_heap_fragmentation) { 0.4 } - let(:max_mem_growth) { 2.0 } - let(:sleep_time_seconds) { 50 } - - before do - stub_env('GITLAB_MEMWD_MAX_STRIKES', 6) - stub_env('GITLAB_MEMWD_SLEEP_TIME_SEC', 50) - stub_env('GITLAB_MEMWD_MAX_MEM_GROWTH', 2.0) - stub_env('GITLAB_MEMWD_MAX_HEAP_FRAG', 0.4) - end - - include_examples 'configures and starts watchdog' - end end - # In tests, the Puma constant does not exist so we cannot use a verified double. - # rubocop: disable RSpec/VerifiedDoubles context 'when puma' do - let(:puma) do - Class.new do - def self.cli_config - Struct.new(:options).new - end - end - end - before do - stub_const('Puma', puma) - stub_const('Puma::Cluster::WorkerHandle', double.as_null_object) - allow(Gitlab::Runtime).to receive(:puma?).and_return(true) end - it_behaves_like 'starts configured watchdog', Gitlab::Memory::Watchdog::PumaHandler + it_behaves_like 'starts configured watchdog', :configure_for_puma end - # rubocop: enable RSpec/VerifiedDoubles context 'when sidekiq' do before do allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true) end - it_behaves_like 'starts configured watchdog', Gitlab::Memory::Watchdog::TermProcessHandler - end - - context 'when other runtime' do - it_behaves_like 'starts configured watchdog', Gitlab::Memory::Watchdog::NullHandler + it_behaves_like 'starts configured watchdog', :configure_for_sidekiq end end diff --git a/spec/initializers/rails_yaml_safe_load_spec.rb b/spec/initializers/rails_yaml_safe_load_spec.rb new file mode 100644 index 00000000000..8cf6a3676e0 --- /dev/null +++ b/spec/initializers/rails_yaml_safe_load_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Rails YAML safe load' do + let(:unsafe_load) { false } + + let(:klass) do + Class.new(ActiveRecord::Base) do + self.table_name = 'issues' + + serialize :description + end + end + + let(:instance) { klass.new(description: data) } + + context 'with default permitted classes' do + let(:data) do + { + 'time' => Time.now, + 'date' => Date.today, + 'number' => 1, + 'hashie-array' => Hashie::Array.new([1, 2]), + 'array' => [5, 6] + } + end + + it 'deserializes data' do + instance.save! + + expect(klass.find(instance.id).description).to eq(data) + end + + context 'with unpermitted classes' do + let(:data) { { 'test' => create(:user) } } + + it 'throws an exception' do + expect { instance.save! }.to raise_error(Psych::DisallowedClass) + end + end + end +end diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb index f9ebab149a5..c31d1c1fbf4 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb @@ -4,11 +4,47 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do describe '#satisfied_by?' do - shared_examples 'an exists rule with a context' do + subject(:satisfied_by?) { described_class.new(globs).satisfied_by?(nil, context) } + + shared_examples 'a rules:exists with a context' do it_behaves_like 'a glob matching rule' do let(:project) { create(:project, :custom_repo, files: files) } end + context 'when the rules:exists has a variable' do + let_it_be(:project) { create(:project, :custom_repo, files: { 'helm/helm_file.txt' => '' }) } + + let(:globs) { ['$HELM_DIR/**/*'] } + + let(:variables_hash) do + { 'HELM_DIR' => 'helm' } + end + + before do + allow(context).to receive(:variables_hash).and_return(variables_hash) + end + + context 'when the ci_variables_rules_exists FF is disabled' do + before do + stub_feature_flags(ci_variable_expansion_in_rules_exists: false) + end + + it { is_expected.to be_falsey } + end + + context 'when the ci_variables_rules_exists FF is enabled' do + context 'when the context has the specified variables' do + it { is_expected.to be_truthy } + end + + context 'when variable expansion does not match' do + let(:variables_hash) { {} } + + it { is_expected.to be_falsey } + end + end + end + context 'after pattern comparision limit is reached' do let(:globs) { ['*definitely_not_a_matching_glob*'] } let(:project) { create(:project, :repository) } @@ -22,26 +58,24 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do end end - subject(:satisfied_by?) { described_class.new(globs).satisfied_by?(nil, context) } - - context 'when context is Build::Context::Build' do - it_behaves_like 'an exists rule with a context' do + context 'when the rules are being evaluated at job level' do + it_behaves_like 'a rules:exists with a context' do let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.commit.sha) } let(:context) { Gitlab::Ci::Build::Context::Build.new(pipeline, sha: project.repository.commit.sha) } end end - context 'when context is Build::Context::Global' do - it_behaves_like 'an exists rule with a context' do + context 'when the rules are being evaluated for an entire pipeline' do + it_behaves_like 'a rules:exists with a context' do let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.commit.sha) } let(:context) { Gitlab::Ci::Build::Context::Global.new(pipeline, yaml_variables: {}) } end end - context 'when context is Config::External::Context' do + context 'when rules are being evaluated with `include`' do let(:context) { Gitlab::Ci::Config::External::Context.new(project: project, sha: sha) } - it_behaves_like 'an exists rule with a context' do + it_behaves_like 'a rules:exists with a context' do let(:sha) { project.repository.commit.sha } end diff --git a/spec/lib/gitlab/memory/watchdog/configuration_spec.rb b/spec/lib/gitlab/memory/watchdog/configuration_spec.rb index 892a4b06ad0..38a39f6a33a 100644 --- a/spec/lib/gitlab/memory/watchdog/configuration_spec.rb +++ b/spec/lib/gitlab/memory/watchdog/configuration_spec.rb @@ -78,36 +78,53 @@ RSpec.describe Gitlab::Memory::Watchdog::Configuration do end end - context 'when two monitors are configured to be used' do - before do - configuration.monitors.use monitor_class_1, false, { message: 'monitor_1_text' }, max_strikes: 5 - configuration.monitors.use monitor_class_2, true, { message: 'monitor_2_text' }, max_strikes: 0 + context 'when two different monitor class are configured' do + shared_examples 'executes monitors and returns correct results' do + it 'calls each monitor and returns correct results', :aggregate_failures do + payloads = [] + thresholds = [] + strikes = [] + monitor_names = [] + + configuration.monitors.call_each do |result| + payloads << result.payload + thresholds << result.threshold_violated? + strikes << result.strikes_exceeded? + monitor_names << result.monitor_name + end + + expect(payloads).to eq([payload1, payload2]) + expect(thresholds).to eq([false, true]) + expect(strikes).to eq([false, true]) + expect(monitor_names).to eq([:monitor1, :monitor2]) + end + end + + context 'when monitors are configured inline' do + before do + configuration.monitors.push monitor_class_1, false, { message: 'monitor_1_text' }, max_strikes: 5 + configuration.monitors.push monitor_class_2, true, { message: 'monitor_2_text' }, max_strikes: 0 + end + + include_examples 'executes monitors and returns correct results' end - it 'calls each monitor and returns correct results', :aggregate_failures do - payloads = [] - thresholds = [] - strikes = [] - monitor_names = [] - - configuration.monitors.call_each do |result| - payloads << result.payload - thresholds << result.threshold_violated? - strikes << result.strikes_exceeded? - monitor_names << result.monitor_name + context 'when monitors are configured in a block' do + before do + configuration.monitors do |stack| + stack.push monitor_class_1, false, { message: 'monitor_1_text' }, max_strikes: 5 + stack.push monitor_class_2, true, { message: 'monitor_2_text' }, max_strikes: 0 + end end - expect(payloads).to eq([payload1, payload2]) - expect(thresholds).to eq([false, true]) - expect(strikes).to eq([false, true]) - expect(monitor_names).to eq([:monitor1, :monitor2]) + include_examples 'executes monitors and returns correct results' end end - context 'when same monitor class is configured to be used twice' do + context 'when same monitor class is configured twice' do before do - configuration.monitors.use monitor_class_1, max_strikes: 1 - configuration.monitors.use monitor_class_1, max_strikes: 1 + configuration.monitors.push monitor_class_1, max_strikes: 1 + configuration.monitors.push monitor_class_1, max_strikes: 1 end it 'calls same monitor only once' do diff --git a/spec/lib/gitlab/memory/watchdog/configurator_spec.rb b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb new file mode 100644 index 00000000000..2c5fae5736d --- /dev/null +++ b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb @@ -0,0 +1,187 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'prometheus/client' +require_dependency 'gitlab/cluster/lifecycle_events' + +RSpec.describe Gitlab::Memory::Watchdog::Configurator do + shared_examples 'as configurator' do |handler_class, sleep_time_env, sleep_time| + it 'configures the correct handler' do + configurator.call(configuration) + + expect(configuration.handler).to be_an_instance_of(handler_class) + end + + it 'configures the correct logger' do + configurator.call(configuration) + + expect(configuration.logger).to eq(logger) + end + + context 'when sleep_time_seconds is not passed through the environment' do + let(:sleep_time_seconds) { sleep_time } + + it 'configures the correct sleep time' do + configurator.call(configuration) + + expect(configuration.sleep_time_seconds).to eq(sleep_time_seconds) + end + end + + context 'when sleep_time_seconds is passed through the environment' do + let(:sleep_time_seconds) { sleep_time - 1 } + + before do + stub_env(sleep_time_env, sleep_time - 1) + end + + it 'configures the correct sleep time' do + configurator.call(configuration) + + expect(configuration.sleep_time_seconds).to eq(sleep_time_seconds) + end + end + end + + shared_examples 'as monitor configurator' do + it 'executes monitors and returns correct results' do + configurator.call(configuration) + + payloads = {} + configuration.monitors.call_each do |result| + payloads[result.monitor_name] = result.payload + end + + expect(payloads).to eq(expected_payloads) + end + end + + let(:configuration) { Gitlab::Memory::Watchdog::Configuration.new } + + # In tests, the Puma constant does not exist so we cannot use a verified double. + # rubocop: disable RSpec/VerifiedDoubles + describe '.configure_for_puma' do + let(:logger) { Gitlab::AppLogger } + let(:puma) do + Class.new do + def self.cli_config + Struct.new(:options).new + end + end + end + + subject(:configurator) { described_class.configure_for_puma } + + def stub_prometheus_metrics + gauge = instance_double(::Prometheus::Client::Gauge) + allow(Gitlab::Metrics).to receive(:gauge).and_return(gauge) + allow(gauge).to receive(:set) + end + + before do + stub_const('Puma', puma) + stub_const('Puma::Cluster::WorkerHandle', double.as_null_object) + stub_prometheus_metrics + end + + it_behaves_like 'as configurator', + Gitlab::Memory::Watchdog::PumaHandler, + 'GITLAB_MEMWD_SLEEP_TIME_SEC', + 60 + + context 'with DISABLE_PUMA_WORKER_KILLER set to true' do + let(:primary_memory) { 2048 } + let(:worker_memory) { max_mem_growth * primary_memory + 1 } + let(:expected_payloads) do + { + heap_fragmentation: { + message: 'heap fragmentation limit exceeded', + memwd_cur_heap_frag: max_heap_fragmentation + 0.1, + memwd_max_heap_frag: max_heap_fragmentation, + memwd_max_strikes: max_strikes, + memwd_cur_strikes: 1 + + }, + unique_memory_growth: { + message: 'memory limit exceeded', + memwd_uss_bytes: worker_memory, + memwd_ref_uss_bytes: primary_memory, + memwd_max_uss_bytes: max_mem_growth * primary_memory, + memwd_max_strikes: max_strikes, + memwd_cur_strikes: 1 + } + } + end + + before do + stub_env('DISABLE_PUMA_WORKER_KILLER', true) + allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(max_heap_fragmentation + 0.1) + allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return({ uss: worker_memory }) + allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).with( + pid: Gitlab::Cluster::PRIMARY_PID + ).and_return({ uss: primary_memory }) + end + + context 'when settings are set via environment variables' do + let(:max_heap_fragmentation) { 0.4 } + let(:max_mem_growth) { 4.0 } + let(:max_strikes) { 4 } + + before do + stub_env('GITLAB_MEMWD_MAX_HEAP_FRAG', 0.4) + stub_env('GITLAB_MEMWD_MAX_MEM_GROWTH', 4.0) + stub_env('GITLAB_MEMWD_MAX_STRIKES', 4) + end + + it_behaves_like 'as monitor configurator' + end + + context 'when settings are not set via environment variables' do + let(:max_heap_fragmentation) { 0.5 } + let(:max_mem_growth) { 3.0 } + let(:max_strikes) { 5 } + + it_behaves_like 'as monitor configurator' + end + end + + context 'with DISABLE_PUMA_WORKER_KILLER set to false' do + let(:expected_payloads) do + { + rss_memory_limit: { + message: 'rss memory limit exceeded', + memwd_rss_bytes: memory_limit + 1, + memwd_max_rss_bytes: memory_limit, + memwd_max_strikes: max_strikes, + memwd_cur_strikes: 1 + } + } + end + + before do + stub_env('DISABLE_PUMA_WORKER_KILLER', false) + allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ total: memory_limit + 1 }) + end + + context 'when settings are set via environment variables' do + let(:memory_limit) { 1300 } + let(:max_strikes) { 4 } + + before do + stub_env('PUMA_WORKER_MAX_MEMORY', 1300) + stub_env('GITLAB_MEMWD_MAX_STRIKES', 4) + end + + it_behaves_like 'as monitor configurator' + end + + context 'when settings are not set via environment variables' do + let(:memory_limit) { 1200 } + let(:max_strikes) { 5 } + + it_behaves_like 'as monitor configurator' + end + end + end + # rubocop: enable RSpec/VerifiedDoubles +end 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 new file mode 100644 index 00000000000..9e25cfda782 --- /dev/null +++ b/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'support/shared_examples/lib/gitlab/memory/watchdog/monitor_result_shared_examples' + +RSpec.describe Gitlab::Memory::Watchdog::Monitor::RssMemoryLimit do + let(:memory_limit) { 2048 } + let(:worker_memory) { 1024 } + + subject(:monitor) do + described_class.new(memory_limit: memory_limit) + end + + before do + allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ total: worker_memory }) + end + + describe '#call' do + context 'when process exceeds threshold' do + let(:worker_memory) { memory_limit + 1 } + let(:payload) do + { + message: 'rss memory limit exceeded', + memwd_rss_bytes: worker_memory, + memwd_max_rss_bytes: memory_limit + } + end + + include_examples 'returns Watchdog Monitor result', threshold_violated: true + end + + context 'when process does not exceed threshold' do + let(:worker_memory) { memory_limit - 1 } + let(:payload) { {} } + + include_examples 'returns Watchdog Monitor result', threshold_violated: false + end + end +end diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb index 9c5f01a4bb2..5d9599d6eab 100644 --- a/spec/lib/gitlab/memory/watchdog_spec.rb +++ b/spec/lib/gitlab/memory/watchdog_spec.rb @@ -69,7 +69,7 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures do config.handler = handler config.logger = logger config.sleep_time_seconds = sleep_time_seconds - config.monitors.use monitor_class, threshold_violated, payload, max_strikes: max_strikes + config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes end allow(handler).to receive(:call).and_return(true) @@ -205,8 +205,8 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures do config.handler = handler config.logger = logger config.sleep_time_seconds = sleep_time_seconds - config.monitors.use monitor_class, threshold_violated, payload, max_strikes: max_strikes - config.monitors.use monitor_class, threshold_violated, payload, max_strikes: max_strikes + config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes + config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes end end diff --git a/spec/mailers/emails/releases_spec.rb b/spec/mailers/emails/releases_spec.rb index d1d7f5e6d6a..e8ca9533256 100644 --- a/spec/mailers/emails/releases_spec.rb +++ b/spec/mailers/emails/releases_spec.rb @@ -16,6 +16,7 @@ RSpec.describe Emails::Releases do subject { Notify.new_release_email(user.id, release) } it_behaves_like 'an email sent from GitLab' + it_behaves_like 'an email with X-GitLab headers containing project details' context 'when the release has a name' do it 'shows the correct subject' do |