summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-14 12:07:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-14 12:07:42 +0000
commitcacc3815006ab7d3828ebe8903f95154b27a6e21 (patch)
tree5adc693664d1ca383d19f8f165b37eea2318387f
parentce684df4733d86a49126792721f549612a778590 (diff)
downloadgitlab-ce-cacc3815006ab7d3828ebe8903f95154b27a6e21.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/CODEOWNERS165
-rw-r--r--.gitlab/issue_templates/Feature Flag Roll Out.md1
-rw-r--r--.rubocop_todo/rake/require.yml1
-rw-r--r--.rubocop_todo/rspec/be.yml1
-rw-r--r--.rubocop_todo/rspec/missing_feature_category.yml1
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue2
-rw-r--r--app/assets/javascripts/members/components/modals/remove_group_link_modal.vue1
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue5
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js2
-rw-r--r--app/helpers/emails_helper.rb4
-rw-r--r--app/helpers/namespaces_helper.rb68
-rw-r--r--app/mailers/emails/service_desk.rb4
-rw-r--r--app/models/ci/runner.rb5
-rw-r--r--app/models/ci/runner_machine.rb47
-rw-r--r--app/models/user.rb6
-rw-r--r--app/services/releases/base_service.rb28
-rw-r--r--app/services/releases/create_service.rb3
-rw-r--r--app/services/releases/update_service.rb11
-rw-r--r--app/views/groups/merge_requests.html.haml2
-rw-r--r--app/workers/merge_requests/delete_source_branch_worker.rb9
-rw-r--r--config/feature_flags/development/add_delete_branch_worker.yml8
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md5
-rw-r--r--doc/administration/sidekiq/sidekiq_memory_killer.md131
-rw-r--r--doc/api/member_roles.md5
-rw-r--r--doc/architecture/blueprints/ci_pipeline_components/index.md55
-rw-r--r--doc/development/code_review.md4
-rw-r--r--doc/subscriptions/quarterly_reconciliation.md18
-rw-r--r--doc/user/gitlab_com/index.md21
-rw-r--r--doc/user/permissions.md72
-rw-r--r--doc/user/project/service_desk.md10
-rw-r--r--lib/api/ci/helpers/runner.rb11
-rw-r--r--lib/api/ci/runner.rb3
-rw-r--r--lib/api/releases.rb19
-rw-r--r--lib/gitlab/memory/watchdog/configurator.rb2
-rw-r--r--lib/sidebars/projects/menus/packages_registries_menu.rb14
-rw-r--r--lib/tasks/gitlab/backup.rake4
-rw-r--r--locale/gitlab.pot9
-rw-r--r--package.json2
-rw-r--r--qa/qa/page/component/invite_members_modal.rb90
-rw-r--r--qa/qa/page/component/members/invite_members_modal.rb92
-rw-r--r--qa/qa/page/component/members/members_filter.rb27
-rw-r--r--qa/qa/page/component/members/members_table.rb99
-rw-r--r--qa/qa/page/component/members/remove_group_modal.rb38
-rw-r--r--qa/qa/page/component/members/remove_member_modal.rb38
-rw-r--r--qa/qa/page/component/members_filter.rb25
-rw-r--r--qa/qa/page/group/members.rb92
-rw-r--r--qa/qa/page/project/members.rb45
-rw-r--r--spec/controllers/groups_controller_spec.rb1
-rw-r--r--spec/features/projects/navbar_spec.rb14
-rw-r--r--spec/helpers/namespaces_helper_spec.rb112
-rw-r--r--spec/lib/api/ci/helpers/runner_spec.rb32
-rw-r--r--spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb22
-rw-r--r--spec/mailers/emails/service_desk_spec.rb87
-rw-r--r--spec/models/ci/runner_machine_spec.rb142
-rw-r--r--spec/models/user_spec.rb15
-rw-r--r--spec/requests/api/ci/runner/jobs_put_spec.rb5
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb57
-rw-r--r--spec/requests/api/ci/runner/runners_verify_post_spec.rb63
-rw-r--r--spec/requests/api/releases_spec.rb69
-rw-r--r--spec/services/pages/destroy_deployments_service_spec.rb22
-rw-r--r--spec/services/releases/create_service_spec.rb71
-rw-r--r--spec/services/releases/update_service_spec.rb77
-rw-r--r--spec/support/helpers/navbar_structure_helper.rb8
-rw-r--r--spec/support/shared_contexts/mailers/emails/service_desk_shared_context.rb40
-rw-r--r--spec/workers/merge_requests/delete_source_branch_worker_spec.rb109
-rw-r--r--yarn.lock8
69 files changed, 1320 insertions, 952 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index c7bb8144d77..588a46df00c 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -410,7 +410,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/administration/audit_reports.md @eread
/doc/administration/auditor_users.md @jglassman1
/doc/administration/auth/ @jglassman1
-/doc/administration/auth/ldap/ @jglassman1
/doc/administration/cicd.md @drcatherinepope
/doc/administration/clusters/ @phillipwells
/doc/administration/compliance.md @eread
@@ -423,11 +422,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/administration/feature_flags.md @axil
/doc/administration/file_hooks.md @ashrafkhamis
/doc/administration/geo/ @axil
-/doc/administration/geo/disaster_recovery/ @axil
-/doc/administration/geo/disaster_recovery/runbooks/ @axil
-/doc/administration/geo/replication/ @axil
-/doc/administration/geo/secondary_proxy/ @axil
-/doc/administration/geo/setup/ @axil
/doc/administration/git_protocol.md @aqualls
/doc/administration/gitaly/ @eread
/doc/administration/housekeeping.md @eread
@@ -452,11 +446,8 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/administration/maintenance_mode/ @axil
/doc/administration/merge_request_diffs.md @aqualls
/doc/administration/monitoring/ @msedlakjakubowski
-/doc/administration/monitoring/gitlab_self_monitoring_project/ @msedlakjakubowski
/doc/administration/monitoring/ip_allowlist.md @jglassman1
-/doc/administration/monitoring/performance/ @msedlakjakubowski
/doc/administration/monitoring/performance/performance_bar.md @jglassman1
-/doc/administration/monitoring/prometheus/ @msedlakjakubowski
/doc/administration/monitoring/prometheus/gitlab_exporter.md @jglassman1
/doc/administration/monitoring/prometheus/index.md @axil
/doc/administration/monitoring/prometheus/web_exporter.md @jglassman1
@@ -464,9 +455,10 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/administration/object_storage.md @axil
/doc/administration/operations/ @axil
/doc/administration/operations/fast_ssh_key_lookup.md @aqualls
+/doc/administration/operations/gitlab_sshd.md @aqualls
/doc/administration/operations/moving_repositories.md @eread
/doc/administration/package_information/ @axil
-/doc/administration/packages/ @claytoncornell
+/doc/administration/packages/ @dianalogan
/doc/administration/pages/ @ashrafkhamis
/doc/administration/polling.md @axil
/doc/administration/postgresql/ @aqualls
@@ -474,7 +466,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/administration/raketasks/ @axil
/doc/administration/raketasks/ldap.md @jglassman1
/doc/administration/raketasks/praefect.md @eread
-/doc/administration/raketasks/uploads/ @axil
/doc/administration/read_only_gitlab.md @axil
/doc/administration/redis/ @axil
/doc/administration/reference_architectures/ @axil
@@ -513,15 +504,16 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/bulk_imports.md @eread
/doc/api/cluster_agents.md @phillipwells
/doc/api/commits.md @aqualls
-/doc/api/container_registry.md @claytoncornell
+/doc/api/container_registry.md @dianalogan
/doc/api/custom_attributes.md @msedlakjakubowski
/doc/api/dependencies.md @rdickenson
-/doc/api/dependency_proxy.md @claytoncornell
+/doc/api/dependency_proxy.md @dianalogan
/doc/api/deploy_keys.md @rdickenson
/doc/api/deploy_tokens.md @rdickenson
/doc/api/deployments.md @rdickenson
/doc/api/discussions.md @aqualls
/doc/api/dora/ @lciutacu
+/doc/api/draft_notes.md @aqualls
/doc/api/environments.md @rdickenson
/doc/api/epic_issues.md @msedlakjakubowski
/doc/api/epic_links.md @msedlakjakubowski
@@ -548,6 +540,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/group_badges.md @lciutacu
/doc/api/group_boards.md @msedlakjakubowski
/doc/api/group_clusters.md @phillipwells
+/doc/api/group_epic_boards.md @msedlakjakubowski
/doc/api/group_import_export.md @eread
/doc/api/group_iterations.md @msedlakjakubowski
/doc/api/group_labels.md @msedlakjakubowski
@@ -578,6 +571,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/lint.md @marcel.amirault
/doc/api/managed_licenses.md @fneill
/doc/api/markdown.md @msedlakjakubowski
+/doc/api/member_roles.md @jglassman1
/doc/api/members.md @jglassman1
/doc/api/merge_request_approvals.md @aqualls
/doc/api/merge_request_context_commits.md @aqualls
@@ -592,8 +586,8 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/notification_settings.md @msedlakjakubowski
/doc/api/oauth2.md @jglassman1
/doc/api/openapi/ @ashrafkhamis
-/doc/api/packages.md @claytoncornell
-/doc/api/packages/ @claytoncornell
+/doc/api/packages.md @dianalogan
+/doc/api/packages/ @dianalogan
/doc/api/pages.md @ashrafkhamis
/doc/api/pages_domains.md @ashrafkhamis
/doc/api/personal_access_tokens.md @eread
@@ -650,45 +644,34 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/api/templates/licenses.md @rdickenson
/doc/api/todos.md @msedlakjakubowski
/doc/api/topics.md @lciutacu
-/doc/api/usage_data.md @claytoncornell
+/doc/api/usage_data.md @dianalogan
/doc/api/users.md @jglassman1
/doc/api/version.md @phillipwells
/doc/api/visual_review_discussions.md @marcel.amirault
-/doc/api/vulnerabilities.md @claytoncornell
-/doc/api/vulnerability_exports.md @claytoncornell
-/doc/api/vulnerability_findings.md @claytoncornell
+/doc/api/vulnerabilities.md @dianalogan
+/doc/api/vulnerability_exports.md @dianalogan
+/doc/api/vulnerability_findings.md @dianalogan
/doc/api/wikis.md @ashrafkhamis
/doc/architecture/blueprints/database/scalability/patterns/ @aqualls
/doc/architecture/blueprints/database_scaling/ @aqualls
/doc/ci/ @drcatherinepope
/doc/ci/caching/ @marcel.amirault
/doc/ci/chatops/ @phillipwells
-/doc/ci/ci_cd_for_external_repos/ @drcatherinepope
/doc/ci/cloud_deployment/ @rdickenson
-/doc/ci/cloud_deployment/ecs/ @rdickenson
/doc/ci/cloud_services/ @marcel.amirault
-/doc/ci/cloud_services/aws/ @marcel.amirault
-/doc/ci/cloud_services/azure/ @marcel.amirault
-/doc/ci/cloud_services/google_cloud/ @marcel.amirault
/doc/ci/directed_acyclic_graph/ @marcel.amirault
-/doc/ci/docker/ @drcatherinepope
/doc/ci/docker/using_docker_images.md @fneill
/doc/ci/environments/ @rdickenson
-/doc/ci/examples/ @drcatherinepope
/doc/ci/examples/authenticating-with-hashicorp-vault/ @marcel.amirault
/doc/ci/examples/deployment/ @rdickenson
/doc/ci/examples/end_to_end_testing_webdriverio/ @marcel.amirault
-/doc/ci/examples/laravel_with_gitlab_and_envoy/ @drcatherinepope
-/doc/ci/examples/semantic-release.md @claytoncornell
+/doc/ci/examples/semantic-release.md @dianalogan
/doc/ci/interactive_web_terminal/ @fneill
-/doc/ci/introduction/ @drcatherinepope
-/doc/ci/jobs/ @drcatherinepope
/doc/ci/jobs/job_control.md @marcel.amirault
/doc/ci/large_repositories/ @fneill
/doc/ci/lint.md @marcel.amirault
/doc/ci/migration/ @marcel.amirault
/doc/ci/pipeline_editor/ @marcel.amirault
-/doc/ci/pipelines/ @drcatherinepope
/doc/ci/pipelines/downstream_pipelines.md @marcel.amirault
/doc/ci/pipelines/index.md @marcel.amirault
/doc/ci/pipelines/job_artifacts.md @marcel.amirault
@@ -697,16 +680,12 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/ci/resource_groups/ @rdickenson
/doc/ci/review_apps/ @marcel.amirault
/doc/ci/runners/ @fneill
-/doc/ci/runners/saas/ @fneill
-/doc/ci/runners/saas/macos/ @fneill
/doc/ci/secrets/ @marcel.amirault
/doc/ci/secure_files/ @marcel.amirault
/doc/ci/services/ @fneill
-/doc/ci/ssh_keys/ @drcatherinepope
/doc/ci/test_cases/ @msedlakjakubowski
/doc/ci/testing/ @marcel.amirault
/doc/ci/testing/code_quality.md @rdickenson
-/doc/ci/triggers/ @drcatherinepope
/doc/ci/variables/ @marcel.amirault
/doc/ci/yaml/ @marcel.amirault
/doc/development/application_limits.md @axil
@@ -731,8 +710,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/development/development_processes.md @sselhorn
/doc/development/distributed_tracing.md @msedlakjakubowski
/doc/development/documentation/ @sselhorn
-/doc/development/documentation/styleguide/ @sselhorn
-/doc/development/documentation/topic_types/ @sselhorn
/doc/development/elasticsearch.md @ashrafkhamis
/doc/development/experiment_guide/ @phillipwells
/doc/development/export_csv.md @eread
@@ -752,7 +729,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/development/git_object_deduplication.md @eread
/doc/development/gitaly.md @eread
/doc/development/gitlab_flavored_markdown/ @ashrafkhamis
-/doc/development/gitlab_flavored_markdown/specification_guide/ @ashrafkhamis
/doc/development/gitlab_shell/ @aqualls
/doc/development/graphql_guide/ @ashrafkhamis
/doc/development/graphql_guide/batchloader.md @aqualls
@@ -775,27 +751,24 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/development/logging.md @msedlakjakubowski
/doc/development/maintenance_mode.md @axil
/doc/development/merge_request_concepts/ @aqualls
-/doc/development/merge_request_concepts/diffs/ @aqualls
/doc/development/omnibus.md @axil
-/doc/development/packages/ @claytoncornell
+/doc/development/packages/ @dianalogan
/doc/development/pages/ @ashrafkhamis
/doc/development/permissions.md @jglassman1
/doc/development/policies.md @jglassman1
/doc/development/product_qualified_lead_guide/ @phillipwells
-/doc/development/project_templates.md @lciutacu
+/doc/development/project_templates.md @aqualls
/doc/development/prometheus_metrics.md @msedlakjakubowski
/doc/development/real_time.md @msedlakjakubowski
/doc/development/rubocop_development_guide.md @sselhorn
/doc/development/sec/ @rdickenson
-/doc/development/sec/security_report_ingestion_overview.md @claytoncornell
+/doc/development/sec/security_report_ingestion_overview.md @dianalogan
/doc/development/secure_coding_guidelines.md @sselhorn
-/doc/development/service_ping/ @claytoncornell
-/doc/development/snowplow/ @claytoncornell
+/doc/development/service_ping/ @dianalogan
+/doc/development/snowplow/ @dianalogan
/doc/development/spam_protection_and_captcha/ @phillipwells
/doc/development/sql.md @aqualls
/doc/development/testing_guide/ @sselhorn
-/doc/development/testing_guide/contract/ @sselhorn
-/doc/development/testing_guide/end_to_end/ @sselhorn
/doc/development/value_stream_analytics.md @lciutacu
/doc/development/value_stream_analytics/ @lciutacu
/doc/development/wikis.md @ashrafkhamis
@@ -807,11 +780,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/drawers/ @ashrafkhamis
/doc/gitlab-basics/ @aqualls
/doc/install/ @axil
-/doc/install/aws/ @axil
-/doc/install/azure/ @axil
-/doc/install/google_cloud_platform/ @axil
-/doc/install/migrate/ @axil
-/doc/install/openshift_and_gitlab/ @axil
+/doc/install/postgresql_extensions.md @aqualls
/doc/integration/ @jglassman1
/doc/integration/advanced_search/ @ashrafkhamis
/doc/integration/akismet.md @phillipwells
@@ -824,7 +793,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/integration/index.md @ashrafkhamis
/doc/integration/jenkins.md @ashrafkhamis
/doc/integration/jira/ @ashrafkhamis
-/doc/integration/jira/dvcs/ @ashrafkhamis
/doc/integration/mattermost/ @axil
/doc/integration/recaptcha.md @phillipwells
/doc/integration/security_partners/ @rdickenson
@@ -837,7 +805,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/operations/incident_management/ @msedlakjakubowski
/doc/operations/index.md @msedlakjakubowski
/doc/operations/metrics/ @msedlakjakubowski
-/doc/operations/metrics/dashboards/ @msedlakjakubowski
/doc/policy/ @axil
/doc/raketasks/ @axil
/doc/raketasks/generate_sample_prometheus_data.md @msedlakjakubowski
@@ -846,24 +813,18 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/raketasks/x509_signatures.md @aqualls
/doc/security/ @jglassman1
/doc/subscriptions/ @fneill
-/doc/subscriptions/gitlab_com/ @fneill
/doc/subscriptions/gitlab_dedicated/ @axil
-/doc/subscriptions/self_managed/ @fneill
/doc/topics/authentication/ @jglassman1
/doc/topics/autodevops/ @phillipwells
-/doc/topics/autodevops/cloud_deployments/ @phillipwells
/doc/topics/awesome_co.md @rdickenson
/doc/topics/git/ @aqualls
-/doc/topics/git/how_to_install_git/ @aqualls
-/doc/topics/git/lfs/ @aqualls
-/doc/topics/git/numerous_undo_possibilities_in_git/ @aqualls
/doc/topics/gitlab_flow.md @aqualls
/doc/topics/offline/ @axil
/doc/topics/plan_and_track.md @msedlakjakubowski
+/doc/topics/your_work.md @rdickenson
/doc/tutorials/ @kpaizee
/doc/update/ @axil
/doc/update/background_migrations.md @aqualls
-/doc/update/package/ @axil
/doc/user/admin_area/analytics/ @lciutacu
/doc/user/admin_area/broadcast_messages.md @phillipwells
/doc/user/admin_area/credentials_inventory.md @jglassman1
@@ -892,7 +853,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/user/admin_area/settings/incident_management_rate_limits.md @msedlakjakubowski
/doc/user/admin_area/settings/index.md @aqualls
/doc/user/admin_area/settings/instance_template_repository.md @aqualls
-/doc/user/admin_area/settings/package_registry_rate_limits.md @claytoncornell
+/doc/user/admin_area/settings/package_registry_rate_limits.md @dianalogan
/doc/user/admin_area/settings/project_integration_management.md @ashrafkhamis
/doc/user/admin_area/settings/push_event_activities_limit.md @aqualls
/doc/user/admin_area/settings/rate_limit_on_issues_creation.md @msedlakjakubowski
@@ -902,54 +863,33 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/user/admin_area/settings/scim_setup.md @jglassman1
/doc/user/admin_area/settings/terraform_limits.md @phillipwells
/doc/user/admin_area/settings/third_party_offers.md @lciutacu
-/doc/user/admin_area/settings/usage_statistics.md @claytoncornell
+/doc/user/admin_area/settings/usage_statistics.md @dianalogan
/doc/user/admin_area/settings/visibility_and_access_controls.md @aqualls
/doc/user/analytics/ @lciutacu
/doc/user/analytics/ci_cd_analytics.md @rdickenson
/doc/user/application_security/ @rdickenson
-/doc/user/application_security/api_fuzzing/ @rdickenson
-/doc/user/application_security/configuration/ @rdickenson
-/doc/user/application_security/container_scanning/ @rdickenson
-/doc/user/application_security/coverage_fuzzing/ @rdickenson
-/doc/user/application_security/cve_id_request.md @claytoncornell
-/doc/user/application_security/dast/ @rdickenson
-/doc/user/application_security/dast/checks/ @rdickenson
-/doc/user/application_security/dast_api/ @rdickenson
-/doc/user/application_security/dependency_list/ @rdickenson
-/doc/user/application_security/dependency_scanning/ @rdickenson
-/doc/user/application_security/generate_test_vulnerabilities/ @claytoncornell
-/doc/user/application_security/iac_scanning/ @rdickenson
-/doc/user/application_security/offline_deployments/ @rdickenson
-/doc/user/application_security/policies/ @claytoncornell
-/doc/user/application_security/sast/ @rdickenson
-/doc/user/application_security/secret_detection/ @rdickenson
-/doc/user/application_security/security_dashboard/ @claytoncornell
-/doc/user/application_security/terminology/ @rdickenson
-/doc/user/application_security/vulnerabilities/ @claytoncornell
-/doc/user/application_security/vulnerability_report/ @claytoncornell
+/doc/user/application_security/cve_id_request.md @dianalogan
+/doc/user/application_security/generate_test_vulnerabilities/ @dianalogan
+/doc/user/application_security/policies/ @dianalogan
+/doc/user/application_security/security_dashboard/ @dianalogan
+/doc/user/application_security/vulnerabilities/ @dianalogan
+/doc/user/application_security/vulnerability_report/ @dianalogan
/doc/user/asciidoc.md @aqualls
/doc/user/award_emojis.md @msedlakjakubowski
/doc/user/clusters/ @phillipwells
-/doc/user/clusters/agent/ @phillipwells
-/doc/user/clusters/agent/gitops/ @phillipwells
-/doc/user/clusters/agent/install/ @phillipwells
-/doc/user/clusters/create/ @phillipwells
/doc/user/compliance/ @eread
-/doc/user/compliance/compliance_report/ @eread
/doc/user/compliance/license_compliance/ @rdickenson
/doc/user/crm/ @msedlakjakubowski
/doc/user/discussions/ @aqualls
+/doc/user/enterprise_user/ @jglassman1
/doc/user/feature_flags.md @sselhorn
/doc/user/free_user_limit.md @phillipwells
/doc/user/group/ @lciutacu
/doc/user/group/clusters/ @phillipwells
/doc/user/group/compliance_frameworks.md @eread
-/doc/user/group/contribution_analytics/ @lciutacu
/doc/user/group/custom_project_templates.md @eread
-/doc/user/group/devops_adoption/ @lciutacu
/doc/user/group/epics/ @msedlakjakubowski
/doc/user/group/import/ @eread
-/doc/user/group/insights/ @lciutacu
/doc/user/group/issues_analytics/ @msedlakjakubowski
/doc/user/group/iterations/ @msedlakjakubowski
/doc/user/group/planning_hierarchy/ @msedlakjakubowski
@@ -958,51 +898,22 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/user/group/roadmap/ @msedlakjakubowski
/doc/user/group/saml_sso/ @jglassman1
/doc/user/group/settings/ @jglassman1
-/doc/user/group/subgroups/ @lciutacu
-/doc/user/group/value_stream_analytics/ @lciutacu
/doc/user/infrastructure/ @phillipwells
-/doc/user/infrastructure/clusters/ @phillipwells
-/doc/user/infrastructure/clusters/connect/ @phillipwells
-/doc/user/infrastructure/clusters/deploy/ @phillipwells
-/doc/user/infrastructure/clusters/manage/ @phillipwells
-/doc/user/infrastructure/clusters/manage/management_project_applications/ @phillipwells
/doc/user/infrastructure/clusters/manage/management_project_applications/runner.md @fneill
-/doc/user/infrastructure/iac/ @phillipwells
/doc/user/markdown.md @msedlakjakubowski
/doc/user/namespace/ @lciutacu
/doc/user/okrs.md @msedlakjakubowski
/doc/user/operations_dashboard/ @rdickenson
-/doc/user/packages/ @claytoncornell
-/doc/user/packages/composer_repository/ @claytoncornell
-/doc/user/packages/conan_repository/ @claytoncornell
-/doc/user/packages/container_registry/ @claytoncornell
-/doc/user/packages/debian_repository/ @claytoncornell
-/doc/user/packages/dependency_proxy/ @claytoncornell
-/doc/user/packages/generic_packages/ @claytoncornell
-/doc/user/packages/go_proxy/ @claytoncornell
-/doc/user/packages/gradle_repository/ @claytoncornell
-/doc/user/packages/harbor_container_registry/ @claytoncornell
-/doc/user/packages/helm_repository/ @claytoncornell
-/doc/user/packages/infrastructure_registry/ @claytoncornell
-/doc/user/packages/maven_repository/ @claytoncornell
-/doc/user/packages/npm_registry/ @claytoncornell
-/doc/user/packages/nuget_repository/ @claytoncornell
-/doc/user/packages/package_registry/ @claytoncornell
-/doc/user/packages/pypi_repository/ @claytoncornell
-/doc/user/packages/rubygems_registry/ @claytoncornell
-/doc/user/packages/terraform_module_registry/ @claytoncornell
-/doc/user/packages/workflows/ @claytoncornell
-/doc/user/packages/yarn_repository/ @claytoncornell
+/doc/user/packages/ @dianalogan
/doc/user/permissions.md @jglassman1
/doc/user/product_analytics/ @lciutacu
/doc/user/profile/ @jglassman1
-/doc/user/profile/account/ @jglassman1
/doc/user/profile/contributions_calendar.md @lciutacu
/doc/user/profile/notifications.md @msedlakjakubowski
/doc/user/project/autocomplete_characters.md @aqualls
/doc/user/project/badges.md @lciutacu
+/doc/user/project/changelogs.md @aqualls
/doc/user/project/clusters/ @phillipwells
-/doc/user/project/clusters/runbooks/ @phillipwells
/doc/user/project/code_intelligence.md @aqualls
/doc/user/project/code_owners.md @aqualls
/doc/user/project/deploy_boards.md @rdickenson
@@ -1025,16 +936,11 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/user/project/labels.md @msedlakjakubowski
/doc/user/project/members/ @lciutacu
/doc/user/project/merge_requests/ @aqualls
-/doc/user/project/merge_requests/approvals/ @aqualls
/doc/user/project/merge_requests/csv_export.md @eread
-/doc/user/project/merge_requests/methods/ @aqualls
-/doc/user/project/merge_requests/reviews/ @aqualls
/doc/user/project/merge_requests/status_checks.md @eread
/doc/user/project/milestones/ @msedlakjakubowski
/doc/user/project/organize_work_with_projects.md @lciutacu
/doc/user/project/pages/ @ashrafkhamis
-/doc/user/project/pages/custom_domains_ssl_tls_certification/ @ashrafkhamis
-/doc/user/project/pages/getting_started/ @ashrafkhamis
/doc/user/project/protected_branches.md @aqualls
/doc/user/project/protected_tags.md @aqualls
/doc/user/project/push_options.md @aqualls
@@ -1042,16 +948,11 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/doc/user/project/releases/ @rdickenson
/doc/user/project/remote_development/ @ashrafkhamis
/doc/user/project/repository/ @aqualls
-/doc/user/project/repository/branches/ @aqualls
/doc/user/project/repository/file_finder.md @ashrafkhamis
-/doc/user/project/repository/gpg_signed_commits/ @aqualls
-/doc/user/project/repository/jupyter_notebooks/ @aqualls
/doc/user/project/repository/managing_large_repositories.md @axil
-/doc/user/project/repository/mirror/ @aqualls
/doc/user/project/repository/reducing_the_repo_size_using_git.md @eread
-/doc/user/project/repository/ssh_signed_commits/ @aqualls
+/doc/user/project/repository/vscode.md @ashrafkhamis
/doc/user/project/repository/web_editor.md @ashrafkhamis
-/doc/user/project/repository/x509_signed_commits/ @aqualls
/doc/user/project/requirements/ @msedlakjakubowski
/doc/user/project/service_desk.md @msedlakjakubowski
/doc/user/project/settings/import_export.md @eread
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md
index 8aa631dce76..5791eca11ff 100644
--- a/.gitlab/issue_templates/Feature Flag Roll Out.md
+++ b/.gitlab/issue_templates/Feature Flag Roll Out.md
@@ -107,6 +107,7 @@ For visibility, all `/chatops` commands that target production should be execute
- [ ] Leave a comment on [the feature issue][main-issue] announcing estimated time when this feature flag will be enabled on GitLab.com.
- [ ] Ensure that any breaking changes have been announced following the [release post process](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations-removals-and-breaking-changes) to ensure GitLab customers are aware.
- [ ] Notify `#support_gitlab-com` and your team channel ([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#communicate-the-change)).
+- [ ] Ensure that the feature flag rollout plan is reviewed by another developer familiar with the domain.
### Global rollout on production
diff --git a/.rubocop_todo/rake/require.yml b/.rubocop_todo/rake/require.yml
index 65b4fd311ad..1f7a488ba35 100644
--- a/.rubocop_todo/rake/require.yml
+++ b/.rubocop_todo/rake/require.yml
@@ -3,7 +3,6 @@ Rake/Require:
Details: grace period
Exclude:
- 'lib/tasks/gitlab/assets.rake'
- - 'lib/tasks/gitlab/backup.rake'
- 'lib/tasks/gitlab/cleanup.rake'
- 'lib/tasks/gitlab/dependency_proxy/migrate.rake'
- 'lib/tasks/gitlab/docs/redirect.rake'
diff --git a/.rubocop_todo/rspec/be.yml b/.rubocop_todo/rspec/be.yml
index dad8a7d730b..17f857d0677 100644
--- a/.rubocop_todo/rspec/be.yml
+++ b/.rubocop_todo/rspec/be.yml
@@ -14,7 +14,6 @@ RSpec/Be:
- 'spec/requests/api/graphql/mutations/snippets/create_spec.rb'
- 'spec/requests/api/pages_domains_spec.rb'
- 'spec/services/pages/delete_service_spec.rb'
- - 'spec/services/pages/destroy_deployments_service_spec.rb'
- 'spec/services/pages/migrate_from_legacy_storage_service_spec.rb'
- 'spec/services/projects/update_pages_service_spec.rb'
- 'spec/support/shared_examples/requests/api/packages_shared_examples.rb'
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml
index 7d604d682ca..ed137e4755a 100644
--- a/.rubocop_todo/rspec/missing_feature_category.yml
+++ b/.rubocop_todo/rspec/missing_feature_category.yml
@@ -7767,7 +7767,6 @@ RSpec/MissingFeatureCategory:
- 'spec/services/packages/update_package_file_service_spec.rb'
- 'spec/services/packages/update_tags_service_spec.rb'
- 'spec/services/pages/delete_service_spec.rb'
- - 'spec/services/pages/destroy_deployments_service_spec.rb'
- 'spec/services/pages/migrate_from_legacy_storage_service_spec.rb'
- 'spec/services/pages/migrate_legacy_storage_to_deployment_service_spec.rb'
- 'spec/services/pages/zip_directory_service_spec.rb'
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
index 24500fbe44d..3b4b7516934 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
@@ -35,7 +35,7 @@ export default {
:title="$options.i18n.buttonTitle"
:aria-label="$options.i18n.buttonTitle"
icon="remove"
- data-qa-selector="delete_group_access_link"
+ data-qa-selector="remove_group_link_button"
@click="showRemoveGroupLinkModal(groupLink)"
/>
</template>
diff --git a/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
index b179ced46e1..b28ca6e385b 100644
--- a/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
@@ -14,6 +14,7 @@ export default {
text: s__('Members|Remove group'),
attributes: {
variant: 'danger',
+ 'data-qa-selector': 'remove_group_button',
},
},
csrf,
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 3f9fb3dec2f..c973d58fcd2 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -135,7 +135,10 @@ export default {
tbodyTrAttr(member) {
return {
...this.tableAttrs.tr,
- ...(member?.id && { 'data-testid': `members-table-row-${member.id}` }),
+ ...(member?.id && {
+ 'data-testid': `members-table-row-${member.id}`,
+ 'data-qa-selector': 'member_row',
+ }),
};
},
paginationLinkGenerator(page) {
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index e001b8286b1..1b3c7ba5a52 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -12,7 +12,6 @@ const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
const APP_OPTIONS = {
[MEMBER_TYPES.user]: {
tableFields: SHARED_FIELDS.concat(['source', 'activity']),
- tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: [
'account',
'granted',
@@ -32,10 +31,6 @@ const APP_OPTIONS = {
},
[MEMBER_TYPES.group]: {
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
- tableAttrs: {
- table: { 'data-qa-selector': 'groups_list' },
- tr: { 'data-qa-selector': 'group_row' },
- },
requestFormatter: groupLinkRequestFormatter,
filteredSearchBar: {
show: true,
@@ -58,7 +53,6 @@ const APP_OPTIONS = {
},
[MEMBER_TYPES.accessRequest]: {
tableFields: SHARED_FIELDS.concat('requested'),
- tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
requestFormatter: groupMemberRequestFormatter,
},
...EE_APP_OPTIONS,
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 37cf345fe77..1075241e172 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,7 +1,5 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import initTerraformNotification from '~/projects/terraform_notification';
import Project from './project';
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
-initTerraformNotification();
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 2fd372a45b8..79a4ed0f9c3 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -21,7 +21,6 @@ const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-project-members-list-app'), {
[MEMBER_TYPES.user]: {
tableFields: SHARED_FIELDS.concat(['source', 'activity']),
- tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: [
'account',
'granted',
@@ -41,10 +40,6 @@ initMembersApp(document.querySelector('.js-project-members-list-app'), {
},
[MEMBER_TYPES.group]: {
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
- tableAttrs: {
- table: { 'data-qa-selector': 'groups_list' },
- tr: { 'data-qa-selector': 'group_row' },
- },
requestFormatter: groupLinkRequestFormatter,
filteredSearchBar: {
show: true,
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 1de36f4a0fb..33d4090011f 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -6,6 +6,7 @@ import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert'
import leaveByUrl from '~/namespaces/leave_by_url';
import initVueNotificationsDropdown from '~/notifications';
import Star from '~/projects/star';
+import initTerraformNotification from '~/projects/terraform_notification';
import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
@@ -44,6 +45,7 @@ initUploadFileTrigger();
initInviteMembersModal();
initInviteMembersTrigger();
initClustersDeprecationAlert();
+initTerraformNotification();
initReadMore();
new Star(); // eslint-disable-line no-new
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 212d72c479a..9f4ed6b8150 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -178,6 +178,10 @@ module EmailsHelper
strip_tags(render_message(:footer_message, style: ''))
end
+ def service_desk_email_additional_text
+ # overridden on EE
+ end
+
def say_hi(user)
_('Hi %{username}!') % { username: sanitize_name(user.name) }
end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 46777ad16d0..01030690daf 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -5,42 +5,6 @@ module NamespacesHelper
params.dig(:project, :namespace_id) || params[:namespace_id]
end
- def namespaces_options(selected = :current_user, display_path: false, groups: nil, extra_group: nil, groups_only: false)
- groups ||= current_user.manageable_groups_with_routes
- users = [current_user.namespace]
- selected_id = selected
-
- unless extra_group.nil? || extra_group.is_a?(Group)
- extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group'
- end
-
- if extra_group && extra_group.is_a?(Group)
- extra_group = dedup_extra_group(extra_group)
-
- if Ability.allowed?(current_user, :read_group, extra_group)
- # Assign the value to an invalid primary ID so that the select box works
- extra_group.id = -1 unless extra_group.persisted?
- selected_id = extra_group.id if selected == :extra_group
- groups |= [extra_group]
- else
- selected_id = current_user.namespace.id
- end
- end
-
- options = []
- options << options_for_group(groups, display_path: display_path, type: 'group')
-
- unless groups_only
- options << options_for_group(users, display_path: display_path, type: 'user')
-
- if selected == :current_user && current_user.namespace
- selected_id = current_user.namespace.id
- end
- end
-
- grouped_options_for_select(options, selected_id)
- end
-
def namespace_icon(namespace, size = 40)
if namespace.is_a?(Group)
group_icon_url(namespace)
@@ -99,38 +63,6 @@ module NamespacesHelper
default_per_page: page_size
}
end
-
- private
-
- # Many importers create a temporary Group, so use the real
- # group if one exists by that name to prevent duplicates.
- # rubocop: disable CodeReuse/ActiveRecord
- def dedup_extra_group(extra_group)
- unless extra_group.persisted?
- existing_group = Group.find_by(path: extra_group.path)
- extra_group = existing_group if existing_group&.persisted?
- end
-
- extra_group
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def options_for_group(namespaces, display_path:, type:)
- group_label = type.pluralize
- elements = namespaces.sort_by(&:human_name).map! do |n|
- [display_path ? n.full_path : n.human_name, n.id,
- data: {
- options_parent: group_label,
- visibility_level: n.visibility_level_value,
- visibility: n.visibility,
- name: n.name,
- show_path: type == 'group' ? group_path(n) : user_path(n),
- edit_path: type == 'group' ? edit_group_path(n) : nil
- }]
- end
-
- [group_label.camelize, elements]
- end
end
NamespacesHelper.prepend_mod_with('NamespacesHelper')
diff --git a/app/mailers/emails/service_desk.rb b/app/mailers/emails/service_desk.rb
index 835abacbed5..1295f978049 100644
--- a/app/mailers/emails/service_desk.rb
+++ b/app/mailers/emails/service_desk.rb
@@ -101,6 +101,10 @@ module Emails
.gsub(/%\{\s*ISSUE_ID\s*\}/, issue_id)
.gsub(/%\{\s*ISSUE_PATH\s*\}/, issue_path)
.gsub(/%\{\s*NOTE_TEXT\s*\}/, note_text)
+ .gsub(/%\{\s*SYSTEM_HEADER\s*\}/, text_header_message.to_s)
+ .gsub(/%\{\s*SYSTEM_FOOTER\s*\}/, text_footer_message.to_s)
+ .gsub(/%\{\s*UNSUBSCRIBE_URL\s*\}/, unsubscribe_sent_notification_url(@sent_notification))
+ .gsub(/%\{\s*ADDITIONAL_TEXT\s*\}/, service_desk_email_additional_text.to_s)
end
def issue_id
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index a0802ee6330..565df2ce621 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -502,7 +502,7 @@ module Ci
token.start_with?(CREATED_RUNNER_TOKEN_PREFIX)
end
- def ensure_machine(system_xid:, &blk)
+ def ensure_machine(system_xid, &blk)
RunnerMachine.safe_find_or_create_by!(runner_id: id, system_xid: system_xid.to_s, &blk) # rubocop: disable Performance/ActiveRecordSubtransactionMethods
end
@@ -598,6 +598,9 @@ module Ci
end
end
+ # TODO Remove in 16.0 when runners are known to send a system_id
+ # For now, heartbeats with version updates might result in two Sidekiq jobs being queued if a runner has a system_id
+ # This is not a problem since the jobs are deduplicated on the version
def schedule_runner_version_update
return unless version
diff --git a/app/models/ci/runner_machine.rb b/app/models/ci/runner_machine.rb
index 2564816dc14..e52659a011f 100644
--- a/app/models/ci/runner_machine.rb
+++ b/app/models/ci/runner_machine.rb
@@ -3,11 +3,15 @@
module Ci
class RunnerMachine < Ci::ApplicationRecord
include FromUnion
+ include RedisCacheable
include Ci::HasRunnerExecutor
include IgnorableColumns
ignore_column :machine_xid, remove_with: '15.11', remove_after: '2022-03-22'
+ # The `UPDATE_CONTACT_COLUMN_EVERY` defines how often the Runner Machine DB entry can be updated
+ UPDATE_CONTACT_COLUMN_EVERY = 40.minutes..55.minutes
+
belongs_to :runner
has_many :build_metadata, class_name: 'Ci::BuildMetadata'
@@ -24,6 +28,8 @@ module Ci
validates :ip_address, length: { maximum: 1024 }
validates :config, json_schema: { filename: 'ci_runner_config' }
+ cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at, :executor_type
+
# The `STALE_TIMEOUT` constant defines the how far past the last contact or creation date a runner machine
# will be considered stale
STALE_TIMEOUT = 7.days
@@ -37,5 +43,46 @@ module Ci
where(contacted_some_time_ago),
remove_duplicates: false).where(created_some_time_ago)
end
+
+ def heartbeat(values)
+ ##
+ # We can safely ignore writes performed by a runner heartbeat. We do
+ # not want to upgrade database connection proxy to use the primary
+ # database after heartbeat write happens.
+ #
+ ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do
+ values = values&.slice(:version, :revision, :platform, :architecture, :ip_address, :config, :executor) || {}
+ values[:contacted_at] = Time.current
+ if values.include?(:executor)
+ values[:executor_type] = Ci::Runner::EXECUTOR_NAME_TO_TYPES.fetch(values.delete(:executor), :unknown)
+ end
+
+ version_changed = values.include?(:version) && values[:version] != version
+
+ cache_attributes(values)
+
+ schedule_runner_version_update if version_changed
+
+ # We save data without validation, it will always change due to `contacted_at`
+ update_columns(values) if persist_cached_data?
+ end
+ end
+
+ private
+
+ def persist_cached_data?
+ # Use a random threshold to prevent beating DB updates.
+ contacted_at_max_age = Random.rand(UPDATE_CONTACT_COLUMN_EVERY)
+
+ real_contacted_at = read_attribute(:contacted_at)
+ real_contacted_at.nil? ||
+ (Time.current - real_contacted_at) >= contacted_at_max_age
+ end
+
+ def schedule_runner_version_update
+ return unless version
+
+ Ci::Runners::ProcessRunnerVersionUpdateWorker.perform_async(version)
+ end
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 6151ba54555..293726b5cc4 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1729,12 +1729,6 @@ class User < ApplicationRecord
end
end
- def manageable_groups_with_routes(include_groups_with_developer_maintainer_access: false)
- manageable_groups(include_groups_with_developer_maintainer_access: include_groups_with_developer_maintainer_access)
- .eager_load(:route)
- .order('routes.path')
- end
-
def namespaces(owned_only: false)
user_groups = owned_only ? owned_groups : groups
personal_namespace = Namespace.where(id: namespace.id)
diff --git a/app/services/releases/base_service.rb b/app/services/releases/base_service.rb
index 7fb59dad508..5d6cb372653 100644
--- a/app/services/releases/base_service.rb
+++ b/app/services/releases/base_service.rb
@@ -58,7 +58,7 @@ module Releases
end
def milestones
- return [] unless param_for_milestone_titles_provided?
+ return [] unless param_for_milestones_exists?
strong_memoize(:milestones) do
MilestonesFinder.new(
@@ -67,22 +67,44 @@ module Releases
project_ids: Array(project.id),
group_ids: Array(project_group_id),
state: 'all',
- title: params[:milestones]
+ title: params[:milestones],
+ ids: params[:milestone_ids]
).execute
end
end
- def inexistent_milestones
+ def inexistent_milestone_titles
return [] unless param_for_milestone_titles_provided?
existing_milestone_titles = milestones.map(&:title)
+
Array(params[:milestones]) - existing_milestone_titles
end
+ def inexistent_milestone_ids
+ return [] unless param_for_milestone_ids_provided?
+
+ existing_milestone_ids = milestones.map(&:id)
+
+ Array(params[:milestone_ids]) - existing_milestone_ids
+ end
+
def param_for_milestone_titles_provided?
!!params[:milestones]
end
+ def param_for_milestone_ids_provided?
+ !!params[:milestone_ids]
+ end
+
+ def param_for_milestones_provided?
+ param_for_milestone_titles_provided? || param_for_milestone_ids_provided?
+ end
+
+ def param_for_milestones_exists?
+ params[:milestones].present? || params[:milestone_ids].present?
+ end
+
def execute_hooks(release, action = 'create')
release.execute_hooks(action)
end
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index 01dd6323d94..a3289f9e552 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -6,7 +6,8 @@ module Releases
return error(_('Access Denied'), 403) unless allowed?
return error(_('You are not allowed to create this tag as it is protected.'), 403) unless can_create_tag?
return error(_('Release already exists'), 409) if release
- return error(format(_("Milestone(s) not found: %{milestones}"), milestones: inexistent_milestones.join(', ')), 400) if inexistent_milestones.any? # rubocop:disable Layout/LineLength
+ return error(format(_("Milestone(s) not found: %{milestones}"), milestones: inexistent_milestone_titles.join(', ')), 400) if inexistent_milestone_titles.any? # rubocop:disable Layout/LineLength
+ return error(format(_("Milestone id(s) not found: %{milestones}"), milestones: inexistent_milestone_ids.join(', ')), 400) if inexistent_milestone_ids.any? # rubocop:disable Layout/LineLength
# should be found before the creation of new tag
# because tag creation can spawn new pipeline
diff --git a/app/services/releases/update_service.rb b/app/services/releases/update_service.rb
index b9b2aba9805..c11d9468814 100644
--- a/app/services/releases/update_service.rb
+++ b/app/services/releases/update_service.rb
@@ -7,8 +7,8 @@ module Releases
return error
end
- if param_for_milestone_titles_provided?
- previous_milestones = release.milestones.map(&:title)
+ if param_for_milestones_provided?
+ previous_milestones = release.milestones.map(&:id)
params[:milestones] = milestones
end
@@ -35,7 +35,8 @@ module Releases
return error(_('Release does not exist'), 404) unless release
return error(_('Access Denied'), 403) unless allowed?
return error(_('params is empty'), 400) if empty_params?
- return error(format(_("Milestone(s) not found: %{milestones}"), milestones: inexistent_milestones.join(', ')), 400) if inexistent_milestones.any? # rubocop:disable Layout/LineLength
+ return error(format(_("Milestone(s) not found: %{milestones}"), milestones: inexistent_milestone_titles.join(', ')), 400) if inexistent_milestone_titles.any? # rubocop:disable Layout/LineLength
+ return error(format(_("Milestone id(s) not found: %{milestones}"), milestones: inexistent_milestone_ids.join(', ')), 400) if inexistent_milestone_ids.any? # rubocop:disable Layout/LineLength
end
def allowed?
@@ -47,9 +48,9 @@ module Releases
end
def milestones_updated?(previous_milestones)
- return false unless param_for_milestone_titles_provided?
+ return false unless param_for_milestones_provided?
- previous_milestones.to_set != release.milestones.map(&:title)
+ previous_milestones.to_set != release.milestones.map(&:id)
end
end
end
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 19a7419f539..8d858ad72d2 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,4 +1,4 @@
-- @can_bulk_update = !!@search_timeout_occurred && can?(current_user, :admin_merge_request, @group) && @group.licensed_feature_available?(:group_bulk_edit) && issuables_count_for_state(:merge_requests, :all) > 0
+- @can_bulk_update = can?(current_user, :admin_merge_request, @group) && @group.licensed_feature_available?(:group_bulk_edit) && issuables_count_for_state(:merge_requests, :all) > 0
- page_title _("Merge requests")
- add_page_specific_style 'page_bundles/issuable_list'
diff --git a/app/workers/merge_requests/delete_source_branch_worker.rb b/app/workers/merge_requests/delete_source_branch_worker.rb
index da1eca067a9..f9dbd85cd44 100644
--- a/app/workers/merge_requests/delete_source_branch_worker.rb
+++ b/app/workers/merge_requests/delete_source_branch_worker.rb
@@ -18,15 +18,10 @@ class MergeRequests::DeleteSourceBranchWorker
# Source branch changed while it's being removed
return if merge_request.source_branch_sha != source_branch_sha
- if Feature.enabled?(:add_delete_branch_worker, merge_request.source_project)
- ::Projects::DeleteBranchWorker.new.perform(merge_request.source_project.id, user_id,
- merge_request.source_branch)
- else
- ::Branches::DeleteService.new(merge_request.source_project, user).execute(merge_request.source_branch)
- end
-
::MergeRequests::RetargetChainService.new(project: merge_request.source_project, current_user: user)
.execute(merge_request)
+
+ ::Projects::DeleteBranchWorker.new.perform(merge_request.source_project.id, user_id, merge_request.source_branch)
rescue ActiveRecord::RecordNotFound
end
end
diff --git a/config/feature_flags/development/add_delete_branch_worker.yml b/config/feature_flags/development/add_delete_branch_worker.yml
deleted file mode 100644
index b0470a7f901..00000000000
--- a/config/feature_flags/development/add_delete_branch_worker.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: add_delete_branch_worker
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102208
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/381640
-milestone: '15.6'
-type: development
-group: group::code review
-default_enabled: false
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 29182077d66..ed55e7d7ff3 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -161,6 +161,8 @@ The following metrics are available:
| `gitlab_diffs_write_cache_real_duration_seconds` | Histogram | 15.8 | Duration in seconds spent on caching highlighted lines and stats on diffs batch request | `controller`, `action` |
| `gitlab_diffs_highlight_cache_decorate_real_duration_seconds` | Histogram | 15.8 | Duration in seconds spent on setting highlighted lines from cache on diffs batch request | `controller`, `action` |
| `gitlab_diffs_render_real_duration_seconds` | Histogram | 15.8 | Duration in seconds spent on serializing and rendering diffs on diffs batch request | `controller`, `action` |
+| `gitlab_memwd_violations_total` | Counter | 15.9 | Total number of times a Ruby process violated a memory threshold | |
+| `gitlab_memwd_violations_handled_total` | Counter | 15.9 | Total number of times Ruby process memory violations were handled | |
## Metrics controlled by a feature flag
@@ -350,6 +352,9 @@ configuration option in `gitlab.yml`. These metrics are served from the
| `geo_dependency_proxy_manifests_verification_total` | Gauge | 15.6 | Number of dependency proxy manifests verifications tried on secondary | `url` |
| `geo_dependency_proxy_manifests_verified` | Gauge | 15.6 | Number of dependency proxy manifests verified on secondary | `url` |
| `geo_dependency_proxy_manifests_verification_failed` | Gauge | 15.6 | Number of dependency proxy manifests verifications failed on secondary | `url` |
+| `gitlab_memwd_violations_total` | Counter | 15.9 | Total number of times a Sidekiq process violated a memory threshold | |
+| `gitlab_memwd_violations_handled_total` | Counter | 15.9 | Total number of times Sidekiq process memory violations were handled | |
+| `sidekiq_watchdog_running_jobs_total` | Counter | 15.9 | Current running jobs when RSS limit was reached | `worker_class` |
## Database load balancing metrics **(PREMIUM SELF)**
diff --git a/doc/administration/sidekiq/sidekiq_memory_killer.md b/doc/administration/sidekiq/sidekiq_memory_killer.md
index 0876f98621d..cb27d44a2e6 100644
--- a/doc/administration/sidekiq/sidekiq_memory_killer.md
+++ b/doc/administration/sidekiq/sidekiq_memory_killer.md
@@ -4,22 +4,21 @@ group: Application Performance
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-# Sidekiq MemoryKiller **(FREE SELF)**
+# Reducing memory use
The GitLab Rails application code suffers from memory leaks. For web requests
-this problem is made manageable using
-[`puma-worker-killer`](https://github.com/schneems/puma_worker_killer) which
-restarts Puma worker processes if it exceeds a memory limit. The Sidekiq
-MemoryKiller applies the same approach to the Sidekiq processes used by GitLab
+this problem is made manageable using a [supervision thread](../operations/puma.md#reducing-memory-use)
+that automatically restarts workers if they exceed a given resident set size (RSS) threshold
+for a certain amount of time.
+We use the same approach to the Sidekiq processes used by GitLab
to process background jobs.
-Unlike puma-worker-killer, which is enabled by default for all GitLab
-installations of GitLab 13.0 and later, the Sidekiq MemoryKiller is enabled by default
-_only_ for Omnibus packages. The reason for this is that the MemoryKiller
-relies on runit to restart Sidekiq after a memory-induced shutdown and GitLab
-installations from source do not all use runit or an equivalent.
+GitLab monitors the available RSS limit by default only for installations using
+the Linux packages (Omnibus) or Docker. The reason for this is that GitLab
+relies on runit to restart Sidekiq after a memory-induced shutdown, and GitLab
+self-compiled or Helm chart based installations don't use runit or an equivalent tool.
-With the default settings, the MemoryKiller causes a Sidekiq restart no
+With the default settings, Sidekiq restarts no
more often than once every 15 minutes, with the restart causing about one
minute of delay for incoming background jobs.
@@ -28,41 +27,75 @@ are cleanly terminated when Sidekiq is restarted, each Sidekiq process should be
run as a process group leader (for example, using `chpst -P`). If using Omnibus or the
`bin/background_jobs` script with `runit` installed, this is handled for you.
-## Configuring the MemoryKiller
-
-The MemoryKiller is controlled using environment variables.
-
-- `SIDEKIQ_MEMORY_KILLER_MAX_RSS` (KB): if this variable is set, and its value is greater
- than 0, the MemoryKiller is enabled. Otherwise the MemoryKiller is disabled.
-
- `SIDEKIQ_MEMORY_KILLER_MAX_RSS` defines the Sidekiq process allowed RSS.
-
- If the Sidekiq process exceeds the allowed RSS for longer than
- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME` the graceful restart is triggered. If the
- Sidekiq process go below the allowed RSS within `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`,
- the restart is aborted.
-
- The default value for Omnibus packages is set
- [in the Omnibus GitLab repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb).
-
-- `SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS` (KB): If the Sidekiq
- process RSS (expressed in kilobytes) exceeds `SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS`,
- an immediate graceful restart of Sidekiq is triggered.
-
-- `SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL`: Define how
- often to check process RSS, default to 3 seconds.
-
-- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults to 900 seconds (15 minutes).
- The usage of this variable is described as part of `SIDEKIQ_MEMORY_KILLER_MAX_RSS`.
-
-- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. This defines the
- maximum time allowed for all Sidekiq jobs to finish. No new jobs are accepted
- during that time, and the process exits as soon as all jobs finish.
-
- If jobs do not finish during that time, the MemoryKiller interrupts all currently
- running jobs by sending `SIGTERM` to the Sidekiq process.
-
- If the process hard shutdown/restart is not performed by Sidekiq,
- the Sidekiq process is forcefully terminated after
- `Sidekiq[:timeout] + 2` seconds. An external supervision mechanism
- (for example, runit) must restart Sidekiq afterwards.
+## Configuring the limits
+
+Sidekiq memory limits are controlled using environment variables.
+
+- `SIDEKIQ_MEMORY_KILLER_MAX_RSS` (KB): defines the Sidekiq process soft limit for allowed RSS.
+ If the Sidekiq process RSS (expressed in kilobytes) exceeds `SIDEKIQ_MEMORY_KILLER_MAX_RSS`,
+ for longer than `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`, the graceful restart is triggered.
+ If `SIDEKIQ_MEMORY_KILLER_MAX_RSS` is not set, or its value is set to 0, the soft limit is not monitored.
+ `SIDEKIQ_MEMORY_KILLER_MAX_RSS` defaults to `2000000`.
+
+- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defines the grace time period in seconds for which the Sidekiq process is allowed to run
+ above the allowed RSS soft limit. If the Sidekiq process goes below the allowed RSS (soft limit)
+ within `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`, the restart is aborted. Default value is 900 seconds (15 minutes).
+
+- `SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS` (KB): defines the Sidekiq process hard limit for allowed RSS.
+ If the Sidekiq process RSS (expressed in kilobytes) exceeds `SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS`,
+ an immediate graceful restart of Sidekiq is triggered. If this value is not set, or set to 0,
+ the hard limit is not be monitored.
+
+- `SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL`: defines how often to check the process RSS. Defaults to 3 seconds.
+
+- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defines the maximum time allowed for all Sidekiq jobs to finish.
+ No new jobs are accepted during that time. Defaults to 30 seconds.
+
+ If the process restart is not performed by Sidekiq, the Sidekiq process is forcefully terminated after
+ [Sidekiq shutdown timeout](https://github.com/mperham/sidekiq/wiki/Signals#term) (defaults to 25 seconds) +2 seconds.
+ If jobs do not finish during that time, all currently running jobs are interrupted with a `SIGTERM` signal
+ sent to the Sidekiq process.
+
+- `GITLAB_MEMORY_WATCHDOG_ENABLED`: enabled by default. Set the `GITLAB_MEMORY_WATCHDOG_ENABLED` to false, to use legacy
+ Daemon Sidekiq Memory Killer implementation used prior GitLab 15.9. Support for setting `GITLAB_MEMORY_WATCHDOG_ENABLED`
+ will be removed in GitLab 16.0.
+
+### Monitor worker restarts
+
+GitLab emits log events if workers are restarted due to high memory usage.
+
+The following is an example of one of these log events in `/var/log/gitlab/gitlab-rails/sidekiq_client.log`:
+
+```json
+{
+ "severity": "WARN",
+ "time": "2023-02-04T09:45:16.173Z",
+ "correlation_id": null,
+ "pid": 2725,
+ "worker_id": "sidekiq_1",
+ "memwd_handler_class": "Gitlab::Memory::Watchdog::SidekiqHandler",
+ "memwd_sleep_time_s": 3,
+ "memwd_rss_bytes": 1079683247,
+ "memwd_max_rss_bytes": 629145600,
+ "memwd_max_strikes": 5,
+ "memwd_cur_strikes": 6,
+ "message": "rss memory limit exceeded",
+ "running_jobs": [
+ {
+ jid: "83efb701c59547ee42ff7068",
+ worker_class: "Ci::DeleteObjectsWorker"
+ },
+ {
+ jid: "c3a74503dc2637f8f9445dd3",
+ worker_class: "Ci::ArchiveTraceWorker"
+ }
+ ]
+}
+```
+
+Where:
+
+- `memwd_rss_bytes` is the actual amount of memory consumed.
+- `memwd_max_rss_bytes` is the RSS limit set through `per_worker_max_memory_mb`.
+- `running jobs` lists the jobs that were running at the time when the process
+ exceeded the RSS limit and started a graceful restart.
diff --git a/doc/api/member_roles.md b/doc/api/member_roles.md
index 693dfb49c82..141d964ff6a 100644
--- a/doc/api/member_roles.md
+++ b/doc/api/member_roles.md
@@ -6,7 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Member roles API **(ULTIMATE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96996) in GitLab 15.4. [Deployed behind the `customizable_roles` flag](../administration/feature_flags.md), disabled by default.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96996) in GitLab 15.4. [Deployed behind the `customizable_roles` flag](../administration/feature_flags.md), disabled by default.
+> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110810) in GitLab 15.9.
## List all member roles of a group
@@ -32,7 +33,7 @@ If successful, returns [`200`](rest/index.md#status-codes) and the following res
Example request:
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/member_roles"
+curl --header "Authorization: Bearer <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/member_roles"
```
Example response:
diff --git a/doc/architecture/blueprints/ci_pipeline_components/index.md b/doc/architecture/blueprints/ci_pipeline_components/index.md
index 1bb459cf033..b1aee7c4217 100644
--- a/doc/architecture/blueprints/ci_pipeline_components/index.md
+++ b/doc/architecture/blueprints/ci_pipeline_components/index.md
@@ -57,7 +57,7 @@ direction for iterations and improvements to the solution.
- The user should be able to import the job inside a given stage or pass the stage names as input parameter
when using the component.
- Failures in mapping the correct stage can result in confusing errors.
-- Some templates are designed to work with AutoDevops but are not generic enough
+- Some templates are designed to work with AutoDevOps but are not generic enough
([example](https://gitlab.com/gitlab-org/gitlab/-/blob/2c0e8e4470001442e999391df81e19732b3439e6/lib/gitlab/ci/templates/AWS/Deploy-ECS.gitlab-ci.yml)).
- Many CI templates, especially those [language specific](https://gitlab.com/gitlab-org/gitlab/-/tree/2c0e8e4470001442e999391df81e19732b3439e6/lib/gitlab/ci/templates)
are tutorial/scaffolding-style templates.
@@ -84,21 +84,7 @@ direction for iterations and improvements to the solution.
- Competitive landscape is showing the need for such feature
- [R2DevOps](https://r2devops.io) implements a catalog of CI templates for GitLab pipelines.
- [GitHub Actions](https://github.com/features/actions) provides an extensive catalog of reusable job steps.
-
-## Implementation guidelines
-
-- Start with the smallest user base. Dogfood the feature for `gitlab-org` and `gitlab-com` groups.
- Involve the Engineering Productivity and other groups authoring pipeline configurations to test
- and validate our solutions.
-- Ensure we can integrate all the feedback gathered, even if that means changing the technical design or
- UX. Until we make the feature GA we should have clear expectations with early adopters.
-- Reuse existing functionality as much as possible. Don't reinvent the wheel on the initial iterations.
- For example: reuse project features like title, description, avatar to build a catalog.
-- Leverage GitLab features for the development lifecycle of the components (testing via `.gitlab-ci.yml`,
- release management, Pipeline Editor, etc.).
-- Design the catalog with self-managed support in mind.
-- Allow the catalog an the workflow to support future types of pipeline constructs and new ways of using them.
-- Design components and catalog following industry best practice related to building deterministic package managers.
+ - [CircleCI Orbs](https://circleci.com/orbs/) provide reusable YAML configuration packages.
## Glossary
@@ -530,7 +516,7 @@ spec:
# rest of the pipeline config
```
-## Limits
+### Limits
Any MVC that exposes a feature should be added with limitations from the beginning.
It's safer to add new features with restrictions than trying to limit a feature after it's being used.
@@ -544,6 +530,41 @@ Some limits we could consider adding:
- max level of nested imports
- max length of the exported component name
+## Publishing components
+
+Users will be able to publish CI Components into a CI Catalog. This can happen
+in a CI pipeline job, similarly to how software is being deployed following
+Continuous Delivery principles. This will allow us to guardrail the quality of
+components being deployed. To ensure that the CI Components meet quality
+standards users will be able to test them before publishing new versions in the
+CI Catalog.
+
+Once a project containing components gets published we will index components'
+metadata. We want to initially index as much metadata as possible, to gain more
+flexibility in how we design CI Catalog's main page. We don't want to be
+constrained by the lack of data available to properly visualize CI Components
+in CI Catalog. In order to do that, we may need to find all components that are
+being published, read their `spec` metadata and index what we find there.
+
+## Implementation guidelines
+
+- Start with the smallest user base. Dogfood the feature for `gitlab-org` and
+ `gitlab-com` groups. Involve the Engineering Productivity and other groups
+ authoring pipeline configurations to test and validate our solutions.
+- Ensure we can integrate all the feedback gathered, even if that means
+ changing the technical design or UX. Until we make the feature GA we should
+ have clear expectations with early adopters.
+- Reuse existing functionality as much as possible. Don't reinvent the wheel on
+ the initial iterations. For example: reuse project features like title,
+ description, avatar to build a catalog.
+- Leverage GitLab features for the development lifecycle of the components
+ (testing via `.gitlab-ci.yml`, release management, Pipeline Editor, etc.).
+- Design the catalog with self-managed support in mind.
+- Allow the catalog and the workflow to support future types of pipeline
+ constructs and new ways of using them.
+- Design components and catalog following industry best practice related to
+ building deterministic package managers.
+
## Iterations
1. Experimentation phase
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 1b39b74a23b..245fb2152cd 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -539,8 +539,8 @@ Before taking the decision to merge:
- If the MR contains both Quality and non-Quality-related changes, the MR should be merged by the relevant maintainer for user-facing changes (backend, frontend, or database) after the Quality related changes are approved by a Software Engineer in Test.
At least one maintainer must approve an MR before it can be merged. MR authors and
-people who add commits to an MR are not authorized to approve the merge request,
-so they must seek a maintainer who has not contributed to the MR to approve the MR before it can be merged.
+people who add commits to an MR are not authorized to approve or merge the MR and
+must seek a maintainer who has not contributed to the MR to approve and merge it.
This policy is in place to satisfy the CHG-04 control of the GitLab
[Change Management Controls](https://about.gitlab.com/handbook/security/security-assurance/security-compliance/guidance/change-management.html).
diff --git a/doc/subscriptions/quarterly_reconciliation.md b/doc/subscriptions/quarterly_reconciliation.md
index 2b45d145aaf..126a34334c4 100644
--- a/doc/subscriptions/quarterly_reconciliation.md
+++ b/doc/subscriptions/quarterly_reconciliation.md
@@ -78,3 +78,21 @@ and expected invoice amount.
seats, and an invoice is generated for a prorated amount. If a credit card
is on file, a payment is automatically applied. Otherwise, an invoice is
sent and subject to your payment terms.
+
+## Quarterly reconciliation eligibility
+
+### You are automatically enrolled in quarterly reconciliation if
+
+- The credit card you used to purchase your subscription is still linked to your GitLab account.
+- You purchased your subscription through an invoice.
+
+### You are excluded from quarterly reconciliation if
+
+- You purchased your subscription from a reseller or another channel partner.
+- You purchased a multi-year subscription.
+- You purchased your subscription with a purchasing order.
+- You are a pubic sector customer.
+- You have an offline environment and used a license file to activate your subscription.
+- You are enrolled in a program that provides a free tier such as the GitLab for Education, GitLab for Open Source Program, or GitLab for Startups.
+
+If you are excluded from quarterly reconciliation and not on a free tier, your true-ups are reconciled annually.
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index d3fa8f28d5c..fb32c64f06c 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -268,15 +268,18 @@ For more information, see [Runner SaaS](../../ci/runners/index.md).
GitLab.com runs [Sidekiq](https://sidekiq.org) with arguments `--timeout=4 --concurrency=4`
and the following environment variables:
-| Setting | GitLab.com | Default |
-|----------------------------------------|-------------|-----------|
-| `SIDEKIQ_DAEMON_MEMORY_KILLER` | - | `1` |
-| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | `2000000` | `2000000` |
-| `SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS` | - | - |
-| `SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL` | - | `3` |
-| `SIDEKIQ_MEMORY_KILLER_GRACE_TIME` | - | `900` |
-| `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT` | - | `30` |
-| `SIDEKIQ_LOG_ARGUMENTS` | `1` | `1` |
+| Setting | GitLab.com | Default |
+|----------------------------------------|------------|-----------|
+| `GITLAB_MEMORY_WATCHDOG_ENABLED` | - | `true` |
+| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | - | `2000000` |
+| `SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS` | - | - |
+| `SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL` | - | `3` |
+| `SIDEKIQ_MEMORY_KILLER_GRACE_TIME` | - | `900` |
+| `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT` | - | `30` |
+| `SIDEKIQ_LOG_ARGUMENTS` | `1` | `1` |
+
+For more information, see how to
+[configure the environment variables](../../administration/sidekiq/sidekiq_memory_killer.md).
NOTE:
The `SIDEKIQ_MEMORY_KILLER_MAX_RSS` setting is `16000000` on Sidekiq import
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index caba19b374b..8ebc0caf123 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -464,3 +464,75 @@ subscriptions.
- [Confidential issues](project/issues/confidential_issues.md)
- [Container Registry permissions](packages/container_registry/index.md#container-registry-visibility-permissions)
- [Release permissions](project/releases/index.md#release-permissions)
+
+## Custom roles **(ULTIMATE)**
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106256) in GitLab 15.7 [with a flag](../administration/feature_flags.md) named `customizable_roles`.
+> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110810) in GitLab 15.9.
+
+Custom roles allow group members who are assigned the Owner role to create roles specific to the needs
+of their organization.
+
+### Create a custom role
+
+To enable custom roles for your group, a group member with the Owner role:
+
+1. Makes sure that there is at least one private project in this group or one of
+ its subgroups, so that you can see the effect of giving a Guest a custom role.
+1. Creates a personal access token with the API scope.
+1. Uses [the API](../api/member_roles.md#add-a-member-role-to-a-group) to create the Guest+1 role for the group.
+
+### Associate a custom role with an existing group member
+
+To associate a custom role with an existing group member, a group member with
+the Owner role:
+
+1. Invites a test user account to the root group as a Guest.
+ At this point, this Guest user cannot see any code on the projects in the group.
+1. Optional. If the Owner does not know the `ID` of the Guest user receiving a custom
+ role, finds that `ID` by making an [API request](../api/member_roles.md#list-all-member-roles-of-a-group).
+
+1. Associates the group member with the Guest+1 role using the [Group and Project Members API endpoint](../api/members.md#edit-a-member-of-a-group-or-project)
+
+ ```shell
+ curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id":$MEMBER_ROLE_ID},"access_level":10' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID"
+ ```
+
+ Where:
+ - `$MEMBER_ROLE_ID`: The `ID` of the member role created in the previous section.
+ - `$GUEST_USER_ID`: The `ID` of the Guest user receiving a custom role.
+
+ Now the Guest+1 user can view code on all projects in the root group.
+
+### Remove a custom role from a group member
+
+To remove a custom role from a group member, use the [Group and Project Members API endpoint](../api/members.md#edit-a-member-of-a-group-or-project)
+and pass an empty `member_role_id` value.
+
+```shell
+curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id":""},"access_level":10' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID"
+```
+
+Now the user is a regular Guest.
+
+### Remove a custom role
+
+Removing a custom role also removes all members with that custom role from
+the group. If you decide to delete a custom role, you must re-add any users with that custom
+role to the group.
+
+To remove a custom role from a group, a group member with
+the Owner role:
+
+1. Optional. If the Owner does not know the `ID` of a custom
+ role, finds that `ID` by making an [API request](../api/member_roles.md#list-all-member-roles-of-a-group).
+1. Uses [the API](../api/member_roles.md#remove-member-role-of-a-group) to delete the custom role.
+
+### Known issues
+
+- Additional permissions can only be applied to users with the Guest role.
+- There is no visual distinction in the UI between the Guest role and the Guest role with additional permission. For more information, see [issue 384099](https://gitlab.com/gitlab-org/gitlab/-/issues/384099).
+- If a user with a custom role is shared with a group or project, their custom role is not transferred over with them. The user has the regular Guest role in the new group or project.
+- If a custom role is deleted, the users associated with that custom role are also removed from the group. For more information, see [issue 370352](https://gitlab.com/gitlab-org/gitlab/-/issues/370352).
+- The API endpoint for associating a custom role with a user only works for users with the Guest role in a group. A project member can be associated with a custom role, but not through the API yet. For more information, see [issue 385495](https://gitlab.com/gitlab-org/gitlab/-/issues/385495).
+- The only way to remove a custom role from a user's membership to a Group is to delete the custom role, which deletes the user membership entirely. See [issue 387769](https://gitlab.com/gitlab-org/gitlab/-/issues/387769).
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
index b62bd75f3bc..ddbb638d753 100644
--- a/doc/user/project/service_desk.md
+++ b/doc/user/project/service_desk.md
@@ -121,6 +121,8 @@ visible in the email template. For more information, see
#### Thank you email
+> UNSUBSCRIBE_URL, SYSTEM_HEADER, SYSTEM_FOOTER, ADDITIONAL_TEXT placeholders [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285512) in GitLab 15.9.
+
When a user submits an issue through Service Desk, GitLab sends a **thank you email**.
You must name the template file `thank_you.md`.
@@ -128,6 +130,10 @@ You can use these placeholders to be automatically replaced in each email:
- `%{ISSUE_ID}`: issue IID
- `%{ISSUE_PATH}`: project path appended with the issue IID
+- `%{UNSUBSCRIBE_URL}`: unsubscribe URL
+- `%{SYSTEM_HEADER}`: [system header message](../admin_area/appearance.md#system-header-and-footer-messages)
+- `%{SYSTEM_FOOTER}`: [system footer message](../admin_area/appearance.md#system-header-and-footer-messages)
+- `%{ADDITIONAL_TEXT}`: [custom additional text](../admin_area/settings/email.md#custom-additional-text)
Because Service Desk issues are created as [confidential](issues/confidential_issues.md) (only project members can see them),
the response email does not contain the issue link.
@@ -142,6 +148,10 @@ You can use these placeholders to be automatically replaced in each email:
- `%{ISSUE_ID}`: issue IID
- `%{ISSUE_PATH}`: project path appended with the issue IID
- `%{NOTE_TEXT}`: note text
+- `%{UNSUBSCRIBE_URL}`: unsubscribe URL
+- `%{SYSTEM_HEADER}`: [system header message](../admin_area/appearance.md#system-header-and-footer-messages)
+- `%{SYSTEM_FOOTER}`: [system footer message](../admin_area/appearance.md#system-header-and-footer-messages)
+- `%{ADDITIONAL_TEXT}`: [custom additional text](../admin_area/settings/email.md#custom-additional-text)
#### New Service Desk issues
diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb
index cbfa2b58672..96f5265ce23 100644
--- a/lib/api/ci/helpers/runner.rb
+++ b/lib/api/ci/helpers/runner.rb
@@ -10,13 +10,15 @@ module API
JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'
JOB_TOKEN_PARAM = :token
+ LEGACY_SYSTEM_XID = '<legacy>'
def authenticate_runner!
track_runner_authentication
forbidden! unless current_runner
- current_runner
- .heartbeat(get_runner_details_from_request)
+ runner_details = get_runner_details_from_request
+ current_runner.heartbeat(runner_details)
+ current_runner_machine&.heartbeat(runner_details)
end
def get_runner_details_from_request
@@ -52,10 +54,10 @@ module API
def current_runner_machine
return if Feature.disabled?(:create_runner_machine)
- return unless params[:system_id]
strong_memoize(:current_runner_machine) do
- current_runner.ensure_machine(system_xid: params[:system_id]) { |m| m.contacted_at = Time.current }
+ system_xid = params.fetch(:system_id, LEGACY_SYSTEM_XID)
+ current_runner&.ensure_machine(system_xid) { |m| m.contacted_at = Time.current }
end
end
@@ -94,6 +96,7 @@ module API
# the heartbeat should be triggered.
if heartbeat_runner
job.runner&.heartbeat(get_runner_ip)
+ job.runner_machine&.heartbeat(get_runner_ip)
end
job
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index ebe66f0a7be..e10959aecc4 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -81,6 +81,7 @@ module API
end
params do
requires :token, type: String, desc: %q(The runner's authentication token)
+ optional :system_id, type: String, desc: %q(The runner's system identifier)
end
post '/verify', urgency: :low, feature_category: :runner do
authenticate_runner!
@@ -193,7 +194,7 @@ module API
[403, 'Forbidden']]
end
params do
- requires :token, type: String, desc: %q(Runner's authentication token)
+ requires :token, type: String, desc: %q(Job token)
requires :id, type: Integer, desc: %q(Job's ID)
optional :state, type: String, desc: %q(Job's status: success, failed)
optional :checksum, type: String, desc: %q(Job's trace CRC32 checksum)
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index e69dc756551..ebf1c03e86b 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -246,8 +246,14 @@ module API
optional :milestones,
type: Array[String],
coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
- desc: 'The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones',
- default: []
+ desc: 'The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones. Cannot be combined with `milestone_ids` parameter.'
+
+ optional :milestone_ids,
+ type: Array[String, Integer],
+ coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce,
+ desc: 'The ID of each milestone the release is associated with. GitLab Premium customers can specify group milestones. Cannot be combined with `milestones` parameter.'
+
+ mutually_exclusive :milestones, :milestone_ids, message: 'Cannot specify milestones and milestone_ids at the same time'
optional :released_at,
type: DateTime,
@@ -292,7 +298,14 @@ module API
optional :milestones,
type: Array[String],
coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
- desc: 'The title of each milestone to associate with the release. GitLab Premium customers can specify group milestones. To remove all milestones from the release, specify `[]`'
+ desc: 'The title of each milestone to associate with the release. GitLab Premium customers can specify group milestones. Cannot be combined with `milestone_ids` parameter. To remove all milestones from the release, specify `[]`'
+
+ optional :milestone_ids,
+ type: Array[String, Integer],
+ coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce,
+ desc: 'The ID of each milestone the release is associated with. GitLab Premium customers can specify group milestones. Cannot be combined with `milestones` parameter. To remove all milestones from the release, specify `[]`'
+
+ mutually_exclusive :milestones, :milestone_ids, message: 'Cannot specify milestones and milestone_ids at the same time'
end
route_setting :authentication, job_token_allowed: true
put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do
diff --git a/lib/gitlab/memory/watchdog/configurator.rb b/lib/gitlab/memory/watchdog/configurator.rb
index 8e6e3ef134b..4a6640ba901 100644
--- a/lib/gitlab/memory/watchdog/configurator.rb
+++ b/lib/gitlab/memory/watchdog/configurator.rb
@@ -12,7 +12,7 @@ module Gitlab
DEFAULT_MAX_HEAP_FRAG = 0.5
DEFAULT_MAX_MEM_GROWTH = 3.0
# grace_time / sleep_interval = max_strikes allowed for Sidekiq process to violate defined limits.
- DEFAULT_SIDEKIQ_GRACE_TIME_S = 300
+ DEFAULT_SIDEKIQ_GRACE_TIME_S = 900
class << self
def configure_for_puma
diff --git a/lib/sidebars/projects/menus/packages_registries_menu.rb b/lib/sidebars/projects/menus/packages_registries_menu.rb
index fc7c564574a..4c6a30af4af 100644
--- a/lib/sidebars/projects/menus/packages_registries_menu.rb
+++ b/lib/sidebars/projects/menus/packages_registries_menu.rb
@@ -10,6 +10,7 @@ module Sidebars
add_item(container_registry_menu_item)
add_item(infrastructure_registry_menu_item)
add_item(harbor_registry_menu_item)
+ add_item(model_experiments_menu_item)
true
end
@@ -80,6 +81,19 @@ module Sidebars
)
end
+ def model_experiments_menu_item
+ if Feature.disabled?(:ml_experiment_tracking, context.project)
+ return ::Sidebars::NilMenuItem.new(item_id: :model_experiments)
+ end
+
+ ::Sidebars::MenuItem.new(
+ title: _('Model Experiments'),
+ link: project_ml_experiments_path(context.project),
+ active_routes: { controller: 'ml/experiments#index' },
+ item_id: :model_experiments
+ )
+ end
+
def packages_registry_disabled?
!::Gitlab.config.packages.enabled ||
!can?(context.current_user, :read_package, context.project&.packages_policy_subject)
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 787df37a8f8..06a032316c5 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'active_record/fixtures'
-
namespace :gitlab do
+ require 'active_record/fixtures'
+
namespace :backup do
PID = Process.pid.freeze
PID_FILE = "#{Rails.application.root}/tmp/backup_restore.pid"
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 914037a5557..45b6d07ae5d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -26191,6 +26191,9 @@ msgstr ""
msgid "MemberInviteEmail|Invitation to join the %{project_or_group} %{project_or_group_name}"
msgstr ""
+msgid "MemberRole|%{role} - custom"
+msgstr ""
+
msgid "MemberRole|can't be changed"
msgstr ""
@@ -27126,6 +27129,9 @@ msgstr[1] ""
msgid "Milestone due date"
msgstr ""
+msgid "Milestone id(s) not found: %{milestones}"
+msgstr ""
+
msgid "Milestone lists not available with your current license"
msgstr ""
@@ -27432,6 +27438,9 @@ msgstr ""
msgid "Modal|Close"
msgstr ""
+msgid "Model Experiments"
+msgstr ""
+
msgid "Model candidate details"
msgstr ""
diff --git a/package.json b/package.json
index 12802b528a3..c6ae8a5d0f7 100644
--- a/package.json
+++ b/package.json
@@ -110,7 +110,7 @@
"clipboard": "^2.0.8",
"compression-webpack-plugin": "^5.0.2",
"copy-webpack-plugin": "^6.4.1",
- "core-js": "^3.27.2",
+ "core-js": "^3.28.0",
"cron-validator": "^1.1.1",
"cronstrue": "^1.122.0",
"cropper": "^2.3.0",
diff --git a/qa/qa/page/component/invite_members_modal.rb b/qa/qa/page/component/invite_members_modal.rb
deleted file mode 100644
index eb791594d22..00000000000
--- a/qa/qa/page/component/invite_members_modal.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Component
- module InviteMembersModal
- extend QA::Page::PageConcern
-
- def self.included(base)
- super
-
- base.view 'app/assets/javascripts/invite_members/components/invite_modal_base.vue' do
- element :invite_button
- element :access_level_dropdown
- element :invite_members_modal_content
- end
-
- base.view 'app/assets/javascripts/invite_members/components/group_select.vue' do
- element :group_select_dropdown_search_field
- element :group_select_dropdown_item
- end
-
- base.view 'app/assets/javascripts/invite_members/components/members_token_select.vue' do
- element :members_token_select_input
- end
-
- base.view 'app/assets/javascripts/invite_members/components/invite_group_trigger.vue' do
- element :invite_a_group_button
- end
-
- base.view 'app/assets/javascripts/invite_members/constants.js' do
- element :invite_members_button
- end
- end
-
- def open_invite_members_modal
- click_element :invite_members_button
- end
-
- def open_invite_group_modal
- click_element :invite_a_group_button
- end
-
- def add_member(username, access_level = 'Developer', refresh_page: true)
- open_invite_members_modal
-
- within_element(:invite_members_modal_content) do
- fill_element(:members_token_select_input, username)
- Support::WaitForRequests.wait_for_requests
- click_button(username, match: :prefer_exact)
- set_access_level(access_level)
- end
-
- send_invite(refresh_page)
- end
-
- def invite_group(group_name, access_level = 'Guest', refresh_page: true)
- open_invite_group_modal
-
- within_element(:invite_members_modal_content) do
- click_button 'Select a group'
-
- Support::Waiter.wait_until { has_element?(:group_select_dropdown_item) }
-
- fill_element :group_select_dropdown_search_field, group_name
- Support::WaitForRequests.wait_for_requests
- click_button group_name
-
- set_access_level(access_level)
- end
-
- send_invite(refresh_page)
- end
-
- def send_invite(refresh = false)
- click_element :invite_button
- Support::WaitForRequests.wait_for_requests
- page.refresh if refresh
- end
-
- private
-
- def set_access_level(access_level)
- # Guest option is selected by default, skipping these steps if desired option is 'Guest'
- select_element(:access_level_dropdown, access_level) unless access_level == 'Guest'
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/component/members/invite_members_modal.rb b/qa/qa/page/component/members/invite_members_modal.rb
new file mode 100644
index 00000000000..0220bb1e4fc
--- /dev/null
+++ b/qa/qa/page/component/members/invite_members_modal.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module Members
+ module InviteMembersModal
+ extend QA::Page::PageConcern
+
+ def self.included(base)
+ super
+
+ base.view 'app/assets/javascripts/invite_members/components/invite_modal_base.vue' do
+ element :invite_button
+ element :access_level_dropdown
+ element :invite_members_modal_content
+ end
+
+ base.view 'app/assets/javascripts/invite_members/components/group_select.vue' do
+ element :group_select_dropdown_search_field
+ element :group_select_dropdown_item
+ end
+
+ base.view 'app/assets/javascripts/invite_members/components/members_token_select.vue' do
+ element :members_token_select_input
+ end
+
+ base.view 'app/assets/javascripts/invite_members/components/invite_group_trigger.vue' do
+ element :invite_a_group_button
+ end
+
+ base.view 'app/assets/javascripts/invite_members/constants.js' do
+ element :invite_members_button
+ end
+ end
+
+ def open_invite_members_modal
+ click_element :invite_members_button
+ end
+
+ def open_invite_group_modal
+ click_element :invite_a_group_button
+ end
+
+ def add_member(username, access_level = 'Developer', refresh_page: true)
+ open_invite_members_modal
+
+ within_element(:invite_members_modal_content) do
+ fill_element(:members_token_select_input, username)
+ Support::WaitForRequests.wait_for_requests
+ click_button(username, match: :prefer_exact)
+ set_access_level(access_level)
+ end
+
+ send_invite(refresh_page)
+ end
+
+ def invite_group(group_name, access_level = 'Guest', refresh_page: true)
+ open_invite_group_modal
+
+ within_element(:invite_members_modal_content) do
+ click_button 'Select a group'
+
+ Support::Waiter.wait_until { has_element?(:group_select_dropdown_item) }
+
+ fill_element :group_select_dropdown_search_field, group_name
+ Support::WaitForRequests.wait_for_requests
+ click_button group_name
+
+ set_access_level(access_level)
+ end
+
+ send_invite(refresh_page)
+ end
+
+ def send_invite(refresh = false)
+ click_element :invite_button
+ Support::WaitForRequests.wait_for_requests
+ page.refresh if refresh
+ end
+
+ private
+
+ def set_access_level(access_level)
+ # Guest option is selected by default, skipping these steps if desired option is 'Guest'
+ select_element(:access_level_dropdown, access_level) unless access_level == 'Guest'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/members/members_filter.rb b/qa/qa/page/component/members/members_filter.rb
new file mode 100644
index 00000000000..8803211ea86
--- /dev/null
+++ b/qa/qa/page/component/members/members_filter.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module Members
+ module MembersFilter
+ extend QA::Page::PageConcern
+
+ def self.included(base)
+ super
+
+ base.view 'app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue' do
+ element :search_bar_input
+ element :search_button
+ end
+ end
+
+ def search_member(username)
+ fill_element :search_bar_input, username
+ click_element :search_button
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/members/members_table.rb b/qa/qa/page/component/members/members_table.rb
new file mode 100644
index 00000000000..46010a0f9ab
--- /dev/null
+++ b/qa/qa/page/component/members/members_table.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module Members
+ module MembersTable
+ extend QA::Page::PageConcern
+
+ def self.included(base)
+ super
+
+ base.class_eval do
+ include MembersFilter
+ include RemoveMemberModal
+ include RemoveGroupModal
+ end
+
+ base.view 'app/assets/javascripts/members/components/table/members_table.vue' do
+ element :member_row
+ end
+
+ base.view 'app/assets/javascripts/members/components/table/role_dropdown.vue' do
+ element :access_level_dropdown
+ element :access_level_link
+ end
+
+ base.view 'app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue' do
+ element :user_action_dropdown
+ end
+
+ base.view 'app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue' do
+ element :delete_member_dropdown_item
+ end
+
+ base.view 'app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue' do
+ element :approve_access_request_button
+ end
+
+ base.view 'app/assets/javascripts/members/components/members_tabs.vue' do
+ element :groups_list_tab
+ end
+
+ base.view 'app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue' do
+ element :remove_group_link_button
+ end
+ end
+
+ def update_access_level(username, access_level)
+ search_member(username)
+
+ within_element(:member_row, text: username) do
+ click_element :access_level_dropdown
+ click_element :access_level_link, text: access_level
+ end
+ end
+
+ def remove_member(username)
+ within_element(:member_row, text: username) do
+ click_element :user_action_dropdown
+ click_element :delete_member_dropdown_item
+ end
+
+ confirm_remove_member
+ end
+
+ def approve_access_request(username)
+ within_element(:member_row, text: username) do
+ click_element :approve_access_request_button
+ end
+ end
+
+ def deny_access_request(username)
+ within_element(:member_row, text: username) do
+ click_element :delete_member_button
+ end
+
+ confirm_remove_member
+ end
+
+ def remove_group(group_name)
+ click_element :groups_list_tab
+
+ within_element(:member_row, text: group_name) do
+ click_element :remove_group_link_button
+ end
+
+ confirm_remove_group
+ end
+
+ def has_group?(group_name)
+ click_element :groups_list_tab
+ has_element?(:member_row, text: group_name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/members/remove_group_modal.rb b/qa/qa/page/component/members/remove_group_modal.rb
new file mode 100644
index 00000000000..03c51757b62
--- /dev/null
+++ b/qa/qa/page/component/members/remove_group_modal.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module Members
+ module RemoveGroupModal
+ extend QA::Page::PageConcern
+
+ def self.included(base)
+ super
+
+ base.view 'app/assets/javascripts/members/components/modals/remove_group_link_modal.vue' do
+ element :remove_group_link_modal_content
+ element :remove_group_button
+ end
+ end
+
+ def confirm_remove_group
+ within_element(:remove_group_link_modal_content) do
+ wait_for_enabled_remove_group_button
+
+ click_element :remove_group_button
+ end
+ end
+
+ private
+
+ def wait_for_enabled_remove_group_button
+ retry_until(sleep_interval: 1, message: 'Waiting for remove group button to be enabled') do
+ has_element?(:remove_group_button, disabled: false, wait: 3)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/members/remove_member_modal.rb b/qa/qa/page/component/members/remove_member_modal.rb
new file mode 100644
index 00000000000..b7b81040ba7
--- /dev/null
+++ b/qa/qa/page/component/members/remove_member_modal.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module Members
+ module RemoveMemberModal
+ extend QA::Page::PageConcern
+
+ def self.included(base)
+ super
+
+ base.view 'app/assets/javascripts/members/components/modals/remove_member_modal.vue' do
+ element :remove_member_modal
+ element :remove_member_button
+ end
+ end
+
+ def confirm_remove_member
+ within_element(:remove_member_modal) do
+ wait_for_enabled_remove_member_button
+
+ click_element :remove_member_button
+ end
+ end
+
+ private
+
+ def wait_for_enabled_remove_member_button
+ retry_until(sleep_interval: 1, message: 'Waiting for remove member button to be enabled') do
+ has_element?(:remove_member_button, disabled: false, wait: 3)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/members_filter.rb b/qa/qa/page/component/members_filter.rb
deleted file mode 100644
index fce4560d255..00000000000
--- a/qa/qa/page/component/members_filter.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Component
- module MembersFilter
- extend QA::Page::PageConcern
-
- def self.included(base)
- super
-
- base.view 'app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue' do
- element :search_bar_input
- element :search_button
- end
- end
-
- def search_member(username)
- fill_element :search_bar_input, username
- click_element :search_button
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/group/members.rb b/qa/qa/page/group/members.rb
index 376f65ee8ac..7756d3d7f08 100644
--- a/qa/qa/page/group/members.rb
+++ b/qa/qa/page/group/members.rb
@@ -4,96 +4,8 @@ module QA
module Page
module Group
class Members < Page::Base
- include Page::Component::InviteMembersModal
- include Page::Component::MembersFilter
-
- view 'app/assets/javascripts/members/components/modals/remove_member_modal.vue' do
- element :remove_member_modal
- element :remove_member_button
- end
-
- view 'app/assets/javascripts/pages/groups/group_members/index.js' do
- element :member_row
- element :groups_list
- element :group_row
- end
-
- view 'app/assets/javascripts/members/components/table/role_dropdown.vue' do
- element :access_level_dropdown
- element :access_level_link
- end
-
- view 'app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue' do
- element :user_action_dropdown
- end
-
- view 'app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue' do
- element :delete_member_dropdown_item
- end
-
- view 'app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue' do
- element :approve_access_request_button
- end
-
- view 'app/assets/javascripts/members/components/members_tabs.vue' do
- element :groups_list_tab
- end
-
- def update_access_level(username, access_level)
- search_member(username)
-
- within_element(:member_row, text: username) do
- click_element :access_level_dropdown
- click_element :access_level_link, text: access_level
- end
- end
-
- def remove_member(username)
- within_element(:member_row, text: username) do
- click_element :user_action_dropdown
- click_element :delete_member_dropdown_item
- end
-
- confirm_remove_member
- end
-
- def deny_access_request(username)
- within_element(:member_row, text: username) do
- click_element :delete_member_button
- end
-
- confirm_remove_member
- end
-
- def approve_access_request(username)
- within_element(:member_row, text: username) do
- click_element :approve_access_request_button
- end
- end
-
- def has_existing_group_share?(group_name)
- click_element :groups_list_tab
-
- within_element(:groups_list) do
- has_element?(:group_row, text: group_name)
- end
- end
-
- private
-
- def confirm_remove_member
- within_element(:remove_member_modal) do
- wait_for_enabled_remove_member_button
-
- click_element :remove_member_button
- end
- end
-
- def wait_for_enabled_remove_member_button
- retry_until(sleep_interval: 1, message: 'Waiting for remove member button to be enabled') do
- has_element?(:remove_member_button, disabled: false, wait: 3)
- end
- end
+ include Page::Component::Members::InviteMembersModal
+ include Page::Component::Members::MembersTable
end
end
end
diff --git a/qa/qa/page/project/members.rb b/qa/qa/page/project/members.rb
index 4692f3621b8..463e3ca6fca 100644
--- a/qa/qa/page/project/members.rb
+++ b/qa/qa/page/project/members.rb
@@ -4,49 +4,8 @@ module QA
module Page
module Project
class Members < Page::Base
- include QA::Page::Component::InviteMembersModal
- include QA::Page::Component::MembersFilter
-
- view 'app/assets/javascripts/members/components/members_tabs.vue' do
- element :groups_list_tab
- end
-
- view 'app/assets/javascripts/invite_members/components/invite_group_trigger.vue' do
- element :invite_a_group_button
- end
-
- view 'app/assets/javascripts/invite_members/constants.js' do
- element :invite_members_button
- end
-
- view 'app/assets/javascripts/pages/projects/project_members/index.js' do
- element :group_row
- end
-
- view 'app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue' do
- element :delete_group_access_link
- end
-
- view 'app/assets/javascripts/members/components/modals/remove_group_link_modal.vue' do
- element :remove_group_link_modal_content
- end
-
- def remove_group(group_name)
- click_element :groups_list_tab
-
- within_element(:group_row, text: group_name) do
- click_element :delete_group_access_link
- end
-
- within_element(:remove_group_link_modal_content) do
- click_button 'Remove group'
- end
- end
-
- def has_group?(group_name)
- click_element :groups_list_tab
- has_element?(:group_row, text: group_name)
- end
+ include Page::Component::Members::InviteMembersModal
+ include Page::Component::Members::MembersTable
end
end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index fa46a4aab2d..9184cd2263e 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -611,7 +611,6 @@ RSpec.describe GroupsController, factory_default: :keep, feature_category: :code
expect(response.body).to have_content('Open Merged Closed All')
expect(response.body).not_to have_content('Open 0 Merged 0 Closed 0 All 0')
- expect(assigns(:can_bulk_update)).to be_falsey
end
end
end
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index 6090d132e3a..01457f77121 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :projects do
stub_feature_flags(show_pages_in_deployments_menu: false)
stub_config(registry: { enabled: false })
- stub_feature_flags(harbor_registry_integration: false)
+ stub_feature_flags(harbor_registry_integration: false, ml_experiment_tracking: false)
insert_package_nav(_('Deployments'))
insert_infrastructure_registry_nav
insert_infrastructure_google_cloud_nav
@@ -97,4 +97,16 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :projects do
it_behaves_like 'verified navigation bar'
end
+
+ context 'when models experiments is available' do
+ before do
+ stub_feature_flags(ml_experiment_tracking: true)
+
+ insert_model_experiments_nav(_('Infrastructure Registry'))
+
+ visit project_path(project)
+ end
+
+ it_behaves_like 'verified navigation bar'
+ end
end
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index f7500709d0e..3e6780d6831 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -45,118 +45,6 @@ RSpec.describe NamespacesHelper do
user_group.add_owner(user)
end
- describe '#namespaces_options' do
- context 'when admin mode is enabled', :enable_admin_mode do
- it 'returns groups without being a member for admin' do
- allow(helper).to receive(:current_user).and_return(admin)
-
- options = helper.namespaces_options(user_group.id, display_path: true, extra_group: user_group.id)
-
- expect(options).to include(admin_group.name)
- expect(options).to include(user_group.name)
- end
- end
-
- context 'when admin mode is disabled' do
- it 'returns only allowed namespaces for admin' do
- allow(helper).to receive(:current_user).and_return(admin)
-
- options = helper.namespaces_options(user_group.id, display_path: true, extra_group: user_group.id)
-
- expect(options).to include(admin_group.name)
- expect(options).not_to include(user_group.name)
- end
- end
-
- it 'returns only allowed namespaces for user' do
- allow(helper).to receive(:current_user).and_return(user)
-
- options = helper.namespaces_options
-
- expect(options).not_to include(admin_group.name)
- expect(options).to include(user_group.name)
- expect(options).to include(user.name)
- end
-
- it 'avoids duplicate groups when extra_group is used' do
- allow(helper).to receive(:current_user).and_return(admin)
-
- options = helper.namespaces_options(user_group.id, display_path: true, extra_group: build(:group, name: admin_group.name))
-
- expect(options.scan("data-name=\"#{admin_group.name}\"").count).to eq(1)
- expect(options).to include(admin_group.name)
- end
-
- context 'when admin mode is disabled' do
- it 'selects existing group' do
- allow(helper).to receive(:current_user).and_return(admin)
- user_group.add_owner(admin)
-
- options = helper.namespaces_options(:extra_group, display_path: true, extra_group: user_group)
-
- expect(options).to include("selected=\"selected\" value=\"#{user_group.id}\"")
- expect(options).to include(admin_group.name)
- end
- end
-
- it 'selects the new group by default' do
- # Ensure we don't select a group with the same name
- create(:group, name: 'new-group', path: 'another-path')
-
- allow(helper).to receive(:current_user).and_return(user)
-
- options = helper.namespaces_options(:extra_group, display_path: true, extra_group: build(:group, name: 'new-group', path: 'new-group'))
-
- expect(options).to include(user_group.name)
- expect(options).not_to include(admin_group.name)
- expect(options).to include("selected=\"selected\" value=\"-1\"")
- end
-
- it 'falls back to current user selection' do
- allow(helper).to receive(:current_user).and_return(user)
-
- options = helper.namespaces_options(:extra_group, display_path: true, extra_group: build(:group, name: admin_group.name))
-
- expect(options).to include(user_group.name)
- expect(options).not_to include(admin_group.name)
- expect(options).to include("selected=\"selected\" value=\"#{user.namespace.id}\"")
- end
-
- it 'returns only groups if groups_only option is true' do
- allow(helper).to receive(:current_user).and_return(user)
-
- options = helper.namespaces_options(nil, groups_only: true)
-
- expect(options).not_to include(user.name)
- expect(options).to include(user_group.name)
- end
-
- context 'when nested groups are available' do
- it 'includes groups nested in groups the user can administer' do
- allow(helper).to receive(:current_user).and_return(user)
- child_group = create(:group, :private, parent: user_group)
-
- options = helper.namespaces_options
-
- expect(options).to include(child_group.name)
- end
-
- it 'orders the groups correctly' do
- allow(helper).to receive(:current_user).and_return(user)
- child_group = create(:group, :private, parent: user_group)
- other_child = create(:group, :private, parent: user_group)
- sub_child = create(:group, :private, parent: child_group)
-
- expect(helper).to receive(:options_for_group)
- .with([user_group, child_group, sub_child, other_child], anything)
- .and_call_original
- allow(helper).to receive(:options_for_group).and_call_original
-
- helper.namespaces_options
- end
- end
- end
-
describe '#cascading_namespace_settings_popover_data' do
attribute = :delayed_project_removal
diff --git a/spec/lib/api/ci/helpers/runner_spec.rb b/spec/lib/api/ci/helpers/runner_spec.rb
index 7f711ba1ca0..8264db8344d 100644
--- a/spec/lib/api/ci/helpers/runner_spec.rb
+++ b/spec/lib/api/ci/helpers/runner_spec.rb
@@ -78,12 +78,6 @@ RSpec.describe API::Ci::Helpers::Runner do
stub_feature_flags(create_runner_machine: true)
end
- it 'does not return runner machine if no system_id specified' do
- allow(helper).to receive(:params).and_return(token: runner.token)
-
- is_expected.to be_nil
- end
-
context 'when runner machine already exists' do
before do
allow(helper).to receive(:params).and_return(token: runner.token, system_id: runner_machine.system_xid)
@@ -96,15 +90,27 @@ RSpec.describe API::Ci::Helpers::Runner do
end
end
- it 'creates a new runner machine if one could be not be found', :aggregate_failures do
- allow(helper).to receive(:params).and_return(token: runner.token, system_id: 'new_system_id')
+ context 'when runner machine cannot be found' do
+ it 'creates a new runner machine', :aggregate_failures do
+ allow(helper).to receive(:params).and_return(token: runner.token, system_id: 'new_system_id')
+
+ expect { current_runner_machine }.to change { Ci::RunnerMachine.count }.by(1)
+
+ expect(current_runner_machine).not_to be_nil
+ expect(current_runner_machine.system_xid).to eq('new_system_id')
+ expect(current_runner_machine.contacted_at).to eq(Time.current)
+ expect(current_runner_machine.runner).to eq(runner)
+ end
+
+ it 'creates a new <legacy> runner machine if system_id is not specified', :aggregate_failures do
+ allow(helper).to receive(:params).and_return(token: runner.token)
- expect { current_runner_machine }.to change { Ci::RunnerMachine.count }.by(1)
+ expect { current_runner_machine }.to change { Ci::RunnerMachine.count }.by(1)
- expect(current_runner_machine).not_to be_nil
- expect(current_runner_machine.system_xid).to eq('new_system_id')
- expect(current_runner_machine.contacted_at).to eq(Time.current)
- expect(current_runner_machine.runner).to eq(runner)
+ expect(current_runner_machine).not_to be_nil
+ expect(current_runner_machine.system_xid).to eq(::API::Ci::Helpers::Runner::LEGACY_SYSTEM_XID)
+ expect(current_runner_machine.runner).to eq(runner)
+ end
end
end
diff --git a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
index b03269c424a..22847ab3c8f 100644
--- a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
before do
stub_container_registry_config(enabled: registry_enabled)
stub_config(packages: { enabled: packages_enabled })
- stub_feature_flags(harbor_registry_integration: false)
+ stub_feature_flags(harbor_registry_integration: false, ml_experiment_tracking: false)
end
context 'when Packages Registry is visible' do
@@ -176,5 +176,25 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
end
end
end
+
+ describe 'Model experiments' do
+ let(:item_id) { :model_experiments }
+
+ context 'when :ml_experiment_tracking is enabled' do
+ it 'shows the menu item' do
+ stub_feature_flags(ml_experiment_tracking: true)
+
+ is_expected.not_to be_nil
+ end
+ end
+
+ context 'when :ml_experiment_tracking is disabled' do
+ it 'does not show the menu item' do
+ stub_feature_flags(ml_experiment_tracking: false)
+
+ is_expected.to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/mailers/emails/service_desk_spec.rb b/spec/mailers/emails/service_desk_spec.rb
index 1f55aabc535..25afa8b48ce 100644
--- a/spec/mailers/emails/service_desk_spec.rb
+++ b/spec/mailers/emails/service_desk_spec.rb
@@ -9,11 +9,13 @@ RSpec.describe Emails::ServiceDesk do
include EmailHelpers
include_context 'gitlab email notification'
+ include_context 'with service desk mailer'
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:email) { 'someone@gitlab.com' }
+ let_it_be(:expected_unsubscribe_url) { unsubscribe_sent_notification_url('b7721fc7e8419911a8bea145236a0519') }
let(:template) { double(content: template_content) }
@@ -21,43 +23,6 @@ RSpec.describe Emails::ServiceDesk do
issue.issue_email_participants.create!(email: email)
end
- before do
- stub_const('ServiceEmailClass', Class.new(ApplicationMailer))
-
- ServiceEmailClass.class_eval do
- include GitlabRoutingHelper
- include EmailsHelper
- include Emails::ServiceDesk
-
- helper GitlabRoutingHelper
- helper EmailsHelper
-
- # this method is implemented in Notify class, we don't need to test it
- def reply_key
- 'test-key'
- end
-
- # this method is implemented in Notify class, we don't need to test it
- def sender(author_id, params = {})
- author_id
- end
-
- # this method is implemented in Notify class
- #
- # We do not need to test the Notify method, it is already tested in notify_spec
- def mail_new_thread(issue, options)
- # we need to rewrite this in order to look up templates in the correct directory
- self.class.mailer_name = 'notify'
-
- # this is needed for default layout
- @unsubscribe_url = 'http://unsubscribe.example.com'
-
- mail(options)
- end
- alias_method :mail_answer_thread, :mail_new_thread
- end
- end
-
shared_examples 'handle template content' do |template_key, attachments_count|
before do
expect(Gitlab::Template::ServiceDeskTemplate).to receive(:find)
@@ -134,13 +99,31 @@ RSpec.describe Emails::ServiceDesk do
it_behaves_like 'handle template content', 'thank_you'
end
- context 'with an issue id and issue path placeholders' do
- let(:template_content) { 'thank you, **your new issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}' }
- let(:expected_body) { "thank you, <strong>your new issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}" }
+ context 'with an issue id, issue path and unsubscribe url placeholders' do
+ let(:template_content) do
+ 'thank you, **your new issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}' \
+ '[Unsubscribe](%{UNSUBSCRIBE_URL})'
+ end
+
+ let(:expected_body) do
+ "<p dir=\"auto\">thank you, <strong>your new issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}" \
+ "<a href=\"#{expected_unsubscribe_url}\">Unsubscribe</a></p>"
+ end
it_behaves_like 'handle template content', 'thank_you'
end
+ context 'with header and footer placeholders' do
+ let(:template_content) do
+ '%{SYSTEM_HEADER}' \
+ 'thank you, **your new issue** has been created.' \
+ '%{SYSTEM_FOOTER}'
+ end
+
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+ end
+
context 'with an issue id placeholder with whitespace' do
let(:template_content) { 'thank you, **your new issue:** %{ ISSUE_ID}' }
let(:expected_body) { "thank you, <strong>your new issue:</strong> ##{issue.iid}" }
@@ -174,13 +157,31 @@ RSpec.describe Emails::ServiceDesk do
it_behaves_like 'handle template content', 'new_note'
end
- context 'with an issue id, issue path and note placeholders' do
- let(:template_content) { 'thank you, **new note on issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}: %{NOTE_TEXT}' }
- let(:expected_body) { "thank you, <strong>new note on issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}: #{note.note}" }
+ context 'with an issue id, issue path, note and unsubscribe url placeholders' do
+ let(:template_content) do
+ 'thank you, **new note on issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}: %{NOTE_TEXT}' \
+ '[Unsubscribe](%{UNSUBSCRIBE_URL})'
+ end
+
+ let(:expected_body) do
+ "<p dir=\"auto\">thank you, <strong>new note on issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}: #{note.note}" \
+ "<a href=\"#{expected_unsubscribe_url}\">Unsubscribe</a></p>"
+ end
it_behaves_like 'handle template content', 'new_note'
end
+ context 'with header and footer placeholders' do
+ let(:template_content) do
+ '%{SYSTEM_HEADER}' \
+ 'thank you, **your new issue** has been created.' \
+ '%{SYSTEM_FOOTER}'
+ end
+
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+ end
+
context 'with an issue id placeholder with whitespace' do
let(:template_content) { 'thank you, **new note on issue:** %{ ISSUE_ID}: %{ NOTE_TEXT }' }
let(:expected_body) { "thank you, <strong>new note on issue:</strong> ##{issue.iid}: #{note.note}" }
diff --git a/spec/models/ci/runner_machine_spec.rb b/spec/models/ci/runner_machine_spec.rb
index 79d383ad182..d0979d8a485 100644
--- a/spec/models/ci/runner_machine_spec.rb
+++ b/spec/models/ci/runner_machine_spec.rb
@@ -52,4 +52,146 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
is_expected.to match_array([runner_machine1.id, runner_machine2.id])
end
end
+
+ describe '#heartbeat', :freeze_time do
+ let(:runner_machine) { create(:ci_runner_machine) }
+ let(:executor) { 'shell' }
+ let(:version) { '15.0.1' }
+ let(:values) do
+ {
+ ip_address: '8.8.8.8',
+ architecture: '18-bit',
+ config: { gpus: "all" },
+ executor: executor,
+ version: version
+ }
+ end
+
+ subject(:heartbeat) do
+ runner_machine.heartbeat(values)
+ end
+
+ context 'when database was updated recently' do
+ before do
+ runner_machine.contacted_at = Time.current
+ end
+
+ it 'schedules version update' do
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version).once
+
+ heartbeat
+
+ expect(runner_machine.runner_version).to be_nil
+ end
+
+ it 'updates cache' do
+ expect_redis_update
+
+ heartbeat
+ end
+
+ context 'with only ip_address specified' do
+ let(:values) do
+ { ip_address: '1.1.1.1' }
+ end
+
+ it 'updates only ip_address' do
+ attrs = Gitlab::Json.dump(ip_address: '1.1.1.1', contacted_at: Time.current)
+
+ Gitlab::Redis::Cache.with do |redis|
+ redis_key = runner_machine.send(:cache_attribute_key)
+ expect(redis).to receive(:set).with(redis_key, attrs, any_args)
+ end
+
+ heartbeat
+ end
+ end
+ end
+
+ context 'when database was not updated recently' do
+ before do
+ runner_machine.contacted_at = 2.hours.ago
+
+ allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version).once
+ end
+
+ context 'with invalid runner_machine' do
+ before do
+ runner_machine.runner = nil
+ end
+
+ it 'still updates redis cache and database' do
+ expect(runner_machine).to be_invalid
+
+ expect_redis_update
+ does_db_update
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
+ .with(version).once
+ end
+ end
+
+ context 'with unchanged runner_machine version' do
+ let(:runner_machine) { create(:ci_runner_machine, version: version) }
+
+ it 'does not schedule ci_runner_versions update' do
+ heartbeat
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
+ end
+ end
+
+ it 'updates redis cache and database' do
+ expect_redis_update
+ does_db_update
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
+ .with(version).once
+ end
+
+ Ci::Runner::EXECUTOR_NAME_TO_TYPES.each_key do |executor|
+ context "with #{executor} executor" do
+ let(:executor) { executor }
+
+ it 'updates with expected executor type' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner_machine.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
+ end
+
+ def expected_executor_type
+ executor.gsub(/[+-]/, '_')
+ end
+ end
+ end
+
+ context "with an unknown executor type" do
+ let(:executor) { 'some-unknown-type' }
+
+ it 'updates with unknown executor type' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner_machine.reload.read_attribute(:executor_type)).to eq('unknown')
+ end
+ end
+ end
+
+ def expect_redis_update
+ Gitlab::Redis::Cache.with do |redis|
+ redis_key = runner_machine.send(:cache_attribute_key)
+ expect(redis).to receive(:set).with(redis_key, anything, any_args).and_call_original
+ end
+ end
+
+ def does_db_update
+ expect { heartbeat }.to change { runner_machine.reload.read_attribute(:contacted_at) }
+ .and change { runner_machine.reload.read_attribute(:architecture) }
+ .and change { runner_machine.reload.read_attribute(:config) }
+ .and change { runner_machine.reload.read_attribute(:executor_type) }
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 7b5b8acdb66..3fb867f8ee8 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2404,21 +2404,6 @@ RSpec.describe User, feature_category: :user_profile do
it_behaves_like 'manageable groups examples'
end
end
-
- describe '#manageable_groups_with_routes' do
- it 'eager loads routes from manageable groups' do
- control_count =
- ActiveRecord::QueryRecorder.new(skip_cached: false) do
- user.manageable_groups_with_routes.map(&:route)
- end.count
-
- create(:group, parent: subgroup)
-
- expect do
- user.manageable_groups_with_routes.map(&:route)
- end.not_to exceed_all_query_limit(control_count)
- end
- end
end
end
diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb
index 22817922b1b..ef3b38e3fc4 100644
--- a/spec/requests/api/ci/runner/jobs_put_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_put_spec.rb
@@ -21,11 +21,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let_it_be(:project) { create(:project, namespace: group, shared_runners_enabled: false) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
+ let_it_be(:runner_machine) { create(:ci_runner_machine, runner: runner) }
let_it_be(:user) { create(:user) }
describe 'PUT /api/v4/jobs/:id' do
let_it_be_with_reload(:job) do
- create(:ci_build, :pending, :trace_live, pipeline: pipeline, project: project, user: user, runner_id: runner.id)
+ create(:ci_build, :pending, :trace_live, pipeline: pipeline, project: project, user: user,
+ runner_id: runner.id, runner_machine: runner_machine)
end
before do
@@ -38,6 +40,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
it 'updates runner info' do
expect { update_job(state: 'success') }.to change { runner.reload.contacted_at }
+ .and change { runner_machine.reload.contacted_at }
end
context 'when status is given' do
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index 618adeb7db0..6e721d40560 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -122,25 +122,56 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
context 'when system_id parameter is specified' do
subject(:request) { request_job(**args) }
- context 'when ci_runner_machines with same system_xid does not exist' do
- let(:args) { { system_id: 's_some_system_id' } }
+ context 'with create_runner_machine FF enabled' do
+ before do
+ stub_feature_flags(create_runner_machine: true)
+ end
+
+ context 'when ci_runner_machines with same system_xid does not exist' do
+ let(:args) { { system_id: 's_some_system_id' } }
+
+ it 'creates respective ci_runner_machines record', :freeze_time do
+ expect { request }.to change { runner.runner_machines.reload.count }.from(0).to(1)
+
+ machine = runner.runner_machines.last
+ expect(machine.system_xid).to eq args[:system_id]
+ expect(machine.runner).to eq runner
+ expect(machine.contacted_at).to eq Time.current
+ end
+ end
+
+ context 'when ci_runner_machines with same system_xid already exists', :freeze_time do
+ let(:args) { { system_id: 's_existing_system_id' } }
+ let!(:runner_machine) do
+ create(:ci_runner_machine, runner: runner, system_xid: args[:system_id], contacted_at: 1.hour.ago)
+ end
- it 'creates respective ci_runner_machines record', :freeze_time do
- expect { request }.to change { runner.runner_machines.reload.count }.from(0).to(1)
+ it 'does not create new ci_runner_machines record' do
+ expect { request }.not_to change { Ci::RunnerMachine.count }
+ end
+
+ it 'updates the contacted_at field' do
+ request
- machine = runner.runner_machines.last
- expect(machine.system_xid).to eq args[:system_id]
- expect(machine.runner).to eq runner
- expect(machine.contacted_at).to eq Time.current
+ expect(runner_machine.reload.contacted_at).to eq Time.current
+ end
end
end
- context 'when ci_runner_machines with same system_xid already exists' do
- let(:args) { { system_id: 's_existing_system_id' } }
- let!(:runner_machine) { create(:ci_runner_machine, runner: runner, system_xid: args[:system_id]) }
+ context 'with create_runner_machine FF disabled' do
+ before do
+ stub_feature_flags(create_runner_machine: false)
+ end
- it 'does not create new ci_runner_machines record' do
- expect { request }.not_to change { Ci::RunnerMachine.count }
+ context 'when ci_runner_machines with same system_xid does not exist' do
+ let(:args) { { system_id: 's_some_system_id' } }
+
+ it 'does not create respective ci_runner_machines record', :freeze_time, :aggregate_failures do
+ expect { request }.not_to change { runner.runner_machines.reload.count }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(runner.runner_machines).to be_empty
+ end
end
end
end
diff --git a/spec/requests/api/ci/runner/runners_verify_post_spec.rb b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
index 22a954cc444..3853e7ed6c9 100644
--- a/spec/requests/api/ci/runner/runners_verify_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
@@ -19,6 +19,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
describe '/api/v4/runners' do
describe 'POST /api/v4/runners/verify' do
let(:runner) { create(:ci_runner) }
+ let(:params) {}
+
+ subject(:verify) { post api('/runners/verify'), params: params }
context 'when no token is provided' do
it 'returns 400 error' do
@@ -29,46 +32,84 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
context 'when invalid token is provided' do
+ let(:params) { { token: 'invalid-token' } }
+
it 'returns 403 error' do
- post api('/runners/verify'), params: { token: 'invalid-token' }
+ verify
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when valid token is provided' do
- subject { post api('/runners/verify'), params: { token: runner.token } }
+ let(:params) { { token: runner.token } }
- it 'verifies Runner credentials' do
- subject
+ context 'with create_runner_machine FF enabled' do
+ before do
+ stub_feature_flags(create_runner_machine: true)
+ end
- expect(response).to have_gitlab_http_status(:ok)
+ it 'verifies Runner credentials' do
+ verify
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it_behaves_like 'storing arguments in the application context for the API' do
+ let(:expected_params) { { client_id: "runner/#{runner.id}" } }
+ end
+
+ context 'when system_id is provided' do
+ let(:params) { { token: runner.token, system_id: 's_some_system_id' } }
+
+ it 'creates a runner_machine' do
+ expect { verify }.to change { Ci::RunnerMachine.count }.by(1)
+ end
+ end
end
- it_behaves_like 'storing arguments in the application context for the API' do
- let(:expected_params) { { client_id: "runner/#{runner.id}" } }
+ context 'with create_runner_machine FF disabled' do
+ before do
+ stub_feature_flags(create_runner_machine: false)
+ end
+
+ it 'verifies Runner credentials' do
+ verify
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'when system_id is provided' do
+ let(:params) { { token: runner.token, system_id: 's_some_system_id' } }
+
+ it 'does not create a runner_machine', :aggregate_failures do
+ expect { verify }.not_to change { Ci::RunnerMachine.count }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
end
context 'when non-expired token is provided' do
- subject { post api('/runners/verify'), params: { token: runner.token } }
+ let(:params) { { token: runner.token } }
it 'verifies Runner credentials' do
runner["token_expires_at"] = 10.days.from_now
runner.save!
- subject
+ verify
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when expired token is provided' do
- subject { post api('/runners/verify'), params: { token: runner.token } }
+ let(:params) { { token: runner.token } }
it 'does not verify Runner credentials' do
runner["token_expires_at"] = 10.days.ago
runner.save!
- subject
+ verify
expect(response).to have_gitlab_http_status(:forbidden)
end
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index e209ad2b2d5..c3f99872cef 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -1215,11 +1215,23 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
end
context 'with a project milestone' do
- let(:milestone_params) { { milestones: [milestone.title] } }
+ shared_examples 'adds milestone' do
+ it 'adds the milestone' do
+ expect(response).to have_gitlab_http_status(:created)
+ expect(returned_milestones).to match_array(['v1.0'])
+ end
+ end
- it 'adds the milestone' do
- expect(response).to have_gitlab_http_status(:created)
- expect(returned_milestones).to match_array(['v1.0'])
+ context 'by title' do
+ let(:milestone_params) { { milestones: [milestone.title] } }
+
+ it_behaves_like 'adds milestone'
+ end
+
+ context 'by id' do
+ let(:milestone_params) { { milestone_ids: [milestone.id] } }
+
+ it_behaves_like 'adds milestone'
end
end
@@ -1408,18 +1420,14 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
context 'when a milestone is passed in' do
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
- let(:milestone_title) { milestone.title }
- let(:params) { { milestones: [milestone_title] } }
+ let!(:milestone2) { create(:milestone, project: project, title: 'v2.0') }
before do
release.milestones << milestone
end
- context 'a different milestone' do
- let(:milestone_title) { 'v2.0' }
- let!(:milestone2) { create(:milestone, project: project, title: milestone_title) }
-
- it 'replaces the milestone' do
+ shared_examples 'updates milestone' do
+ it 'updates the milestone' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1427,8 +1435,20 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
end
end
+ context 'by title' do
+ let(:params) { { milestones: [milestone2.title] } }
+
+ it_behaves_like 'updates milestone'
+ end
+
+ context 'by id' do
+ let(:params) { { milestone_ids: [milestone2.id] } }
+
+ it_behaves_like 'updates milestone'
+ end
+
context 'an identical milestone' do
- let(:milestone_title) { 'v1.0' }
+ let(:params) { { milestones: [milestone.title] } }
it 'does not change the milestone' do
subject
@@ -1439,7 +1459,7 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
end
context 'an empty milestone' do
- let(:milestone_title) { nil }
+ let(:params) { { milestones: [] } }
it 'removes the milestone' do
subject
@@ -1476,13 +1496,26 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
context 'with all new' do
let!(:milestone2) { create(:milestone, project: project, title: 'milestone2') }
let!(:milestone3) { create(:milestone, project: project, title: 'milestone3') }
- let(:params) { { milestones: [milestone2.title, milestone3.title] } }
- it 'replaces the milestones' do
- subject
+ shared_examples 'update milestones' do
+ it 'replaces the milestones' do
+ subject
- expect(response).to have_gitlab_http_status(:ok)
- expect(returned_milestones).to match_array(%w(milestone2 milestone3))
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(returned_milestones).to match_array(%w(milestone2 milestone3))
+ end
+ end
+
+ context 'by title' do
+ let(:params) { { milestones: [milestone2.title, milestone3.title] } }
+
+ it_behaves_like 'update milestones'
+ end
+
+ context 'by id' do
+ let(:params) { { milestone_ids: [milestone2.id, milestone3.id] } }
+
+ it_behaves_like 'update milestones'
end
end
end
diff --git a/spec/services/pages/destroy_deployments_service_spec.rb b/spec/services/pages/destroy_deployments_service_spec.rb
index 0f8e8b6573e..0ca8cbbb681 100644
--- a/spec/services/pages/destroy_deployments_service_spec.rb
+++ b/spec/services/pages/destroy_deployments_service_spec.rb
@@ -2,28 +2,26 @@
require 'spec_helper'
-RSpec.describe Pages::DestroyDeploymentsService do
- let(:project) { create(:project) }
+RSpec.describe Pages::DestroyDeploymentsService, feature_category: :pages do
+ let_it_be(:project) { create(:project) }
let!(:old_deployments) { create_list(:pages_deployment, 2, project: project) }
let!(:last_deployment) { create(:pages_deployment, project: project) }
let!(:newer_deployment) { create(:pages_deployment, project: project) }
let!(:deployment_from_another_project) { create(:pages_deployment) }
it 'destroys all deployments of the project' do
- expect do
- described_class.new(project).execute
- end.to change { PagesDeployment.count }.by(-4)
+ expect { described_class.new(project).execute }
+ .to change { PagesDeployment.count }.by(-4)
- expect(deployment_from_another_project.reload).to be
+ expect(deployment_from_another_project.reload).to be_persisted
end
it 'destroy only deployments older than last deployment if it is provided' do
- expect do
- described_class.new(project, last_deployment.id).execute
- end.to change { PagesDeployment.count }.by(-2)
+ expect { described_class.new(project, last_deployment.id).execute }
+ .to change { PagesDeployment.count }.by(-2)
- expect(last_deployment.reload).to be
- expect(newer_deployment.reload).to be
- expect(deployment_from_another_project.reload).to be
+ expect(last_deployment.reload).to be_persisted
+ expect(newer_deployment.reload).to be_persisted
+ expect(deployment_from_another_project.reload).to be_persisted
end
end
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 5f49eed3e77..9768ceb12e8 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Releases::CreateService do
+RSpec.describe Releases::CreateService, feature_category: :continuous_integration do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:tag_name) { project.repository.tag_names.first }
@@ -132,6 +132,15 @@ RSpec.describe Releases::CreateService do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}")
end
+
+ it 'raises an error saying the milestone id is inexistent' do
+ inexistent_milestone_id = non_existing_record_id
+ service = described_class.new(project, user, params.merge!({ milestone_ids: [inexistent_milestone_id] }))
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Milestone id(s) not found: #{inexistent_milestone_id}")
+ end
end
context 'when existing milestone is passed in' do
@@ -140,15 +149,27 @@ RSpec.describe Releases::CreateService do
let(:params_with_milestone) { params.merge!({ milestones: [title] }) }
let(:service) { described_class.new(milestone.project, user, params_with_milestone) }
- it 'creates a release and ties this milestone to it' do
- result = service.execute
+ shared_examples 'creates release' do
+ it 'creates a release and ties this milestone to it' do
+ result = service.execute
- expect(project.releases.count).to eq(1)
- expect(result[:status]).to eq(:success)
+ expect(project.releases.count).to eq(1)
+ expect(result[:status]).to eq(:success)
+
+ release = project.releases.last
+
+ expect(release.milestones).to match_array([milestone])
+ end
+ end
- release = project.releases.last
+ context 'by title' do
+ it_behaves_like 'creates release'
+ end
+
+ context 'by ids' do
+ let(:params_with_milestone) { params.merge!({ milestone_ids: [milestone.id] }) }
- expect(release.milestones).to match_array([milestone])
+ it_behaves_like 'creates release'
end
context 'when another release was previously created with that same milestone linked' do
@@ -164,18 +185,31 @@ RSpec.describe Releases::CreateService do
end
end
- context 'when multiple existing milestone titles are passed in' do
+ context 'when multiple existing milestones are passed in' do
let(:title_1) { 'v1.0' }
let(:title_2) { 'v1.0-rc' }
let!(:milestone_1) { create(:milestone, :active, project: project, title: title_1) }
let!(:milestone_2) { create(:milestone, :active, project: project, title: title_2) }
- let!(:params_with_milestones) { params.merge!({ milestones: [title_1, title_2] }) }
- it 'creates a release and ties it to these milestones' do
- described_class.new(project, user, params_with_milestones).execute
- release = project.releases.last
+ shared_examples 'creates multiple releases' do
+ it 'creates a release and ties it to these milestones' do
+ described_class.new(project, user, params_with_milestones).execute
+ release = project.releases.last
+
+ expect(release.milestones.map(&:title)).to include(title_1, title_2)
+ end
+ end
+
+ context 'by title' do
+ let!(:params_with_milestones) { params.merge!({ milestones: [title_1, title_2] }) }
+
+ it_behaves_like 'creates multiple releases'
+ end
+
+ context 'by ids' do
+ let!(:params_with_milestones) { params.merge!({ milestone_ids: [milestone_1.id, milestone_2.id] }) }
- expect(release.milestones.map(&:title)).to include(title_1, title_2)
+ it_behaves_like 'creates multiple releases'
end
end
@@ -198,6 +232,17 @@ RSpec.describe Releases::CreateService do
service.execute
end.not_to change(Release, :count)
end
+
+ context 'with milestones as ids' do
+ let!(:params_with_milestones) { params.merge!({ milestone_ids: [milestone.id, non_existing_record_id] }) }
+
+ it 'raises an error' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Milestone id(s) not found: #{non_existing_record_id}")
+ end
+ end
end
context 'no milestone association behavior' do
diff --git a/spec/services/releases/update_service_spec.rb b/spec/services/releases/update_service_spec.rb
index 7461470a844..6bddea48251 100644
--- a/spec/services/releases/update_service_spec.rb
+++ b/spec/services/releases/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Releases::UpdateService do
+RSpec.describe Releases::UpdateService, feature_category: :continuous_integration do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:new_name) { 'A new name' }
@@ -60,18 +60,22 @@ RSpec.describe Releases::UpdateService do
release.milestones << milestone
end
- context 'a different milestone' do
- let(:new_title) { 'v2.0' }
-
+ shared_examples 'updates milestones' do
it 'updates the related milestone accordingly' do
- result = service.execute
release.reload
+ result = service.execute
expect(release.milestones.first.title).to eq(new_title)
expect(result[:milestones_updated]).to be_truthy
end
end
+ context 'a different milestone' do
+ let(:new_title) { 'v2.0' }
+
+ it_behaves_like 'updates milestones'
+ end
+
context 'an identical milestone' do
let(:new_title) { 'v1.0' }
@@ -79,11 +83,17 @@ RSpec.describe Releases::UpdateService do
expect { service.execute }.to raise_error(ActiveRecord::RecordInvalid)
end
end
+
+ context 'by ids' do
+ let(:new_title) { 'v2.0' }
+ let(:params_with_milestone) { params.merge!({ milestone_ids: [new_milestone.id] }) }
+
+ it_behaves_like 'updates milestones'
+ end
end
context "when an 'empty' milestone is passed in" do
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
- let(:params_with_empty_milestone) { params.merge!({ milestones: [] }) }
before do
release.milestones << milestone
@@ -91,12 +101,26 @@ RSpec.describe Releases::UpdateService do
service.params = params_with_empty_milestone
end
- it 'removes the old milestone and does not associate any new milestone' do
- result = service.execute
- release.reload
+ shared_examples 'removes milestones' do
+ it 'removes the old milestone and does not associate any new milestone' do
+ result = service.execute
+ release.reload
+
+ expect(release.milestones).not_to be_present
+ expect(result[:milestones_updated]).to be_truthy
+ end
+ end
- expect(release.milestones).not_to be_present
- expect(result[:milestones_updated]).to be_truthy
+ context 'by title' do
+ let(:params_with_empty_milestone) { params.merge!({ milestones: [] }) }
+
+ it_behaves_like 'removes milestones'
+ end
+
+ context 'by id' do
+ let(:params_with_empty_milestone) { params.merge!({ milestone_ids: [] }) }
+
+ it_behaves_like 'removes milestones'
end
end
@@ -104,22 +128,35 @@ RSpec.describe Releases::UpdateService do
let(:new_title_1) { 'v2.0' }
let(:new_title_2) { 'v2.0-rc' }
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
- let(:params_with_milestones) { params.merge!({ milestones: [new_title_1, new_title_2] }) }
let(:service) { described_class.new(project, user, params_with_milestones) }
+ let!(:new_milestone_1) { create(:milestone, project: project, title: new_title_1) }
+ let!(:new_milestone_2) { create(:milestone, project: project, title: new_title_2) }
before do
- create(:milestone, project: project, title: new_title_1)
- create(:milestone, project: project, title: new_title_2)
release.milestones << milestone
end
- it 'removes the old milestone and update the release with the new ones' do
- result = service.execute
- release.reload
+ shared_examples 'updates multiple milestones' do
+ it 'removes the old milestone and update the release with the new ones' do
+ result = service.execute
+ release.reload
+
+ milestone_titles = release.milestones.map(&:title)
+ expect(milestone_titles).to match_array([new_title_1, new_title_2])
+ expect(result[:milestones_updated]).to be_truthy
+ end
+ end
+
+ context 'by title' do
+ let(:params_with_milestones) { params.merge!({ milestones: [new_title_1, new_title_2] }) }
+
+ it_behaves_like 'updates multiple milestones'
+ end
+
+ context 'by id' do
+ let(:params_with_milestones) { params.merge!({ milestone_ids: [new_milestone_1.id, new_milestone_2.id] }) }
- milestone_titles = release.milestones.map(&:title)
- expect(milestone_titles).to match_array([new_title_1, new_title_2])
- expect(result[:milestones_updated]).to be_truthy
+ it_behaves_like 'updates multiple milestones'
end
end
end
diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb
index 48c6e590e1b..d9384094657 100644
--- a/spec/support/helpers/navbar_structure_helper.rb
+++ b/spec/support/helpers/navbar_structure_helper.rb
@@ -85,6 +85,14 @@ module NavbarStructureHelper
)
end
+ def insert_model_experiments_nav(within)
+ insert_after_sub_nav_item(
+ within,
+ within: _('Packages and registries'),
+ new_sub_nav_item_name: _('Model Experiments')
+ )
+ end
+
def insert_observability_nav
insert_after_nav_item(
_('Kubernetes'),
diff --git a/spec/support/shared_contexts/mailers/emails/service_desk_shared_context.rb b/spec/support/shared_contexts/mailers/emails/service_desk_shared_context.rb
new file mode 100644
index 00000000000..4aa4d500f5c
--- /dev/null
+++ b/spec/support/shared_contexts/mailers/emails/service_desk_shared_context.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'with service desk mailer' do
+ before do
+ stub_const('ServiceEmailClass', Class.new(ApplicationMailer))
+
+ ServiceEmailClass.class_eval do
+ include GitlabRoutingHelper
+ include EmailsHelper
+ include Emails::ServiceDesk
+
+ helper GitlabRoutingHelper
+ helper EmailsHelper
+
+ # this method is implemented in Notify class, we don't need to test it
+ def reply_key
+ 'b7721fc7e8419911a8bea145236a0519'
+ end
+
+ # this method is implemented in Notify class, we don't need to test it
+ def sender(author_id, params = {})
+ author_id
+ end
+
+ # this method is implemented in Notify class
+ #
+ # We do not need to test the Notify method, it is already tested in notify_spec
+ def mail_new_thread(issue, options)
+ # we need to rewrite this in order to look up templates in the correct directory
+ self.class.mailer_name = 'notify'
+
+ # this is needed for default layout
+ @unsubscribe_url = 'http://unsubscribe.example.com'
+
+ mail(options)
+ end
+ alias_method :mail_answer_thread, :mail_new_thread
+ end
+ end
+end
diff --git a/spec/workers/merge_requests/delete_source_branch_worker_spec.rb b/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
index a7e4ffad259..e17ad02e272 100644
--- a/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
+++ b/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
@@ -13,114 +13,53 @@ RSpec.describe MergeRequests::DeleteSourceBranchWorker do
before do
allow_next_instance_of(::Projects::DeleteBranchWorker) do |instance|
allow(instance).to receive(:perform).with(merge_request.source_project.id, user.id,
- merge_request.source_branch)
+ merge_request.source_branch)
end
end
- context 'when the add_delete_branch_worker feature flag is enabled' do
- context 'with a non-existing merge request' do
- it 'does nothing' do
- expect(::Projects::DeleteBranchWorker).not_to receive(:new)
-
- worker.perform(non_existing_record_id, sha, user.id)
- end
- end
+ context 'with a non-existing merge request' do
+ it 'does nothing' do
+ expect(::Projects::DeleteBranchWorker).not_to receive(:new)
- context 'with a non-existing user' do
- it 'does nothing' do
- expect(::Projects::DeleteBranchWorker).not_to receive(:new)
-
- worker.perform(merge_request.id, sha, non_existing_record_id)
- end
+ worker.perform(non_existing_record_id, sha, user.id)
end
+ end
- context 'with existing user and merge request' do
- it 'creates a new delete branch worker async' do
- expect_next_instance_of(::Projects::DeleteBranchWorker) do |instance|
- expect(instance).to receive(:perform).with(merge_request.source_project.id, user.id,
- merge_request.source_branch)
- end
-
- worker.perform(merge_request.id, sha, user.id)
- end
-
- context 'source branch sha does not match' do
- it 'does nothing' do
- expect(::Projects::DeleteBranchWorker).not_to receive(:new)
-
- worker.perform(merge_request.id, 'new-source-branch-sha', user.id)
- end
- end
- end
+ context 'with a non-existing user' do
+ it 'does nothing' do
+ expect(::Projects::DeleteBranchWorker).not_to receive(:new)
- it_behaves_like 'an idempotent worker' do
- let(:job_args) { [merge_request.id, sha, user.id] }
+ worker.perform(merge_request.id, sha, non_existing_record_id)
end
end
- context 'when the add_delete_branch_worker feature flag is disabled' do
- before do
- stub_feature_flags(add_delete_branch_worker: false)
- end
-
- context 'with a non-existing merge request' do
- it 'does nothing' do
- expect(::Branches::DeleteService).not_to receive(:new)
- expect(::MergeRequests::RetargetChainService).not_to receive(:new)
-
- worker.perform(non_existing_record_id, sha, user.id)
+ context 'with existing user and merge request' do
+ it 'calls delete branch worker' do
+ expect_next_instance_of(::Projects::DeleteBranchWorker) do |instance|
+ expect(instance).to receive(:perform).with(merge_request.source_project.id, user.id,
+ merge_request.source_branch)
end
+
+ worker.perform(merge_request.id, sha, user.id)
end
- context 'with a non-existing user' do
+ context 'source branch sha does not match' do
it 'does nothing' do
- expect(::Branches::DeleteService).not_to receive(:new)
- expect(::MergeRequests::RetargetChainService).not_to receive(:new)
+ expect(::Projects::DeleteBranchWorker).not_to receive(:new)
- worker.perform(merge_request.id, sha, non_existing_record_id)
+ worker.perform(merge_request.id, 'new-source-branch-sha', user.id)
end
end
- context 'with existing user and merge request' do
- it 'calls service to delete source branch' do
- expect_next_instance_of(::Branches::DeleteService) do |instance|
- expect(instance).to receive(:execute).with(merge_request.source_branch)
- end
+ context 'when delete worker raises an error' do
+ it 'still retargets the merge request' do
+ expect(::Projects::DeleteBranchWorker).to receive(:new).and_raise(StandardError)
- worker.perform(merge_request.id, sha, user.id)
- end
-
- it 'calls service to try retarget merge requests' do
expect_next_instance_of(::MergeRequests::RetargetChainService) do |instance|
expect(instance).to receive(:execute).with(merge_request)
end
- worker.perform(merge_request.id, sha, user.id)
- end
-
- context 'source branch sha does not match' do
- it 'does nothing' do
- expect(::Branches::DeleteService).not_to receive(:new)
- expect(::MergeRequests::RetargetChainService).not_to receive(:new)
-
- worker.perform(merge_request.id, 'new-source-branch-sha', user.id)
- end
- end
-
- context 'when delete service returns an error' do
- let(:service_result) { ServiceResponse.error(message: 'placeholder') }
-
- it 'still retargets the merge request' do
- expect_next_instance_of(::Branches::DeleteService) do |instance|
- expect(instance).to receive(:execute).with(merge_request.source_branch).and_return(service_result)
- end
-
- expect_next_instance_of(::MergeRequests::RetargetChainService) do |instance|
- expect(instance).to receive(:execute).with(merge_request)
- end
-
- worker.perform(merge_request.id, sha, user.id)
- end
+ expect { worker.perform(merge_request.id, sha, user.id) }.to raise_error(StandardError)
end
end
diff --git a/yarn.lock b/yarn.lock
index fe595bfd9b4..416a49623fe 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4178,10 +4178,10 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
-core-js@^3.27.2, core-js@^3.6.5:
- version "3.27.2"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.27.2.tgz#85b35453a424abdcacb97474797815f4d62ebbf7"
- integrity sha512-9ashVQskuh5AZEZ1JdQWp1GqSoC1e1G87MzRqg2gIfVAQ7Qn9K+uFj8EcniUFA4P2NLZfV+TOlX1SzoKfo+s7w==
+core-js@^3.28.0, core-js@^3.6.5:
+ version "3.28.0"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.28.0.tgz#ed8b9e99c273879fdfff0edfc77ee709a5800e4a"
+ integrity sha512-GiZn9D4Z/rSYvTeg1ljAIsEqFm0LaN9gVtwDCrKL80zHtS31p9BAjmTxVqTQDMpwlMolJZOFntUG2uwyj7DAqw==
core-util-is@~1.0.0:
version "1.0.3"