diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-14 12:07:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-14 12:07:42 +0000 |
commit | cacc3815006ab7d3828ebe8903f95154b27a6e21 (patch) | |
tree | 5adc693664d1ca383d19f8f165b37eea2318387f | |
parent | ce684df4733d86a49126792721f549612a778590 (diff) | |
download | gitlab-ce-cacc3815006ab7d3828ebe8903f95154b27a6e21.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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" |