From 8cd0a57c7ee0cb6b0dbeafc77d05aa72650f4477 Mon Sep 17 00:00:00 2001 From: Adithya Krishna <aadithya794@gmail.com> Date: Sun, 7 Aug 2022 20:59:43 +0530 Subject: [PATCH 001/169] Replaced usage of toBeFalsy with toBe Signed-off-by: Adithya Krishna <aadithya794@gmail.com> --- spec/frontend/ide/stores/modules/commit/actions_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js index d65039e89cc317..4e8467de759000 100644 --- a/spec/frontend/ide/stores/modules/commit/actions_spec.js +++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js @@ -210,7 +210,7 @@ describe('IDE commit module actions', () => { branch, }); store.state.openFiles.forEach((entry) => { - expect(entry.changed).toBeFalsy(); + expect(entry.changed).toBe(false); }); }); -- GitLab From d9998a504c503b0070cb0c2652a07f2c522dfef5 Mon Sep 17 00:00:00 2001 From: Sofia Vistas <svistas@gitlab.com> Date: Mon, 1 Aug 2022 17:44:27 +0300 Subject: [PATCH 002/169] Refactor autodevops test Previously the test was using certificates to connect to Kubernetes clusters and run autodevops. This MR refactors the test to use GitLab Agent and also the necessary resources and test flow. --- .rubocop_todo/layout/line_length.yml | 1 - .rubocop_todo/style/lambda.yml | 1 - qa/qa/fixtures/auto_devops_rack/Dockerfile | 9 - qa/qa/fixtures/auto_devops_rack/Gemfile | 5 - qa/qa/fixtures/auto_devops_rack/Gemfile.lock | 15 -- qa/qa/fixtures/auto_devops_rack/Rakefile | 9 - qa/qa/fixtures/auto_devops_rack/config.ru | 3 - .../infrastructure/kubernetes/index.rb | 9 +- qa/qa/resource/clusters/agent.rb | 17 +- qa/qa/resource/clusters/agent_token.rb | 21 +-- .../kubernetes_cluster/project_cluster.rb | 50 ------ qa/qa/service/cluster_provider/gcloud.rb | 38 +++-- qa/qa/service/kubernetes_cluster.rb | 14 +- .../create_project_with_auto_devops_spec.rb | 160 ++++++++++-------- .../kubernetes/kubernetes_integration_spec.rb | 38 ----- .../kubernetes/kubernetes_agent_spec.rb | 2 +- 16 files changed, 129 insertions(+), 263 deletions(-) delete mode 100644 qa/qa/fixtures/auto_devops_rack/Dockerfile delete mode 100644 qa/qa/fixtures/auto_devops_rack/Gemfile delete mode 100644 qa/qa/fixtures/auto_devops_rack/Gemfile.lock delete mode 100644 qa/qa/fixtures/auto_devops_rack/Rakefile delete mode 100644 qa/qa/fixtures/auto_devops_rack/config.ru delete mode 100644 qa/qa/resource/kubernetes_cluster/project_cluster.rb delete mode 100644 qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 80c0f986552726..d960824c8b1f4a 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -3675,7 +3675,6 @@ Layout/LineLength: - 'qa/qa/ee/page/project/secure/security_dashboard.rb' - 'qa/qa/ee/page/project/secure/show.rb' - 'qa/qa/ee/resource/license.rb' - - 'qa/qa/fixtures/auto_devops_rack/config.ru' - 'qa/qa/flow/sign_up.rb' - 'qa/qa/git/repository.rb' - 'qa/qa/page/base.rb' diff --git a/.rubocop_todo/style/lambda.yml b/.rubocop_todo/style/lambda.yml index 5b898417d96ad6..525e2c31797bca 100644 --- a/.rubocop_todo/style/lambda.yml +++ b/.rubocop_todo/style/lambda.yml @@ -217,7 +217,6 @@ Style/Lambda: - 'lib/gitlab/sidekiq_signals.rb' - 'lib/gitlab/utils/measuring.rb' - 'lib/gitlab/visibility_level.rb' - - 'qa/qa/fixtures/auto_devops_rack/config.ru' - 'rubocop/cop/rspec/modify_sidekiq_middleware.rb' - 'rubocop/cop/rspec/timecop_freeze.rb' - 'rubocop/cop/rspec/timecop_travel.rb' diff --git a/qa/qa/fixtures/auto_devops_rack/Dockerfile b/qa/qa/fixtures/auto_devops_rack/Dockerfile deleted file mode 100644 index 6ab2795dd40aa6..00000000000000 --- a/qa/qa/fixtures/auto_devops_rack/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM ruby:2.6.5-alpine -ADD ./ /app/ -WORKDIR /app -ENV RACK_ENV production -ENV PORT 5000 -EXPOSE 5000 - -RUN bundle install -CMD ["bundle","exec", "rackup", "-p", "5000"] diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile b/qa/qa/fixtures/auto_devops_rack/Gemfile deleted file mode 100644 index 2c7c77adf94a38..00000000000000 --- a/qa/qa/fixtures/auto_devops_rack/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -source 'https://rubygems.org' -gem 'rack' -gem 'rake' diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock deleted file mode 100644 index 04a85be4b2f630..00000000000000 --- a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock +++ /dev/null @@ -1,15 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - rack (2.2.3) - rake (12.3.3) - -PLATFORMS - ruby - -DEPENDENCIES - rack - rake - -BUNDLED WITH - 1.17.3 diff --git a/qa/qa/fixtures/auto_devops_rack/Rakefile b/qa/qa/fixtures/auto_devops_rack/Rakefile deleted file mode 100644 index a6d08103d555e0..00000000000000 --- a/qa/qa/fixtures/auto_devops_rack/Rakefile +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require 'rake/testtask' - -task default: %w[test] - -task :test do - puts "ok" -end diff --git a/qa/qa/fixtures/auto_devops_rack/config.ru b/qa/qa/fixtures/auto_devops_rack/config.ru deleted file mode 100644 index aea28ef18935ef..00000000000000 --- a/qa/qa/fixtures/auto_devops_rack/config.ru +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, StringIO.new("Hello World! #{ENV['OPTIONAL_MESSAGE']}\n")] } diff --git a/qa/qa/page/project/infrastructure/kubernetes/index.rb b/qa/qa/page/project/infrastructure/kubernetes/index.rb index 34d2ad554293a0..4c759a049e1458 100644 --- a/qa/qa/page/project/infrastructure/kubernetes/index.rb +++ b/qa/qa/page/project/infrastructure/kubernetes/index.rb @@ -10,18 +10,13 @@ class Index < Page::Base element :clusters_actions_button end - def connect_existing_cluster - within_element(:clusters_actions_button) { click_button(class: 'dropdown-toggle-split') } - click_link 'Connect a cluster (certificate - deprecated)' + def connect_cluster + click_element(:clusters_actions_button) end def has_cluster?(cluster) has_element?(:cluster, cluster_name: cluster.to_s) end - - def click_on_cluster(cluster) - click_on cluster.cluster_name - end end end end diff --git a/qa/qa/resource/clusters/agent.rb b/qa/qa/resource/clusters/agent.rb index b190634f357f5c..9574289a2edd10 100644 --- a/qa/qa/resource/clusters/agent.rb +++ b/qa/qa/resource/clusters/agent.rb @@ -26,25 +26,18 @@ def resource_web_url(resource) end def api_get_path - "gid://gitlab/Clusters::Agent/#{id}" + "/projects/#{project.id}/cluster_agents/#{id}" end def api_post_path - "/graphql" + "/projects/#{project.id}/cluster_agents" end def api_post_body - <<~GQL - mutation createAgent { - createClusterAgent(input: { projectPath: "#{project.full_path}", name: "#{@name}" }) { - clusterAgent { - id - name - } - errors - } + { + id: project.id, + name: name } - GQL end end end diff --git a/qa/qa/resource/clusters/agent_token.rb b/qa/qa/resource/clusters/agent_token.rb index c1cf5c2f37bbd3..cbd2964c31d0dc 100644 --- a/qa/qa/resource/clusters/agent_token.rb +++ b/qa/qa/resource/clusters/agent_token.rb @@ -5,7 +5,7 @@ module Resource module Clusters class AgentToken < QA::Resource::Base attribute :id - attribute :secret + attribute :token attribute :agent do QA::Resource::Clusters::Agent.fabricate_via_api! end @@ -20,26 +20,19 @@ def resource_web_url(resource) end def api_get_path - "gid://gitlab/Clusters::AgentToken/#{id}" + "/projects/#{agent.project.id}/cluster_agents/#{agent.id}/tokens/#{id}" end def api_post_path - "/graphql" + "/projects/#{agent.project.id}/cluster_agents/#{agent.id}/tokens" end def api_post_body - <<~GQL - mutation createToken { - clusterAgentTokenCreate(input: { clusterAgentId: "gid://gitlab/Clusters::Agent/#{agent.id}" name: "token-#{agent.id}" }) { - secret # This is the value you need to use on the next step - token { - createdAt - id - } - errors - } + { + id: agent.project.id, + agent_id: agent.id, + name: agent.name } - GQL end end end diff --git a/qa/qa/resource/kubernetes_cluster/project_cluster.rb b/qa/qa/resource/kubernetes_cluster/project_cluster.rb deleted file mode 100644 index 0443b26064ec86..00000000000000 --- a/qa/qa/resource/kubernetes_cluster/project_cluster.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -module QA - module Resource - module KubernetesCluster - # TODO: This resource is currently broken, since one-click apps have been removed. - # See https://gitlab.com/gitlab-org/gitlab/-/issues/333818 - class ProjectCluster < Base - attr_writer :cluster, - :install_ingress, :install_prometheus, :install_runner, :domain - - attribute :project do - Resource::Project.fabricate! - end - - attribute :ingress_ip do - @cluster.fetch_external_ip_for_ingress - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform( - &:go_to_infrastructure_kubernetes) - - Page::Project::Infrastructure::Kubernetes::Index.perform( - &:connect_existing_cluster) - - Page::Project::Infrastructure::Kubernetes::AddExisting.perform do |cluster_page| - cluster_page.set_cluster_name(@cluster.cluster_name) - cluster_page.set_api_url(@cluster.api_url) - cluster_page.set_ca_certificate(@cluster.ca_certificate) - cluster_page.set_token(@cluster.token) - cluster_page.uncheck_rbac! unless @cluster.rbac - cluster_page.add_cluster! - end - - Page::Project::Infrastructure::Kubernetes::Show.perform do |show| - if @install_ingress - ingress_ip - - show.set_domain("#{@ingress_ip}.nip.io") - show.save_domain - end - end - end - end - end - end -end diff --git a/qa/qa/service/cluster_provider/gcloud.rb b/qa/qa/service/cluster_provider/gcloud.rb index 77677745f7a3dc..14c13eecb8d337 100644 --- a/qa/qa/service/cluster_provider/gcloud.rb +++ b/qa/qa/service/cluster_provider/gcloud.rb @@ -33,14 +33,32 @@ def teardown delete_cluster end - def install_ingress - QA::Runtime::Logger.info "Attempting to install Ingress on cluster #{cluster_name}" - shell 'kubectl create -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-0.31.0/deploy/static/provider/cloud/deploy.yaml' - wait_for_ingress + # kas is hardcoded to staging since this test should only run in staging for now + def install_kubernetes_agent(agent_token) + install_helm + + shell <<~CMD.tr("\n", ' ') + helm repo add gitlab https://charts.gitlab.io && + helm repo update && + helm upgrade --install test gitlab/gitlab-agent + --namespace gitlab-agent + --create-namespace + --set image.tag=#{Runtime::Env.gitlab_agentk_version} + --set config.token=#{agent_token} + --set config.kasAddress=wss://kas.staging.gitlab.com + CMD end private + def install_helm + shell <<~CMD.tr("\n", ' ') + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 && + chmod 700 get_helm.sh && + ./get_helm.sh + CMD + end + def login_if_not_already_logged_in if Runtime::Env.has_gcloud_credentials? attempt_login_with_env_vars @@ -104,18 +122,6 @@ def delete_cluster def get_region Runtime::Env.gcloud_region || @available_regions.delete(@available_regions.sample) end - - def wait_for_ingress - QA::Runtime::Logger.info 'Waiting for Ingress controller pod to be initialized' - - Support::Retrier.retry_until(max_attempts: 60, sleep_interval: 1) do - service_available?('kubectl get pods --all-namespaces -l app.kubernetes.io/component=controller | grep -o "ingress-nginx-controller.*1/1"') - end - end - - def service_available?(command) - system("#{command} > /dev/null 2>&1") - end end end end diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb index dafce4acc33c76..59bfacf9195b4d 100644 --- a/qa/qa/service/kubernetes_cluster.rb +++ b/qa/qa/service/kubernetes_cluster.rb @@ -41,8 +41,8 @@ def to_s cluster_name end - def install_ingress - @provider.install_ingress + def install_kubernetes_agent(agent_token) + @provider.install_kubernetes_agent(agent_token) end def create_secret(secret, secret_name) @@ -73,16 +73,6 @@ def add_sample_policy(project, policy_name: 'sample-policy') shell('kubectl apply -f -', stdin_data: network_policy) end - def fetch_external_ip_for_ingress - install_ingress - - # need to wait since the ingress-nginx service has an initial delay set of 10 seconds - sleep 12 - ingress_ip = `kubectl get svc --all-namespaces --no-headers=true -l app.kubernetes.io/name=ingress-nginx -o custom-columns=:'status.loadBalancer.ingress[0].ip' | grep -v 'none'` - QA::Runtime::Logger.debug "Has ingress address set to: #{ingress_ip}" - ingress_ip - end - private def fetch_api_url diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index f1a2eb713902c4..b839855c5004fb 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -1,78 +1,67 @@ # frozen_string_literal: true module QA - RSpec.describe 'Configure', - only: { subdomain: %i[staging staging-canary] }, - quarantine: { - issue: 'https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1198', - type: :waiting_on - } do - let(:project) do - Resource::Project.fabricate_via_api! do |project| - project.name = 'autodevops-project' - project.auto_devops_enabled = true + RSpec.describe 'Configure', only: { subdomain: %i[staging staging-canary] } do + describe 'Auto DevOps with a Kubernetes Agent' do + let!(:app_project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'autodevops-app-project' + project.template_name = 'express' + project.auto_devops_enabled = true + end end - end - before do - set_kube_ingress_base_domain(project) - disable_optional_jobs(project) - end + let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::Gcloud).create! } - describe 'Auto DevOps support' do - context 'when rbac is enabled' do - let(:cluster) { Service::KubernetesCluster.new.create! } + let!(:kubernetes_agent) do + Resource::Clusters::Agent.fabricate_via_api! do |agent| + agent.name = 'agent1' + agent.project = app_project + end + end - after do - cluster&.remove! - project.remove_via_api! + let!(:agent_token) do + Resource::Clusters::AgentToken.fabricate_via_api! do |token| + token.agent = kubernetes_agent end + end - it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348061' do - Flow::Login.sign_in - - Resource::KubernetesCluster::ProjectCluster.fabricate! do |k8s_cluster| - k8s_cluster.project = project - k8s_cluster.cluster = cluster - k8s_cluster.install_ingress = true - end - - Resource::Repository::ProjectPush.fabricate! do |push| - push.project = project - push.directory = Pathname - .new(__dir__) - .join('../../../../../fixtures/auto_devops_rack') - push.commit_message = 'Create Auto DevOps compatible rack application' - end - - Flow::Pipeline.visit_latest_pipeline - - Page::Project::Pipeline::Show.perform do |pipeline| - pipeline.click_job('build') - end - Page::Project::Job::Show.perform do |job| - expect(job).to be_successful(timeout: 600) - - job.click_element(:pipeline_path) - end - - Page::Project::Pipeline::Show.perform do |pipeline| - pipeline.click_job('test') - end - Page::Project::Job::Show.perform do |job| - expect(job).to be_successful(timeout: 600) - - job.click_element(:pipeline_path) - end - - Page::Project::Pipeline::Show.perform do |pipeline| - pipeline.click_job('production') - end - Page::Project::Job::Show.perform do |job| - expect(job).to be_successful(timeout: 1200) - - job.click_element(:pipeline_path) - end + before do + cluster.install_kubernetes_agent(agent_token.token) + upload_agent_config(app_project, kubernetes_agent.name) + + set_kube_ingress_base_domain(app_project) + set_kube_context(app_project) + disable_optional_jobs(app_project) + end + + after do + cluster&.remove! + end + + it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348061' do + Flow::Login.sign_in + + app_project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) + Page::Project::Pipeline::New.perform(&:click_run_pipeline_button) + + Page::Project::Pipeline::Show.perform do |pipeline| + pipeline.click_job('build') + end + Page::Project::Job::Show.perform do |job| + expect(job).to be_successful(timeout: 600) + + job.click_element(:pipeline_path) + end + + Page::Project::Pipeline::Show.perform do |pipeline| + pipeline.click_job('production') + end + Page::Project::Job::Show.perform do |job| + expect(job).to be_successful(timeout: 600) end end end @@ -88,12 +77,43 @@ def set_kube_ingress_base_domain(project) end end + def set_kube_context(project) + Resource::CiVariable.fabricate_via_api! do |resource| + resource.project = project + resource.key = 'KUBE_CONTEXT' + resource.value = "#{project.path_with_namespace}:#{kubernetes_agent.name}" + resource.masked = false + end + end + + def upload_agent_config(project, agent) + Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add kubernetes agent configuration' + commit.add_files( + [ + { + file_path: ".gitlab/agents/#{agent}/config.yaml", + content: <<~YAML + ci_access: + projects: + - id: #{project.path_with_namespace} + YAML + } + ] + ) + end + end + end + def disable_optional_jobs(project) %w[ - CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED - SAST_DISABLED DAST_DISABLED DEPENDENCY_SCANNING_DISABLED - CONTAINER_SCANNING_DISABLED BROWSER_PERFORMANCE_DISABLED - SECRET_DETECTION_DISABLED + TEST_DISABLED CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED + BROWSER_PERFORMANCE_DISABLED LOAD_PERFORMANCE_DISABLED + SAST_DISABLED SECRET_DETECTION_DISABLED DEPENDENCY_SCANNING_DISABLED + CONTAINER_SCANNING_DISABLED DAST_DISABLED REVIEW_DISABLED + CODE_INTELLIGENCE_DISABLED CLUSTER_IMAGE_SCANNING_DISABLED ].each do |key| Resource::CiVariable.fabricate_via_api! do |resource| resource.project = project diff --git a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb deleted file mode 100644 index 94f9e9ec1f66f8..00000000000000 --- a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module QA - RSpec.describe 'Configure', except: { job: 'review-qa-*' } do - describe 'Kubernetes Cluster Integration', :orchestrated, :requires_admin, :skip_live_env do - context 'Project Clusters' do - let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! } - let(:project) do - Resource::Project.fabricate_via_api! do |project| - project.name = 'project-with-k8s' - project.description = 'Project with Kubernetes cluster integration' - end - end - - before do - Flow::Login.sign_in_as_admin - end - - after do - cluster.remove! - end - - it 'can create and associate a project cluster', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348062' do - Resource::KubernetesCluster::ProjectCluster.fabricate_via_browser_ui! do |k8s_cluster| - k8s_cluster.project = project - k8s_cluster.cluster = cluster - end.project.visit! - - Page::Project::Menu.perform(&:go_to_infrastructure_kubernetes) - - Page::Project::Infrastructure::Kubernetes::Index.perform do |index| - expect(index).to have_cluster(cluster) - end - end - end - end - end -end diff --git a/qa/qa/specs/features/ee/api/7_configure/kubernetes/kubernetes_agent_spec.rb b/qa/qa/specs/features/ee/api/7_configure/kubernetes/kubernetes_agent_spec.rb index 61cdac1ac334e7..6c16ec7b4a42b2 100644 --- a/qa/qa/specs/features/ee/api/7_configure/kubernetes/kubernetes_agent_spec.rb +++ b/qa/qa/specs/features/ee/api/7_configure/kubernetes/kubernetes_agent_spec.rb @@ -41,7 +41,7 @@ def manifest_deployed? end def install_agentk(cluster, agent_token) - cluster.create_secret(agent_token.secret, 'gitlab-agent-token') + cluster.create_secret(agent_token.token, 'gitlab-agent-token') kas_wss_address = "wss://kas.staging.gitlab.com" agent_manifest_template = read_agent_fixture('agentk-manifest.yaml.erb') -- GitLab From 86803ea345cca1a479ac33abbade3f074fb404ee Mon Sep 17 00:00:00 2001 From: Martin Tan <lmtan@jihulab.com> Date: Thu, 25 Aug 2022 18:26:48 +0800 Subject: [PATCH 003/169] Feat: add prepend to user access denied reason --- lib/gitlab/auth/user_access_denied_reason.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb index ff6dc7313bb3f2..322dfa74d09e80 100644 --- a/lib/gitlab/auth/user_access_denied_reason.rb +++ b/lib/gitlab/auth/user_access_denied_reason.rb @@ -57,3 +57,5 @@ def rejection_type end end end + +Gitlab::Auth::UserAccessDeniedReason.prepend_mod -- GitLab From 3b99025bb3e373a9203592a37bd56696c9070422 Mon Sep 17 00:00:00 2001 From: Bryce Chidester <bryce@cobryce.com> Date: Thu, 25 Aug 2022 17:00:05 +0000 Subject: [PATCH 004/169] Adjust example for running git-fsck repository check The example command given for performing a git-fsck manually on the Gitaly filesystem has the "git" command run as root. In the Omnibus installation, which is the assumed context based on the example command given, the git data is owned by user "git", and git will produce a "dubious ownership" error. Performing the git-fsck as the appropriate user avoids this error, and prevents any permissions-related issues down the road if the user happens to swap "fsck" with "gc" or any other writing-command. --- doc/administration/repository_checks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md index a97c8611239380..90944d1b4300b5 100644 --- a/doc/administration/repository_checks.md +++ b/doc/administration/repository_checks.md @@ -68,7 +68,7 @@ You can run [`git fsck`](https://git-scm.com/docs/git-fsck) using the command li 1. Run the check. For example: ```shell - sudo /opt/gitlab/embedded/bin/git -C /var/opt/gitlab/git-data/repositories/@hashed/0b/91/0b91...f9.git fsck + sudo -u git /opt/gitlab/embedded/bin/git -C /var/opt/gitlab/git-data/repositories/@hashed/0b/91/0b91...f9.git fsck ``` ## What to do if a check failed -- GitLab From 0ec62c79a3d64171218529cc69e71fb375eb7d96 Mon Sep 17 00:00:00 2001 From: Thomas Hutterer <thutterer@gitlab.com> Date: Tue, 30 Aug 2022 13:52:25 +0200 Subject: [PATCH 005/169] Add comment to explain side effect import --- .../groups/settings/repository/create_deploy_token/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/pages/groups/settings/repository/create_deploy_token/index.js b/app/assets/javascripts/pages/groups/settings/repository/create_deploy_token/index.js index 6a7c6028c95944..1943704ac3d178 100644 --- a/app/assets/javascripts/pages/groups/settings/repository/create_deploy_token/index.js +++ b/app/assets/javascripts/pages/groups/settings/repository/create_deploy_token/index.js @@ -1 +1,6 @@ +// This "page" is only rendered as response to the create_deploy_token form. +// It shows the secret token to the user one time, but is otherwise identical +// with the Settings/Repository page. +// +// This is why we just import the other page's JavaScript here. import '../show/index'; -- GitLab From 883ee3552f720ee963366e4b6a3711d4082937c0 Mon Sep 17 00:00:00 2001 From: imand3r <ianderson@gitlab.com> Date: Fri, 26 Aug 2022 10:28:14 -0700 Subject: [PATCH 006/169] Add context to spam verdict service error logs The extra field, error, is added to the log message so that alerts can be easily generated when errors occur while checking for spam. --- app/services/spam/spam_constants.rb | 1 + app/services/spam/spam_verdict_service.rb | 2 +- lib/gitlab/spamcheck/client.rb | 2 +- .../spam/spam_verdict_service_spec.rb | 22 ++++++++++++------- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/services/spam/spam_constants.rb b/app/services/spam/spam_constants.rb index d300525710cd87..9ac3bcf8a1d055 100644 --- a/app/services/spam/spam_constants.rb +++ b/app/services/spam/spam_constants.rb @@ -2,6 +2,7 @@ module Spam module SpamConstants + ERROR_TYPE = 'spamcheck' BLOCK_USER = 'block' DISALLOW = 'disallow' CONDITIONAL_ALLOW = 'conditional_allow' diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb index 382545556ab55d..08634ec840cce9 100644 --- a/app/services/spam/spam_verdict_service.rb +++ b/app/services/spam/spam_verdict_service.rb @@ -85,7 +85,7 @@ def spamcheck_verdict [result, attribs] rescue StandardError => e - Gitlab::ErrorTracking.log_exception(e) + Gitlab::ErrorTracking.log_exception(e, error: ERROR_TYPE) # Default to ALLOW if any errors occur [ALLOW, attribs, true] diff --git a/lib/gitlab/spamcheck/client.rb b/lib/gitlab/spamcheck/client.rb index b7ac6224e5ca2c..0b9f3baa4de24c 100644 --- a/lib/gitlab/spamcheck/client.rb +++ b/lib/gitlab/spamcheck/client.rb @@ -34,7 +34,7 @@ def initialize end def spam?(spammable:, user:, context: {}, extra_features: {}) - metadata = { 'authorization' => Gitlab::CurrentSettings.spam_check_api_key } + metadata = { 'authorization' => Gitlab::CurrentSettings.spam_check_api_key || '' } protobuf_args = { spammable: spammable, user: user, context: context, extra_features: extra_features } pb, grpc_method = build_protobuf(**protobuf_args) diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb index 02dbc1004bfb5b..b89c96129c216c 100644 --- a/spec/services/spam/spam_verdict_service_spec.rb +++ b/spec/services/spam/spam_verdict_service_spec.rb @@ -371,6 +371,9 @@ end it 'returns nil' do + expect(Gitlab::ErrorTracking).to receive(:log_exception).with( + an_instance_of(GRPC::Aborted), error: ::Spam::SpamConstants::ERROR_TYPE + ) expect(subject).to eq([ALLOW, attribs, true]) end end @@ -383,17 +386,20 @@ expect(subject).to eq [DISALLOW, attribs] end end - end - context 'if the endpoint times out' do - let(:attribs) { nil } + context 'if the endpoint times out' do + let(:attribs) { nil } - before do - allow(spam_client).to receive(:spam?).and_raise(GRPC::DeadlineExceeded) - end + before do + allow(spam_client).to receive(:spam?).and_raise(GRPC::DeadlineExceeded) + end - it 'returns nil' do - expect(subject).to eq([ALLOW, attribs, true]) + it 'returns nil' do + expect(Gitlab::ErrorTracking).to receive(:log_exception).with( + an_instance_of(GRPC::DeadlineExceeded), error: ::Spam::SpamConstants::ERROR_TYPE + ) + expect(subject).to eq([ALLOW, attribs, true]) + end end end end -- GitLab From 04125e1b5bf02c8cf10bb597a6b56d26754a4928 Mon Sep 17 00:00:00 2001 From: chenqiting <qtchen@jihulab.com> Date: Wed, 31 Aug 2022 14:42:17 +0800 Subject: [PATCH 007/169] Fix: notify locale on removed milestone issue email --- app/views/notify/removed_milestone_issue_email.html.haml | 2 +- locale/gitlab.pot | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/notify/removed_milestone_issue_email.html.haml b/app/views/notify/removed_milestone_issue_email.html.haml index 7e9205b64919cf..f411ea23832c3a 100644 --- a/app/views/notify/removed_milestone_issue_email.html.haml +++ b/app/views/notify/removed_milestone_issue_email.html.haml @@ -1,2 +1,2 @@ %p - Milestone removed + = s_('Notify|Milestone removed') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9f0e910acc2c91..db59abc2e84442 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -26926,6 +26926,9 @@ msgstr "" msgid "Notify|Milestone changed to %{milestone}" msgstr "" +msgid "Notify|Milestone removed" +msgstr "" + msgid "Notify|New issue: %{project_issue_url}" msgstr "" -- GitLab From d890c47a4bc970c8bc99f3811fafa1dea3760e48 Mon Sep 17 00:00:00 2001 From: JeremyWuuuuu <jeremyw@jihulab.com> Date: Wed, 31 Aug 2022 15:23:54 +0800 Subject: [PATCH 008/169] Feat: enable JH subscription components * Allow certain components gets loaded from JH when available. --- .../subscriptions/show/components/subscription_breakdown.vue | 2 +- .../subscriptions/show/components/subscription_details_card.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_breakdown.vue b/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_breakdown.vue index 72da6094590db4..aaa292c06d1010 100644 --- a/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_breakdown.vue +++ b/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_breakdown.vue @@ -2,6 +2,7 @@ import { GlButton, GlModalDirective } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; +import SubscriptionDetailsHistory from 'jh_else_ee/admin/subscriptions/show/components/subscription_details_history.vue'; import { activateCloudLicense, licensedToHeaderText, @@ -17,7 +18,6 @@ import { import SubscriptionActivationBanner from './subscription_activation_banner.vue'; import SubscriptionActivationModal from './subscription_activation_modal.vue'; import SubscriptionDetailsCard from './subscription_details_card.vue'; -import SubscriptionDetailsHistory from './subscription_details_history.vue'; import SubscriptionDetailsUserInfo from './subscription_details_user_info.vue'; export const subscriptionDetailsFields = [ diff --git a/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_details_card.vue b/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_details_card.vue index d522839969ceca..67a1cb7aa966a5 100644 --- a/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_details_card.vue +++ b/ee/app/assets/javascripts/admin/subscriptions/show/components/subscription_details_card.vue @@ -4,8 +4,8 @@ import { identity } from 'lodash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getTimeago } from '~/lib/utils/datetime_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import SubscriptionDetailsTable from 'jh_else_ee/admin/subscriptions/show/components/subscription_details_table.vue'; import { getLicenseTypeLabel } from '../utils'; -import SubscriptionDetailsTable from './subscription_details_table.vue'; const subscriptionDetailsFormatRules = { id: getIdFromGraphQLId, -- GitLab From 4d31610fc1303c2031c34bdb41045c3e8e182237 Mon Sep 17 00:00:00 2001 From: Imre Farkas <ifarkas@gitlab.com> Date: Thu, 21 Apr 2022 16:35:00 +0200 Subject: [PATCH 009/169] Add feature_category to Admin::SpamLogsController --- app/controllers/admin/spam_logs_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb index e4e866a8b601b9..3a55fc4b951a5e 100644 --- a/app/controllers/admin/spam_logs_controller.rb +++ b/app/controllers/admin/spam_logs_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Admin::SpamLogsController < Admin::ApplicationController - feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned + feature_category :instance_resiliency # rubocop: disable CodeReuse/ActiveRecord def index -- GitLab From b8795855384b41a63a6c9982c68382cbc3c93dbe Mon Sep 17 00:00:00 2001 From: Zehua Zhang <zhzhang@jihulab.com> Date: Wed, 31 Aug 2022 16:36:37 +0800 Subject: [PATCH 010/169] Add i18n support for profile/notifications page --- app/views/profiles/notifications/_email_settings.html.haml | 2 +- app/views/profiles/notifications/show.html.haml | 2 +- locale/gitlab.pot | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/views/profiles/notifications/_email_settings.html.haml b/app/views/profiles/notifications/_email_settings.html.haml index b4db99a8bd4f42..c4de33dcd9e51c 100644 --- a/app/views/profiles/notifications/_email_settings.html.haml +++ b/app/views/profiles/notifications/_email_settings.html.haml @@ -1,6 +1,6 @@ - form = local_assigns.fetch(:form) .form-group - = form.label :notification_email, class: "label-bold" + = form.label :notification_email, _('Notification Email'), class: "label-bold" = form.select :notification_email, @user.public_verified_emails, { include_blank: _('Use primary email (%{email})') % { email: @user.email }, selected: @user.notification_email }, class: "select2", disabled: local_assigns.fetch(:email_change_disabled, nil) .help-block = local_assigns.fetch(:help_text, nil) diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 26c9b2f0ee1fb5..0f4b130a77485d 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -25,7 +25,7 @@ = gitlab_ui_form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications gl-mt-3' } do |f| = render_if_exists 'profiles/notifications/email_settings', form: f - = label_tag :global_notification_level, "Global notification level", class: "label-bold" + = label_tag :global_notification_level, _('Global notification level'), class: "label-bold" %br .clearfix .form-group.float-left.global-notification-setting diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 93b74d3d54c8d3..170a0ffd2ec467 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -17889,6 +17889,9 @@ msgstr "" msgid "Global Shortcuts" msgstr "" +msgid "Global notification level" +msgstr "" + msgid "Global notification settings" msgstr "" @@ -26598,6 +26601,9 @@ msgstr "" msgid "Nothing to preview." msgstr "" +msgid "Notification Email" +msgstr "" + msgid "Notification events" msgstr "" -- GitLab From 27962c00497ab87b1b8d9c14b00b7749411359ac Mon Sep 17 00:00:00 2001 From: Marc Shaw <mshaw@gitlab.com> Date: Thu, 1 Sep 2022 10:49:02 +0200 Subject: [PATCH 011/169] Remove the feature flag remove_branch_caching_feature_flag MR: gitlab.com/gitlab-org/gitlab/-/merge_requests/96748 Changelog: performance --- .../development/api_caching_branches.yml | 8 ------ lib/api/branches.rb | 28 ++++++------------- 2 files changed, 9 insertions(+), 27 deletions(-) delete mode 100644 config/feature_flags/development/api_caching_branches.yml diff --git a/config/feature_flags/development/api_caching_branches.yml b/config/feature_flags/development/api_caching_branches.yml deleted file mode 100644 index 310d643529e6de..00000000000000 --- a/config/feature_flags/development/api_caching_branches.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: api_caching_branches -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61157 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330371 -milestone: '13.12' -type: development -group: group::source code -default_enabled: false diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 446f24683a4311..90db81cea1d6cd 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -52,25 +52,15 @@ class Branches < ::API::Base merged_branch_names = repository.merged_branch_names(branches.map(&:name)) - if Feature.enabled?(:api_caching_branches, user_project, type: :development) - present_cached( - branches, - with: Entities::Branch, - current_user: current_user, - project: user_project, - merged_branch_names: merged_branch_names, - expires_in: 10.minutes, - cache_context: -> (branch) { [current_user&.cache_key, merged_branch_names.include?(branch.name)] } - ) - else - present( - branches, - with: Entities::Branch, - current_user: current_user, - project: user_project, - merged_branch_names: merged_branch_names - ) - end + present_cached( + branches, + with: Entities::Branch, + current_user: current_user, + project: user_project, + merged_branch_names: merged_branch_names, + expires_in: 10.minutes, + cache_context: -> (branch) { [current_user&.cache_key, merged_branch_names.include?(branch.name)] } + ) end end -- GitLab From d0749f2e3a39d29cdc0df2b566948f1baa648440 Mon Sep 17 00:00:00 2001 From: Vasilii Iakliushin <viakliushin@gitlab.com> Date: Thu, 1 Sep 2022 14:24:49 +0200 Subject: [PATCH 012/169] Fix 500 error for Commits API Contributes to https://gitlab.com/gitlab-org/gitlab/-/issues/360312 **Problem** We skip the check for the branch name when the repository is empty. **Solution** Verify the branch name for the empty repository case too. Changelog: fixed --- app/services/commits/create_service.rb | 2 +- spec/requests/api/commits_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index fc18420f6e4706..a498d39d34e7dc 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -66,7 +66,7 @@ def validate! validate_on_branch! validate_branch_existence! - validate_new_branch_name! if different_branch? + validate_new_branch_name! if project.empty_repo? || different_branch? end def validate_permissions! diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 68fe45cd0260f8..122c7bb98ec483 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -473,6 +473,26 @@ it_behaves_like "successfully creates the commit" end + context 'when repository is empty' do + let!(:project) { create(:project, :empty_repo) } + + context 'when params are valid' do + before do + post api(url, user), params: valid_c_params + end + + it_behaves_like "successfully creates the commit" + end + + context 'when branch name is invalid' do + before do + post api(url, user), params: valid_c_params.merge(branch: 'wrong:name') + end + + it { expect(response).to have_gitlab_http_status(:bad_request) } + end + end + context 'a new file with utf8 chars in project repo' do before do post api(url, user), params: valid_utf8_c_params -- GitLab From 3e7bcc1f79cc08a5648cf48143b373dc84ce987e Mon Sep 17 00:00:00 2001 From: Vasilii Iakliushin <viakliushin@gitlab.com> Date: Wed, 31 Aug 2022 17:16:50 +0200 Subject: [PATCH 013/169] Enable `escape_gitaly_refs` by default Contributes to https://gitlab.com/gitlab-org/gitlab/-/issues/366437 **Problem** References from Gitaly can contain invalid UTF-8 characters that simply deleted. **Solution** Encode invalid characters instead of deleting them. Changelog: added --- config/feature_flags/development/escape_gitaly_refs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/feature_flags/development/escape_gitaly_refs.yml b/config/feature_flags/development/escape_gitaly_refs.yml index bee62c669ee5a9..b42cc4c07e5485 100644 --- a/config/feature_flags/development/escape_gitaly_refs.yml +++ b/config/feature_flags/development/escape_gitaly_refs.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366437 milestone: '15.2' type: development group: group::source code -default_enabled: false +default_enabled: true -- GitLab From ac218dfdbd3e3030fa4870cd745b39b3a31cad68 Mon Sep 17 00:00:00 2001 From: Igor Drozdov <idrozdov@gitlab.com> Date: Fri, 29 Jul 2022 21:12:00 +0200 Subject: [PATCH 014/169] Fix parsing commit trailers without specified email Changelog: fixed --- lib/banzai/filter/commit_trailers_filter.rb | 2 +- spec/lib/banzai/filter/commit_trailers_filter_spec.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/banzai/filter/commit_trailers_filter.rb b/lib/banzai/filter/commit_trailers_filter.rb index 0525527bb6f69d..817bea42757678 100644 --- a/lib/banzai/filter/commit_trailers_filter.rb +++ b/lib/banzai/filter/commit_trailers_filter.rb @@ -48,7 +48,7 @@ def trailer_filter(text) text.lines.map! do |line| trailer, rest = line.split(':', 2) - next line unless trailer.downcase.end_with?('-by') + next line unless trailer.downcase.end_with?('-by') && rest.present? chunks = rest.split author_email = chunks.pop.delete_prefix('<').delete_suffix('>') diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb index 2bdf702083a235..c22517621c198b 100644 --- a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb @@ -137,6 +137,13 @@ expect(doc.to_html).to match Regexp.escape(exp) end + it 'trailers without emails' do + exp = message = commit_html(Array.new(5) { 'Merged-By:' }.join("\n")) + doc = filter(message) + + expect(doc.to_html).to match Regexp.escape(exp) + end + it 'trailers that are inline the commit message body' do message = commit_html %( #{FFaker::Lorem.sentence} #{commit_message} #{FFaker::Lorem.sentence} -- GitLab From 43ee97d619c2a84fcb6fde2edb9b7ab3023f0e66 Mon Sep 17 00:00:00 2001 From: Doug Stull <dstull@gitlab.com> Date: Thu, 1 Sep 2022 16:07:43 -0400 Subject: [PATCH 015/169] Update container classes for free user cap alerts - fix container settings --- app/helpers/page_layout_helper.rb | 4 ++++ .../free_user_cap/alert_component.html.haml | 2 +- .../namespaces/free_user_cap/alert_component.rb | 2 +- .../namespaces/free_user_cap/shared.rb | 4 ---- .../views/shared/_free_user_cap_alert.html.haml | 4 ++-- .../free_user_cap/alert_component_spec.rb | 2 +- spec/helpers/page_layout_helper_spec.rb | 16 ++++++++++++++++ 7 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 0c057a29bec73b..c06654637060cb 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -150,6 +150,10 @@ def container_class css_class.join(' ') end + def full_content_class + "#{container_class} #{@content_class}" # rubocop:disable Rails/HelperInstanceVariable + end + def page_itemtype(itemtype = nil) if itemtype @page_itemtype = { itemscope: true, itemtype: itemtype } diff --git a/ee/app/components/namespaces/free_user_cap/alert_component.html.haml b/ee/app/components/namespaces/free_user_cap/alert_component.html.haml index 3e6ea116aaa33c..6421ec5aa5d94c 100644 --- a/ee/app/components/namespaces/free_user_cap/alert_component.html.haml +++ b/ee/app/components/namespaces/free_user_cap/alert_component.html.haml @@ -1,4 +1,4 @@ -%div{ class: "#{container_class}" } +%div{ class: container_class } = render Pajamas::AlertComponent.new(variant: variant, alert_options: { class: Namespaces::FreeUserCap::Shared::ALERT_CLASS, data: alert_data }, title: alert_attributes[:title], diff --git a/ee/app/components/namespaces/free_user_cap/alert_component.rb b/ee/app/components/namespaces/free_user_cap/alert_component.rb index fe7c15d440fdfc..699a22c5018081 100644 --- a/ee/app/components/namespaces/free_user_cap/alert_component.rb +++ b/ee/app/components/namespaces/free_user_cap/alert_component.rb @@ -102,7 +102,7 @@ def link_end end def container_class - Shared.fluid_container_class(content_class) + Shared.container_class(content_class) end def free_user_limit diff --git a/ee/app/components/namespaces/free_user_cap/shared.rb b/ee/app/components/namespaces/free_user_cap/shared.rb index b55008970c7e42..0c76f84824f4b2 100644 --- a/ee/app/components/namespaces/free_user_cap/shared.rb +++ b/ee/app/components/namespaces/free_user_cap/shared.rb @@ -15,10 +15,6 @@ def self.container_class(content_class) "#{CONTAINER_CLASSES} #{content_class}" end - def self.fluid_container_class(content_class) - "container-fluid container-limited #{container_class(content_class)}" - end - # region: standard shared ---------------------------------------- def self.default_render?(user:, namespace:) diff --git a/ee/app/views/shared/_free_user_cap_alert.html.haml b/ee/app/views/shared/_free_user_cap_alert.html.haml index 72348e9d1eb94d..793df385350fcb 100644 --- a/ee/app/views/shared/_free_user_cap_alert.html.haml +++ b/ee/app/views/shared/_free_user_cap_alert.html.haml @@ -2,8 +2,8 @@ - if ::Namespaces::FreeUserCap::Standard.new(source.root_ancestor).feature_enabled? = render Namespaces::FreeUserCap::AlertComponent.new(namespace: source.root_ancestor, user: current_user, - content_class: @content_class) + content_class: full_content_class) - else = render Namespaces::FreeUserCap::PreviewAlertComponent.new(namespace: source.root_ancestor, user: current_user, - content_class: @content_class) + content_class: full_content_class) diff --git a/ee/spec/components/namespaces/free_user_cap/alert_component_spec.rb b/ee/spec/components/namespaces/free_user_cap/alert_component_spec.rb index 2c903d5687600b..a701cf29efb931 100644 --- a/ee/spec/components/namespaces/free_user_cap/alert_component_spec.rb +++ b/ee/spec/components/namespaces/free_user_cap/alert_component_spec.rb @@ -30,7 +30,7 @@ expect(rendered_component).to have_css('.gl-alert-actions') expect(rendered_component) - .to match("container-fluid container-limited gl-overflow-auto #{content_class}") + .to match("gl-overflow-auto #{content_class}") expect(rendered_component) .to have_css("[data-testid='user-over-limit-free-plan-alert']" \ diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index d0d399ad10fe86..1e16d9697447f9 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -222,6 +222,22 @@ end end + describe '#full_content_class' do + before do + allow(helper).to receive(:current_user).and_return(build(:user)) + end + + it 'has a content_class set' do + assign(:content_class, '_content_class_') + + expect(helper.full_content_class).to eq 'container-fluid container-limited _content_class_' + end + + it 'has no content_class set' do + expect(helper.full_content_class).to eq 'container-fluid container-limited ' + end + end + describe '#user_status_properties' do let(:user) { build(:user) } -- GitLab From 0728b4c565cb7df93dcb84c9d9e7ba6c0e5e48c7 Mon Sep 17 00:00:00 2001 From: Mehmet Emin INAC <minac@gitlab.com> Date: Wed, 31 Aug 2022 02:11:35 +0300 Subject: [PATCH 016/169] Create partitioned `security_findings` table Instead of creating the table from scratch, we are using the existing table as a partition to save the data. Changelog: other --- config/initializers/postgres_partitioning.rb | 3 +- ...ble_to_gitlab_partitions_dynamic_schema.rb | 201 ++++++++++++++++++ db/schema_migrations/20220816075639 | 1 + db/structure.sql | 64 +++--- ee/app/models/security/finding.rb | 7 + .../single_numeric_list_partition.rb | 2 +- ...o_gitlab_partitions_dynamic_schema_spec.rb | 106 +++++++++ 7 files changed, 350 insertions(+), 34 deletions(-) create mode 100644 db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb create mode 100644 db/schema_migrations/20220816075639 create mode 100644 spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb diff --git a/config/initializers/postgres_partitioning.rb b/config/initializers/postgres_partitioning.rb index 4de6e706f16fd1..99e824d7128241 100644 --- a/config/initializers/postgres_partitioning.rb +++ b/config/initializers/postgres_partitioning.rb @@ -10,7 +10,8 @@ if Gitlab.ee? Gitlab::Database::Partitioning.register_models([ IncidentManagement::PendingEscalations::Alert, - IncidentManagement::PendingEscalations::Issue + IncidentManagement::PendingEscalations::Issue, + Security::Finding ]) else Gitlab::Database::Partitioning.register_tables([ diff --git a/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb b/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb new file mode 100644 index 00000000000000..9da3f890af3a02 --- /dev/null +++ b/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +class MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + INDEX_MAPPING_OF_PARTITION = { + index_security_findings_on_unique_columns: :security_findings_1_uuid_scan_id_partition_number_idx, + index_security_findings_on_confidence: :security_findings_1_confidence_idx, + index_security_findings_on_project_fingerprint: :security_findings_1_project_fingerprint_idx, + index_security_findings_on_scan_id_and_deduplicated: :security_findings_1_scan_id_deduplicated_idx, + index_security_findings_on_scan_id_and_id: :security_findings_1_scan_id_id_idx, + index_security_findings_on_scanner_id: :security_findings_1_scanner_id_idx, + index_security_findings_on_severity: :security_findings_1_severity_idx + }.freeze + + INDEX_MAPPING_AFTER_CREATING_FROM_PARTITION = { + partition_name_placeholder_pkey: :security_findings_pkey, + partition_name_placeholder_uuid_scan_id_partition_number_idx: :index_security_findings_on_unique_columns, + partition_name_placeholder_confidence_idx: :index_security_findings_on_confidence, + partition_name_placeholder_project_fingerprint_idx: :index_security_findings_on_project_fingerprint, + partition_name_placeholder_scan_id_deduplicated_idx: :index_security_findings_on_scan_id_and_deduplicated, + partition_name_placeholder_scan_id_id_idx: :index_security_findings_on_scan_id_and_id, + partition_name_placeholder_scanner_id_idx: :index_security_findings_on_scanner_id, + partition_name_placeholder_severity_idx: :index_security_findings_on_severity + }.freeze + + INDEX_MAPPING_AFTER_CREATING_FROM_ITSELF = { + security_findings_pkey1: :security_findings_pkey, + security_findings_uuid_scan_id_partition_number_idx1: :index_security_findings_on_unique_columns, + security_findings_confidence_idx1: :index_security_findings_on_confidence, + security_findings_project_fingerprint_idx1: :index_security_findings_on_project_fingerprint, + security_findings_scan_id_deduplicated_idx1: :index_security_findings_on_scan_id_and_deduplicated, + security_findings_scan_id_id_idx1: :index_security_findings_on_scan_id_and_id, + security_findings_scanner_id_idx1: :index_security_findings_on_scanner_id, + security_findings_severity_idx1: :index_security_findings_on_severity + }.freeze + + LATEST_PARTITION_SQL = <<~SQL + SELECT + partitions.relname AS partition_name + FROM pg_inherits + JOIN pg_class parent ON pg_inherits.inhparent = parent.oid + JOIN pg_class partitions ON pg_inherits.inhrelid = partitions.oid + WHERE + parent.relname = 'security_findings' + ORDER BY (regexp_matches(partitions.relname, 'security_findings_(\\d+)'))[1]::int DESC + LIMIT 1 + SQL + + def up + execute(<<~SQL) + LOCK TABLE vulnerability_scanners, security_scans, security_findings IN ACCESS EXCLUSIVE MODE + SQL + + execute(<<~SQL) + ALTER TABLE security_findings RENAME TO security_findings_1; + SQL + + execute(<<~SQL) + ALTER INDEX security_findings_pkey RENAME TO security_findings_1_pkey; + SQL + + execute(<<~SQL) + CREATE TABLE security_findings ( + LIKE security_findings_1 INCLUDING ALL + ) PARTITION BY LIST (partition_number); + SQL + + execute(<<~SQL) + ALTER SEQUENCE security_findings_id_seq OWNED BY #{connection.current_schema}.security_findings.id; + SQL + + execute(<<~SQL) + ALTER TABLE security_findings + ADD CONSTRAINT fk_rails_729b763a54 FOREIGN KEY (scanner_id) REFERENCES vulnerability_scanners(id) ON DELETE CASCADE; + SQL + + execute(<<~SQL) + ALTER TABLE security_findings + ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE; + SQL + + execute(<<~SQL) + ALTER TABLE security_findings_1 SET SCHEMA gitlab_partitions_dynamic; + SQL + + execute(<<~SQL) + ALTER TABLE security_findings ATTACH PARTITION gitlab_partitions_dynamic.security_findings_1 FOR VALUES IN (1); + SQL + + execute(<<~SQL) + ALTER TABLE security_findings DROP CONSTRAINT check_partition_number; + SQL + + rename_indices('gitlab_partitions_dynamic', INDEX_MAPPING_OF_PARTITION) + end + + def down + execute(<<~SQL) + LOCK TABLE vulnerability_scanners, security_scans, security_findings IN ACCESS EXCLUSIVE MODE + SQL + + # If there is already a partition for the `security_findings` table, + # we can promote that table to be the original one to save the data. + # Otherwise, we have to bring back the non-partitioned `security_findings` + # table from the partitioned one. + if latest_partition + create_non_partitioned_security_findings_with_data + else + create_non_partitioned_security_findings_without_data + end + end + + private + + def latest_partition + @latest_partition ||= execute(LATEST_PARTITION_SQL).first&.fetch('partition_name', nil) + end + + def latest_partition_number + latest_partition.match(/security_findings_(\d+)/).captures.first + end + + # rubocop:disable Migration/DropTable (These methods are called from the `down` method) + def create_non_partitioned_security_findings_with_data + execute(<<~SQL) + ALTER TABLE security_findings DETACH PARTITION gitlab_partitions_dynamic.#{latest_partition}; + SQL + + execute(<<~SQL) + ALTER TABLE gitlab_partitions_dynamic.#{latest_partition} SET SCHEMA #{connection.current_schema}; + SQL + + execute(<<~SQL) + ALTER SEQUENCE security_findings_id_seq OWNED BY #{latest_partition}.id; + SQL + + execute(<<~SQL) + DROP TABLE security_findings; + SQL + + execute(<<~SQL) + ALTER TABLE #{latest_partition} RENAME TO security_findings; + SQL + + execute(<<~SQL) + ALTER TABLE security_findings ADD CONSTRAINT check_partition_number CHECK ((partition_number = #{latest_partition_number})); + SQL + + index_mapping = INDEX_MAPPING_AFTER_CREATING_FROM_PARTITION.transform_keys do |key| + key.to_s.sub('partition_name_placeholder', latest_partition) + end + + rename_indices(connection.current_schema, index_mapping) + end + + def create_non_partitioned_security_findings_without_data + execute(<<~SQL) + ALTER TABLE security_findings RENAME TO security_findings_1; + SQL + + execute(<<~SQL) + CREATE TABLE security_findings ( + LIKE security_findings_1 INCLUDING ALL + ); + SQL + + execute(<<~SQL) + ALTER SEQUENCE security_findings_id_seq OWNED BY #{connection.current_schema}.security_findings.id; + SQL + + execute(<<~SQL) + DROP TABLE security_findings_1; + SQL + + execute(<<~SQL) + ALTER TABLE security_findings ADD CONSTRAINT check_partition_number CHECK ((partition_number = 1)); + SQL + + execute(<<~SQL) + ALTER TABLE ONLY security_findings + ADD CONSTRAINT fk_rails_729b763a54 FOREIGN KEY (scanner_id) REFERENCES vulnerability_scanners(id) ON DELETE CASCADE; + SQL + + execute(<<~SQL) + ALTER TABLE ONLY security_findings + ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE; + SQL + + rename_indices(connection.current_schema, INDEX_MAPPING_AFTER_CREATING_FROM_ITSELF) + end + + def rename_indices(schema, mapping) + mapping.each do |index_name, new_index_name| + execute(<<~SQL) + ALTER INDEX #{schema}.#{index_name} RENAME TO #{new_index_name}; + SQL + end + end + # rubocop:enable Migration/DropTable +end diff --git a/db/schema_migrations/20220816075639 b/db/schema_migrations/20220816075639 new file mode 100644 index 00000000000000..66496258f7cb8e --- /dev/null +++ b/db/schema_migrations/20220816075639 @@ -0,0 +1 @@ +8767df2ee3d58f6737129b61b7953d4b11e2a3417780eebf5c456a8242c218d7 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 9a2b98050460dc..582ccb9eb25420 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -457,6 +457,22 @@ CREATE TABLE loose_foreign_keys_deleted_records ( ) PARTITION BY LIST (partition); +CREATE TABLE security_findings ( + id bigint NOT NULL, + scan_id bigint NOT NULL, + scanner_id bigint NOT NULL, + severity smallint NOT NULL, + confidence smallint, + project_fingerprint text, + deduplicated boolean DEFAULT false NOT NULL, + uuid uuid, + overridden_uuid uuid, + partition_number integer DEFAULT 1 NOT NULL, + CONSTRAINT check_6c2851a8c9 CHECK ((uuid IS NOT NULL)), + CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40)) +) +PARTITION BY LIST (partition_number); + CREATE TABLE verification_codes ( created_at timestamp with time zone DEFAULT now() NOT NULL, visitor_id_code text NOT NULL, @@ -20914,22 +20930,6 @@ CREATE SEQUENCE scim_oauth_access_tokens_id_seq ALTER SEQUENCE scim_oauth_access_tokens_id_seq OWNED BY scim_oauth_access_tokens.id; -CREATE TABLE security_findings ( - id bigint NOT NULL, - scan_id bigint NOT NULL, - scanner_id bigint NOT NULL, - severity smallint NOT NULL, - confidence smallint, - project_fingerprint text, - deduplicated boolean DEFAULT false NOT NULL, - uuid uuid, - overridden_uuid uuid, - partition_number integer DEFAULT 1 NOT NULL, - CONSTRAINT check_6c2851a8c9 CHECK ((uuid IS NOT NULL)), - CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40)), - CONSTRAINT check_partition_number CHECK ((partition_number = 1)) -); - CREATE SEQUENCE security_findings_id_seq START WITH 1 INCREMENT BY 1 @@ -30041,20 +30041,6 @@ CREATE INDEX index_secure_ci_builds_on_user_id_name_created_at ON ci_builds USIN CREATE INDEX index_security_ci_builds_on_name_and_id_parser_features ON ci_builds USING btree (name, id) WHERE (((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('secret_detection'::character varying)::text, ('coverage_fuzzing'::character varying)::text, ('license_scanning'::character varying)::text, ('apifuzzer_fuzz'::character varying)::text, ('apifuzzer_fuzz_dnd'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text)); -CREATE INDEX index_security_findings_on_confidence ON security_findings USING btree (confidence); - -CREATE INDEX index_security_findings_on_project_fingerprint ON security_findings USING btree (project_fingerprint); - -CREATE INDEX index_security_findings_on_scan_id_and_deduplicated ON security_findings USING btree (scan_id, deduplicated); - -CREATE INDEX index_security_findings_on_scan_id_and_id ON security_findings USING btree (scan_id, id); - -CREATE INDEX index_security_findings_on_scanner_id ON security_findings USING btree (scanner_id); - -CREATE INDEX index_security_findings_on_severity ON security_findings USING btree (severity); - -CREATE UNIQUE INDEX index_security_findings_on_unique_columns ON security_findings USING btree (uuid, scan_id, partition_number); - CREATE INDEX index_security_scans_on_created_at ON security_scans USING btree (created_at); CREATE INDEX index_security_scans_on_date_created_at_and_id ON security_scans USING btree (date(timezone('UTC'::text, created_at)), id); @@ -30697,6 +30683,20 @@ CREATE UNIQUE INDEX partial_index_sop_configs_on_project_id ON security_orchestr CREATE INDEX partial_index_user_id_app_id_created_at_token_not_revoked ON oauth_access_tokens USING btree (resource_owner_id, application_id, created_at) WHERE (revoked_at IS NULL); +CREATE INDEX security_findings_confidence_idx ON ONLY security_findings USING btree (confidence); + +CREATE INDEX security_findings_project_fingerprint_idx ON ONLY security_findings USING btree (project_fingerprint); + +CREATE INDEX security_findings_scan_id_deduplicated_idx ON ONLY security_findings USING btree (scan_id, deduplicated); + +CREATE INDEX security_findings_scan_id_id_idx ON ONLY security_findings USING btree (scan_id, id); + +CREATE INDEX security_findings_scanner_id_idx ON ONLY security_findings USING btree (scanner_id); + +CREATE INDEX security_findings_severity_idx ON ONLY security_findings USING btree (severity); + +CREATE UNIQUE INDEX security_findings_uuid_scan_id_partition_number_idx ON ONLY security_findings USING btree (uuid, scan_id, partition_number); + CREATE UNIQUE INDEX snippet_user_mentions_on_snippet_id_and_note_id_index ON snippet_user_mentions USING btree (snippet_id, note_id); CREATE UNIQUE INDEX snippet_user_mentions_on_snippet_id_index ON snippet_user_mentions USING btree (snippet_id) WHERE (note_id IS NULL); @@ -33789,7 +33789,7 @@ ALTER TABLE ONLY project_custom_attributes ALTER TABLE ONLY ci_pending_builds ADD CONSTRAINT fk_rails_725a2644a3 FOREIGN KEY (build_id) REFERENCES ci_builds(id) ON DELETE CASCADE; -ALTER TABLE ONLY security_findings +ALTER TABLE security_findings ADD CONSTRAINT fk_rails_729b763a54 FOREIGN KEY (scanner_id) REFERENCES vulnerability_scanners(id) ON DELETE CASCADE; ALTER TABLE ONLY dast_scanner_profiles @@ -34227,7 +34227,7 @@ ALTER TABLE ONLY approval_project_rules_users ALTER TABLE ONLY lists ADD CONSTRAINT fk_rails_baed5f39b7 FOREIGN KEY (milestone_id) REFERENCES milestones(id) ON DELETE CASCADE; -ALTER TABLE ONLY security_findings +ALTER TABLE security_findings ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE; ALTER TABLE ONLY packages_debian_project_component_files diff --git a/ee/app/models/security/finding.rb b/ee/app/models/security/finding.rb index 1bba69de05982d..41276a1e6c96a8 100644 --- a/ee/app/models/security/finding.rb +++ b/ee/app/models/security/finding.rb @@ -10,9 +10,16 @@ module Security class Finding < ApplicationRecord include EachBatch + include PartitionedTable self.table_name = 'security_findings' self.primary_key = :id # As ActiveRecord does not support compound PKs + self.ignored_columns = [:partition_number] # This is temporary as we will change the partition logic + + partitioned_by :partition_number, + strategy: :sliding_list, + next_partition_if: -> (_) { false }, + detach_partition_if: -> (_) { false } belongs_to :scan, inverse_of: :findings, optional: false belongs_to :scanner, class_name: 'Vulnerabilities::Scanner', inverse_of: :security_findings, optional: false diff --git a/lib/gitlab/database/partitioning/single_numeric_list_partition.rb b/lib/gitlab/database/partitioning/single_numeric_list_partition.rb index 23ac73a0e53159..421fffe8dbf92c 100644 --- a/lib/gitlab/database/partitioning/single_numeric_list_partition.rb +++ b/lib/gitlab/database/partitioning/single_numeric_list_partition.rb @@ -8,7 +8,7 @@ class SingleNumericListPartition def self.from_sql(table, partition_name, definition) # A list partition can support multiple values, but we only support a single number - matches = definition.match(/FOR VALUES IN \('(?<value>\d+)'\)/) + matches = definition.match(/FOR VALUES IN \('?(?<value>\d+)'?\)/) raise ArgumentError, 'Unknown partition definition' unless matches diff --git a/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb b/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb new file mode 100644 index 00000000000000..7d4546bdf79419 --- /dev/null +++ b/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema do + describe '#up' do + let(:partitions_sql) do + <<~SQL + SELECT + partitions.relname AS partition_name + FROM pg_inherits + JOIN pg_class parent ON pg_inherits.inhparent = parent.oid + JOIN pg_class partitions ON pg_inherits.inhrelid = partitions.oid + WHERE + parent.relname = 'security_findings' + SQL + end + + it 'changes the `security_findings` table to be partitioned' do + expect { migrate! }.to change { security_findings_partitioned? }.from(false).to(true) + .and change { execute(partitions_sql) }.from([]).to(['security_findings_1']) + end + end + + describe '#down' do + context 'when there is a partition' do + let(:users) { table(:users) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:scanners) { table(:vulnerability_scanners) } + let(:security_scans) { table(:security_scans) } + let(:security_findings) { table(:security_findings) } + + let(:user) { users.create!(email: 'test@gitlab.com', projects_limit: 5) } + let(:namespace) { namespaces.create!(name: 'gtlb', path: 'gitlab', type: Namespaces::UserNamespace.sti_name) } + let(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id, name: 'foo') } + let(:scanner) { scanners.create!(project_id: project.id, external_id: 'bandit', name: 'Bandit') } + let(:security_scan) { security_scans.create!(build_id: 1, scan_type: 1) } + + let(:security_findings_count_sql) { 'SELECT COUNT(*) FROM security_findings' } + + before do + migrate! + + security_findings.create!( + scan_id: security_scan.id, + scanner_id: scanner.id, + uuid: SecureRandom.uuid, + severity: 0, + confidence: 0 + ) + end + + it 'creates the original table with the data from the existing partition' do + expect { schema_migrate_down! }.to change { security_findings_partitioned? }.from(true).to(false) + .and not_change { execute(security_findings_count_sql) }.from([1]) + end + + context 'when there are more than one partitions' do + before do + migrate! + + execute(<<~SQL) + CREATE TABLE gitlab_partitions_dynamic.security_findings_11 + PARTITION OF security_findings FOR VALUES IN (11) + SQL + end + + it 'creates the original table from the latest existing partition' do + expect { schema_migrate_down! }.to change { security_findings_partitioned? }.from(true).to(false) + .and change { execute(security_findings_count_sql) }.from([1]).to([0]) + end + end + end + + context 'when there is no partition' do + before do + migrate! + + execute('DROP TABLE gitlab_partitions_dynamic.security_findings_1') + end + + it 'creates the original table' do + expect { schema_migrate_down! }.to change { security_findings_partitioned? }.from(true).to(false) + end + end + end + + def security_findings_partitioned? + sql = <<~SQL + SELECT + COUNT(*) + FROM + pg_partitioned_table + INNER JOIN pg_class ON pg_class.oid = pg_partitioned_table.partrelid + WHERE pg_class.relname = 'security_findings' + SQL + + execute(sql).first != 0 + end + + def execute(sql) + ActiveRecord::Base.connection.execute(sql).values.flatten + end +end -- GitLab From 25ba681d97014cc889579dd6e80845e01dd811dd Mon Sep 17 00:00:00 2001 From: Mehmet Emin INAC <minac@gitlab.com> Date: Wed, 31 Aug 2022 18:07:51 +0300 Subject: [PATCH 017/169] Set partition number based on check constraint --- ...ble_to_gitlab_partitions_dynamic_schema.rb | 33 +++++++++++++++---- ...o_gitlab_partitions_dynamic_schema_spec.rb | 28 ++++++++-------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb b/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb index 9da3f890af3a02..d18bb8a0103623 100644 --- a/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb +++ b/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb @@ -47,22 +47,31 @@ class MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema < Gitlab::Databas LIMIT 1 SQL + CURRENT_CHECK_CONSTRAINT_SQL = <<~SQL + SELECT + pg_get_constraintdef(oid) + FROM + pg_catalog.pg_constraint + WHERE + conname = 'check_partition_number' + SQL + def up execute(<<~SQL) LOCK TABLE vulnerability_scanners, security_scans, security_findings IN ACCESS EXCLUSIVE MODE SQL execute(<<~SQL) - ALTER TABLE security_findings RENAME TO security_findings_1; + ALTER TABLE security_findings RENAME TO security_findings_#{candidate_partition_number}; SQL execute(<<~SQL) - ALTER INDEX security_findings_pkey RENAME TO security_findings_1_pkey; + ALTER INDEX security_findings_pkey RENAME TO security_findings_#{candidate_partition_number}_pkey; SQL execute(<<~SQL) CREATE TABLE security_findings ( - LIKE security_findings_1 INCLUDING ALL + LIKE security_findings_#{candidate_partition_number} INCLUDING ALL ) PARTITION BY LIST (partition_number); SQL @@ -81,18 +90,22 @@ def up SQL execute(<<~SQL) - ALTER TABLE security_findings_1 SET SCHEMA gitlab_partitions_dynamic; + ALTER TABLE security_findings_#{candidate_partition_number} SET SCHEMA gitlab_partitions_dynamic; SQL execute(<<~SQL) - ALTER TABLE security_findings ATTACH PARTITION gitlab_partitions_dynamic.security_findings_1 FOR VALUES IN (1); + ALTER TABLE security_findings ATTACH PARTITION gitlab_partitions_dynamic.security_findings_#{candidate_partition_number} FOR VALUES IN (#{candidate_partition_number}); SQL execute(<<~SQL) ALTER TABLE security_findings DROP CONSTRAINT check_partition_number; SQL - rename_indices('gitlab_partitions_dynamic', INDEX_MAPPING_OF_PARTITION) + index_mapping = INDEX_MAPPING_OF_PARTITION.transform_values do |value| + value.to_s.sub('partition_name_placeholder', "security_findings_#{candidate_partition_number}") + end + + rename_indices('gitlab_partitions_dynamic', index_mapping) end def down @@ -113,6 +126,14 @@ def down private + def current_check_constraint + execute(CURRENT_CHECK_CONSTRAINT_SQL).first['pg_get_constraintdef'] + end + + def candidate_partition_number + @candidate_partition_number ||= current_check_constraint.match(/partition_number\s?=\s?(\d+)/).captures.first + end + def latest_partition @latest_partition ||= execute(LATEST_PARTITION_SQL).first&.fetch('partition_name', nil) end diff --git a/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb b/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb index 7d4546bdf79419..b5bb86edce2a4c 100644 --- a/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb +++ b/spec/migrations/move_security_findings_table_to_gitlab_partitions_dynamic_schema_spec.rb @@ -4,19 +4,19 @@ require_migration! RSpec.describe MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema do - describe '#up' do - let(:partitions_sql) do - <<~SQL - SELECT - partitions.relname AS partition_name - FROM pg_inherits - JOIN pg_class parent ON pg_inherits.inhparent = parent.oid - JOIN pg_class partitions ON pg_inherits.inhrelid = partitions.oid - WHERE - parent.relname = 'security_findings' - SQL - end + let(:partitions_sql) do + <<~SQL + SELECT + partitions.relname AS partition_name + FROM pg_inherits + JOIN pg_class parent ON pg_inherits.inhparent = parent.oid + JOIN pg_class partitions ON pg_inherits.inhrelid = partitions.oid + WHERE + parent.relname = 'security_findings' + SQL + end + describe '#up' do it 'changes the `security_findings` table to be partitioned' do expect { migrate! }.to change { security_findings_partitioned? }.from(false).to(true) .and change { execute(partitions_sql) }.from([]).to(['security_findings_1']) @@ -78,7 +78,9 @@ before do migrate! - execute('DROP TABLE gitlab_partitions_dynamic.security_findings_1') + execute(partitions_sql).each do |partition_name| + execute("DROP TABLE gitlab_partitions_dynamic.#{partition_name}") + end end it 'creates the original table' do -- GitLab From c1434afbb4aec436762bf697bd3779fb19ebb89a Mon Sep 17 00:00:00 2001 From: Mehmet Emin INAC <minac@gitlab.com> Date: Wed, 31 Aug 2022 20:42:55 +0300 Subject: [PATCH 018/169] Use add_check_constraint helper --- ...ble_to_gitlab_partitions_dynamic_schema.rb | 194 +++++++++--------- 1 file changed, 102 insertions(+), 92 deletions(-) diff --git a/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb b/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb index d18bb8a0103623..7b80b6a15bd783 100644 --- a/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb +++ b/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true +# rubocop:disable Migration/WithLockRetriesDisallowedMethod class MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema < Gitlab::Database::Migration[2.0] - enable_lock_retries! + disable_ddl_transaction! INDEX_MAPPING_OF_PARTITION = { index_security_findings_on_unique_columns: :security_findings_1_uuid_scan_id_partition_number_idx, @@ -49,70 +50,68 @@ class MoveSecurityFindingsTableToGitlabPartitionsDynamicSchema < Gitlab::Databas CURRENT_CHECK_CONSTRAINT_SQL = <<~SQL SELECT - pg_get_constraintdef(oid) + pg_get_constraintdef(pg_catalog.pg_constraint.oid) FROM pg_catalog.pg_constraint + INNER JOIN pg_class ON pg_class.oid = pg_catalog.pg_constraint.conrelid WHERE - conname = 'check_partition_number' + conname = 'check_partition_number' AND + pg_class.relname = 'security_findings' SQL def up - execute(<<~SQL) - LOCK TABLE vulnerability_scanners, security_scans, security_findings IN ACCESS EXCLUSIVE MODE - SQL + with_lock_retries do + lock_tables - execute(<<~SQL) - ALTER TABLE security_findings RENAME TO security_findings_#{candidate_partition_number}; - SQL + execute(<<~SQL) + ALTER TABLE security_findings RENAME TO security_findings_#{candidate_partition_number}; + SQL - execute(<<~SQL) - ALTER INDEX security_findings_pkey RENAME TO security_findings_#{candidate_partition_number}_pkey; - SQL + execute(<<~SQL) + ALTER INDEX security_findings_pkey RENAME TO security_findings_#{candidate_partition_number}_pkey; + SQL - execute(<<~SQL) - CREATE TABLE security_findings ( - LIKE security_findings_#{candidate_partition_number} INCLUDING ALL - ) PARTITION BY LIST (partition_number); - SQL + execute(<<~SQL) + CREATE TABLE security_findings ( + LIKE security_findings_#{candidate_partition_number} INCLUDING ALL + ) PARTITION BY LIST (partition_number); + SQL - execute(<<~SQL) - ALTER SEQUENCE security_findings_id_seq OWNED BY #{connection.current_schema}.security_findings.id; - SQL + execute(<<~SQL) + ALTER SEQUENCE security_findings_id_seq OWNED BY #{connection.current_schema}.security_findings.id; + SQL - execute(<<~SQL) - ALTER TABLE security_findings - ADD CONSTRAINT fk_rails_729b763a54 FOREIGN KEY (scanner_id) REFERENCES vulnerability_scanners(id) ON DELETE CASCADE; - SQL + execute(<<~SQL) + ALTER TABLE security_findings + ADD CONSTRAINT fk_rails_729b763a54 FOREIGN KEY (scanner_id) REFERENCES vulnerability_scanners(id) ON DELETE CASCADE; + SQL - execute(<<~SQL) - ALTER TABLE security_findings - ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE; - SQL + execute(<<~SQL) + ALTER TABLE security_findings + ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE; + SQL - execute(<<~SQL) - ALTER TABLE security_findings_#{candidate_partition_number} SET SCHEMA gitlab_partitions_dynamic; - SQL + execute(<<~SQL) + ALTER TABLE security_findings_#{candidate_partition_number} SET SCHEMA gitlab_partitions_dynamic; + SQL - execute(<<~SQL) - ALTER TABLE security_findings ATTACH PARTITION gitlab_partitions_dynamic.security_findings_#{candidate_partition_number} FOR VALUES IN (#{candidate_partition_number}); - SQL + execute(<<~SQL) + ALTER TABLE security_findings ATTACH PARTITION gitlab_partitions_dynamic.security_findings_#{candidate_partition_number} FOR VALUES IN (#{candidate_partition_number}); + SQL - execute(<<~SQL) - ALTER TABLE security_findings DROP CONSTRAINT check_partition_number; - SQL + execute(<<~SQL) + ALTER TABLE security_findings DROP CONSTRAINT check_partition_number; + SQL - index_mapping = INDEX_MAPPING_OF_PARTITION.transform_values do |value| - value.to_s.sub('partition_name_placeholder', "security_findings_#{candidate_partition_number}") - end + index_mapping = INDEX_MAPPING_OF_PARTITION.transform_values do |value| + value.to_s.sub('partition_name_placeholder', "security_findings_#{candidate_partition_number}") + end - rename_indices('gitlab_partitions_dynamic', index_mapping) + rename_indices('gitlab_partitions_dynamic', index_mapping) + end end def down - execute(<<~SQL) - LOCK TABLE vulnerability_scanners, security_scans, security_findings IN ACCESS EXCLUSIVE MODE - SQL - # If there is already a partition for the `security_findings` table, # we can promote that table to be the original one to save the data. # Otherwise, we have to bring back the non-partitioned `security_findings` @@ -126,6 +125,12 @@ def down private + def lock_tables + execute(<<~SQL) + LOCK TABLE vulnerability_scanners, security_scans, security_findings IN ACCESS EXCLUSIVE MODE + SQL + end + def current_check_constraint execute(CURRENT_CHECK_CONSTRAINT_SQL).first['pg_get_constraintdef'] end @@ -144,71 +149,75 @@ def latest_partition_number # rubocop:disable Migration/DropTable (These methods are called from the `down` method) def create_non_partitioned_security_findings_with_data - execute(<<~SQL) - ALTER TABLE security_findings DETACH PARTITION gitlab_partitions_dynamic.#{latest_partition}; - SQL + with_lock_retries do + lock_tables - execute(<<~SQL) - ALTER TABLE gitlab_partitions_dynamic.#{latest_partition} SET SCHEMA #{connection.current_schema}; - SQL + execute(<<~SQL) + ALTER TABLE security_findings DETACH PARTITION gitlab_partitions_dynamic.#{latest_partition}; + SQL - execute(<<~SQL) - ALTER SEQUENCE security_findings_id_seq OWNED BY #{latest_partition}.id; - SQL + execute(<<~SQL) + ALTER TABLE gitlab_partitions_dynamic.#{latest_partition} SET SCHEMA #{connection.current_schema}; + SQL - execute(<<~SQL) - DROP TABLE security_findings; - SQL + execute(<<~SQL) + ALTER SEQUENCE security_findings_id_seq OWNED BY #{latest_partition}.id; + SQL - execute(<<~SQL) - ALTER TABLE #{latest_partition} RENAME TO security_findings; - SQL + execute(<<~SQL) + DROP TABLE security_findings; + SQL - execute(<<~SQL) - ALTER TABLE security_findings ADD CONSTRAINT check_partition_number CHECK ((partition_number = #{latest_partition_number})); - SQL + execute(<<~SQL) + ALTER TABLE #{latest_partition} RENAME TO security_findings; + SQL + + index_mapping = INDEX_MAPPING_AFTER_CREATING_FROM_PARTITION.transform_keys do |key| + key.to_s.sub('partition_name_placeholder', latest_partition) + end - index_mapping = INDEX_MAPPING_AFTER_CREATING_FROM_PARTITION.transform_keys do |key| - key.to_s.sub('partition_name_placeholder', latest_partition) + rename_indices(connection.current_schema, index_mapping) end - rename_indices(connection.current_schema, index_mapping) + add_check_constraint(:security_findings, "(partition_number = #{latest_partition_number})", :check_partition_number) end def create_non_partitioned_security_findings_without_data - execute(<<~SQL) - ALTER TABLE security_findings RENAME TO security_findings_1; - SQL + with_lock_retries do + lock_tables - execute(<<~SQL) - CREATE TABLE security_findings ( - LIKE security_findings_1 INCLUDING ALL - ); - SQL + execute(<<~SQL) + ALTER TABLE security_findings RENAME TO security_findings_1; + SQL - execute(<<~SQL) - ALTER SEQUENCE security_findings_id_seq OWNED BY #{connection.current_schema}.security_findings.id; - SQL + execute(<<~SQL) + CREATE TABLE security_findings ( + LIKE security_findings_1 INCLUDING ALL + ); + SQL - execute(<<~SQL) - DROP TABLE security_findings_1; - SQL + execute(<<~SQL) + ALTER SEQUENCE security_findings_id_seq OWNED BY #{connection.current_schema}.security_findings.id; + SQL - execute(<<~SQL) - ALTER TABLE security_findings ADD CONSTRAINT check_partition_number CHECK ((partition_number = 1)); - SQL + execute(<<~SQL) + DROP TABLE security_findings_1; + SQL - execute(<<~SQL) - ALTER TABLE ONLY security_findings - ADD CONSTRAINT fk_rails_729b763a54 FOREIGN KEY (scanner_id) REFERENCES vulnerability_scanners(id) ON DELETE CASCADE; - SQL + execute(<<~SQL) + ALTER TABLE ONLY security_findings + ADD CONSTRAINT fk_rails_729b763a54 FOREIGN KEY (scanner_id) REFERENCES vulnerability_scanners(id) ON DELETE CASCADE; + SQL - execute(<<~SQL) - ALTER TABLE ONLY security_findings - ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE; - SQL + execute(<<~SQL) + ALTER TABLE ONLY security_findings + ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE; + SQL + + rename_indices(connection.current_schema, INDEX_MAPPING_AFTER_CREATING_FROM_ITSELF) + end - rename_indices(connection.current_schema, INDEX_MAPPING_AFTER_CREATING_FROM_ITSELF) + add_check_constraint(:security_findings, "(partition_number = 1)", :check_partition_number) end def rename_indices(schema, mapping) @@ -220,3 +229,4 @@ def rename_indices(schema, mapping) end # rubocop:enable Migration/DropTable end +# rubocop:enable Migration/WithLockRetriesDisallowedMethod -- GitLab From 76c8b3d29c2f880042a385946b61072729fb86a7 Mon Sep 17 00:00:00 2001 From: Mehmet Emin INAC <minac@gitlab.com> Date: Fri, 2 Sep 2022 02:34:10 +0300 Subject: [PATCH 019/169] Move migration to `post_migrate` --- ...security_findings_table_to_gitlab_partitions_dynamic_schema.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename db/{migrate => post_migrate}/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb (100%) diff --git a/db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb b/db/post_migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb similarity index 100% rename from db/migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb rename to db/post_migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb -- GitLab From 25fd50651bf2f9519322fc1e75b48b18782ee9bf Mon Sep 17 00:00:00 2001 From: Mike Kozono <mkozono@gitlab.com> Date: Thu, 1 Sep 2022 14:59:45 -1000 Subject: [PATCH 020/169] Geo: Remove geo_file_transfer_validation FF It has been enabled by default since it was added in 12.1. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/14477 It was never documented, and I never heard of someone disabling it. Changelog: other EE: true --- .../development/geo_file_transfer_validation.yml | 8 -------- ee/lib/gitlab/geo/replication/blob_downloader.rb | 2 -- 2 files changed, 10 deletions(-) delete mode 100644 ee/config/feature_flags/development/geo_file_transfer_validation.yml diff --git a/ee/config/feature_flags/development/geo_file_transfer_validation.yml b/ee/config/feature_flags/development/geo_file_transfer_validation.yml deleted file mode 100644 index aef9e2e4a21250..00000000000000 --- a/ee/config/feature_flags/development/geo_file_transfer_validation.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: geo_file_transfer_validation -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16382 -rollout_issue_url: -milestone: '12.3' -type: development -group: group::geo -default_enabled: true diff --git a/ee/lib/gitlab/geo/replication/blob_downloader.rb b/ee/lib/gitlab/geo/replication/blob_downloader.rb index b7508e0c8f111b..d9b0a653d2e9a1 100644 --- a/ee/lib/gitlab/geo/replication/blob_downloader.rb +++ b/ee/lib/gitlab/geo/replication/blob_downloader.rb @@ -229,8 +229,6 @@ def checksum_mismatch?(file_path) # types of artifacts are not checksummed at all at the moment. return false if primary_checksum.blank? - return false unless Feature.enabled?(:geo_file_transfer_validation) - primary_checksum != actual_checksum(file_path) end -- GitLab From 1ef7f978e35cd9250a5ef2450e582878628858f1 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu <heinrich@gitlab.com> Date: Fri, 5 Aug 2022 21:52:33 +0800 Subject: [PATCH 021/169] Add pipelined Redis commands to performance bar This also includes them in `redis_calls` and `redis_duration_s` in the logs. Changelog: added --- lib/gitlab/instrumentation/redis_base.rb | 14 ++--- .../instrumentation/redis_interceptor.rb | 57 +++++++++++-------- lib/gitlab/reactive_cache_set_cache.rb | 6 +- .../duplicate_jobs/duplicate_job.rb | 10 ++-- lib/peek/views/redis_detailed.rb | 6 +- .../gitlab/instrumentation/redis_base_spec.rb | 19 +++++-- .../instrumentation/redis_interceptor_spec.rb | 40 ++++++++++++- spec/lib/peek/views/redis_detailed_spec.rb | 22 +++---- 8 files changed, 119 insertions(+), 55 deletions(-) diff --git a/lib/gitlab/instrumentation/redis_base.rb b/lib/gitlab/instrumentation/redis_base.rb index 0beab008f73393..0bd10597f241da 100644 --- a/lib/gitlab/instrumentation/redis_base.rb +++ b/lib/gitlab/instrumentation/redis_base.rb @@ -20,21 +20,19 @@ def add_duration(duration) ::RequestStore[call_duration_key] += duration end - def add_call_details(duration, args) + def add_call_details(duration, commands) return unless Gitlab::PerformanceBar.enabled_for_request? - # redis-rb passes an array (e.g. [[:get, key]]) - return unless args.length == 1 detail_store << { - cmd: args.first, + commands: commands, duration: duration, backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(caller) } end - def increment_request_count + def increment_request_count(amount = 1) ::RequestStore[request_count_key] ||= 0 - ::RequestStore[request_count_key] += 1 + ::RequestStore[request_count_key] += amount end def increment_read_bytes(num_bytes) @@ -78,9 +76,9 @@ def enable_redis_cluster_validation self end - def instance_count_request + def instance_count_request(amount = 1) @request_counter ||= Gitlab::Metrics.counter(:gitlab_redis_client_requests_total, 'Client side Redis request count, per Redis server') - @request_counter.increment({ storage: storage_key }) + @request_counter.increment({ storage: storage_key }, amount) end def instance_count_exception(ex) diff --git a/lib/gitlab/instrumentation/redis_interceptor.rb b/lib/gitlab/instrumentation/redis_interceptor.rb index 14474693ddfab4..7e2acb91b9498f 100644 --- a/lib/gitlab/instrumentation/redis_interceptor.rb +++ b/lib/gitlab/instrumentation/redis_interceptor.rb @@ -13,27 +13,15 @@ def initialize(backtrace) end end - def call(*args, &block) - start = Gitlab::Metrics::System.monotonic_time # must come first so that 'start' is always defined - instrumentation_class.instance_count_request - instrumentation_class.redis_cluster_validate!(args.first) - - super(*args, &block) - rescue ::Redis::BaseError => ex - instrumentation_class.instance_count_exception(ex) - raise ex - ensure - duration = Gitlab::Metrics::System.monotonic_time - start - - unless APDEX_EXCLUDE.include?(command_from_args(args)) - instrumentation_class.instance_observe_duration(duration) + def call(command) + instrument_call([command]) do + super end + end - if ::RequestStore.active? - # These metrics measure total Redis usage per Rails request / job. - instrumentation_class.increment_request_count - instrumentation_class.add_duration(duration) - instrumentation_class.add_call_details(duration, args) + def call_pipeline(pipeline) + instrument_call(pipeline.commands) do + super end end @@ -50,6 +38,31 @@ def read private + def instrument_call(commands) + start = Gitlab::Metrics::System.monotonic_time # must come first so that 'start' is always defined + instrumentation_class.instance_count_request(commands.size) + + commands.each { |c| instrumentation_class.redis_cluster_validate!(c) } + + yield + rescue ::Redis::BaseError => ex + instrumentation_class.instance_count_exception(ex) + raise ex + ensure + duration = Gitlab::Metrics::System.monotonic_time - start + + unless exclude_from_apdex?(commands) + commands.each { instrumentation_class.instance_observe_duration(duration / commands.size) } + end + + if ::RequestStore.active? + # These metrics measure total Redis usage per Rails request / job. + instrumentation_class.increment_request_count(commands.size) + instrumentation_class.add_duration(duration) + instrumentation_class.add_call_details(duration, commands) + end + end + def measure_write_size(command) size = 0 @@ -97,10 +110,8 @@ def instrumentation_class @options[:instrumentation_class] # rubocop:disable Gitlab/ModuleWithInstanceVariables end - def command_from_args(args) - command = args[0] - command = command[0] if command.is_a?(Array) - command.to_s.downcase + def exclude_from_apdex?(commands) + commands.any? { |command| APDEX_EXCLUDE.include?(command.first.to_s.downcase) } end end end diff --git a/lib/gitlab/reactive_cache_set_cache.rb b/lib/gitlab/reactive_cache_set_cache.rb index fbf96023f30d48..2de3c07712fafc 100644 --- a/lib/gitlab/reactive_cache_set_cache.rb +++ b/lib/gitlab/reactive_cache_set_cache.rb @@ -15,8 +15,10 @@ def clear_cache!(key) keys = read(key).map { |value| "#{cache_namespace}:#{value}" } keys << cache_key(key) - redis.pipelined do |pipeline| - keys.each_slice(1000) { |subset| pipeline.unlink(*subset) } + Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do + redis.pipelined do |pipeline| + keys.each_slice(1000) { |subset| pipeline.unlink(*subset) } + end end end end diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb index 7533770e254fc3..ab126ea474982a 100644 --- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb +++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb @@ -112,10 +112,12 @@ def latest_wal_locations end def delete! - with_redis do |redis| - redis.multi do |multi| - multi.del(idempotency_key, deduplicated_flag_key) - delete_wal_locations!(multi) + Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do + with_redis do |redis| + redis.multi do |multi| + multi.del(idempotency_key, deduplicated_flag_key) + delete_wal_locations!(multi) + end end end end diff --git a/lib/peek/views/redis_detailed.rb b/lib/peek/views/redis_detailed.rb index 44ec0ec0f687a1..76c283bf802d84 100644 --- a/lib/peek/views/redis_detailed.rb +++ b/lib/peek/views/redis_detailed.rb @@ -16,7 +16,11 @@ def detail_store private def format_call_details(call) - super.merge(cmd: format_command(call[:cmd]), + cmd = call[:commands].map do |command| + format_command(command) + end.join(', ') + + super.merge(cmd: cmd, instance: call[:storage]) end diff --git a/spec/lib/gitlab/instrumentation/redis_base_spec.rb b/spec/lib/gitlab/instrumentation/redis_base_spec.rb index a7e08b5a9bdfd4..f9dd0c94c97d78 100644 --- a/spec/lib/gitlab/instrumentation/redis_base_spec.rb +++ b/spec/lib/gitlab/instrumentation/redis_base_spec.rb @@ -65,6 +65,13 @@ expect(instrumentation_class_b.get_request_count).to eq(2) end end + + it 'increments by the given amount' do + instrumentation_class_a.increment_request_count(2) + instrumentation_class_a.increment_request_count(3) + + expect(instrumentation_class_a.get_request_count).to eq(5) + end end describe '.increment_write_bytes' do @@ -103,21 +110,21 @@ context 'storage key overlapping' do it 'keys do not overlap across storages' do 2.times do - instrumentation_class_a.add_call_details(0.3, [:set]) - instrumentation_class_b.add_call_details(0.4, [:set]) + instrumentation_class_a.add_call_details(0.3, [[:set]]) + instrumentation_class_b.add_call_details(0.4, [[:set]]) end expect(instrumentation_class_a.detail_store).to match( [ - a_hash_including(cmd: :set, duration: 0.3, backtrace: an_instance_of(Array)), - a_hash_including(cmd: :set, duration: 0.3, backtrace: an_instance_of(Array)) + a_hash_including(commands: [[:set]], duration: 0.3, backtrace: an_instance_of(Array)), + a_hash_including(commands: [[:set]], duration: 0.3, backtrace: an_instance_of(Array)) ] ) expect(instrumentation_class_b.detail_store).to match( [ - a_hash_including(cmd: :set, duration: 0.4, backtrace: an_instance_of(Array)), - a_hash_including(cmd: :set, duration: 0.4, backtrace: an_instance_of(Array)) + a_hash_including(commands: [[:set]], duration: 0.4, backtrace: an_instance_of(Array)), + a_hash_including(commands: [[:set]], duration: 0.4, backtrace: an_instance_of(Array)) ] ) end diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb index 09280402e2b217..b6def9f3e3d1dc 100644 --- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb +++ b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb @@ -47,11 +47,22 @@ let(:instrumentation_class) { Gitlab::Redis::SharedState.instrumentation_class } it 'counts successful requests' do - expect(instrumentation_class).to receive(:instance_count_request).and_call_original + expect(instrumentation_class).to receive(:instance_count_request).with(1).and_call_original Gitlab::Redis::SharedState.with { |redis| redis.call(:get, 'foobar') } end + it 'counts successful pipelined requests' do + expect(instrumentation_class).to receive(:instance_count_request).with(2).and_call_original + + Gitlab::Redis::SharedState.with do |redis| + redis.pipelined do |pipeline| + pipeline.call(:get, 'foobar') + pipeline.call(:get, 'foobarbaz') + end + end + end + it 'counts exceptions' do expect(instrumentation_class).to receive(:instance_count_exception) .with(instance_of(Redis::CommandError)).and_call_original @@ -84,6 +95,20 @@ Gitlab::Redis::SharedState.with { |redis| redis.call(*command) } end end + + context 'with pipelined commands' do + it 'measures requests that do not have blocking commands' do + expect(instrumentation_class).to receive(:instance_observe_duration).twice.with(a_value > 0) + .and_call_original + + Gitlab::Redis::SharedState.with do |redis| + redis.pipelined do |pipeline| + pipeline.call(:get, 'foobar') + pipeline.call(:get, 'foobarbaz') + end + end + end + end end describe 'commands not in the apdex' do @@ -109,6 +134,19 @@ end end end + + context 'with pipelined commands' do + it 'skips requests that have blocking commands' do + expect(instrumentation_class).not_to receive(:instance_observe_duration) + + Gitlab::Redis::SharedState.with do |redis| + redis.pipelined do |pipeline| + pipeline.call(:get, 'foo') + pipeline.call(:brpop, 'foobar', '0.01') + end + end + end + end end end end diff --git a/spec/lib/peek/views/redis_detailed_spec.rb b/spec/lib/peek/views/redis_detailed_spec.rb index a757af50dcbcfe..5d75a6522e4e1b 100644 --- a/spec/lib/peek/views/redis_detailed_spec.rb +++ b/spec/lib/peek/views/redis_detailed_spec.rb @@ -7,17 +7,19 @@ using RSpec::Parameterized::TableSyntax - where(:cmd, :expected) do - [:auth, 'test'] | 'auth <redacted>' - [:set, 'key', 'value'] | 'set key <redacted>' - [:set, 'bad'] | 'set bad' - [:hmset, 'key1', 'value1', 'key2', 'value2'] | 'hmset key1 <redacted>' - [:get, 'key'] | 'get key' + where(:commands, :expected) do + [[:auth, 'test']] | 'auth <redacted>' + [[:set, 'key', 'value']] | 'set key <redacted>' + [[:set, 'bad']] | 'set bad' + [[:hmset, 'key1', 'value1', 'key2', 'value2']] | 'hmset key1 <redacted>' + [[:get, 'key']] | 'get key' + [[:get, 'key1'], [:get, 'key2']] | 'get key1, get key2' + [[:set, 'key1', 'value'], [:set, 'key2', 'value']] | 'set key1 <redacted>, set key2 <redacted>' end with_them do it 'scrubs Redis commands' do - Gitlab::Instrumentation::Redis::SharedState.detail_store << { cmd: cmd, duration: 1.second } + Gitlab::Instrumentation::Redis::SharedState.detail_store << { commands: commands, duration: 1.second } expect(subject.results[:details].count).to eq(1) expect(subject.results[:details].first) @@ -29,9 +31,9 @@ end it 'returns aggregated results' do - Gitlab::Instrumentation::Redis::Cache.detail_store << { cmd: [:get, 'test'], duration: 0.001 } - Gitlab::Instrumentation::Redis::Cache.detail_store << { cmd: [:get, 'test'], duration: 1.second } - Gitlab::Instrumentation::Redis::SharedState.detail_store << { cmd: [:get, 'test'], duration: 1.second } + Gitlab::Instrumentation::Redis::Cache.detail_store << { commands: [[:get, 'test']], duration: 0.001 } + Gitlab::Instrumentation::Redis::Cache.detail_store << { commands: [[:get, 'test']], duration: 1.second } + Gitlab::Instrumentation::Redis::SharedState.detail_store << { commands: [[:get, 'test']], duration: 1.second } expect(subject.results[:calls]).to eq(3) expect(subject.results[:duration]).to eq('2001.00ms') -- GitLab From 786322afa64d1dd7057565f075f4fd5ad25fe3ee Mon Sep 17 00:00:00 2001 From: Roy Zwambag <rzwambag@gitlab.com> Date: Fri, 2 Sep 2022 10:16:50 +0200 Subject: [PATCH 022/169] Add curly brackets to fix the matcher in ruby 3 --- .../git_abuse/namespace_throttle_service_spec.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ee/spec/services/users/abuse/git_abuse/namespace_throttle_service_spec.rb b/ee/spec/services/users/abuse/git_abuse/namespace_throttle_service_spec.rb index 51273ac058dcc7..a51f71dc6d4a06 100644 --- a/ee/spec/services/users/abuse/git_abuse/namespace_throttle_service_spec.rb +++ b/ee/spec/services/users/abuse/git_abuse/namespace_throttle_service_spec.rb @@ -76,13 +76,13 @@ end it 'logs the event', :aggregate_failures do - expect(Gitlab::AppLogger).to receive(:info).with( + expect(Gitlab::AppLogger).to receive(:info).with({ message: "User exceeded max projects download within set time period for namespace", username: user.username, max_project_downloads: limit, time_period_s: time_period_in_seconds, namespace_id: namespace.id - ) + }) expect(Gitlab::AppLogger).to receive(:info).with({ message: "Namespace-level user ban", @@ -129,13 +129,13 @@ end it 'logs the notification event but not the ban event', :aggregate_failures do - expect(Gitlab::AppLogger).to receive(:info).with( + expect(Gitlab::AppLogger).to receive(:info).with({ message: "User exceeded max projects download within set time period for namespace", username: owner.username, max_project_downloads: limit, time_period_s: time_period_in_seconds, namespace_id: namespace.id - ) + }) expect(Gitlab::AppLogger).not_to receive(:info).with({ message: "Namespace-level user ban", @@ -169,20 +169,20 @@ end it 'logs the notification event but not the ban event' do - expect(Gitlab::AppLogger).to receive(:info).with( + expect(Gitlab::AppLogger).to receive(:info).with({ message: "User exceeded max projects download within set time period for namespace", username: user.username, max_project_downloads: limit, time_period_s: time_period_in_seconds, namespace_id: namespace.id - ) + }) - expect(Gitlab::AppLogger).not_to receive(:info).with( + expect(Gitlab::AppLogger).not_to receive(:info).with({ message: "Namespace-level user ban", username: user.username, namespace_id: namespace.id, ban_by: described_class.name - ) + }) execute end -- GitLab From f5054716b8504d6fd0e89831a1fbafd3e643de8d Mon Sep 17 00:00:00 2001 From: Alexandru Croitor <acroitor@gitlab.com> Date: Thu, 1 Sep 2022 15:42:48 +0300 Subject: [PATCH 023/169] Add streaming audit event for work item and MR deletion Add the capability to track work item or MR deletion as a streaming audit event. Changelog: added --- .../services/ee/issuable/destroy_service.rb | 19 +++++++ ee/config/audit_events/types/delete_epic.yaml | 8 +++ .../audit_events/types/delete_issue.yaml | 8 +++ .../types/delete_merge_request.yaml | 8 +++ .../audit_events/types/delete_work_item.yaml | 8 +++ .../ee/issuable/destroy_service_spec.rb | 49 +++++++++++++++++++ 6 files changed, 100 insertions(+) create mode 100644 ee/config/audit_events/types/delete_epic.yaml create mode 100644 ee/config/audit_events/types/delete_issue.yaml create mode 100644 ee/config/audit_events/types/delete_merge_request.yaml create mode 100644 ee/config/audit_events/types/delete_work_item.yaml diff --git a/ee/app/services/ee/issuable/destroy_service.rb b/ee/app/services/ee/issuable/destroy_service.rb index 82f83405baba7c..eba865e545ce6d 100644 --- a/ee/app/services/ee/issuable/destroy_service.rb +++ b/ee/app/services/ee/issuable/destroy_service.rb @@ -9,6 +9,7 @@ module DestroyService override :after_destroy def after_destroy(issuable) + log_audit_event(issuable) track_usage_ping_epic_destroyed(issuable) if issuable.is_a?(Epic) super @@ -27,6 +28,24 @@ def track_usage_ping_epic_destroyed(epic) namespace: epic.group ) end + + def log_audit_event(issuable) + return unless current_user + + issuable_name = issuable.is_a?(Issue) ? issuable.work_item_type.name : issuable.class.name + + audit_context = { + name: "delete_#{issuable.to_ability_name}", + stream_only: true, + author: current_user, + target: issuable, + scope: issuable.resource_parent, + message: "Removed #{issuable_name}(#{issuable.title} with IID: #{issuable.iid} and ID: #{issuable.id})", + target_details: { title: issuable.title, iid: issuable.iid, id: issuable.id, type: issuable_name } + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end end end end diff --git a/ee/config/audit_events/types/delete_epic.yaml b/ee/config/audit_events/types/delete_epic.yaml new file mode 100644 index 00000000000000..0101633380c8cc --- /dev/null +++ b/ee/config/audit_events/types/delete_epic.yaml @@ -0,0 +1,8 @@ +name: delete_epic +description: Event triggered on successful epic deletion +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/370487 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96773 +group: "group::project management" +milestone: 15.4 +saved_to_database: false +streamed: true \ No newline at end of file diff --git a/ee/config/audit_events/types/delete_issue.yaml b/ee/config/audit_events/types/delete_issue.yaml new file mode 100644 index 00000000000000..f5df5696ef741a --- /dev/null +++ b/ee/config/audit_events/types/delete_issue.yaml @@ -0,0 +1,8 @@ +name: delete_issue +description: Event triggered on successful issue deletion +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/370487 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96773 +group: "group::project management" +milestone: 15.4 +saved_to_database: false +streamed: true \ No newline at end of file diff --git a/ee/config/audit_events/types/delete_merge_request.yaml b/ee/config/audit_events/types/delete_merge_request.yaml new file mode 100644 index 00000000000000..e03b65879337a6 --- /dev/null +++ b/ee/config/audit_events/types/delete_merge_request.yaml @@ -0,0 +1,8 @@ +name: delete_merge_request +description: Event triggered on successful merge request deletion +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/370487 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96773 +group: "group::project management" +milestone: 15.4 +saved_to_database: false +streamed: true \ No newline at end of file diff --git a/ee/config/audit_events/types/delete_work_item.yaml b/ee/config/audit_events/types/delete_work_item.yaml new file mode 100644 index 00000000000000..4567c50c8a121e --- /dev/null +++ b/ee/config/audit_events/types/delete_work_item.yaml @@ -0,0 +1,8 @@ +name: delete_work_item +description: Event triggered on successful work item deletion +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/370487 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96773 +group: "group::project management" +milestone: 15.4 +saved_to_database: false +streamed: true \ No newline at end of file diff --git a/ee/spec/services/ee/issuable/destroy_service_spec.rb b/ee/spec/services/ee/issuable/destroy_service_spec.rb index 580b6ecfef62f2..87e1c40cb1b560 100644 --- a/ee/spec/services/ee/issuable/destroy_service_spec.rb +++ b/ee/spec/services/ee/issuable/destroy_service_spec.rb @@ -32,6 +32,55 @@ subject.execute(issuable) end + + RSpec.shared_examples 'logs delete issuable audit event' do + it 'logs audit event' do + audit_context = { + name: "delete_#{issuable.to_ability_name}", + stream_only: true, + author: user, + scope: scope, + target: issuable, + message: "Removed #{issuable_name}(#{issuable.title} with IID: #{issuable.iid} and ID: #{issuable.id})", + target_details: { title: issuable.title, iid: issuable.iid, id: issuable.id, type: issuable_name } + } + + expect(::Gitlab::Audit::Auditor).to receive(:audit).with(audit_context) + + service.execute(issuable) + end + end + + context 'when issuable is an issue' do + let(:issuable_name) { issuable.work_item_type.name } + let(:scope) { issuable.project } + + it_behaves_like 'logs delete issuable audit event' + end + + context 'when issuable is an epic' do + let(:issuable) { create(:epic) } + let(:issuable_name) { 'Epic' } + let(:scope) { issuable.group } + + it_behaves_like 'logs delete issuable audit event' + end + + context 'when issuable is a task' do + let(:issuable) { create(:work_item, :task) } + let(:issuable_name) { issuable.work_item_type.name } + let(:scope) { issuable.project } + + it_behaves_like 'logs delete issuable audit event' + end + + context 'when issuable is a merge_request' do + let(:issuable) { create(:merge_request) } + let(:issuable_name) { 'MergeRequest' } + let(:scope) { issuable.project } + + it_behaves_like 'logs delete issuable audit event' + end end end end -- GitLab From 9fc3b8c4ea3bb911e7e91a5c58a3cfe2b558043d Mon Sep 17 00:00:00 2001 From: Rajendra Kadam <rkadam@gitlab.com> Date: Thu, 1 Sep 2022 20:31:22 +0530 Subject: [PATCH 024/169] Add check for permissions in timeline events dropdown --- .../issues/show/components/incidents/timeline_events_item.vue | 4 ++-- .../issues/show/components/incidents/timeline_events_tab.vue | 4 ++-- app/assets/javascripts/issues/show/index.js | 2 ++ app/helpers/issuables_helper.rb | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue index 866111981b2b83..cbf3c387fa3438 100644 --- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue +++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue @@ -16,7 +16,7 @@ export default { directives: { SafeHtml: GlSafeHtmlDirective, }, - inject: ['canUpdate'], + inject: ['canUpdateTimelineEvent'], props: { occurredAt: { type: String, @@ -61,7 +61,7 @@ export default { <div v-safe-html="noteHtml"></div> </div> <gl-dropdown - v-if="canUpdate" + v-if="canUpdateTimelineEvent" right class="event-note-actions gl-ml-auto gl-align-self-start" icon="ellipsis_v" diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue index 443a27695cad26..ac41a7f7c45224 100644 --- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue +++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue @@ -20,7 +20,7 @@ export default { IncidentTimelineEventsList, }, i18n: timelineTabI18n, - inject: ['canUpdate', 'fullPath', 'issuableId'], + inject: ['canUpdateTimelineEvent', 'fullPath', 'issuableId'], data() { return { isEventFormVisible: false, @@ -98,7 +98,7 @@ export default { :class="{ 'gl-pl-0': !hasTimelineEvents }" @hide-new-timeline-events-form="hideEventForm" /> - <gl-button v-if="canUpdate" variant="default" class="gl-mb-3 gl-mt-7" @click="showEventForm"> + <gl-button v-if="canUpdateTimelineEvent" variant="default" class="gl-mb-3 gl-mt-7" @click="showEventForm"> {{ $options.i18n.addEventButton }} </gl-button> </gl-tab> diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js index 459a38048378dc..e5eed9f6b79651 100644 --- a/app/assets/javascripts/issues/show/index.js +++ b/app/assets/javascripts/issues/show/index.js @@ -32,6 +32,7 @@ export function initIncidentApp(issueData = {}) { const { canCreateIncident, canUpdate, + canUpdateTimelineEvent, iid, issuableId, projectNamespace, @@ -51,6 +52,7 @@ export function initIncidentApp(issueData = {}) { provide: { issueType: INCIDENT_TYPE, canCreateIncident, + canUpdateTimelineEvent, canUpdate, fullPath, iid, diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 3b9cf7d05f8009..96daf398243da8 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -240,6 +240,7 @@ def issuable_initial_data(issuable) updateEndpoint: "#{issuable_path(issuable)}.json", canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable), canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable), + canUpdateTimelineEvent: can?(current_user, :admin_incident_management_timeline_event, issuable), issuableRef: issuable.to_reference, markdownPreviewPath: preview_markdown_path(parent, target_type: issuable.model_name, target_id: issuable.iid), markdownDocsPath: help_page_path('user/markdown'), -- GitLab From aba51cf48ec4e8257faa8ea21e0ef7b5702f3c5b Mon Sep 17 00:00:00 2001 From: Rajendra Kadam <rkadam@gitlab.com> Date: Thu, 1 Sep 2022 20:33:23 +0530 Subject: [PATCH 025/169] Run prettier on events tab vue --- .../show/components/incidents/timeline_events_tab.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue index ac41a7f7c45224..5f70d9acac9151 100644 --- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue +++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue @@ -98,7 +98,12 @@ export default { :class="{ 'gl-pl-0': !hasTimelineEvents }" @hide-new-timeline-events-form="hideEventForm" /> - <gl-button v-if="canUpdateTimelineEvent" variant="default" class="gl-mb-3 gl-mt-7" @click="showEventForm"> + <gl-button + v-if="canUpdateTimelineEvent" + variant="default" + class="gl-mb-3 gl-mt-7" + @click="showEventForm" + > {{ $options.i18n.addEventButton }} </gl-button> </gl-tab> -- GitLab From d4f0d984673f1d626ffd493e93a4a970670d6451 Mon Sep 17 00:00:00 2001 From: Rajendra Kadam <rkadam@gitlab.com> Date: Fri, 2 Sep 2022 14:54:08 +0530 Subject: [PATCH 026/169] Update specs for event item and event tab --- .../show/components/incidents/timeline_events_item_spec.js | 6 +++--- .../show/components/incidents/timeline_events_tab_spec.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js index edbbc9f8f4e660..28223e92ade622 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js @@ -19,7 +19,7 @@ describe('IncidentTimelineEventList', () => { ...propsData, }, provide: { - canUpdate: false, + canUpdateTimelineEvent: false, ...provide, }, }); @@ -81,14 +81,14 @@ describe('IncidentTimelineEventList', () => { }); it('shows dropdown and delete item when user has update permission', () => { - mountComponent({ provide: { canUpdate: true } }); + mountComponent({ provide: { canUpdateTimelineEvent: true } }); expect(findDropdown().exists()).toBe(true); expect(findDeleteButton().exists()).toBe(true); }); it('triggers a delete when the delete button is clicked', async () => { - mountComponent({ provide: { canUpdate: true } }); + mountComponent({ provide: { canUpdateTimelineEvent: true } }); findDeleteButton().trigger('click'); diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js index f260b803b7ac2f..5bac1d6e7ad9e6 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js @@ -36,7 +36,7 @@ describe('TimelineEventsTab', () => { provide: { fullPath: 'group/project', issuableId: '1', - canUpdate: true, + canUpdateTimelineEvent: true, ...provide, }, apolloProvider: mockApollo, @@ -136,7 +136,7 @@ describe('TimelineEventsTab', () => { it('should not show a button when user cannot update', () => { mountComponent({ mockApollo: createMockApolloProvider(emptyResponse), - provide: { canUpdate: false }, + provide: { canUpdateTimelineEvent: false }, }); expect(findAddEventButton().exists()).toBe(false); -- GitLab From c3ab806c6a482713426745bfe204885df76e5184 Mon Sep 17 00:00:00 2001 From: Rajendra Kadam <rkadam@gitlab.com> Date: Fri, 2 Sep 2022 14:59:16 +0530 Subject: [PATCH 027/169] Fix FE specs for timeline events --- ee/spec/helpers/ee/issuables_helper_spec.rb | 1 + .../show/components/incidents/timeline_events_list_spec.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ee/spec/helpers/ee/issuables_helper_spec.rb b/ee/spec/helpers/ee/issuables_helper_spec.rb index 0bfe54a432776a..025bfc8ee94410 100644 --- a/ee/spec/helpers/ee/issuables_helper_spec.rb +++ b/ee/spec/helpers/ee/issuables_helper_spec.rb @@ -22,6 +22,7 @@ canAdmin: true, canDestroy: true, canUpdate: true, + canUpdateTimelineEvent: true, confidential: epic.confidential, endpoint: "/groups/#{@group.full_path}/-/epics/#{epic.iid}", epicLinksEndpoint: "/groups/#{@group.full_path}/-/epics/#{epic.iid}/links", diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js index 18bd91f0465ee0..549b819fa197cb 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js @@ -52,7 +52,7 @@ describe('IncidentTimelineEventList', () => { provide: { fullPath: 'group/project', issuableId: '1', - canUpdate: true, + canUpdateTimelineEvent: true, }, apolloProvider, }); -- GitLab From 81ddd10525c415fe1d8fcd4d27af5e41c7fb9893 Mon Sep 17 00:00:00 2001 From: Nataliia Radina <nradina@gitlab.com> Date: Fri, 2 Sep 2022 15:14:15 +0200 Subject: [PATCH 028/169] Fix wrapping in source files Changelog: fixed --- app/assets/stylesheets/framework/highlight.scss | 3 +-- app/assets/stylesheets/pages/search.scss | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index ab426f388c6e98..a63ce66e681f63 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -31,8 +31,7 @@ width: 100%; padding-left: 10px; padding-right: 10px; - white-space: break-spaces; - word-break: break-word; + white-space: pre; &:empty::before { content: '\200b'; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 6c909b8d9faa7f..f65c45d6d893f1 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -383,6 +383,10 @@ input[type='checkbox']:hover { .line_holder { pre { padding: 0; // This overrides the existing style that will add space between each line. + .line { + @include gl-word-break-word; + white-space: break-spaces; + } } svg { -- GitLab From 969384f5aa5a710f717b3b2d3e1db1e280ac445a Mon Sep 17 00:00:00 2001 From: Sascha Eggenberger <seggenberger@gitlab.com> Date: Fri, 2 Sep 2022 15:46:11 +0200 Subject: [PATCH 029/169] Change code block border-radius to default To be visually more in sync with other elements change the border-radius of code blocks to use the default .25rem radius. Changelog: changed --- app/assets/stylesheets/framework/typography.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 559f14090d508c..e79fb84396784e 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -333,7 +333,7 @@ font-size: 13px; line-height: 1.6em; overflow-x: auto; - border-radius: 2px; + border-radius: $border-radius-default; // Multi-line code blocks should scroll horizontally code { -- GitLab From 4ef7ec9e8cb38f56e1a204d6146e6dd17628e6d4 Mon Sep 17 00:00:00 2001 From: Peter Leitzen <pleitzen@gitlab.com> Date: Fri, 1 Apr 2022 17:56:05 +0200 Subject: [PATCH 030/169] Limit Sentry API size for all responses All checks are behind a feature flag `:error_tracking_sentry_limit`. The previously existing check for issues still works with feature flag disabled. --- .../project_error_tracking_setting.rb | 16 ++- .../error_tracking_sentry_limit.yml | 8 ++ lib/error_tracking/sentry_client.rb | 20 +++- lib/error_tracking/sentry_client/issue.rb | 12 +- lib/gitlab/utils/deep_size.rb | 4 - .../sentry_client/event_spec.rb | 5 +- .../sentry_client/issue_link_spec.rb | 7 +- .../sentry_client/issue_spec.rb | 24 ++-- .../sentry_client/projects_spec.rb | 1 + .../error_tracking/sentry_client/repo_spec.rb | 3 +- spec/lib/gitlab/utils/deep_size_spec.rb | 6 - .../project_error_tracking_setting_spec.rb | 106 +++++++++--------- .../lib/sentry/client_shared_examples.rb | 49 +++++++- 13 files changed, 165 insertions(+), 96 deletions(-) create mode 100644 config/feature_flags/development/error_tracking_sentry_limit.yml diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index 4953f24755cd10..12d73ef0d720d2 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -23,6 +23,7 @@ class ProjectErrorTrackingSetting < ApplicationRecord self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] } self.reactive_cache_work_type = :external_dependency + self.reactive_cache_hard_limit = ErrorTracking::SentryClient::RESPONSE_SIZE_LIMIT self.table_name = 'project_error_tracking_settings' @@ -103,9 +104,18 @@ def self.build_api_url_from(api_host:, project_slug:, organization_slug:) api_host end + def sentry_response_limit_enabled? + Feature.enabled?(:error_tracking_sentry_limit, project) + end + + def reactive_cache_limit_enabled? + sentry_response_limit_enabled? + end + def sentry_client strong_memoize(:sentry_client) do - ::ErrorTracking::SentryClient.new(api_url, token) + ::ErrorTracking::SentryClient + .new(api_url, token, validate_size_guarded_by_feature_flag: sentry_response_limit_enabled?) end end @@ -127,14 +137,14 @@ def list_sentry_projects def issue_details(opts = {}) with_reactive_cache('issue_details', opts.stringify_keys) do |result| - ensure_issue_belongs_to_project!(result[:issue].project_id) + ensure_issue_belongs_to_project!(result[:issue].project_id) if result[:issue] result end end def issue_latest_event(opts = {}) with_reactive_cache('issue_latest_event', opts.stringify_keys) do |result| - ensure_issue_belongs_to_project!(result[:latest_event].project_id) + ensure_issue_belongs_to_project!(result[:latest_event].project_id) if result[:latest_event] result end end diff --git a/config/feature_flags/development/error_tracking_sentry_limit.yml b/config/feature_flags/development/error_tracking_sentry_limit.yml new file mode 100644 index 00000000000000..75a32fa211429c --- /dev/null +++ b/config/feature_flags/development/error_tracking_sentry_limit.yml @@ -0,0 +1,8 @@ +--- +name: error_tracking_sentry_limit +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84209 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372427 +milestone: '15.4' +type: development +group: group::observability +default_enabled: false diff --git a/lib/error_tracking/sentry_client.rb b/lib/error_tracking/sentry_client.rb index 6a341ddbe86897..029389ab5d68f0 100644 --- a/lib/error_tracking/sentry_client.rb +++ b/lib/error_tracking/sentry_client.rb @@ -10,16 +10,32 @@ class SentryClient Error = Class.new(StandardError) MissingKeysError = Class.new(StandardError) + ResponseInvalidSizeError = Class.new(StandardError) + + RESPONSE_SIZE_LIMIT = 1.megabyte attr_accessor :url, :token - def initialize(api_url, token) + def initialize(api_url, token, validate_size_guarded_by_feature_flag: false) @url = api_url @token = token + @validate_size_guarded_by_feature_flag = validate_size_guarded_by_feature_flag + end + + def validate_size_guarded_by_feature_flag? + @validate_size_guarded_by_feature_flag end private + def validate_size(response) + return if Gitlab::Utils::DeepSize.new(response, max_size: RESPONSE_SIZE_LIMIT).valid? + + limit = ActiveSupport::NumberHelper.number_to_human_size(RESPONSE_SIZE_LIMIT) + message = "Sentry API response is too big. Limit is #{limit}." + raise ResponseInvalidSizeError, message + end + def api_urls @api_urls ||= SentryClient::ApiUrls.new(@url) end @@ -86,6 +102,8 @@ def handle_request_exceptions def handle_response(response) raise_error "Sentry response status code: #{response.code}" unless response.code.between?(200, 204) + validate_size(response.parsed_response) if validate_size_guarded_by_feature_flag? + { body: response.parsed_response, headers: response.headers } end diff --git a/lib/error_tracking/sentry_client/issue.rb b/lib/error_tracking/sentry_client/issue.rb index d0e6bd783f39ca..3c846eb063520f 100644 --- a/lib/error_tracking/sentry_client/issue.rb +++ b/lib/error_tracking/sentry_client/issue.rb @@ -4,7 +4,6 @@ module ErrorTracking class SentryClient module Issue BadRequestError = Class.new(StandardError) - ResponseInvalidSizeError = Class.new(StandardError) SENTRY_API_SORT_VALUE_MAP = { # <accepted_by_client> => <accepted_by_sentry_api> @@ -19,7 +18,9 @@ def list_issues(**keyword_args) issues = response[:issues] pagination = response[:pagination] - validate_size(issues) + # We check validate size only with feture flag disabled because when + # enabled we already check it when parsing the response. + validate_size(issues) unless validate_size_guarded_by_feature_flag? handle_mapping_exceptions do { @@ -64,13 +65,6 @@ def list_issue_sentry_query(issue_status:, limit:, sort: nil, search_term: '', c }.compact end - def validate_size(issues) - return if Gitlab::Utils::DeepSize.new(issues).valid? - - message = "Sentry API response is too big. Limit is #{Gitlab::Utils::DeepSize.human_default_max_size}." - raise ResponseInvalidSizeError, message - end - def get_issue(issue_id:) http_get(api_urls.issue_url(issue_id))[:body] end diff --git a/lib/gitlab/utils/deep_size.rb b/lib/gitlab/utils/deep_size.rb index e185786e638ae3..20f2d699e2b8e5 100644 --- a/lib/gitlab/utils/deep_size.rb +++ b/lib/gitlab/utils/deep_size.rb @@ -25,10 +25,6 @@ def valid? !too_big? && !too_deep? end - def self.human_default_max_size - ActiveSupport::NumberHelper.number_to_human_size(DEFAULT_MAX_SIZE) - end - private def evaluate diff --git a/spec/lib/error_tracking/sentry_client/event_spec.rb b/spec/lib/error_tracking/sentry_client/event_spec.rb index 64e674f1e9b1dd..d65bfa310183ef 100644 --- a/spec/lib/error_tracking/sentry_client/event_spec.rb +++ b/spec/lib/error_tracking/sentry_client/event_spec.rb @@ -32,6 +32,7 @@ subject { client.issue_latest_event(issue_id: issue_id) } it_behaves_like 'calls sentry api' + it_behaves_like 'Sentry API response size limit' it 'has correct return type' do expect(subject).to be_a(Gitlab::ErrorTracking::ErrorEvent) @@ -50,7 +51,7 @@ end end - context 'error object created from sentry response' do + context 'with error object created from sentry response' do it_behaves_like 'assigns error tracking event correctly' it 'parses the stack trace' do @@ -58,7 +59,7 @@ expect(subject.stack_trace_entries).not_to be_empty end - context 'error without stack trace' do + context 'with error without stack trace' do before do sample_response['entries'] = [] stub_sentry_request(sentry_request_url, body: sample_response) diff --git a/spec/lib/error_tracking/sentry_client/issue_link_spec.rb b/spec/lib/error_tracking/sentry_client/issue_link_spec.rb index f86d328ef89b9c..75e7ac8304e080 100644 --- a/spec/lib/error_tracking/sentry_client/issue_link_spec.rb +++ b/spec/lib/error_tracking/sentry_client/issue_link_spec.rb @@ -9,6 +9,7 @@ let_it_be(:error_tracking_setting) { create(:project_error_tracking_setting, api_url: sentry_url) } let_it_be(:issue) { create(:issue, project: error_tracking_setting.project) } + let(:token) { 'test-token' } let(:client) { error_tracking_setting.sentry_client } let(:sentry_issue_id) { 11111111 } @@ -22,11 +23,12 @@ subject { client.create_issue_link(integration_id, sentry_issue_id, issue) } + it_behaves_like 'Sentry API response size limit' it_behaves_like 'calls sentry api' it { is_expected.to be_present } - context 'redirects' do + context 'with redirects' do let(:sentry_api_url) { sentry_issue_link_url } it_behaves_like 'no Sentry redirects', :put @@ -45,11 +47,12 @@ let(:issue_link_sample_response) { Gitlab::Json.parse(fixture_file('sentry/plugin_link_sample_response.json')) } let!(:sentry_api_request) { stub_sentry_request(sentry_issue_link_url, :post, body: sentry_api_response) } + it_behaves_like 'Sentry API response size limit' it_behaves_like 'calls sentry api' it { is_expected.to be_present } - context 'redirects' do + context 'with redirects' do let(:sentry_api_url) { sentry_issue_link_url } it_behaves_like 'no Sentry redirects', :post diff --git a/spec/lib/error_tracking/sentry_client/issue_spec.rb b/spec/lib/error_tracking/sentry_client/issue_spec.rb index d7bb0ca5c9a6f8..46cc47ac53bb9a 100644 --- a/spec/lib/error_tracking/sentry_client/issue_spec.rb +++ b/spec/lib/error_tracking/sentry_client/issue_spec.rb @@ -58,6 +58,8 @@ it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error it_behaves_like 'issues have correct length', 3 + it_behaves_like 'maps Sentry exceptions' + it_behaves_like 'Sentry API response size limit', enabled_by_default: true shared_examples 'has correct external_url' do describe '#external_url' do @@ -178,18 +180,6 @@ end end - context 'when sentry api response is too large' do - it 'raises exception' do - deep_size = instance_double(Gitlab::Utils::DeepSize, valid?: false) - allow(Gitlab::Utils::DeepSize).to receive(:new).with(sentry_api_response).and_return(deep_size) - - expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError, - 'Sentry API response is too big. Limit is 1 MB.') - end - end - - it_behaves_like 'maps Sentry exceptions' - context 'when search term is present' do let(:search_term) { 'NoMethodError' } let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&query=is:unresolved NoMethodError" } @@ -219,10 +209,14 @@ end let(:sentry_request_url) { "#{sentry_url}/issues/#{issue_id}/" } - let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: issue_sample_response) } + let(:sentry_api_response) { issue_sample_response } + let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) } subject { client.issue_details(issue_id: issue_id) } + it_behaves_like 'maps Sentry exceptions' + it_behaves_like 'Sentry API response size limit' + context 'with error object created from sentry response' do using RSpec::Parameterized::TableSyntax @@ -321,6 +315,10 @@ subject { client.update_issue(issue_id: issue_id, params: params) } + it_behaves_like 'Sentry API response size limit' do + let(:sentry_api_response) { {} } + end + it_behaves_like 'calls sentry api' do let(:sentry_api_request) { stub_sentry_request(sentry_request_url, :put) } end diff --git a/spec/lib/error_tracking/sentry_client/projects_spec.rb b/spec/lib/error_tracking/sentry_client/projects_spec.rb index 247f9c1c085a00..b3c0b37ff2a984 100644 --- a/spec/lib/error_tracking/sentry_client/projects_spec.rb +++ b/spec/lib/error_tracking/sentry_client/projects_spec.rb @@ -35,6 +35,7 @@ it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project it_behaves_like 'has correct length', 2 + it_behaves_like 'Sentry API response size limit' context 'essential keys missing in API response' do let(:sentry_api_response) do diff --git a/spec/lib/error_tracking/sentry_client/repo_spec.rb b/spec/lib/error_tracking/sentry_client/repo_spec.rb index 9a1c7a69c3d7e1..445a8e35f8e2b7 100644 --- a/spec/lib/error_tracking/sentry_client/repo_spec.rb +++ b/spec/lib/error_tracking/sentry_client/repo_spec.rb @@ -19,12 +19,13 @@ subject { client.repos(organization_slug) } it_behaves_like 'calls sentry api' + it_behaves_like 'Sentry API response size limit' it { is_expected.to all( be_a(Gitlab::ErrorTracking::Repo)) } it { expect(subject.length).to eq(1) } - context 'redirects' do + context 'with redirects' do let(:sentry_api_url) { sentry_repos_url } it_behaves_like 'no Sentry redirects' diff --git a/spec/lib/gitlab/utils/deep_size_spec.rb b/spec/lib/gitlab/utils/deep_size_spec.rb index 473efbc1eaeaf4..6b0be4590f1862 100644 --- a/spec/lib/gitlab/utils/deep_size_spec.rb +++ b/spec/lib/gitlab/utils/deep_size_spec.rb @@ -58,10 +58,4 @@ it { is_expected.not_to be_valid } end end - - describe '.human_default_max_size' do - it 'returns 1 MB' do - expect(described_class.human_default_max_size).to eq('1 MB') - end - end end diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb index 0685144dea6e50..30e73d84cfbf39 100644 --- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb +++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb @@ -187,9 +187,38 @@ end end + describe '#reactive_cache_limit_enabled?' do + subject { setting.reactive_cache_limit_enabled? } + + it { is_expected.to eq(true) } + + context 'when feature flag disabled' do + before do + stub_feature_flags(error_tracking_sentry_limit: false) + end + + it { is_expected.to eq(false) } + end + end + describe '#sentry_client' do - it 'returns sentry client' do - expect(subject.sentry_client).to be_a(ErrorTracking::SentryClient) + subject { setting.sentry_client } + + it { is_expected.to be_a(ErrorTracking::SentryClient) } + it { is_expected.to have_attributes(url: setting.api_url, token: setting.token) } + + describe '#validate_size_guarded_by_feature_flag?' do + subject { setting.sentry_client.validate_size_guarded_by_feature_flag? } + + it { is_expected.to eq(true) } + + context 'when feature flag disabled' do + before do + stub_feature_flags(error_tracking_sentry_limit: false) + end + + it { is_expected.to eq(false) } + end end end @@ -222,70 +251,39 @@ end end - context 'when sentry client raises ErrorTracking::SentryClient::Error' do - before do - synchronous_reactive_cache(subject) - - allow(subject).to receive(:sentry_client).and_return(sentry_client) - allow(sentry_client).to receive(:list_issues).with(opts) - .and_raise(ErrorTracking::SentryClient::Error, 'error message') - end - - it 'returns error' do - expect(result).to eq( - error: 'error message', - error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE - ) - end - end - - context 'when sentry client raises ErrorTracking::SentryClient::MissingKeysError' do - before do - synchronous_reactive_cache(subject) - - allow(subject).to receive(:sentry_client).and_return(sentry_client) - allow(sentry_client).to receive(:list_issues).with(opts) - .and_raise(ErrorTracking::SentryClient::MissingKeysError, - 'Sentry API response is missing keys. key not found: "id"') - end - - it 'returns error' do - expect(result).to eq( - error: 'Sentry API response is missing keys. key not found: "id"', - error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS - ) - end - end + describe 'client errors' do + using RSpec::Parameterized::TableSyntax - context 'when sentry client raises ErrorTracking::SentryClient::ResponseInvalidSizeError' do - let(:error_msg) { "Sentry API response is too big. Limit is #{Gitlab::Utils::DeepSize.human_default_max_size}." } + sc = ErrorTracking::SentryClient + pets = described_class + msg = 'something' before do synchronous_reactive_cache(subject) allow(subject).to receive(:sentry_client).and_return(sentry_client) - allow(sentry_client).to receive(:list_issues).with(opts) - .and_raise(ErrorTracking::SentryClient::ResponseInvalidSizeError, error_msg) end - it 'returns error' do - expect(result).to eq( - error: error_msg, - error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_INVALID_SIZE - ) + where(:exception, :error_type, :error_message) do + sc::Error | pets::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE | msg + sc::MissingKeysError | pets::SENTRY_API_ERROR_TYPE_MISSING_KEYS | msg + sc::ResponseInvalidSizeError | pets::SENTRY_API_ERROR_INVALID_SIZE | msg + sc::BadRequestError | pets::SENTRY_API_ERROR_TYPE_BAD_REQUEST | msg + StandardError | nil | 'Unexpected Error' end - end - context 'when sentry client raises StandardError' do - before do - synchronous_reactive_cache(subject) + with_them do + it 'returns an error' do + allow(sentry_client).to receive(:list_issues).with(opts) + .and_raise(exception, msg) - allow(subject).to receive(:sentry_client).and_return(sentry_client) - allow(sentry_client).to receive(:list_issues).with(opts).and_raise(StandardError) - end + expected_result = { + error: error_message, + error_type: error_type + }.compact - it 'returns error' do - expect(result).to eq(error: 'Unexpected Error') + expect(result).to eq(expected_result) + end end end end diff --git a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb index d73c7b6848d239..1c0e0061385579 100644 --- a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb +++ b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb @@ -43,7 +43,7 @@ } exceptions.each do |exception, message| - context "#{exception}" do + context exception do before do stub_request( http_method || :get, @@ -58,3 +58,50 @@ end end end + +# Expects to following variables: +# - subject +# - sentry_api_response +# - sentry_url, token - only if enabled_by_default: false +RSpec.shared_examples 'Sentry API response size limit' do |enabled_by_default: false| + let(:invalid_deep_size) { instance_double(Gitlab::Utils::DeepSize, valid?: false) } + + before do + allow(Gitlab::Utils::DeepSize) + .to receive(:new) + .with(sentry_api_response, any_args) + .and_return(invalid_deep_size) + end + + if enabled_by_default + it 'raises an exception when response is too large' do + expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError, + 'Sentry API response is too big. Limit is 1 MB.') + end + else + context 'when guarded by feature flag' do + let(:client) do + ErrorTracking::SentryClient.new(sentry_url, token, validate_size_guarded_by_feature_flag: feature_flag) + end + + context 'with feature flag enabled' do + let(:feature_flag) { true } + + it 'raises an exception when response is too large' do + expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError, + 'Sentry API response is too big. Limit is 1 MB.') + end + end + + context 'with feature flag disabled' do + let(:feature_flag) { false } + + it 'does not check the limit and thus not raise' do + expect { subject }.not_to raise_error + + expect(Gitlab::Utils::DeepSize).not_to have_received(:new) + end + end + end + end +end -- GitLab From f2879933aa37c76613b8259ed4238c5c27272143 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Fri, 2 Sep 2022 09:14:25 -0600 Subject: [PATCH 031/169] Update unresolved threads docs --- .../discussions/img/create-new-issue_v15.png | Bin 5672 -> 0 bytes .../discussions/img/create_new_issue_v15_4.png | Bin 0 -> 11883 bytes .../discussions/img/unresolved_threads_v15.png | Bin 2793 -> 0 bytes .../img/unresolved_threads_v15_4.png | Bin 0 -> 3692 bytes doc/user/discussions/index.md | 6 +++--- 5 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 doc/user/discussions/img/create-new-issue_v15.png create mode 100644 doc/user/discussions/img/create_new_issue_v15_4.png delete mode 100644 doc/user/discussions/img/unresolved_threads_v15.png create mode 100644 doc/user/discussions/img/unresolved_threads_v15_4.png diff --git a/doc/user/discussions/img/create-new-issue_v15.png b/doc/user/discussions/img/create-new-issue_v15.png deleted file mode 100644 index 779196b6ba43d7aa93dd3ea64716fa53436675cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5672 zcmbVwXH?V8w{9R15C}yf{6LEI4xtwjFiP)8uL(#~X$n#WDG4ngU3wRS2+~18S|n7d z(vcRBqDYnUDsVXO`LFZo-nH(!d##z-YwtbJlxOz-FtJ7knoufMDi8<+)z-Ri0s?`7 zE4`BveAU<R%f^C0B(X+%rs|iMmsgha{cn4FdvG`$gTb7ipLcY0n3$OS`SZsaX?b#T z;^N}6v$G>DEq!=+*!}S>6bk*bJFl;=$HvAcCnvkRHun4XZ&MRP1mf=P+qZXjcR$pY zobJq~q@>i<)#>VJJ2^RNXlM)#4Z&cr&K6urNr{)27dJQe+1Z(@s_MeRf}x=yKR<s_ zQIVgI``5wNqpiulzCJu2|1!jPb%tPVZDnR=wz9Gk8XCH|xVZ6caPcdC5MMtwHrCVA zlbaF$#NDZ^AT2yR{K=CiMcJ<c0s<WDZ8SCReQv7~78c&#-kzPE{jogUSXDSVIayv_ zzPY(MH`?jv=Z8chU7Q>;GBTo~qGF?huVj6Fy{)aSva+(EpdcqFCo3i9fw8`lqC#&+ z&8t_h;^N|-K7DFq`9MKIp{63YvNSU_F-lxq{Nm(?|Fb92(a{YJ4IPbTJ1ZkCEiLGm z0p~}=_wU~)#)e_B*x=`0rFkhAe~*bo;&@++_Y)T~GO}Sp)Bg8~k>2Lo+S=poPE`<y z(OUcdUDJTMpEHulU=n(8mszM_E+<4<`Z#+3hHq+S3hfnJ|0}}nBY$pQZl9<u@vO64 z8(}zLVJP6aCxMXRA>Ky(#8P!>C1d0KquMkh3*^@QWw2=_s8Tj4&$=5Rq>0y3WYriL zgPSB`lArz`T{T$M<6q0N9&jx2bT?Ym180B`D+arke8oMZAaA_Wa_<e;rsj=F?M_g| zc;x8aqMxJl7YFMvQf1nGN-F2Mp6zaoW@QVEKtiX<SzP#Yu&H%+#LC&GOFvm9?94Lf zrt<M{kVA^|1=F3vK0pY>(mqasoWA^con7OJw{sSka<A+~^+m&ogEr{JMe?feki|I4 zEld$R#PV{`&<>2_Kg*%@?sUldZ4JBZTm0cLphUAH5xviZ`<3FWqZaY@HQq9h_THb^ znW$t6c*$2X;nMWM<oQ25A9Ke_J;q!og*Td%S=X*>tvzL->X|*W5f|S0CHT#3IKJx$ z=|Aox`1ACcf#<J+jNF;+5Nd#Y1SY^9AOBr3I^p88@$*WNuZk>~)IsAUx^Cmi;a?A> zOYoKww#Fm@rM(cHhvcW`<QQH<D9EMa?usDwYvWs1(Z0ei;wwt+7MJbim8qCajtSX4 zeh#q|M+3JNOBbUEkw1AODv0ek0UI-r%Wh(kw2TdCZotzItt5bNfjkKlgKWDM23$ko z$5HQYRJ&2m9PKQ8Q3uA8+QK(FxfL<Hh#@V9KEmyQ;Sy8>w389_f+tS~nF-1sM5QeU z8amW88Cc&5NW2-3(qeaR{)os+MTI~-A4>6Wn`(d+!TI^LPI=zyLY<C%)Fo$uMliZ@ z?eZBixwgaOn}?EXgR?>1seA<5m%ELG<ZJb?YeW)-(sY?*Zh_9=_(thvaaMvXd|@a= z1?Ece?lBS>IIVD&dp1f+n+AVNoKyQ&Rc*BK5YSu3R<n5H5+T(uf8^<10>CDGiqhiY zGIk8E!<Y?^=dA)~@|L4ZPxNKFbG+u`F&mv8yjaFMUq7nog9{bI15fLkVw9;*7FX61 z!u{;~{C$LI{dVWDBapZ0>5Nulj@oCaSvTh?Nre1^mb6=X8At1s;H|FqLI}%(mdR!$ zsO5)~Ceh(%DB>93&cs9+leSN7ejvwq=p@4rI=`w8h@+F~?3-KFoVl=!C9Ua9+qU!? z&=#&ie(N#gNjYyE-jc_54E9zRHRB5fJtV&@Sj`C*s1PoU+6VGe<Dw)ZMfQ_Qe;C-7 z%q>Vrpe1cArCwotINHRQNI99i-6TKSSQQPEqiPbR&!TlTG{IiwH(jmNqHFD2iUp7y zP#(=`ImF2t2d^yldOk}s+;R({6*KT;_x`i%P$G=W26|7$suW3Lp#zM4+otjpr*smP zYe-5+grwMbZ^5Ua7Du3?t;fxeQ2_1ew+y-FLGILTLa9@MFsemU`;zhZcbm5fIiNa5 zh^9kjl7!`QHbOfpnatpvPhA7A1?gy1_AZKI_?M~(I2%~r`9}Geae*0m(8TlF_y4|b zPAzA_gqP+3<iY0tEP0LLv=cqyhMlHbG#4L@yFEqImQWbEr}hmoDj^2K{2t$X`z{82 zC&#%gk1X~-{B#*A<d7UoXxzRqe3M1%^vPS@oJ!Dm<^!1$HR{Q!*uwKx|AoU}VY}n@ zfM20r9mKci{jm0lHMB&T4fZ!u-yX^-x@+HZOQZR-7CASKOKp1U;75Uw<1soeY9hE_ zgVha(y6-O{lJ)_P<*h-NzcF?g4=$Nt-G)z5TcneDo78H`^BZj-0QMUM_oqjoQZb*Y zb<`gfY6+~N+OzLT`h^!|FEUBX9s%a8GxI}+`lbD_(S<K34-N*VJNt1wO|ft)wN=Nj zoTU0HtrQ=l0}o(Y3v4f1$*KY9z1gaFMjPGmccL%1#&HthO!V<sczds<wC(I2(oY0O zAh*jYlz;H`z?Ty)+wy}dQp0`|{4&5nOcM3%dbWN!n*0z5p`nF@m`j6vg?LhQY^m=3 zCvUp^Z|atWi)`lX`hA}C)1fz!XMB$LpZLeZ9ju}I++-2QinGrJdu@Hxx)rQ5{y{N3 z{OhM4SLT@%i>e&8#*^^sz-QDLpBlrW8EI�NtiZX2t@3y8=&sF2FONJ|?G+2kg^W zlsduW|5k=z@I;OT>6<rWqp!(mY?+|BsdA(H9qsp{8NB}9NTACzz^JCO>}5$WbI0e` zJ-C>3zDfOxT_o3c?Z!$G-hukN>Az*`<Ny~GK0Sf@%-U!f4x{_(NN%lc6wl<guO?<* z*aQB5H(o-WTM0~Gc1SMSM_0oyA!h|V<`e3%m$YvCodJg`2Z-pRPs?G{PKJ;o{HT2) zK7fxM(-ew|;tb@%*SyjwPJFEzkAJ;usI<NTm&)sR8*Yh&(P8WNcqFLQoT;m!2l6WM z_iD1e-@Iyrzk5%Q@i8P)1pEJ7m0?r!`Fkg>2l0+fn1UfFCF5yi78##>LnL_-2jd5X z(MZ2YqQTS1(XNh#HD<i*({3;NU+iro$0c^09c3)-aH#3?oVLv3X9GAG#TiQ;%e-7K zjzveu9S%*K;*;oxw__&;88Gg)@g<}N$LbfA>;(B`NBkF&Wu7-(GDh=b_>--IXB^Y3 zmZYuTR7;=8bW#o<WgD~Vfg9POMA^r;k?hl}n<-LO06`a7!+gnPb_joXH4RQRWe@s+ zgY^QIZ~z7S#fWUlOBA9l3!l3PeU%LUkqOg09D_z~tZZk7a2D-H&LLTGJJqQBjS9mb z^EKw~yc$aFxdEC^gG{J-cpMa~Vq0Lnpp7TWo|v(U!V6ZmE1<bG^)SFkEYU0{TTc}& zX6xlTl6|A5R3;Ph=y59ZIqGwx!XgR2vU!rc8_q$ZdNP~CXY}vJ8~fug;M_KB@{|VM z@b8?XN(Dw&=jlPO^?1DF20yPAOMGuA`B`cn<ER%Ti!<D1-rki$vYzgpxAxR-p+)#z zpAAI17ix$9e>H{CC^jT3Rdy0-8CTIYZ6vF?43|hIr2`8h1V!2J&iyo#eu;%U#39Ct z4}IDA$u*jL{}6qy$``DYX;pP3KJ?2dh}orMw$_O;8g&cX9f?)WWK@Z0MJElL2^ZY@ zouL@6W1(`cG~Z*~iOmiVTw>?wkZF|KB*mrQWmc=A`3QRCJW6gP5-Gmu;jF*sES3{8 zcE_c$-@IeOZdK?S2UA55&dL+6E%Od-QE4jrw%J(pAC>0QEk4HM?)N71@qvs^%=bK{ z(85l-dyi8i4g45$LR-Guxz;Kl{^F>hf#B;86ao3i%*Sy1{L54Z98D;YU-cCJ!;OwO zPER=asIN}N(<QBFls*N=9(YokYDBoUceh-Jgm}XUYJhi_1@KzUtAcEVk|^3uiG~kz zV~zf!Ud+_DzHTalClZM({*oHZI=ogk*E4e9M({1mAmXZZ^y5!8*od6I(rRW$@-c1& zbb{aI*+FjGk+X;1Yd?|05QZW5)Fazm!&g;Ssc;U{>l#zMSc*wUcz%2EiD`EdoI@?X zE5Ke*`8E8?K01zi5+bV1QF-iq@5*Q1;f$Zb-N}F3#D&rVM3(AMZ7=@nI`Gn5b6U5N z!y@rB9GqShHiS1(KAhO|=gpp8V-DLI6UqaNiDhRpC2z|<73Zcmf~{jSpgc2NWgL{k zwmN`2e26-xee1{~h`g=r=VbVu>||OQL02lBhU@3FSXV_9JmK|q!om=Lv9N>t**dzY z+F}N;idm6oRXOjf!v{Dh(4yFwRn{Vnomh99_^`7TvY-vsHg;;aiiV#Dc#y$`B6c;k zko){Y9bS-;s+WeSpwLG_bgL&Q%kg5fSU^FJMOK}(V(%|0A+0SAm>&u%`s=q7b)^Sr ziTa>b@=G#>gU)4&M7c<Zf~$CTkl%F=66<u0t&iH=X48Qp72A``r>#5tw>aoL<{38v z)LWfQOePCXWp;nl_$xpncUd|z`kOZ}4$!bLtXl6cXc-ds52)sJjuNx=$7}#zu(B57 zyR?}piGA2UYxgjDWsjx9erqkFOR6}Lb3KaTNa%FVygSp1dJ{_<pC$uhyZ(*j`RVan zd4{92I6=1H$Dn|_jf1PPO6Z-&0Nld;s5!?NFB=|!18JNI-h)ywFK6$2I;Qp1NRyN# z;OGhF*Y0KnFNMtgQ2r`D6ocj|Yp#^S#-6$OCTnA){U8omHz9*swrkPGNw*1YGHP$& zAo;Y0g3g%))Vp+gmn6VOEQ)$e&GdDPc(K7y?BoI~Kimzv7bDFi$e$b-Q8S?8b0<pZ z{dc@u)2(r(twSGrv++P&KKIXTl-{s>WxV6QJxgc+;V3=S9uU3P(nm*>I>q|%qK4)r z7Z_*+m>}MB{!nUM=2%O}jEZzWxi@NNT1QKR!sw*!LdhE@wy)9QZx1o}LPjuXd*rd4 zq2&Rpst&UtojT<;1nabhPLmQ)o1t`qe=bh;%)Gt7A9#8r+z<Dy+I9{?KI_^`H;WJv zMp@Zj=FI(fF%QZyfv6gq=gFK9FCgc@rean$#bZ@gqQMt%MCCAbCJ@Z1Ohh`_Ic-Ve zbiPsy2W~zJQGF1P2}0930!qjpuMO3x)Vg7L33O%iwcNMLP4G*+Je!JhA?VRuh-Xnl zY%PgwzWHCjdp4b!xn3^J@dH|r5C5|uX67ovW|>Hi(D5T6g9%i?=REMvz7L%nZ$ahl z)iWi&S;yFC;-fmq(Ypa3Dhl=P^R=hkj8c+y1FyEp0A{P6B=R0A+AzlJ5HG>@n*04B zsZYg!`)X8@5j|>9ePMfFe!$e~7yV`4fxnmntg*u{?Ii65#-QG>l_!{~Z$!Tr+V*bv zNg5PHN~GGvcn<eGofdqDr5c-Z6Bal`HE4Y$$EKa}W{KVM`Me{E4QQwSU};1+syMq$ z3rf8~;7^^Y{&Xh=9$M!Qk$40)X2bOEjz&VIigCc+xGuFE$ppu0omISD@Mz?%0HM~P zBtKLacN*+@^2LUqONyS{B(MHk)93AjtBKhHL$2Sy1jj}r>5>3q0~=9JnS!QR9drRP zBYEj*r0b>9s8?%hawGVf9zxQR$wToc6@yVa2)BQ4D6K`v534e&6Blh6Xzpd&#g%-V zv!eO#f$}2hmhZ9Yr+|YOXU&eA)al2FXq3Eubx-;{sa&k%*OPcjG~!+4kQGzzwp!w` zU~a9A+44MyRuZfm_U(O*TVY&+eAZ{Jxbo)-^tC!UsdUUdFt2>*3|mfXPxE7mMnH47 z+WFU1@&oU5O)Ld+DrKROBX-T-m;AP}q_w##Hvc-F)DtJ_A7Kkxv1xd6v71#V6bWeW zi;@td{hQk}DO_oC$i)8zO|i>=irgn%a=t6Mn69(i0Ho1A28;Wc!bE3EV<hL7bfp4G z7vq>Z${+DJU5~`zM!_^Hy86jZf*QiTxQEwyxHxWh#wef?_W@7kuTJ~lA06~fs+qL$ zt<_}$d3M`XJ%#r|3PyTY{<5Y-uyo<)86^jb5M{(YmjvSk1wGz*hnF_|8}y@PF#yLh z5WvZX=MMj&z-=gBT6*Izmj;g&HbB{HnG;WbtDybb<6nMpy?;iuy%dM$@8fzDiUAnh zzh8m^D}-QgNwT>u<XH2;ff8xqi29l_A~4@+BPlFUY(w6L19*_vL?fC~%P5J)p&o|k zh=OmY4`^Cq9X}JdNOT#7DM0Oy9?#X1Ufsy9q?VV-h&W+dYyJa_7~?o9Yjp0e|If3+ zFIrqwpBIaNzm$>OWPbVM?g!O0*?kh^g2NF3nk8rn+3ULLpVOHpB)L{J_K<OC#ie<2 z8&Ms(pa^f{9AS-#y$iMH=IrgcBK4Z+A8M}{_E2_+VdW&F2{xduTwn>U^PYMQ*j36K z-uN~Nw5opG`3o|*sgl5;UK<&H;_f)g8sjTA%ehj|sQ&Tb(43c#pfH~2Jhd{xH!Pv` zy*?A*Q1FpVS!KCrQlTq3!<Z7hpK^t}arC+=!ycX108qfg)JEBRAnp2<AWVGwf`!yx zGMV#od61~3MGbzYuNk>$!!-vzbTDEr`Nrz=S+1&6CW<G;dYZo8Uq3~ndr)K=3}X!c zErOuA8Nl5|Whp?Y2jWI7{($IvVbPBkeO^26!`4-qBA00H#@Ev6bT^ovAH*Dw)@!6C zWdGhw>24A|oZXy~6e6FPHF#ozjW~&U@W;g!tBIDDEMR|BXI3J-7&aKjOAz2gXiB^} z+IcpNd9diMFR)~dtzS89-u!DyH4IqOh!JMJWh5DcTFv8qLUWg0LB6^%Fyh})Rkl<n zF+w~dKKcJP>>mFBdQd=2_ug`NOi!keU5j_~t%Wm?O8B-+c&^&y>OlFD<UEUn0@Z!Q zVSDI6fk0LSycz+N-NIJVV}vM)!ci!y?mlOaLR%5`A%lac+Rd$RCO~cYQ*|Xu&w(3+ z2Zoxus3*|0$0gwB22xSA$|n>3CO{@OLcO?T*+rX<C{~h51@6E)R{1*mc&4zSLh5Vb zH7w{jU*Q&+SG^-P!IE0L9io1B8`R%vxKFi=sVWQ=&=OTJiEn<FLBM)51t#5gQW=Z$ zLE7Qh!ca7J*>_vv$9T|98JZ<(X}b50d~@Uw@S{>x!zbRkdNpRFk{41TIekBD3#oA_ p8Ogse;)UkT5Bo^pVuTION#ixE0$J_JyZ-z8P+Q&Lel-Fa@n2S+kvaeX diff --git a/doc/user/discussions/img/create_new_issue_v15_4.png b/doc/user/discussions/img/create_new_issue_v15_4.png new file mode 100644 index 0000000000000000000000000000000000000000..3720b601cc552f600b4940f62b758f15651e6cae GIT binary patch literal 11883 zcmb_?XH=6-w|3|tRiYwNRR|zmO6UPW5g{N7f`D|T6Pi+!(4;6ujnn|4gCa#h0VzSG zgY@1)liorNCCS0(dCywkx6aRV?jMtxoon{K_MSa+&tB_}eyFd_eBs&!006+Ob6@Kb z06+->04QYXsm>*mOZ5KdKRXZgjP8=p9};P2XNN>4OG!!X>}=1^&ttJz;`u`&ot>T? z9v<%C@h2xI1OfrSgWucT!{c|>*4Ct?rM9-VHa0fq=jO4Eji;xl`}=$H^73cIv&qRx z85tQU6l!H<H8eDY-^I7KwuXj=CMG7<*47>$A4f(;y1Tm{9UUz%FBcXTc64;u*w`2u z83hFe;c&RVzCLYjZB0$hjEs!U&CRm1vfsad$Hc@`S68d4L6npfb8>RV#>OfuEB*cb zEi5dQRg{~Wnidx3Q7BY>L%olWPkelQYHF&1fq|*1sk5{5!s6nK7cahi`Le#T&SMk3 z0RV{0>u6~h`Auw$YJ8x&&ZKE*WcG)|!Sa2Az>Z<4+-Tdd81CrUo%VthbHxu8hb=t) z1or*3eJRd2j`V1IELS#eJ6AYf@r&fF9|n7B1XCUaegVamo^CyRbp-T03z*S*CSWcD zm0*iY;@C)<de#uJu%IZTSZg5|*Op%RvvlZ8G|ptgv!BRyD8dov+xt}vn|0wrTbt<O zUWBu|u0jb)>Sm=c;#k9;JH8|ju`S3QkJM2-^g=!7hCL86vEUO;q>B@JU9eE_#EpB~ zQhlT$*Xv32LUhq;#~!d<AtU`)?4U@md8Y#Do5fc&=Z%EnkcG{PkK$FG+RrYuRqSq# zz-8=xcctU1wqJ)gyG}=XoPG0s^&>s=4mT!mLy>sWir>obYee7B0duyIH`4IBlj@h+ zq-PSo(w0ADHf>p1@vwImHyyLD=w^zTJtLFTz8%#s*Vg%zd}rNC3bggE>1~+%9pvxZ zF*WHxe&~PkYn3#FH^ZUy_3!d#JABAn=f1a{2JA@ZCH);6f?73p<2uPo#oF}CA$Uns z-()Ffu=%E!yv}RbT*^DJC4^~vn^%A3J1}$ps|e(laNZRBihS1tftXLtc3yMzb1WBQ zNBLfEk$H{jzZTA3-g*(VGVoPGhT6f+gdd;GJ#6n3b)`t}p@~_HQXPKcIu4mY;@BQ* zlk%MT1b_bC&w@pFwzefM7Z}=GPcp5-cJ!TayG3i-yaykv;H%GCF1x`_*WS&>j<UWy zMW<DGR&4c`rbf5@ao2eY|91JT|5X(z6yi^;aYv#kOT>dL6BBjT*RAH?9|I`BnTSf8 zi|^C+e-NQ!ljFNlLhSL~r;`_}U*`)7)w?5s5(tI%$dl%q-iRMIZ>b+AEanbx!seON zA3xb}S^sP~|6b8Nb6&&a-9H-M*&g@$wnu9(on0Dy>NqA9iZZLhGVC(k(G%cJsZi89 zC<F-n)r#uw6nLwje-1SE-81X>9krM{x(N#nb$F|%_HDUc7T+G!_bL(n#9jMeI0xB` zTgo0R#GBF;dn;Zvh(SMKngmMura`f2uiHIw&JOR+8#!KCQXmsU^AGJ#HpL~uxT5xF z6nZMRw_R1w+BQynI7ekb-8!X{IO@^v7SNlU`1UmM#^-jPYqmcnN0U*ORoLj?_3y_g zuh8XU{Oo%`v(*t$=M0;Fj7f;&Bid+lns4syfN7_y*Cw>Q2G$S3o*9>UB|=f>3?w+} zpnvYtN&Vx>9YIM^S5AAEt}sxH+7Z=&%g$Roq0M|B(`HwjC@JYv?t-Ym6GpEDyzGg; zUF`dMtm(o(R$Qm}+Y+!C?N%C=*hPf}MP@a*r48|uiR7?vK}xqBYL|Q!Z*!Qh3?Kg} zNOAsNxZc9#aO3`8hb*_;D!Vqu`DZ`g^Ez>PZyELrgkE>Cb8TeZLXarhUcc(v9!P#G zet=UGAq6c;7)SgY@dqZ=23!Hh8{ifbyKbBKQ|VPXg{`gpfH8qtZ#&+fo>}R;-ty!6 zkebgz0T4dKClHx|jjH2s2(?HvL#as}xY(z4tyw0srlRtrqc`oS)c9imPMiKa?H=S) zlJs%!0d}YgjtG1?iA9WFvZs|0|L`}`3~wy9g51XEBz7Jm{T#ZXjcAPp&q>}A!l3o> zvi=rC-V$P}FWJkNiS@6>x`wWkpD&vQJ_d&thR3ZYJrIu35o830M@RVq>E|xq=MR5J zw@4<V176g93Hvnppz?w7;hLob{MC!LTA_;C!{L<QX#aI=A6I<f>p~=y@_3_yYYu~X zIbEGpx?CR`D~aQNa%b_v;?{yMrdD0#)PmLXRkLb>BH}xlN%My=OOoyOXE0n%;*WnG zTOet^%WtW3MhkBNSCk<26S!kAWw|XXI`f{VWrhcAr4bQ2WYA-7^Os^TJnfKXaMVq{ zES8xgreJ;UZM$qgR{Exs3x58|fX3er5}hM^YEm_`oyyf;ZoU{ERWs2&TElVrTO5Se zj?LIJcYh;uKAbmw1qUQQfd<|5+)e&-x2+j=^yq!ga44YkSBC3-SBUJ){_zj<uRq|? z8QTrNhe_281Fl};o5a*WpCFdD_~jpFmoo%BT_93=<qBNPA4r=6><d~O=$`BD8wN$N zUE;+wdytrzdW*tL+Z}wMCh!8i(nv{P_;<Bnov~aoS^mC+uEPqo%D8kwfR>R}+p7$_ zzSu)7<r9@s>I3Wm;hvf1D$guS{D44kqIPR4HM2-1=9eFe?G|q)iEF^Y_uAYyN5<OQ z1oOGEe?p~IP?y@g$?SHqtw_A#{XS-@eu7H66cSl24JmHWQTbv)XnT@_0jsyoKLECD zYdiopmOhhop=Odr12#wJR4imq8V6_pyu7ng24|z}+UU9|<*7+7r6zl-ZTV$hJB|Nd z_LxAkiq%^Aj6Lw+C4^r;g3#^sSF_lwxz%C1tb$2iHAbn8fzL>-qb_;T5g+Z?x9h)- zD`%-XN{j|?NA#NbQ60EoRVvkgfNKOYmC&9)T)BBL1OjKk%D`jjd<~UZbCIh@Gxq62 zF^-LOxmK2c7kPosx87^$SfuY;^?;Tq3TiLm^YX_VM|Q_OR_iV|2i`395sn=!fy8W* zH^+EtL0MTzHxEeelA!gw5MT4y8f6$j@U@wyyEla#r<+Yp-qg`PYC@nEN_EWV?q!~S zpplwgMu#PH$nfPU-E_%p8V{a*$%&-SN<<M)iv$E%i|rQdPQi<877X4|=7E!@+d5a2 z)*#<ct7T30%4ck^fDa3X!N)T2*js(k3#riDjS2?%{d&OuYf!#MdKrP8teN6Wx3fE9 z2)Kqf1}*mw<kC&q4|!J0*8|Mm$n;Z;6C=T|_!Vq1nbFssFW#<!4!rv?Q;8J3-aCGU zaF=iCNkxZf*-6offYK=5xMjY>7OkCovVH>E{)8b_rai$e8tluh@%9np+8jgGec8A) z`3(#{xF`#)m59o0SB4CD7<>e0ep2gdbkhPh7|t7OOB}~Mo~(rdSa)HvnN|SquknM; zhT59-1#yA`AuM&{INaBzbcUc$x?QPiRL*}O!$M!1J>pD?;kS=h-t2+*p&Qvbf^l4@ zgKOyAbr&hRP^vU1FB?bp8MD>YoV{!5K0^%@?+R#EA`eGr2Y3@`FaWFU?dwjUY=$E& z#qa08pGH0_p}5MI=1sY!PduVvP`C$-#Wj4v%*!_`3PeNZ#8o<2zgIEx0q$X6tYy7j zIrg>PbD3iRh`UF;_JOmbxi5jikJ?Pk7rW&JpWy^*Ue`k+IRC?D2%gC`7k#&Y<2f*! zqg5PQn19ISas38>@_FKRfV<<~ct_jN#<Ec3YhpesVd}A<m}c4KaUj2Fd!k4t={VPJ zf!wh2Q4o<8ZI^;}aw0z}yllootS7%$8PL9`OJiiW{JIf1HJQ7PV$yRitLFB6+-7n+ zZt(*)3-;kGAJ`Uq;B){TIFP%i?hz#l<^B!gQ1pjo`$eCzEh`$H((cF)4P1>IM#zr^ z06<UY?pYqFT4aCnXY3+Y`3;NMG@bCe@Q*2fx-ZYU4IcEc#p6O|1Q5T#zd>FDS*fj9 zTKL{~>b?@@h*7R^*_w8$yp?0F-K*RfWtJIwbva{9xR6R5+E~yn=%DUbqR=3?M)q%N z>I{z(_BEz8>sYPuH6HCxFIrxb{K@K>A3b43HF=YS<;~BG*f2`v{2AC-oCCATBVTt^ zdCMXz*T?UUrFt@f<P8SyNseo6C})<FG7MJ#F4DJBim+<Lef4sW2T3<=GPZB=hRZ)e z<{nX`=B}Y+&B6rKs;}j)CAD`pGxm1uL(5x9l_#K2C)eco@6Vd3%SSwrH=NpnW)mf) zii_YiQ}ykaeoQOlD=3s&Gw>Hj6F{LHDpN)wPbIWU8N@dsRuZF{2I1h*AeT_Cw`6?- zv#-OV9Oga(h;{K_GhR0B!f%LXPP-acMW-UUqbjBies1T7_ag6L!y%V49^wRCHpr@Q z)E8Fr?T}G>bshvvLxcW{x@4#fl)D1_o;Z5N4{LWCyCEN;ei}!NU;NllWs@=ZCG5@k zqx<Ce9bLz_4Q<mOPZy@JG}rW757e8c1A}Lq=Y1(IwPf%(kyxDOo0HL^dLb7f7{4wB zbL$R}Hz0=%2O!1ZSqCJWs~VGuqi{VECme+*KuTcj01e8di<>cRH!)XVg@fMky#NYL z(k0wmxW|~eBg4|$|2U^^17I|I<VkA#^(&V3{=_#NS(JJ<MA^qrGP>xup9iDZh3f>y zJCf*Dvsa1kDh+CVR-3nfthB?Q`s}-u8jW)u&NMW|Xf4t#Oy9iUvE4v5*yBD-`*D*i z6mn8rwH_9V$$@3#d=L>0Ivk5uygx^&MO-))bx~m`mPqBj?(5In4wbE(sb#y%^ZeR) zA6D*D4CTzFJ|5~^*MaA}R8hS+<!5c|@ilHg_k54Mt&4`Qh&Qiq3O4{`ZO^>e=ZweE z_o>{-GeqeVPyjz-n@A5wT@&_*o7Hz{%28Pnv7|v5o-t>n0CRf#CqugqEjPOi)aXf% zC7Zw-3~v)|9?GNbEj9Q8iShmn)PxZK-l~+&?ST5b9t-%Iun&3BhgfxyW^`u18e%<` zh%8n;=C`1*q>=1OsWGadbDqMQ|GD~!ymrP72kr%I7ObOvL}iqnR}T*?^EO&{Z1b-E zO1I5><U_6`FN4gV+Fv<QnIJn<k(Tie<LDNLge`F0jH?>5q8Gfq2YG;N4jNQkN2lR0 z!GW5niiQqOgMP5|+n%Sm#h8K>^#05NRxtel+T)kn5RLd3X#@w>r~MVZX^2)Ns$u@q z?YQ9pN6BPl!<t@_0!P4#3nC|=rIG*WNmhoa<HC^yh7(@0ugWKwZ5L}F_&B{@+0>0Y zB3mkk?U(9us!tugY1Y$Rw8S0xXFi{v`@+e~oaTP56bZr!j3xO;ch-|xzfmx-)1ReC z;Ig*ygQ}2=t44ZB+kM~^Ord`)*%yv#7#fu-#s5Bkb?luKly0JZ@(_{7w3gH0rww0E zTuwn|Yv^Ry>;W7#>R(^7hF+XWI48;FcYe0vx)B+pzV7Fz?a{B|h7Q>!u7DYEhuW@r zkec9X*S5(a)N9*)A3Bf>`{R!j2rTg0T=s{8w_NVxa%U%0^RS2<oJ~V4ateIBW4uB< zOC2F^`ApzyPeDsde;T4|j(Olf@4ditYfI-eeD(#>7xpIu^{e8-=@jDpT{1Sw6AcXM zdp`Uc+tR8ahUH|5O#h;cO(YyAz@upqdxLN9FfG^aJNYFXh1C{N`8+QzEbI6qSEKKu zRI0yAoF}(R60vj}tze*jgufBij*gLG`XI3ji&$-|lfyP!UGS6GHZM8_2~fnEklp=d zdxB(=t5&4}J9_5z*|0lo*DST-RCS5Kz3*v6!$m=171ZbvbcLSTPy+r5X*-jB#1XAA z*WSN=3fk{0<5>NRl&|L)r+eMDVGIXq^KWEx|CK!<3VRj=FRXln1KF=-NO}n$7<8K- zSW5J2Xkf}d!{&KpH+UmL@<`GCMO$}yUtJHjI3tWlOK0fc_$#ZG@KS2I8sBBai<&DQ zf=I(g_w07Z8=|S2w&wUBtznYMHp?-_0ueU+0j({L^i|c~O@|?8WY(D$%Qj;v$H%6b zCv)!a1jkisxlJt{fL(lfxZFz6=~w>vGohYMFKfjf(38S_G_{EIc<_62bImgrS3%JA zTSU-u2Fce--g*(@>7GP2SzcO1eJ<1>+3lKrPtzs=mtXm}K0%{0_tEd<K#lFl`6&r< zb9l@HEQ4mW4&}YS0L6ES#G?)9M+EBrQy0B|MP6o>%Re&CQDJreEh?GOS_MaE8KeF& zZ)scJ@Y?7EYDzaBuCj|!sOkza`pj^56>L)qq9R9o*Vb7o6-fybL55n+a-7EoX_J4x zrPc|)8GsFpxctjQTmM_?!*i9cfvB1@t{%(+eBgQl0%$3n(v6)v)0nZ#J|}%c5ah2! zj@WBcco)b1*NyNe&|Oy&^KvhKigx~dEH#pBAM-axyNK0ubL?jSdf@+7CwBnWw8I(Y zz2Li?KKZA8wUFxT0PN1O*m^%<+W)WHooIcw^DrZIKcA86ylc4&*S}pokHXbS^{Ial z#|Qh`dARV=s$4}PcMoRv+iN)Er+xIkLEPYpVjA+dxg5iJ1m=g)-RsL9B=Kzn<O8e@ zaxi7AckWDgrYQQ{n$xhoEu@W4+2L1${FQJUvEJ_*K9d2{w-l+Rgte#_i}?9&WZrnR zP9h^KYJBei1J@>={RnJ&wXG;FD7oD|^+HJo+;W5rf~s)akn<a&zXg~g+3etPWc<O- zj!pgLd87{6R#Xu8h0F^_JyJsT*bkH=3Q4UgZbunyGT-4_R(k76uw9{7?r!jRHEspA zMF!;{-u#Y{3*@&a96H73hDyJK;v1srG9PAMB|Rj@vUI|EA(Xw#d?IzQJ#(hf5xP$y zm(AfHUgC#C6r7ySU@KNC$BOA31m3xFWFhGu-PE#YI>=0esZMw%O<#Gpp~b@u&Xx}P zIxO^*gOwcV#|#ImHX;6+XhB=Yo^y-PM=g7zK#z>Fs~I_Vzqfd(k~QR_i`6=QRL7LL ztf5tkLA`sbEs@B3NX>$dJ%agbhw{7rh86|UJLi1%_;Phz4|GOa1fe5C;P<lrPa(g; zMlViHC_Z4coERU63LvIC1lS|TCub1hm{lI5CuiNKJ~wKn(^PXCW}hr%{*WAA6eS0` zA3U3d4ERuUV1x?iCyxp`GE+^H=uRofL-7LsW3|NJJH8dLsr7b}Zc-Jq=oD`CkDH9R z6FULhApUVa?>vYSPP<h5(3$!#up3V7KLcltWNrUgiy`~t<%5sR8@E~2ZLAWQa9Ic= zs;do8=iFkKxdOstPat*P09qcn#(<V(6QM`L`{{#0#Wa_DwJtl?KB`eXBQK16W$sI* zIh5R{FntnAoOa}GF;NPLEhz1Ned?OFuQQ(w+ueE&2RbPk*3rww`euI58`R<nqb%gR zMoP0D_LapM9_jvE<*IulRzUCGOg3Z`yf2;b@oelvKCso7CRhhJ?W3ZOYQ0t(;yInb z|NJ0^SkYK^%`$a!ntvTZ`J>s$j6xrB??hmI9bI>Nv=8->i=HoVYw;5`^ejUrZL#d( zzc~I%XblwJdXY3uAtu{Gkr@9P#jU5SMzR)N%bB@BH+%1l?^orZs2KO%JtsMag}@c@ z#`S~p{8V*!qhx4sW&9{$xlZ|Ta+-7Ox$j2E-b5AVl}}U>Eb-lFzB@(fdSWCXaU08Y zFjKUq$n*%`FElO;2eS4^h8p+zQEZr;<fl2-(WW^eD2#@Y^-Y{Ev)6W0w;LDxuno$0 zoe<24iL4*Jg1PXxR(Ho!J5xfn4_aIm9ggqN(VlnFSiirw-X{}jF6Wj^sJL+9<kxc^ z%^Ur%y?;Yo=S&gWUYC3Wkj!)+d2I?5UPz>9UROI2;L3x|7hjO~c(J3Le3X#NRTr@v zjtjZ_5%a4*NZXUgDB$sc+Xg%g0@wnd70m5@PIXPm2hM*0*<xu*#&K7b9*y$;k@1!K zpf=qUHzD~e55rTYcdafy1u9miiy!>QyH)b;djIUCqDS%zCd;sf9lqJe;n+z^q7a?= zj&#h2XjLQeGAp_xE|P&wc2`txG$MLl;?@_C_6sCb_`%?(D6*2hAW5_^{IIw@TgXZ9 zRP=g9$D{Yk<FRP9j(e+UzrOpcnhCFj{HPi)*U>W1nX|aX7QFZqFiESv$Fmm7*XSrJ z9rm`7q*!f8Ud0=`TSuX01-LIII0kLKDHVOaQA%-l(C5BR5LMZ83i$5^i$!xyy38cK zk1eH3-+%6Ua?lLHO6<<}gyH#(GMsn~qp2*&;atYGGdY{1EZ6X5R$YvsR~5QQ|MJS1 z)%nU`+^B><>Y(g>!ea@e!vecymU~=QpLHJ1d($Va5q`DvlnX%BG;WUJsGmRZg56m0 zl047HzpL;wvUEpG_+IEJzi1HSgj$1<f=T)odS+iYmA+(9i(JwfLKOATBp0w$zRkgZ zVF2<WIr4R6)pUR<&<L-ZkXsJ~bTnxDAksxRi2qK(4H`@wpTTELA9R`W#ZQut+(mN2 zdxxB>8*SPc`<#8~zBj6XV8iH2u->`mEOy44ubQg?mp!}k;NIAe*#RH5JK|-?(sOe% z)m4>5c#8nH4KBORF=)W2ne^~bXxM(($6nq^>j)ViU`M!%0>Mq&N%|D)T_oLj&6U=F zP2{d>;TcW$v)pNh7b|w+ZL&}rznM$f<#~SQrMfaFJZ5+UsLmqa_K78a?FRMd1M%D0 zCE>tNpi9q_^>xlT`Yx^vua8bDdDAuVCCfUkgv;*S%un4woK?H+T)ec9^UsPXK{IBd zj!y&FO{s5+F~*uaoeqmu99g3HJ?g~nq!e(w8nVAxFd58s0tvJ89;EXcP`sc$<`@KZ z)D04kD94tqQX7MHrE*CF@&%Zz@_1P?%gQlT9rN>gtS$h119xj}GBto|ny`X${n!v) zken3uaznGgO<3T=WxDbg9!5LIUzB|pUkDaRDHchX0aCXMe~e*7?x~SZ+04MixANzP zOEZ&?9DL=KpdaQq7(Q~u!mKz?J9geu=dTj{pzkl-;R})3=H;Fos9x$*D#;jD=WR9) zIV%i&9;Ud;&2k;*w*N*Q^q+l7kqIb+f1z)gbux3GJmcD;CNzJ@xW2u$ZZjx-_)@Na zgfUb`EQ||pHKdg%{6O_hOqs#_DO2I|%GGe8^r2g%+vP^8o0GjPEjg+tp9}C8%Xj5I zBHVAkVq*Esbrb#}b^wf5zpjf*qI6CksZI#%Nvu&MQZyb1&Y744T8{ktf+F+Ad`)`q zO>+mEtY&^TB;Ryv-OBx_$W~ypwI?HmWXBbuMZ|~%FTLl;(KqF+K@sOh?Hw|MR}1Yo zOhAC+R?1XbzLe&g)8XahDMY$2KnNFwQtLqK;BsjZT7mVhO3j0>T~`f`17CLyRPDt7 zrdxkb{$zzv43cgbvdr31Yc6)Z4s0ooeDN!wsZ>@}dQj+CauvPe%>Ec2kKu_&HRwRU zUjFZXw7Zh@@DX7guW%0Gi+b~^p{+NL{IiE=5CCXYXWQNW0a7{>?DlJfH4#Bmf199= ze>!1UhSHM-SoxXvP9LnPh(th(k-1g&(L0|ZBv2QGj+F?iTZ_kb?DqcD{J(%o5<zEU zwIfTpS^Q?57yBCvpIOCH`$tkf;uR6OWZpPISA0Mwa~*AgplofN%4^n(`UR*dlxpDC zi@@zf)~?oh^bpSeEljLIhY0;n_#eYb639Gg@-N~P?TU9pqQo0?2;Z?CSID>%blueS z5Q(3xL^?V}Z>VNi>O;(tnUFRfj~`h?jVC*A+%Rfmh@Y)0cH@UOp{MBmRBB2xw>%;c z8RydEs(BjrW%gwz(fU*=8t<K%GDQMirkkstIc*_v5s#-%Maa?*PoEK0n<x3!_-;7u zJ`DUVecUtEd<e!<C?p|Z8<b^?*b!t7B{%saStoDr^d!(oy$xB2yw5X)D62nz6>vH~ zIz`v<09v#|IU>}LOio~qsb|F`-nC6ud>0x~2v2Vw%hfr@R8j<j8_fX^g1%uY2&3~* zOM=xgM)yfKqI5u{2!0bx`?=SP&KV=AQZj{T!~CvqrzORYomXMUZxh!XQkO&2M1A?I zz2Iigta4oI{&doHlE0UkUfRVK_uh)^{7vZWJhCGc^yUTW;R;xOIZHY<rPX+;4JCCs z1s&NRyWmk$)F-yJe~EMlAF3i~)As7Z{%pzt9)Ai-3juI%(V=_u?A(PiY81nn%pQyf zxrepQ{=R_ZB8YL!Ol9tUJt<}4cX%eB>GTQ7H5%&R()lfFowd%!3DnH>Qw{N>T#as8 zFE@?9^Kw-Ww+Qw^$j?;(Eo`f!xMB6;EG^5^tp*9cW`QRv2zFYjOdGa^%EU5;P1qya zxj(8Id&C&(T7_WRQxx=_I=ubb=`P@Bq1yW|?5<85MF58Rw&Tf6C7C4{pyk;D`Ii0a z(o@{^!aS1OrMp&g&BSC<MKH4CtRKjMKWO<_a9iHKFY?Pzv#^Ia)BRY@(4XFRMUncP z$u2<;>TJK;vOj~MwD4~uLyjJ(`i!Oa={X`%YSyK5EWugIymGHHm_v>>_1!3qm@RL1 zt7473gGu@)BUsAR1}l0N$ttf2>NLIf(!rMveoSdY@eg?EgTyu|cg(7MyOnI=<>)A+ z4$8RXK60hF4J;S&8G|!&n^kB~i@fuvCfO^jR(%w=SytKsL{w8PU3=LR*?1c>G9A7N zqlaJj4)F|lEx?PtmMujs^yGmrD*O=a&(uBQL1$_f3T6Q3ziapwqPCzprfLVFEMc<D z$_y6=2y+G|QE@GRotii}f?<S7mO7s`mO{=t`vt}@5OfGqz;G(b=FMqQbBAcw^oGP9 z95=TO1h(xLc`Z~kJn6ZurMTd&;GeU7r?a%`J8$WysWi!K_Cw~`2SFf@YfCQ1tDwsg zkT>z+KfB}AO|m@A1_ZK*(22~oYg@r)q2`Ts*AMNQWM9B8!le(FS{M+V^Wzj7#v~=& zZ}q$=EbZ<M;t%vrpRA39R8qA~Ni_oh0+6&~v8mc;qlf)vRIc{cv}Py%wYY8>*LxC8 zzaBA(K5PY(@FiApL8YtG>1hLjFDxGtg;U#k?RkE0hO$rK7Fh~Q=2$>)e1o~ZkuUs+ z8aHo@Sr&v+Z--uZgP|ynf{Z~WC~E7uCVU%T)T?7hKx>#mt?Gwtfq&lY(*>Gzs^ATe z0~`7CF$wD@S~WXJ8VM$mEK^wcNj!z*4w6!3v=<t4mBVNN{soix$K}jYy2I>DmPx~{ z3D_H*dY-SlS543D{|Hc~v|b0$(pKsBz5$A`I4q6iM(-Hdvf6Uv(XuvmEebM5@Hylu zugVg~Lfr~VGqOMsO0Z239gv(^w562&2w@zPCVoz9`|Q;!YIYmrK#c|53Tfr+8{BQu zyHwepVO@QX{jHS>(?&hZ1qfK@JVn{hQ<U4arGS5mwe2JSFY9}{sVm!)AuPA6Id)xA zuhtD41&v8pz@C|JZG7tI_*iC>Ds0le+_LYI8r2Atdl$ptK~Y#VXz3`~$-LFOkEkiY z!KWE1J!$vlp!USOsrQYXU1ey58-ilvBd#wl%Q8GL)%lx&UjcPyKjom8_$WqS@)R80 zbzW|yh!|d7&6bReYfzZOu(h7d`dMI&Nm9_OqY(A+M?v@x&#FfOKV<~PvMwiHx$D2@ zS(1#vxBl`|en=AY@QQ!yRc~fo;^#6^Ns(GNrISwaw$Yhkzf%7FL143BiLTdJtgt?N z%W`9uFp~<$;+V`GnNLLM3^4!5FHj-PIaSz_!hQabaaPHtgpZ<Pr-fmLCSd){tm)oi zg3Ig%@3veB!@4px{L<7NQ}jNfS`7+>XXpA+7{wGz>H!T&VFcMNef6C5A!(1xJCnf) z4G^DKaq1F6Yafck?Nt#PSGx~%ty`-{u)89z=O^V9MNbg`SEZp^482qt4Ak9($&|ht zC}}n|a8vbZ8b#>sd_*3-TuQ&%#+sgUQ}^tMo(i#8C^q|8oUb&O((wCyT%D#tv9C-@ zfi!UqD*Dg_qd@n$|75XG=Wgfopw69>U2ScHySR@JFXy{Id59zEb+~7rK&&58ZrxzV z_jbk4FH_!>R!fj_j9bk@4Us_OH>9Qpq5iZ%_gCF_W78|1>0gU#@?b1w&yDNMZn^I$ zaQ_lpmbDW>I3kg<W#kmgb+qyHjuK)~+o94%9-<T?w7l`c`$dJ$r1l>34wQMDx;nom zge;b+`~e5t?yye4QfA&09;8b(L=XeSsm7d-rH$fwjIH(SX04l9rEo?GPeRtRdhVVv z{CRVLF+-)rxAW}gQh?__FQ1%MFSA39P>i$r-7oJZja{Qsqq*c}oo7{wCs5yJ6Mm?a zkjpI?-5MV`_b&2!V5#2W2yC7**muK{z!J=x<tU%UDEQfAufiN~`RV=OYL+G4z}j%} z{u?F3iN`{0JqP2S&$8pco}D}=(ki6u6#(gMOPqUZuDuFVvnpZ=MrEpUQWUPG#(1@y zT2ZPOKVQA0$hW;!!eh<mDdC|8F)sPM92$Ti1TlGfjcoc;`ia)81BAjF8Lzt+JNj#6 z^$Px!^Ci;=%?;vWalU|lsVVX>j&uTzcvYFnSJRwxyMLQ6%TFDjF>8=<3y81>xC>(q z)bG!Tutni~SSk|Othb(eU8#xA06rMP%1o1r#Xo;Wk`f=6n37>4WadB5Zb$CSr@@pA zd!wo7qpTDrN$GB*9No&yZ$>K^zL97sznvDahVZK)c>VVHhXR50oIwQM`32rnXzIzA z&8HzXVdy9mdFMj#<2X!Ac(7`jV^EYs$sPKabGu;|0pX~7DrBCr%tOWs>He4c-!MEe z!cJIsPqJA7HL*&5<l?f$(P?k-r!(0j`tYKlw8y<Udqu9)M!hKzRZoC|(6ismJomT! zI4@%XTl<9lU@7b-Rs~LfUi+OnURD3qXrQFX*{z$hUVn(LgJGavEs!09gqg=K99C=3 z8fx1x_MK^vNuTK)$dTm2tKw%QgF&a8t&Hw}{OgE8{V%PEOJ}>~audrp-yCDp`Hoat z(e*FVASs6uUC|4VGX=07*J!&4P$uO5)Oia=0Dr~enmrc4(r411=k5O^+-Yk*>V(-6 zNCfE6*1&f(>jA9NHIN`o6bx)9f5T~cv#zKOVWk1i@dH-nk!P3_=oiCk8~x|I56QF% zI*;n+ME21tR7#a?|GxydvcsFup-JR20_or*3VL9+^}-axIUF>pQ0i%)hGhx4UOt`< zWR<f-#4R;){woWS?fGK&9yhJcuK2di0gc2?t!1(jj8E`bnaLm$w@3+~2E5YnF}cp~ z+IwT;ju;2rT8oa>i|#6`Pf+yTO9gH{rFfmB&TtNq>v*aAyS`U5Xt5nMl~~PUXgKRk z1DkJ1?9K7}<upF3#YS~5nlO%ANUm-k*21qnS}?>1?5~|m1gIs`q&iYTK;$)DV+rdd zFTt!9;PIGTrqDjHC8lu56QwSis3h*IK*cTE<rURqU;UZ#&THrNjth-P0?5cYksWlQ z5YJLZu#6x=;$hMh&FtO<5H;u$X5ojUk|{@JW7HREObD0m?!AOtdim9~0nO%dR|WpM z7lN}t%GIpssF}Yzo9KXgw=hnr(M@&hD}h_;-BiT0N4TJZ+wW)5Z0;X0=RA+0EHe~5 zOT5jYibcE@)~)!(>Nx`AtUCuLo$BT7sHk2gq;^8|NXkE|V{uy3KFNXVNyk?Ub_*EA z6cMx`_Z8+1jVit=GV=fmMS;ULJ>=I`rFWH3xrfYuBb~|Qll(YESpWGA#RP^n+>MFw zAAE3+?A~%ckn;PZH%VpE>F4*|h=QN6Y<H!)<*#wUUE!l3U9|w(H0=!5)rVXkpi6a^ zhd^t{l4Z(<9;HF-$)FODf(doT^LNstD=SjV=$`L-_a8?JwqY16=kaQ@MTB5?rSZ~A zd7B1WXG6q)$cNz;Ir|*+pEs$fp&d)x#oNFcY6%8SE}4amF?X-nDL`9l*__>)lu%Sx zRs;#>fegb5lnzf@fZjY#;r2@LIIETRxZ6GfaLCnt-sQrg{%n`IOrlHj0vsVzHJrlz zp+}$tXXAJ43<RI`xolsfcXjgxkOxP#<9}PoQ=MSJndpi{;AyNAo_UISThJ^U31)-# zT+%xlDVB6LxiT(bI{<)i#DER&z31hzkcdOn*yRq~t41`dq0kKQ%ztAKTEM=xF!%Id zaP+Vl)hxP4aF5NnqkyeTSWTPmC*G=v(As@P9itcVvc<)?i4RScDPA*@67^Ez6a3fU z0@(lQErI3UYIm7d$w0^!K3mzXc_HB++SPdURrfPc1ljj*%Y}`eYlYeV6_S$wX;JCQ z`4oWQ5dKBnOQ;V)HyIy{3qOTc<H9A6|1EJBVNK@$qr~k!1J$knTjI)K8Mgm7)$rMb zB8JD<Mwq2QHh=k2Uz`mkv&2S3E>OP0dR2x%f_WRP)?bkJm2RO)Ps7~EvpPei0Ud{e z6px|F4xP9^;IGA@Jg17uuz7+6Hkv14{3|Bx800=gV8M1goG=gkcZ@IuuSXUmP$h?l zUp`Kko<pv<o4w4JA72Bn(pvp6S5BQiIoC1!!U$~g2%SrS^0LjxD{a*pbA+AldrKMo z8L%`vc*e=q5Ki+k8i9(G=(uch*J<HO`L)+ly(lY2aH(}*$c=v39fP+fJl@U?B?~9a zRuG!KDT3drk`c(#`(cz8R>vZ(y8!jsDZlqLW$&#QIW09m)c}Nt#OWYVjV-vlU*rnr z)nm(h(!U!gJ2V$KDP79aJ<H6Q7WfHcSF|;4?t$BW&Q{<~)y^*yZ<X%$o`d#Z5^}hc z)J(IHLt-E?YV~<z9_A>R7!P-(uY|aNjF-ySq2!w@v&{hd_Dpzfec%No9gx40?@)%0 z9C+;KiHo;f{m~Zr=O>nD&q87N!-KVWyhUhrAu^OqnHSu!RcIJDbGj?nrL&~yxgDRS zqr@B>e^PKF$wr~RUQ!ZiqoB6OeYz~L7o?Dvf!1P-?m8(rmV7unC3Ib`-lIB_$+PUj zUXUO3VCV-OoABs?!JZv|HuamWKqAxrHKmERnz+Mm?}cjsfg2_320G_|UI;gCnp5hQ zSritS*Ch@TuRgj#|L&?g<wdq8KvV5~{&p1;(j#&)f+_lT9fAA06sQ|uOQ1j3LH<PM a13x{m1>3)Wa{l!=K<BQ$R>>Xf(EkGOI9x&i literal 0 HcmV?d00001 diff --git a/doc/user/discussions/img/unresolved_threads_v15.png b/doc/user/discussions/img/unresolved_threads_v15.png deleted file mode 100644 index 113af20effc36e0c14ee68544d1b4871a4ef8343..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2793 zcmV<F3KsQ=P)<h;3K|Lk000e1NJLTq00K$?003MF0{{R3rtNA30003FP)t-s|NsB} z@!c>mFu%XQW@ct(U|2IVGs3#6va+&NQcpKFHT&<|xv`_bxTyK<+U()Z;@HOC)We*W zi}UE!(#g3^OGxnL((mNa&BU})P)tWgL&UtTqMML`eRbB%ymWAB^z`($uAyXNUia+S z=G@A~#l^C!pX}`H+tI*0J34G?WI8!G$G@<jpPzVmcw1Og`0Uw}j)aGTdT(!U{`c$I z+1Z(rh<9;lUtL%~K0U3bnx>zWYiD5f>DIrsrlp>fetL3ta%=tc<LKVZ$jHd4qLzz> zeR_3nLP0;u!m^HufJ8$<{`~gv@bKK++`zc0m6eroZDyaCjf8)9;^N}&<Is$ZjM352 zyu7^F*w}b*w#5Jd36@DjK~#9!?AEaj10fJZ(K$^c6afvK2!Yi7cjEkf_F336Ape(d zHu_ot0000000000004l$Q{uj1E_*C`qNeO0%c<%eal?$FP9+bKbd4g-YS)RI-Gchq z>JyrMqe%;kXHCjMy=?Ug`Lau8!@P<PsvR_9N(y!*O6!V`QED19B{e5LrFHE($PS~X z6rA<ahM9dfhHqaprG+u2JNxz8lr|8CVR+tWXn=%63P}@b3q>ji3y3l!;Hf%}Q_&CD z)^^(e|3~_6HU^qjj7_MFv-g)|H5+$kFIQA4N~-lrQBo;Ns`W}yQYlKR^-57vDN3sK z>T%oI>i1iUk}3g8nN%vHD5;)?QtPIlT4>m7DN4_?Ra0sWQ=x_}Md?|#YDzb$Kk6w; z&$3lh;zQ|naeT|i($*>{6`T8|{5k!nu5{g`=pE|1OMj`X>!%d6OMR2pqE%DkQz-NW zuD_KurNSA!KEI;>(p%sqMXv<BroZ$G*ru3i$kJN$FO=TJrNo$iCQ4~AhF`~`)fbnN zG3gu;_q3GUqa&9R>oJs?sh*_NOWnkyG)!^dZ<C{B2uZt^d0_(;gH~L8N(Er!$tk5@ zaA``a$dsgko}kp@n|PGoWrhM&D=|tr09|iuY~YYq*E6ETk|J2Djz%tpaz~>o{n2A6 zZOQXQD9FU4#D5|)eEE?WrA?qjUgv;*O!Hnj4S)9>$KXrZIp%wJW{SbxxID`bh%?M| zzFnZ8@^to|=9%4`RbHfII6Pa{aGZjiW98f;1sUypw-};iI7PsAobnx|#&mj2QP$(; z7uVysxppC-6s$M0q$rj~@`>=wQ9Md)Dfn+bCME6%za}wC6`(!8O;b=2sFBwjU?zWs zoiVr%Cq!~=+5p5B)UUm7lt}iv0#3W~(bc^nKA+DinSf2Q8X#U+JcDTbl4Q9{Z`l5g zQWJiX()%np+j?j{^$9rrtl>kdfz!SiJSDW=Xp$mX8ik+Wnei#Hr9e`AO8q2HB^@wG z2H@%*N_lUq>?~w;lH&^G*5%;0Pofh1HU`Ai;a4dKlxPDmNmS6F@nek*$T~$5bs^JE z@251DM4`8rucrpTtV5i!1KC%m6k2aoN$K>1mR$Knc%~bd5?hjz;#1-`4c{esDp_+% zwWXARoeL{x1D%0CzTB%f8)O0aRL}2e5}5;_WSSQf;J`<*ebC*#=IMig(#N}f&I6}J z#x_gapMGSlwm3UX9msx}?%Tj3CCBXo*RJadO5A4CYEGB{EnANZ;A&PqnD{?(PXIm1 zoq4!I>y0WYxOw=|@Gv$d7Qm8{pwt&1xDunZ9UMh3#nvpRv{T^gpaWF>ahW)y1H2*b z1kenk17Mf9sxIDF(EZ`J4&MZn9Ka?~&9i*S3>@+DtlaNSV8`HV!^Zs|yHPX5LN&lx zww_PWn#7sFn8<_HC9W<hh1MHU(qk<JgZPx#5=)9tNghhcQA+<}-t3yw1fnoJ=e@uH z)}Ue|)X*9~s>Rl7XB?CIk&L4;$wix_>Hq&Jl|5u3J1vZb$@tu4aAi4WA9oLD5#e{S z>;o7ktHevv2x!KS2f&O3mq1UnS4i@PrQk7irAjmdEMDL4h)4%|d|B?yfWZr$m8249 z+}D7Y6py3^jl_I-E1Pwa@k%ntkuKgLsq~X4rF>N4fxxt~esADBZB{a5!qO1O=OnlV zma^r7SF}x;LN?Jr`M_JL(t@X-AHYZmVi2=MpJ)|@;{01Bmjn}_m=KTTs|!f;0^YGo zGF~S0l9j$3W?$es8C5z8B>MI3=Z8F1GOxgB-LzcEk@HL=UX%$sFa!{JPiZRB=lkMU zUcjUZL!c*BDjBG8bDO9Qz>s}B1((=J)zj)q-+^LMJY*Ke`qn^zn48d&A5kYY-i(QK zdXk<`+TGb#rPp}$&7AZBXrER^;4x7p{qHKBLBf;C*D;Tq(csoH$uX`=mFN+$sHBNS z)k7SQ8d14)vbRcVJSVXi#M<doh)_w1H{<Pndg&miVnF8rRU$Imb&Ch>;%_QFB&o!P zY>G^`XcO@~;9y5Zv=*XSsT`@~1<eJ%W*dO)qF4=H9`7|PCB-uYwCl>nqV5B{V$Nkz zRwpIitVPH4f@WSN%1$Pg_zCgcRbtE<(2l7j)0J_GN*m5iRC@Qo`HNi10IHEn<f2Ab z(B>p_rTE3C#5nt^q{dSPz$v`Um27#-p)2ub-U><IX31^iq?1V{-W1MLrCZ<c$`Ntn zW@*odPKrteDP7C5szfKpZ+X;3n*9!NObWOGY7|qc0$5r&^*J&ZZd;bf&^6Y&2dFA- zlT}jVdBf8yPksFMv8=P}q{f>&>6^d*yPbPrDtUmvqOd&Er5HMJ!?!_+D)A7Egncuy zT-EsfR2BDBrXLR7$^a^Hl~%wI7(C}=ec_JHQ^O2!N<klYib^JcRia92JY`@g!kGdH z<y_E?HH1n^yt$M9NXkAceU3e3a$qXa6fl-vZ|ws$lVUsqT4m6@LW)XU^gDYfEM2aH zHvnyPQ*LhAAzK5c56#m)aArm-@ukr>>3P)qr^V(acy(I&fIDh#wop`}JHVYZRh5$B z=>x;>gNlnCVO<RO7wZ{VB{kmH=l>pk`<NkD`gnA7^n3TT((cd4{8X}P2)R3^IMwHd z_h~B80*LK~Rds|6MUMr9I4z>MN=-!f{4IIXDFxVI{IQ}^1p%r`N%3^SI66Qiv}Hzo ziW2YZ^ZF0u#(v54CC9Y8bq&F#`hPq=GXS`QB~YN3xyVu;GYbbhv`6m3teguqCuVqp z&Kt8&w1bNPf|e$%Vopvq?5Hh>c_&Q%)PCSKP-3ow29ZtQlVA-j65|=T;`5zM0Sc&! zOZB^aiV|-o4pk-jGl9HSQYqQ%WkC^%Dck&VI#0IUrii)cwJ74mt6L>Xp=8y)IYq4O zun(2D$MZeoncKY=UA203ZdKyVlS5MJ4?A~k0ssudK(O`x6Q?y9Kp1g1K&ppS@)|GA z|G^nARY^`N_bYaAb%I0hky5qf`3WgiOCH79uSltmQ(Z`jTcm`PxJ61xiCd)f+Ph;L z24Nr!qn5mghkyl;OQ2={O`^C+ffA*IJMmuS0ZyOX{Wv9J$#hBvPa{h;cxr|@vQ#t7 z#^EDNH4ZOJfsH7gv((!ir_}n0lHXEJOZnocb#GUnQrl}i-Va?$>C>chm+ml{^xrxu vQ#8x8mLImc*NgReD*ylh00000z=OO1$q!qQFMXj+00000NkvXXu0mjf{F-iK diff --git a/doc/user/discussions/img/unresolved_threads_v15_4.png b/doc/user/discussions/img/unresolved_threads_v15_4.png new file mode 100644 index 0000000000000000000000000000000000000000..1d1669de0f17e89ec4e67b17bb6620280d459167 GIT binary patch literal 3692 zcmX|E2{_d2_cthsP~GyAWt6m0h-{TXX|C+cr6OZ1$z?=c4Q9qpWxFYRrXnRYH~TVX zlsz(8=UNA29gQ)V8MA!n``3Rv@AI5<KF@Q``@Wy&Ip;agdE#gf-?#V3UNJGTeb&EU zbrutYh<5z@dnA5oy*AE{sd2P*wE{tKeVw_^+BrMIT4%C!b#-?Ti?#0U?X9b)o1L9K zJUqOE84N~pa`N=_bkDb*=;&xeLqmOi{kXU|Q&ZE=pFiv9=qxQQ)z#Idr>B$2<Y&*G zSz21!+1X}gWd#NX&dtq@kB=1<6@`a~H#IfU>2xP2Co?lMB9RyxiiE@Al@%2n4ktf9 z|I62}A3l7zd-rZhNl8aXM<gn8ety2ItLv7>EjKqeUteFL5D*Ygx$f`^x4vrO`gr{3 zm)IRLVll6QGQ~3SU+({jH)loD%J)A{-<)zE)F|@T8Z+mR@SUZRYxyr{PtSKj>=G`% zg7yg}U)<P#M$$VpRpJxJPl%buk75;O9>vDLSyZha#N@&dl}I;St9yIj(j!PQV2mZ+ zfGXC%F@GK}Wh|L6h<VLi1I5onI(K<L*{kGu<{oU2`n?TV-j|w?2uaGzLo?v~MHUHS zS$QVmP@h=p{Y;JqFkOH>b?t`lg}$8AiwMVBC_QRZvD>0A*>_ih?VqzXzp#w=Xo_p> zIoc^jN6m40b_?&UhLWgYFK;u}O7Mng1e>um&%42j8R<=(kY~d<z4vN^)l=RUUetfC zGU-<PdblJhf6iPFy>bNvhw@c@w-hu+_wQDEn0BLWR#rx_vZRTxYSHg_PGKfStLGc^ zh5INncaKWm-x8DR;I*2%4XK6NWYR%Pg&9?IWl2rdfB!oK*P{ekJga%%qM>~UQ>ei0 zm{w<5%cP~5!*0Z@(=ePp87M&rW|Nb(OyTKSJyx%z-H7JXP$Rqv#|3j6IJ^6r^@|P# z?{jgw0Shwo$##pn3+^yM{MN28lcaKqo9VZB;=Bi0CU(49cCY6#xMY&`!&rDa=cP_2 zX#`ue5hRb5m4#SFhvh*|oYz9AlT<{%t_@pW7b?xXzPC~cXo6mNEfskGGI=T4XZz7J ztXk44K{mgc1Z`UnPr|xmQ0#(@ReHUnvH1Qwdhva@#j_3z<0M+Q^nU<eG<Wz<B+JMy z{1r3*?!^c!qF}LC79<j2h2&(+Aif-0oR#%J1ts^z2JquBjd%Y@7ex~V(>pmfK&o() ziNt0F0O4GBI`|sgaMlB-2^;HOr=*k5$<~ivX~lBBCmKCCzUQlW`nNmgoiV|!3hIQT z>S{-&dqB{~eJVKMmEu43ic{BRoW%s;BQ@_ASdV|tnOIrf9m9?7HPxp&1Nd+o&|&!k zptRPw*w;`3y3?|@z>DY=e$&HmU;&Tk4$F}pR@ZTkTz)nL=YMl}GS6*o(+lAvmhwfo z0e#@++8Yyn&v-5He)Mw@_FVuc4?HNESRH_y&Gmy7STcYh72a4T$vHjjAd4#&KINP` zt?|rAN+`SJsyiDWdoo)0)FQY+d!Nmkq_ql~jsN`9mWezv;?_x0aeogJw%Dsg5K4gj za01r>DrG`@ct0?@V<Q0$q#$nv|MW0v;Uj3=-DaK{d&3&}GL1fcMnf>O$iz!moYD4E zHE7zxc3{2<ID1TOd<3^XdiT8WBi3vST8-y{PYb-#ACs+6-@s^$7oX9k!OL+1-YRo2 z9k<UDQe$pqN9Y+%Lo6}bF&%)4*xK%u!qaQw;REjS;<_Pu#0zr%)s+1${p>r{YLRrr zl`MBqRl!4WMeX!JMN_D`>vdXCc~6`9Ag&YXGI9cJfDckL+8av=dvg+AxEQ5)gkePX zhErjG7b2E!)-``FOezBXPha}*7{?(CL|WFqY({v|GCZu^<J{W;Yb>p95xfA4TF;U8 z`{MRQ#(FA6TXTFkOPy&IgIl<LwNn$AT-(%G;l1l(ocTwl!Q?&BGvWuOmY!_WP`Y-r z43zK|+5?(Fyvc-uQL|TqR6HaXOPvckS<oVpX+x{uiWjAlP8=OuwP;8zSs@#F`cf#H zQ6DAh{K}V>;L`Z+8N9{u=tcA>3@IR2;i98wlv-5(o~@&mkXTz^m?>c13g!90Jh$!b zrJ&z7d^??v^Xg|WI7YSQVQBdnPKK9qmR%xFW8;k2rF!Lk)2dCaJ~j8qD@XYXxeTdi zQr42d#BXNp4=UFj_hV7(v@~r?-4RYyDCV34%qXPYN6erE*c~&$IG$>r8I=Y;sE%0; zF%%G{1x`q$p+S3sr)8Mz6aE<|`iZ%KB!iD3ow6M?9!fvo^>DGG*pXo|^FU()?&6=Y zRa`v~VA6MDvsW~t;ngT@&(k7T@SvM89iU??mi6)8!W&=LA2;vBmguIb2#pA<$7rg_ znbcg2Z9+y&(Ap<cbpzn+KL<e{)yHqW4rqxYCTk=V2RUkT1zO70W54fde+xBL*=wbQ z9v#HUN~%Rj$r%k|@(`K5?M%I_!?3itkL?J^`23OQojM^JCa_0-{*cJC0~xWGQPLj- zU!ev-$JfiA9*=9&bIAOY6WHu43mo=kGM$m~Vztv6eDOX=)FmX_fMtBv0mt4@Wno(H z2zl(}5Vm|$qRDU@UW#N*xB~92`OPu>tz_(;nzEAt8uF6?@z^3_#Jn%z%>3cj{_%io zrX_OZlDkhmPhhM`9Xj!(PNsy;{DJ2kq|ko@1@!E)j0{!(kk8>aPI0r%6AAma$d&kd zl_T|O#cbYR#;6UpkS}`p9n`{z;;B#gEH~A4YFH_?VV@|BjenUn%)7a(voxnKk3>%0 zNKyI`WP1=h_VHHV9dGNF62v(3=z4dQshjIPBm)^MC?w?A!ghH_E@WfqBOiQNL%Ft= zRog3uRh*8qE>NgMaNN1Ld0|!*C@$2-{eeK5*gkGTRbYoSoEOH}f|VI;g$N{U;XXif zP*WHCw`9nrSl>e4v7FP@q2GU_b1cKH0m+!QpQRPia*wUirWwUvgF4>(+EIDj43vX% z^Kd&v-W{7bSjdZrwM93VQs)#kp25C8_uTnJSLl-hdzlrO3M1PornCe`d21CW(z@T> z5-*`JRY`DRv-8`uDXLw3O8WMM8Kx8id&O5R2X{)TYqiTGHXcmZ2RefnLB%1GZvR$R z5dRNSQhYp7MSK8R^eW`CU>@?e#tgk?-tuWwK~}~1)61BByk{%l7d3HI$ii`=gl8%7 z_R!7{;8%(+huMUo)~mM>o}+1(NB11S+F`pIC+3A*Lw0YJ&utVMO(1$f7pg9yUR)d` zjdiu-%Bl4iKs;}Zw5^JVk|1BXg)T6fs&O-a>Z|dPgV&=ExtLYs(TSjm6g!wF^?F3V z6n2vy;iBApN&T%?RVf3ZvA%3b-c0S3ogGOy*!iQ;_z8YBvkXbu@(}v^vzhyIG0K_$ zilXYHA4tqJp1)-`YA(3GcCuw&SU>y^nw8e3CiWtIlwu=C4Y&R|u+wZ;c~lCB%Mlf` zX&DgR$K>Wcsa@u$^bL{|Szh|;IYdK1@?Z2EtR+LGuBQcolFHY-xi!L3mSZV2{JmM$ znpVX>^n=1-OygKN7qHI|Me4yn{rNqb$A@}bU>;DY_#3*rT}@V&zP&Tg2}W%U_Qli? zi+UTNCOHorhL<J|u#ATP_xVG~>k_2hyT2tK=$Szhl4CrFaDBqt!{^bBKlNcmW*IpC zCYzo}6|2?q_~^bi^1xj@sVesZ<WnUO*f{y^&^L3k3m7F85V^pae-mj<8X>_~hVi~E zhnY`o;c#X5(cMIPmwAEk@Hsa#hI1Q7N>m)CBd$g&HY2tF#KHj(PvOdoOU6e`7QIW% zkx=(XqoCgtLv65L0@E@W6(Z%Z6@<<X64Q4OBK_rbdatFt1D;3}J(q$7fe;IM{Ypdt z$!LO3Dt;<r&npp&wFqO|knN9(8i5EV@T-X(jB41zzwQk(MjO+X{fujRC~{vMw=1=U zpMiH*v_+L-o6IsY&K5Jo$0#Ym!dA*|sZb?_vW<h<@g!D}O!u9lJA6U~pB-|7s}uUY zA2H5N4o0~YS6P{f$4v+X&0?pV@P2i*prkIvjYN!SWt*y<ojsNzlKCjV%4m>_^ED)X zYO5m6$>~)=EskmTXqFroU2YGyG`gw=S1?$e^=HrcFFLGU4#x(M;2`FSrK>06IHiPq ziCLx#2^l1!dQh1XUuF)P*_ZayC>Ov)mYR52%kXplrq7h7%$C*wUfktt@Jn}<REjI! zom5NR_2W97;zzITiG)ue+FYgbcmXsmi#_a~7979(T|D*(R#v}Q85NY>u4r15D8DGJ z<8)<cv^q5E7B8629g-p;E1CG;BVn_R<0^K;a8;V7w@-AEDQaZ4Q||cBpDJSYtGUGJ zJq?FcOr;TI3pIB5$DFN`j9%xtARuxw{(~j#2~ySC^30H8gAog*VUiiiTlq1b@k0YI zZiHu*dt`;sVXBmssWh%FX`{+anZp@&EkWZ;^!^f%oB&MI&rR$IS0M1|-?m1VSL;M6 znUxqU%x!JW<3g~G=p%P|nMJrdaJk;T%fSfN)*A)xL;g`7Z^RF*H3cen2wVVd&>*zu z|LqshnF_);9?~Pqn4o@RB@aOt=m?#u*x4MuNQN}bMQp<uQ+j)2bQP>Dw6$BVuVF9k z_pmdRG$_|+#2b{sf~fcjE=cSmV!r`RX48ID*b?S8beSd)Npn{C2L&QVBvd3ayA53# z@H_Xe7D{p99Jn+;I-aq5I%+F3D3BpwC$!K~1JG1bMP$?TqxSkbCTw~ri#-_C3QQ(b mnZ0ZSz<5UPe>SS<ZaF3Hm+pv%)4#SzF>5RPtDh{r<NgbBd)-C= literal 0 HcmV?d00001 diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 3fb0be6480cfec..7540ae8450fe72 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -309,15 +309,15 @@ To resolve a thread: At the top of the page, the number of unresolved threads is updated: - + ### Move all unresolved threads in a merge request to an issue If you have multiple unresolved threads in a merge request, you can create an issue to resolve them separately. In the merge request, at the top of the page, -select **Create issue to resolve all threads** (**{issue-new}**): +click the ellipsis icon button (**{ellipsis_v}**) in the threads control and then select **Create issue to resolve all threads**: - + All threads are marked as resolved, and a link is added from the merge request to the newly created issue. -- GitLab From d61169fb48e4fdc679f4be0124ba910d95a7f252 Mon Sep 17 00:00:00 2001 From: Pavel Shutsin <pshutsin@gitlab.com> Date: Fri, 2 Sep 2022 18:32:32 +0200 Subject: [PATCH 032/169] Improve confidentiality check query Make confidentiality leverage index scan where possible --- app/finders/issues_finder.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 663dda73a6a1a2..9f96abcd4e546f 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -60,10 +60,10 @@ def with_confidentiality_access_check # count of issues assigned to the user for the header bar. return issues.all if current_user && assignee_filter.includes_user?(current_user) - return issues.where('issues.confidential IS NOT TRUE') if params.user_cannot_see_confidential_issues? + return issues.public_only if params.user_cannot_see_confidential_issues? issues.where(' - issues.confidential IS NOT TRUE + issues.confidential = FALSE OR (issues.confidential = TRUE AND (issues.author_id = :user_id OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id) -- GitLab From 4207d8825ca5edb8fa6ec324ec993c0a93c159c3 Mon Sep 17 00:00:00 2001 From: Alishan Ladhani <aladhani@gitlab.com> Date: Fri, 2 Sep 2022 16:02:23 -0400 Subject: [PATCH 033/169] Add `column` keyword to `add_concurrent_foreign_key` example --- doc/development/database/add_foreign_key_to_existing_column.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/database/add_foreign_key_to_existing_column.md b/doc/development/database/add_foreign_key_to_existing_column.md index 8a8fe3c0a1ee02..4be3296b2bb2d1 100644 --- a/doc/development/database/add_foreign_key_to_existing_column.md +++ b/doc/development/database/add_foreign_key_to_existing_column.md @@ -71,7 +71,7 @@ Migration file for adding `NOT VALID` foreign key: ```ruby class AddNotValidForeignKeyToEmailsUser < Gitlab::Database::Migration[2.0] def up - add_concurrent_foreign_key :emails, :users, on_delete: :cascade, validate: false + add_concurrent_foreign_key :emails, :users, column: :user_id, on_delete: :cascade, validate: false end def down -- GitLab From cbaa3fab770430e70d242eefb6cb057dc27ab019 Mon Sep 17 00:00:00 2001 From: Mehmet Emin INAC <minac@gitlab.com> Date: Fri, 2 Sep 2022 23:36:54 +0300 Subject: [PATCH 034/169] Don't try creating partitions if the table is not partitioned --- .../partitioning/partition_manager.rb | 14 ++++ .../partitioning/partition_manager_spec.rb | 75 +++++++++++++------ 2 files changed, 65 insertions(+), 24 deletions(-) diff --git a/lib/gitlab/database/partitioning/partition_manager.rb b/lib/gitlab/database/partitioning/partition_manager.rb index aac91eaadb1c4a..0429d209d53492 100644 --- a/lib/gitlab/database/partitioning/partition_manager.rb +++ b/lib/gitlab/database/partitioning/partition_manager.rb @@ -16,6 +16,8 @@ def initialize(model) end def sync_partitions + return skip_synching_partitions unless table_partitioned? + Gitlab::AppLogger.info( message: "Checking state of dynamic postgres partitions", table_name: model.table_name, @@ -129,6 +131,18 @@ def with_lock_retries(&block) connection: connection ).run(&block) end + + def table_partitioned? + Gitlab::Database::PostgresPartitionedTable.find_by_name_in_current_schema(model.table_name).present? + end + + def skip_synching_partitions + Gitlab::AppLogger.warn( + message: "Skipping synching partitions", + table_name: model.table_name, + connection_name: @connection_name + ) + end end end end diff --git a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb index dca4548a0a399d..7b7ef77ca2d320 100644 --- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb +++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb @@ -21,20 +21,11 @@ def has_partition(model, month) let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table, connection: connection) } let(:connection) { ActiveRecord::Base.connection } - let(:table) { "issues" } + let(:table) { "my_model_example_table" } let(:partitioning_strategy) do double(missing_partitions: partitions, extra_partitions: [], after_adding_partitions: nil) end - before do - allow(connection).to receive(:table_exists?).and_call_original - allow(connection).to receive(:table_exists?).with(table).and_return(true) - allow(connection).to receive(:execute).and_call_original - expect(partitioning_strategy).to receive(:validate_and_fix) - - stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT) - end - let(:partitions) do [ instance_double(Gitlab::Database::Partitioning::TimePartition, table: 'bar', partition_name: 'foo', to_sql: "SELECT 1"), @@ -42,19 +33,49 @@ def has_partition(model, month) ] end - it 'creates the partition' do - expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE") - expect(connection).to receive(:execute).with(partitions.first.to_sql) - expect(connection).to receive(:execute).with(partitions.second.to_sql) + context 'when the given table is partitioned' do + before do + create_partitioned_table(connection, table) - sync_partitions + allow(connection).to receive(:table_exists?).and_call_original + allow(connection).to receive(:table_exists?).with(table).and_return(true) + allow(connection).to receive(:execute).and_call_original + expect(partitioning_strategy).to receive(:validate_and_fix) + + stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT) + end + + it 'creates the partition' do + expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE") + expect(connection).to receive(:execute).with(partitions.first.to_sql) + expect(connection).to receive(:execute).with(partitions.second.to_sql) + + sync_partitions + end + + context 'when an error occurs during partition management' do + it 'does not raise an error' do + expect(partitioning_strategy).to receive(:missing_partitions).and_raise('this should never happen (tm)') + + expect { sync_partitions }.not_to raise_error + end + end end - context 'when an error occurs during partition management' do - it 'does not raise an error' do - expect(partitioning_strategy).to receive(:missing_partitions).and_raise('this should never happen (tm)') + context 'when the table is not partitioned' do + let(:table) { 'this_does_not_need_to_be_real_table' } - expect { sync_partitions }.not_to raise_error + it 'does not try creating the partitions' do + expect(connection).not_to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE") + expect(Gitlab::AppLogger).to receive(:warn).with( + { + message: 'Skipping synching partitions', + table_name: table, + connection_name: 'main' + } + ) + + sync_partitions end end end @@ -74,11 +95,7 @@ def has_partition(model, month) end before do - connection.execute(<<~SQL) - CREATE TABLE my_model_example_table - (id serial not null, created_at timestamptz not null, primary key (id, created_at)) - PARTITION BY RANGE (created_at); - SQL + create_partitioned_table(connection, 'my_model_example_table') end it 'creates partitions' do @@ -98,6 +115,8 @@ def has_partition(model, month) end before do + create_partitioned_table(connection, table) + allow(connection).to receive(:table_exists?).and_call_original allow(connection).to receive(:table_exists?).with(table).and_return(true) expect(partitioning_strategy).to receive(:validate_and_fix) @@ -260,4 +279,12 @@ def num_partitions(model) expect { described_class.new(my_model).sync_partitions }.to change { has_partition(my_model, 2.months.ago.beginning_of_month) }.from(true).to(false).and(change { num_partitions(my_model) }.by(0)) end end + + def create_partitioned_table(connection, table) + connection.execute(<<~SQL) + CREATE TABLE #{table} + (id serial not null, created_at timestamptz not null, primary key (id, created_at)) + PARTITION BY RANGE (created_at); + SQL + end end -- GitLab From b6cc362b68b0ff1b5bacf837d8b899ff30c22dda Mon Sep 17 00:00:00 2001 From: Mehmet Emin INAC <minac@gitlab.com> Date: Fri, 2 Sep 2022 23:43:25 +0300 Subject: [PATCH 035/169] Update migration version --- ...curity_findings_table_to_gitlab_partitions_dynamic_schema.rb} | 0 db/schema_migrations/20220816075639 | 1 - db/schema_migrations/20220902204048 | 1 + 3 files changed, 1 insertion(+), 1 deletion(-) rename db/post_migrate/{20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb => 20220902204048_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb} (100%) delete mode 100644 db/schema_migrations/20220816075639 create mode 100644 db/schema_migrations/20220902204048 diff --git a/db/post_migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb b/db/post_migrate/20220902204048_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb similarity index 100% rename from db/post_migrate/20220816075639_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb rename to db/post_migrate/20220902204048_move_security_findings_table_to_gitlab_partitions_dynamic_schema.rb diff --git a/db/schema_migrations/20220816075639 b/db/schema_migrations/20220816075639 deleted file mode 100644 index 66496258f7cb8e..00000000000000 --- a/db/schema_migrations/20220816075639 +++ /dev/null @@ -1 +0,0 @@ -8767df2ee3d58f6737129b61b7953d4b11e2a3417780eebf5c456a8242c218d7 \ No newline at end of file diff --git a/db/schema_migrations/20220902204048 b/db/schema_migrations/20220902204048 new file mode 100644 index 00000000000000..c5fc6ee144859a --- /dev/null +++ b/db/schema_migrations/20220902204048 @@ -0,0 +1 @@ +577a3808889d0e53af3c45ee38e852b8e653f7292c0144769811e4662e9c8c7b \ No newline at end of file -- GitLab From 7518d63ccceedea20a6f1016fa9b32138c60a1db Mon Sep 17 00:00:00 2001 From: Matt Kasa <mkasa@gitlab.com> Date: Thu, 4 Aug 2022 11:32:08 -0700 Subject: [PATCH 036/169] Add find_duplicate_indexes helper Relates to https://gitlab.com/gitlab-org/gitlab/-/issues/370275 --- .../index_helpers.rb | 34 +++++++++++++++++++ .../index_helpers_spec.rb | 27 +++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb index c9a3b5caf797ee..15b542cf089eae 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb @@ -77,8 +77,42 @@ def remove_concurrent_partitioned_index_by_name(table_name, index_name) end end + # Finds duplicate indexes for a given schema and table. This finds + # indexes where the index definition is identical but the names are + # different. Returns an array of arrays containing duplicate index name + # pairs. + # + # Example: + # + # find_duplicate_indexes('table_name_goes_here') + def find_duplicate_indexes(table_name, schema_name: connection.current_schema) + find_indexes(table_name, schema_name: schema_name) + .group_by { |r| r['index_id'] } + .select { |_, v| v.size > 1 } + .map { |_, indexes| indexes.map { |index| index['index_name'] } } + end + private + def find_indexes(table_name, schema_name: connection.current_schema) + indexes = connection.select_all(<<~SQL, 'SQL', [schema_name, table_name]) + SELECT n.nspname AS schema_name, + c.relname AS table_name, + i.relname AS index_name, + regexp_replace(pg_get_indexdef(i.oid), 'INDEX .*? USING', '_') AS index_id + FROM pg_index x + JOIN pg_class c ON c.oid = x.indrelid + JOIN pg_class i ON i.oid = x.indexrelid + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE (c.relkind = ANY (ARRAY['r'::"char", 'm'::"char", 'p'::"char"])) + AND (i.relkind = ANY (ARRAY['i'::"char", 'I'::"char"])) + AND n.nspname = $1 + AND c.relname = $2; + SQL + + indexes.to_a + end + def find_partitioned_table(table_name) partitioned_table = Gitlab::Database::PostgresPartitionedTable.find_by_name_in_current_schema(table_name) diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb index edb8ae36c4524b..7465f69b87c8d1 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb @@ -26,6 +26,7 @@ CREATE TABLE #{table_name} ( id serial NOT NULL, created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL, PRIMARY KEY (id, created_at) ) PARTITION BY RANGE (created_at); @@ -204,4 +205,30 @@ def expect_add_concurrent_index_and_call_original(table, column, index) end end end + + describe '#find_duplicate_indexes' do + context 'when duplicate and non-duplicate indexes exist' do + let(:nonduplicate_column_name) { 'updated_at' } + let(:nonduplicate_index_name) { 'updated_at_idx' } + let(:duplicate_column_name) { 'created_at' } + let(:duplicate_index_name1) { 'created_at_idx' } + let(:duplicate_index_name2) { 'index_on_created_at' } + + before do + connection.execute(<<~SQL) + CREATE INDEX #{nonduplicate_index_name} ON #{table_name} (#{nonduplicate_column_name}); + CREATE INDEX #{duplicate_index_name1} ON #{table_name} (#{duplicate_column_name}); + CREATE INDEX #{duplicate_index_name2} ON #{table_name} (#{duplicate_column_name}); + SQL + end + + subject do + migration.find_duplicate_indexes(table_name) + end + + it 'finds the duplicate index' do + expect(subject).to match_array([match_array([duplicate_index_name1, duplicate_index_name2])]) + end + end + end end -- GitLab From b5dfe128c891e60e2c8bdcb09fab4877cf63259c Mon Sep 17 00:00:00 2001 From: Pedro Pombeiro <noreply@pedro.pombei.ro> Date: Sat, 3 Sep 2022 19:21:43 +0200 Subject: [PATCH 037/169] dev-dep: Upgrade solargraph gem to 0.46.0 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 7a1fd731b82c6c..58f37862d21241 100644 --- a/Gemfile +++ b/Gemfile @@ -346,7 +346,7 @@ gem 'warning', '~> 1.3.0' group :development do gem 'lefthook', '~> 1.1.1', require: false gem 'rubocop' - gem 'solargraph', '~> 0.45.0', require: false + gem 'solargraph', '~> 0.46.0', require: false gem 'letter_opener_web', '~> 2.0.0' gem 'lookbook' diff --git a/Gemfile.lock b/Gemfile.lock index d02f181665c406..34b1a073a7e57f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1308,7 +1308,7 @@ GEM slack-messenger (2.3.4) snowplow-tracker (0.6.1) contracts (~> 0.7, <= 0.11) - solargraph (0.45.0) + solargraph (0.46.0) backport (~> 1.2) benchmark bundler (>= 1.17.2) @@ -1756,7 +1756,7 @@ DEPENDENCIES simplecov-lcov (~> 0.8.0) slack-messenger (~> 2.3.4) snowplow-tracker (~> 0.6.1) - solargraph (~> 0.45.0) + solargraph (~> 0.46.0) spamcheck (~> 1.0.0) spring (~> 2.1.0) spring-commands-rspec (~> 1.0.4) -- GitLab From 883be1c5171fc615e7bc60d2a787dc2f65539499 Mon Sep 17 00:00:00 2001 From: Adithya Krishna <aadithya794@gmail.com> Date: Sun, 7 Aug 2022 21:26:20 +0530 Subject: [PATCH 038/169] Replaced usage of toBeTruthy with toBeDefined Signed-off-by: Adithya Krishna <aadithya794@gmail.com> --- spec/frontend/work_items/components/item_title_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js index de20369eb1bc66..13e04ef66711a0 100644 --- a/spec/frontend/work_items/components/item_title_spec.js +++ b/spec/frontend/work_items/components/item_title_spec.js @@ -49,6 +49,6 @@ describe('ItemTitle', () => { findInputEl().element.innerText = mockUpdatedTitle; await findInputEl().trigger(sourceEvent); - expect(wrapper.emitted(eventName)).toBeTruthy(); + expect(wrapper.emitted(eventName)).toBeDefined(); }); }); -- GitLab From 9c5960ffcd51bb43c32f1f1dab90f1e84bf171df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thiago=20Figueir=C3=B3?= <tfigueiro@gitlab.com> Date: Mon, 5 Sep 2022 09:20:03 +1000 Subject: [PATCH 039/169] Fix code owners entries for protect -> govern --- .gitlab/CODEOWNERS | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index b25977cf5d2049..56f91732c91b93 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -151,7 +151,7 @@ Dangerfile @gl-quality/eng-prod /lib/gitlab/ci/templates/ @gitlab-org/maintainers/cicd-templates /lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah /lib/gitlab/ci/templates/Security/ @gonzoyumo @twoodham @sethgitlab @thiagocsf -/lib/gitlab/ci/templates/Security/Container-Scanning.*.yml @gitlab-org/protect/container-security-backend +/lib/gitlab/ci/templates/Security/Container-Scanning.*.yml @gitlab-org/govern/security-policies-backend ^[Project Alias] /ee/app/models/project_alias.rb @patrickbajao @@ -183,23 +183,23 @@ Dangerfile @gl-quality/eng-prod /ee/app/services/app_sec/dast/ @gitlab-org/secure/dynamic-analysis-be ^[Container Security] -/ee/app/views/projects/threat_monitoring/** @gitlab-org/protect/container-security-frontend -/ee/app/views/projects/security/policies/** @gitlab-org/protect/container-security-frontend -/ee/spec/views/projects/security/policies/** @gitlab-org/protect/container-security-frontend -/ee/app/assets/javascripts/pages/projects/threat_monitoring/** @gitlab-org/protect/container-security-frontend -/ee/app/assets/javascripts/threat_monitoring/** @gitlab-org/protect/container-security-frontend -/ee/spec/frontend/threat_monitoring/** @gitlab-org/protect/container-security-frontend +/ee/app/views/projects/threat_monitoring/** @gitlab-org/govern/security-policies-frontend +/ee/app/views/projects/security/policies/** @gitlab-org/govern/security-policies-frontend +/ee/spec/views/projects/security/policies/** @gitlab-org/govern/security-policies-frontend +/ee/app/assets/javascripts/pages/projects/threat_monitoring/** @gitlab-org/govern/security-policies-frontend +/ee/app/assets/javascripts/threat_monitoring/** @gitlab-org/govern/security-policies-frontend +/ee/spec/frontend/threat_monitoring/** @gitlab-org/govern/security-policies-frontend -/ee/app/controllers/projects/threat_monitoring_controller.rb @gitlab-org/protect/container-security-backend -/ee/spec/controllers/projects/threat_monitoring_controller_spec.rb @gitlab-org/protect/container-security-backend -/ee/app/controllers/projects/security/policies_controller.rb @gitlab-org/protect/container-security-backend -/ee/spec/requests/projects/security/policies_controller_spec.rb @gitlab-org/protect/container-security-backend -/ee/app/models/security/orchestration_policy_configuration.rb @gitlab-org/protect/container-security-backend -/ee/spec/models/security/orchestration_policy_configuration_spec.rb @gitlab-org/protect/container-security-backend -/app/models/clusters/applications/cilium.rb @gitlab-org/protect/container-security-backend -/spec/models/clusters/applications/cilium_spec.rb @gitlab-org/protect/container-security-backend -/ee/app/services/security/orchestration/** @gitlab-org/protect/container-security-backend -/ee/spec/services/security/orchestration/** @gitlab-org/protect/container-security-backend +/ee/app/controllers/projects/threat_monitoring_controller.rb @gitlab-org/govern/security-policies-backend +/ee/spec/controllers/projects/threat_monitoring_controller_spec.rb @gitlab-org/govern/container-security-backend +/ee/app/controllers/projects/security/policies_controller.rb @gitlab-org/govern/security-policies-backend +/ee/spec/requests/projects/security/policies_controller_spec.rb @gitlab-org/govern/security-policies-backend +/ee/app/models/security/orchestration_policy_configuration.rb @gitlab-org/govern/security-policies-backend +/ee/spec/models/security/orchestration_policy_configuration_spec.rb @gitlab-org/govern/security-policies-backend +/app/models/clusters/applications/cilium.rb @gitlab-org/govern/security-policies-backend +/spec/models/clusters/applications/cilium_spec.rb @gitlab-org/govern/security-policies-backend +/ee/app/services/security/orchestration/** @gitlab-org/govern/security-policies-backend +/ee/spec/services/security/orchestration/** @gitlab-org/govern/security-policies-backend ^[Code Owners] /ee/lib/gitlab/code_owners.rb @reprazent @kerrizor @garyh -- GitLab From 8fd5d445fe44e9039c722ffc9724a19cdadb8bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thiago=20Figueir=C3=B3?= <tfigueiro@gitlab.com> Date: Mon, 5 Sep 2022 09:58:54 +1000 Subject: [PATCH 040/169] Fix code owners entries for threat insights -> govern --- .gitlab/CODEOWNERS | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index b25977cf5d2049..df094595a55a68 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -160,18 +160,18 @@ Dangerfile @gl-quality/eng-prod # Secure & Threat Management ownership delineation # https://about.gitlab.com/handbook/engineering/development/threat-management/delineate-secure-threat-management.html#technical-boundaries ^[Threat Insights] -/app/finders/security/ @gitlab-org/secure/threat-insights-backend-team -/app/models/vulnerability.rb @gitlab-org/secure/threat-insights-backend-team -/ee/app/finders/security/ @gitlab-org/secure/threat-insights-backend-team -/ee/app/models/security/ @gitlab-org/secure/threat-insights-backend-team -/ee/app/models/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team -/ee/app/policies/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team -/ee/app/policies/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team -/ee/app/presenters/projects/security/ @gitlab-org/secure/threat-insights-backend-team -/ee/lib/api/vulnerabilit*.rb @gitlab-org/secure/threat-insights-backend-team -/ee/spec/policies/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team -/ee/spec/policies/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team -/ee/spec/presenters/projects/security/ @gitlab-org/secure/threat-insights-backend-team +/app/finders/security/ @gitlab-org/govern/threat-insights-backend-team +/app/models/vulnerability.rb @gitlab-org/govern/threat-insights-backend-team +/ee/app/finders/security/ @gitlab-org/govern/threat-insights-backend-team +/ee/app/models/security/ @gitlab-org/govern/threat-insights-backend-team +/ee/app/models/vulnerabilities/ @gitlab-org/govern/threat-insights-backend-team +/ee/app/policies/vulnerabilities/ @gitlab-org/govern/threat-insights-backend-team +/ee/app/policies/vulnerability*.rb @gitlab-org/govern/threat-insights-backend-team +/ee/app/presenters/projects/security/ @gitlab-org/govern/threat-insights-backend-team +/ee/lib/api/vulnerabilit*.rb @gitlab-org/govern/threat-insights-backend-team +/ee/spec/policies/vulnerabilities/ @gitlab-org/govern/threat-insights-backend-team +/ee/spec/policies/vulnerability*.rb @gitlab-org/govern/threat-insights-backend-team +/ee/spec/presenters/projects/security/ @gitlab-org/govern/threat-insights-backend-team ^[Secure] /ee/lib/gitlab/ci/parsers/license_compliance/ @gitlab-org/secure/composition-analysis-be -- GitLab From d5c2548002ad6c409f471036669cc48f0b51523a Mon Sep 17 00:00:00 2001 From: Justin Ho <hduong@gitlab.com> Date: Sun, 4 Sep 2022 22:49:34 +0700 Subject: [PATCH 041/169] Fix Oauth event bubbling when user signs in When user is signed in, we send the `sign-in-oauth` event from the SignInPage. However, the rendering of this component depends on the `userSignedIn` condition which causes the event to not bubble properly. --- .../subscriptions/components/app.vue | 5 +++-- .../jira_connect/subscriptions/store/state.js | 8 +++++-- .../subscriptions/components/app_spec.js | 21 ++++++++++--------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue index 66aea60c5b524d..fa2a5909cf75af 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue @@ -84,6 +84,7 @@ export default { */ fetchSubscriptionsOauth() { if (!this.isOauthEnabled) return; + if (!this.userSignedIn) return; this.fetchSubscriptions(this.subscriptionsPath); }, @@ -146,12 +147,12 @@ export default { <div class="gl-layout-w-limited gl-mx-auto gl-px-5 gl-mb-7"> <sign-in-page - v-if="!userSignedIn" + v-show="!userSignedIn" :has-subscriptions="hasSubscriptions" @sign-in-oauth="onSignInOauth" @error="onSignInError" /> - <subscriptions-page v-else :has-subscriptions="hasSubscriptions" /> + <subscriptions-page v-if="userSignedIn" :has-subscriptions="hasSubscriptions" /> </div> </div> </main> diff --git a/app/assets/javascripts/jira_connect/subscriptions/store/state.js b/app/assets/javascripts/jira_connect/subscriptions/store/state.js index 03a83f18b4c7e4..82a8517b51188b 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/store/state.js +++ b/app/assets/javascripts/jira_connect/subscriptions/store/state.js @@ -1,4 +1,8 @@ -export default function createState({ subscriptions = [], subscriptionsLoading = false } = {}) { +export default function createState({ + subscriptions = [], + subscriptionsLoading = false, + currentUser = null, +} = {}) { return { alert: undefined, @@ -9,7 +13,7 @@ export default function createState({ subscriptions = [], subscriptionsLoading = addSubscriptionLoading: false, addSubscriptionError: false, - currentUser: null, + currentUser, currentUserError: null, accessToken: null, diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js index 9894141be5af92..369ddda8dbe20c 100644 --- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js @@ -31,8 +31,8 @@ describe('JiraConnectApp', () => { const findUserLink = () => wrapper.findComponent(UserLink); const findBrowserSupportAlert = () => wrapper.findComponent(BrowserSupportAlert); - const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => { - store = createStore({ subscriptions: [mockSubscription] }); + const createComponent = ({ provide, mountFn = shallowMountExtended, initialState = {} } = {}) => { + store = createStore({ ...initialState, subscriptions: [mockSubscription] }); jest.spyOn(store, 'dispatch').mockImplementation(); wrapper = mountFn(JiraConnectApp, { @@ -60,7 +60,7 @@ describe('JiraConnectApp', () => { }); it(`${shouldRenderSignInPage ? 'renders' : 'does not render'} sign in page`, () => { - expect(findSignInPage().exists()).toBe(shouldRenderSignInPage); + expect(findSignInPage().isVisible()).toBe(shouldRenderSignInPage); if (shouldRenderSignInPage) { expect(findSignInPage().props('hasSubscriptions')).toBe(true); } @@ -133,7 +133,7 @@ describe('JiraConnectApp', () => { }); it('renders link when `linkUrl` is set', async () => { - createComponent({ mountFn: mountExtended }); + createComponent({ provide: { usersPath: '' }, mountFn: mountExtended }); store.commit(SET_ALERT, { message: __('test message %{linkStart}test link%{linkEnd}'), @@ -211,21 +211,22 @@ describe('JiraConnectApp', () => { describe('when `jiraConnectOauth` feature flag is enabled', () => { const mockSubscriptionsPath = '/mockSubscriptionsPath'; - beforeEach(() => { + beforeEach(async () => { jest.spyOn(api, 'fetchSubscriptions').mockResolvedValue({ data: { subscriptions: [] } }); + jest.spyOn(AccessorUtilities, 'canUseCrypto').mockReturnValue(true); createComponent({ + initialState: { + currentUser: { name: 'root' }, + }, provide: { glFeatures: { jiraConnectOauth: true }, subscriptionsPath: mockSubscriptionsPath, }, }); - }); - describe('when component mounts', () => { - it('dispatches `fetchSubscriptions` action', async () => { - expect(store.dispatch).toHaveBeenCalledWith('fetchSubscriptions', mockSubscriptionsPath); - }); + findSignInPage().vm.$emit('sign-in-oauth'); + await nextTick(); }); describe('when oauth button emits `sign-in-oauth` event', () => { -- GitLab From 8c52f0a86c7640c351f42cf4f36b435457defe13 Mon Sep 17 00:00:00 2001 From: Justin Ho <hduong@gitlab.com> Date: Sun, 4 Sep 2022 23:05:07 +0700 Subject: [PATCH 042/169] Add logic to persist / retrieve baseURL When we use the multiversion select to choose which version of GitLab we want to use (.com or self-hosted), we need to reload the page in order to update the Content Security Policy from the backend to include the new URL of the self-hosted instance. Thus we need to store the baseURL to localStorage on initial save, reload the page then retrieve it. --- .../jira_connect/subscriptions/constants.js | 1 + .../jira_connect/subscriptions/utils.js | 41 ++++++++++++++++--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js index 8faafb1b0d0085..2d33ded5345ad9 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/constants.js +++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js @@ -3,6 +3,7 @@ import { helpPagePath } from '~/helpers/help_page_helper'; export const DEFAULT_GROUPS_PER_PAGE = 10; export const ALERT_LOCALSTORAGE_KEY = 'gitlab_alert'; +export const BASE_URL_LOCALSTORAGE_KEY = 'gitlab_base_url'; export const MINIMUM_SEARCH_TERM_LENGTH = 3; export const ADD_NAMESPACE_MODAL_ID = 'add-namespace-modal'; diff --git a/app/assets/javascripts/jira_connect/subscriptions/utils.js b/app/assets/javascripts/jira_connect/subscriptions/utils.js index b2d03a1fbba7b1..dd845d21d873ff 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/utils.js +++ b/app/assets/javascripts/jira_connect/subscriptions/utils.js @@ -1,32 +1,45 @@ import AccessorUtilities from '~/lib/utils/accessor'; import { objectToQuery } from '~/lib/utils/url_utility'; -import { ALERT_LOCALSTORAGE_KEY } from './constants'; +import { ALERT_LOCALSTORAGE_KEY, BASE_URL_LOCALSTORAGE_KEY } from './constants'; const isFunction = (fn) => typeof fn === 'function'; +const canUseLocalStorage = () => AccessorUtilities.canUseLocalStorage(); + +const persistToStorage = (key, payload) => { + localStorage.setItem(key, payload); +}; + +const retrieveFromStorage = (key) => { + return localStorage.getItem(key); +}; + +const removeFromStorage = (key) => { + localStorage.removeItem(key); +}; /** * Persist alert data to localStorage. */ export const persistAlert = ({ title, message, linkUrl, variant } = {}) => { - if (!AccessorUtilities.canUseLocalStorage()) { + if (!canUseLocalStorage()) { return; } const payload = JSON.stringify({ title, message, linkUrl, variant }); - localStorage.setItem(ALERT_LOCALSTORAGE_KEY, payload); + persistToStorage(ALERT_LOCALSTORAGE_KEY, payload); }; /** * Return alert data from localStorage. */ export const retrieveAlert = () => { - if (!AccessorUtilities.canUseLocalStorage()) { + if (!canUseLocalStorage()) { return null; } - const initialAlertJSON = localStorage.getItem(ALERT_LOCALSTORAGE_KEY); + const initialAlertJSON = retrieveFromStorage(ALERT_LOCALSTORAGE_KEY); // immediately clean up - localStorage.removeItem(ALERT_LOCALSTORAGE_KEY); + removeFromStorage(ALERT_LOCALSTORAGE_KEY); if (!initialAlertJSON) { return null; @@ -35,6 +48,22 @@ export const retrieveAlert = () => { return JSON.parse(initialAlertJSON); }; +export const persistBaseUrl = (baseUrl) => { + if (!canUseLocalStorage()) { + return; + } + + persistToStorage(BASE_URL_LOCALSTORAGE_KEY, baseUrl); +}; + +export const retrieveBaseUrl = () => { + if (!canUseLocalStorage()) { + return null; + } + + return retrieveFromStorage(BASE_URL_LOCALSTORAGE_KEY); +}; + export const getJwt = () => { return new Promise((resolve) => { if (isFunction(AP?.context?.getToken)) { -- GitLab From f03c8e88aabd7764c948dee4aaa0bc68973b5d5e Mon Sep 17 00:00:00 2001 From: Justin Ho <hduong@gitlab.com> Date: Sun, 4 Sep 2022 23:17:48 +0700 Subject: [PATCH 043/169] Add basePath setup to OAuth button Also refactor some of the code in multiversion component. --- .../components/sign_in_oauth_button.vue | 24 +++++++++++++++---- .../jira_connect/subscriptions/constants.js | 1 + .../sign_in_gitlab_multiversion/index.vue | 9 ++++--- locale/gitlab.pot | 3 +++ 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue index ad3e70bcb5f2a6..c9cdef0792d835 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue @@ -2,8 +2,10 @@ import { mapActions, mapMutations } from 'vuex'; import { GlButton } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; +import { sprintf } from '~/locale'; import { I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, + I18N_CUSTOM_SIGN_IN_BUTTON_TEXT, OAUTH_WINDOW_OPTIONS, PKCE_CODE_CHALLENGE_DIGEST_ALGORITHM, } from '~/jira_connect/subscriptions/constants'; @@ -17,14 +19,29 @@ export default { GlButton, }, inject: ['oauthMetadata'], + props: { + gitlabBasePath: { + type: String, + required: false, + default: undefined, + }, + }, data() { return { - token: null, loading: false, codeVerifier: null, canUseCrypto: AccessorUtilities.canUseCrypto(), }; }, + computed: { + buttonText() { + if (!this.gitlabBasePath) { + return I18N_DEFAULT_SIGN_IN_BUTTON_TEXT; + } + + return sprintf(I18N_CUSTOM_SIGN_IN_BUTTON_TEXT, { url: this.gitlabBasePath }); + }, + }, created() { window.addEventListener('message', this.handleWindowMessage); }, @@ -105,9 +122,6 @@ export default { return data.access_token; }, }, - i18n: { - defaultButtonText: I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, - }, }; </script> <template> @@ -119,7 +133,7 @@ export default { @click="startOAuthFlow" > <slot> - {{ $options.i18n.defaultButtonText }} + {{ buttonText }} </slot> </gl-button> </template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js index 2d33ded5345ad9..d30622a367274c 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/constants.js +++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js @@ -9,6 +9,7 @@ export const MINIMUM_SEARCH_TERM_LENGTH = 3; export const ADD_NAMESPACE_MODAL_ID = 'add-namespace-modal'; export const I18N_DEFAULT_SIGN_IN_BUTTON_TEXT = s__('Integrations|Sign in to GitLab'); +export const I18N_CUSTOM_SIGN_IN_BUTTON_TEXT = s__('Integrations|Sign in to %{url}'); export const I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE = s__('Integrations|Failed to sign in to GitLab.'); export const I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE = s__( 'Integrations|Failed to load subscriptions.', diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue index 4f5aa4c255cf63..dd8c6a8c6ce089 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue @@ -1,6 +1,7 @@ <script> import { GlButton } from '@gitlab/ui'; import { s__ } from '~/locale'; + import SignInOauthButton from '../../../components/sign_in_oauth_button.vue'; import VersionSelectForm from './version_select_form.vue'; @@ -17,11 +18,8 @@ export default { }; }, computed: { - hasSelectedVersion() { - return this.gitlabBasePath !== null; - }, subtitle() { - return this.hasSelectedVersion + return this.gitlabBasePath ? this.$options.i18n.signInSubtitle : this.$options.i18n.versionSelectSubtitle; }, @@ -53,11 +51,12 @@ export default { <p data-testid="subtitle">{{ subtitle }}</p> </div> - <version-select-form v-if="!hasSelectedVersion" class="gl-mt-7" @submit="onVersionSelect" /> + <version-select-form v-if="!gitlabBasePath" class="gl-mt-7" @submit="onVersionSelect" /> <div v-else class="gl-text-center"> <sign-in-oauth-button class="gl-mb-5" + :gitlab-base-path="gitlabBasePath" @sign-in="$emit('sign-in-oauth', $event)" @error="onSignInError" /> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bcfc87fa07845f..65eb29710677d6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -21323,6 +21323,9 @@ msgstr "" msgid "Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}" msgstr "" +msgid "Integrations|Sign in to %{url}" +msgstr "" + msgid "Integrations|Sign in to GitLab" msgstr "" -- GitLab From 98608106eb196c572bea536d94d356e8e76bbbc9 Mon Sep 17 00:00:00 2001 From: Justin Ho <hduong@gitlab.com> Date: Sun, 4 Sep 2022 23:28:10 +0700 Subject: [PATCH 044/169] Extract GitLab.com base path to constant --- app/assets/javascripts/jira_connect/subscriptions/constants.js | 2 ++ .../sign_in_gitlab_multiversion/version_select_form.vue | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js index d30622a367274c..2a7d96f24eb24b 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/constants.js +++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js @@ -28,6 +28,8 @@ export const I18N_ADD_SUBSCRIPTIONS_ERROR_MESSAGE = s__( 'Integrations|Failed to link namespace. Please try again.', ); +export const GITLAB_COM_BASE_PATH = 'https://gitlab.com'; + const OAUTH_WINDOW_SIZE = 800; export const OAUTH_WINDOW_OPTIONS = [ 'resizable=yes', diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue index 0fa745ed7e3c3e..9b0bc4765372bf 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue @@ -9,13 +9,14 @@ import { } from '@gitlab/ui'; import { __, s__ } from '~/locale'; +import { GITLAB_COM_BASE_PATH } from '~/jira_connect/subscriptions/constants'; + const RADIO_OPTIONS = { saas: 'saas', selfManaged: 'selfManaged', }; const DEFAULT_RADIO_OPTION = RADIO_OPTIONS.saas; -const GITLAB_COM_BASE_PATH = 'https://gitlab.com'; export default { name: 'VersionSelectForm', -- GitLab From 7cfadd8beb494b9c38a89b35213626d9d093eaf0 Mon Sep 17 00:00:00 2001 From: Justin Ho <hduong@gitlab.com> Date: Mon, 5 Sep 2022 09:19:41 +0700 Subject: [PATCH 045/169] Add more specs to cover existing changes --- .../components/sign_in_oauth_button.vue | 2 +- .../components/sign_in_oauth_button_spec.js | 19 +++++++++++++++++++ .../sign_in_gitlab_multiversion/index_spec.js | 7 +++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue index c9cdef0792d835..d2528e61f21946 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue @@ -73,7 +73,7 @@ export default { window.open( oauthAuthorizeURLWithChallenge, - this.$options.i18n.defaultButtonText, + I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, OAUTH_WINDOW_OPTIONS, ); }, diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js index 383ed2225cd467..94e5c5b6b9655e 100644 --- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js @@ -2,9 +2,12 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; +import { sprintf } from '~/locale'; + import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue'; import { I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, + I18N_CUSTOM_SIGN_IN_BUTTON_TEXT, OAUTH_WINDOW_OPTIONS, } from '~/jira_connect/subscriptions/constants'; import axios from '~/lib/utils/axios_utils'; @@ -68,6 +71,22 @@ describe('SignInOauthButton', () => { expect(findButton().props('category')).toBe('primary'); }); + describe('when `gitlabBasePath` is passed', () => { + const mockBasePath = 'gitlab.mycompany.com'; + + it('uses custom text for button', () => { + createComponent({ + props: { + gitlabBasePath: mockBasePath, + }, + }); + + expect(findButton().text()).toBe( + sprintf(I18N_CUSTOM_SIGN_IN_BUTTON_TEXT, { url: mockBasePath }), + ); + }); + }); + it.each` scenario | cryptoAvailable ${'when crypto API is available'} | ${true} diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js index f4be8bf121da2e..48f57194363ac2 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js @@ -8,6 +8,8 @@ import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_o describe('SignInGitlabMultiversion', () => { let wrapper; + const mockBasePath = 'gitlab.mycompany.com'; + const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm); const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton); const findSubtitle = () => wrapper.findByTestId('subtitle'); @@ -32,7 +34,7 @@ describe('SignInGitlabMultiversion', () => { it('hides the version select form and shows the sign in button', async () => { createComponent(); - findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com'); + findVersionSelectForm().vm.$emit('submit', mockBasePath); await nextTick(); expect(findVersionSelectForm().exists()).toBe(false); @@ -46,13 +48,14 @@ describe('SignInGitlabMultiversion', () => { beforeEach(async () => { createComponent(); - findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com'); + findVersionSelectForm().vm.$emit('submit', mockBasePath); await nextTick(); }); describe('sign in button', () => { it('renders sign in button', () => { expect(findSignInOauthButton().exists()).toBe(true); + expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath); }); describe('when button emits `sign-in` event', () => { -- GitLab From 150cc78d945e12d2594522e83e02bc0cb1616a81 Mon Sep 17 00:00:00 2001 From: Evan Read <eread@gitlab.com> Date: Mon, 5 Sep 2022 15:29:13 +1000 Subject: [PATCH 046/169] Fix some invalid Markdown that causes some Nanoc build warnings --- doc/development/code_review.md | 2 +- doc/user/permissions.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 35897012b60ac2..79146e0b8bc0dd 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -147,7 +147,7 @@ with [domain expertise](#domain-experts). | `~workhorse` changes | [Workhorse maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_workhorse). | | `~frontend` changes (*1*) | [Frontend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_frontend). | | `~UX` user-facing changes (*3*) | [Product Designer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_reviewers_UX). Refer to the [design and user interface guidelines](contributing/design.md) for details. | -| Adding a new JavaScript library (*1*) | <ul><li>[Frontend foundations member](https://about.gitlab.com/direction/ecosystem/foundations/) if the library significantly increases the [bundle size](https://gitlab.com/gitlab-org/frontend/playground/webpack-memory-metrics/-/blob/master/doc/report.md)</li><li>A [legal department member](https://about.gitlab.com/handbook/legal/) if the license used by the new library hasn't been approved for use in GitLab</li></ul> More information about license compatibility can be found in our [GitLab Licensing and Compatibility documentation](licensing.md). | +| Adding a new JavaScript library (*1*) | - [Frontend foundations member](https://about.gitlab.com/direction/ecosystem/foundations/) if the library significantly increases the [bundle size](https://gitlab.com/gitlab-org/frontend/playground/webpack-memory-metrics/-/blob/master/doc/report.md).<br/>- A [legal department member](https://about.gitlab.com/handbook/legal/) if the license used by the new library hasn't been approved for use in GitLab.<br/><br/>More information about license compatibility can be found in our [GitLab Licensing and Compatibility documentation](licensing.md). | | A new dependency or a file system change | [Distribution team member](https://about.gitlab.com/company/team/). See how to work with the [Distribution team](https://about.gitlab.com/handbook/engineering/development/enablement/systems/distribution/#how-to-work-with-distribution) for more details. | | `~documentation` changes | [Technical writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments) based on assignments in the appropriate [DevOps stage group](https://about.gitlab.com/handbook/product/categories/#devops-stages). | | Changes to development guidelines | Follow the [review process](development_processes.md#development-guidelines-review) and get the approvals accordingly. | diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 79d87cddb47a5e..a14a3843d4f235 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -95,7 +95,7 @@ The following table lists project permissions available for each role: | [Issues](project/issues/index.md):<br>View [Design Management](project/issues/design_management.md) pages | ✓ | ✓ | ✓ | ✓ | ✓ | | [Issues](project/issues/index.md):<br>View [related issues](project/issues/related_issues.md) | ✓ | ✓ | ✓ | ✓ | ✓ | | [Issues](project/issues/index.md):<br>Set [weight](project/issues/issue_weight.md) | ✓ (*15*) | ✓ | ✓ | ✓ | ✓ | -| [Issues]](project/issues/index.md):<br>Set [parent epic](group/epics/manage_epics.md#add-an-existing-issue-to-an-epic) | | ✓ | ✓ | ✓ | ✓ | +| [Issues](project/issues/index.md):<br>Set [parent epic](group/epics/manage_epics.md#add-an-existing-issue-to-an-epic) | | ✓ | ✓ | ✓ | ✓ | | [Issues](project/issues/index.md):<br>View [confidential issues](project/issues/confidential_issues.md) | (*2*) | ✓ | ✓ | ✓ | ✓ | | [Issues](project/issues/index.md):<br>Close / reopen (*19*) | | ✓ | ✓ | ✓ | ✓ | | [Issues](project/issues/index.md):<br>Lock threads | | ✓ | ✓ | ✓ | ✓ | -- GitLab From c9329e0eef2e3dec9940fd7a2081254dbb627161 Mon Sep 17 00:00:00 2001 From: Dheeraj Joshi <djoshi@gitlab.com> Date: Fri, 2 Sep 2022 17:43:34 +0530 Subject: [PATCH 047/169] Put DAST Basic-auth option behind feature flag This moves the recently added basic auth feature to be rolled out with `dastApiScanner` feature flag. Changelog: removed EE: true --- doc/user/application_security/dast/index.md | 5 +---- .../components/dast_site_profile_form.vue | 12 +++++++++--- .../components/dast_site_profile_form_spec.js | 6 +++++- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 760ddb964308b1..b8558fcedf2078 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -1157,16 +1157,13 @@ A site profile contains: - **Target URL**: The URL that DAST runs against. - **Excluded URLs**: A comma-separated list of URLs to exclude from the scan. - **Request headers**: A comma-separated list of HTTP request headers, including names and values. These headers are added to every request made by DAST. -- **Authentication (for website)**: +- **Authentication**: - **Authenticated URL**: The URL of the page containing the sign-in HTML form on the target website. The username and password are submitted with the login form to create an authenticated scan. - **Username**: The username used to authenticate to the website. - **Password**: The password used to authenticate to the website. - **Username form field**: The name of username field at the sign-in HTML form. - **Password form field**: The name of password field at the sign-in HTML form. - **Submit form field**: The `id` or `name` of the element that when clicked submits the sign-in HTML form. -- **Authentication (for API scan)**: - - **Username**: The username used to authenticate to the API. - - **Password**: The password used to authenticate to the API. When an API site type is selected, a [host override](#host-override) is used to ensure the API being scanned is on the same host as the target. This is done to reduce the risk of running an active scan against the wrong API. diff --git a/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form.vue b/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form.vue index fd556a7f84c80e..b95510ae4f5a8e 100644 --- a/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form.vue +++ b/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form.vue @@ -160,14 +160,17 @@ export default { isTargetAPI() { return this.form.fields.targetType.value === TARGET_TYPES.API.value; }, - shouldRenderScanMethod() { + isAPISecurityEnabled() { return this.glFeatures.dastApiScanner && this.isTargetAPI; }, + shouldRenderScanMethod() { + return this.isAPISecurityEnabled; + }, selectedScanMethod() { return SCAN_METHODS[this.form.fields.scanMethod.value]; }, isAuthEnabled() { - return this.authSection.fields.enabled; + return this.authSection.fields.enabled && (!this.isTargetAPI || this.isAPISecurityEnabled); }, isSubmitBlocked() { return !this.form.state || (this.isAuthEnabled && !this.authSection.state); @@ -188,7 +191,9 @@ export default { profileName, targetUrl, targetType, - auth: this.serializedAuthFields, + ...((!this.isTargetAPI || this.isAPISecurityEnabled) && { + auth: this.serializedAuthFields, + }), ...(excludedUrls && { excludedUrls: this.parsedExcludedUrls, }), @@ -386,6 +391,7 @@ export default { </gl-form-group> <dast-site-auth-section + v-if="!isTargetAPI || isAPISecurityEnabled" v-model="authSection" :is-target-api="isTargetAPI" :disabled="isPolicyProfile" diff --git a/ee/spec/frontend/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form_spec.js b/ee/spec/frontend/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form_spec.js index 9a5cc5ee2fb1d2..94e31dfcf4d228 100644 --- a/ee/spec/frontend/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form_spec.js +++ b/ee/spec/frontend/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form_spec.js @@ -351,7 +351,7 @@ describe('DastSiteProfileForm', () => { beforeEach(() => { createShallowComponent({ propsData: { - profile: policySiteProfiles[0], + profile: { ...policySiteProfiles[0], targetType: TARGET_TYPES.API.value }, }, provide: { glFeatures: { dastApiScanner: false } }, }); @@ -361,6 +361,10 @@ describe('DastSiteProfileForm', () => { expect(findScanMethodInput().exists()).toBe(false); expect(scanFilePathInput().exists()).toBe(false); }); + + it('should not show authentication section', async () => { + expect(findAuthSection().exists()).toBe(false); + }); }); describe('when profile is used in yaml config', () => { -- GitLab From 302c4b5f0fe7fdb592ea11672dce00819eedf534 Mon Sep 17 00:00:00 2001 From: Dheeraj Joshi <djoshi@gitlab.com> Date: Mon, 5 Sep 2022 09:44:08 +0530 Subject: [PATCH 048/169] Address review suggestion --- .../components/dast_site_profile_form.vue | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form.vue b/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form.vue index b95510ae4f5a8e..54684ccb9b308b 100644 --- a/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form.vue +++ b/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_site_profiles/components/dast_site_profile_form.vue @@ -163,6 +163,9 @@ export default { isAPISecurityEnabled() { return this.glFeatures.dastApiScanner && this.isTargetAPI; }, + isAuthFieldSupport() { + return !this.isTargetAPI || this.isAPISecurityEnabled; + }, shouldRenderScanMethod() { return this.isAPISecurityEnabled; }, @@ -170,7 +173,7 @@ export default { return SCAN_METHODS[this.form.fields.scanMethod.value]; }, isAuthEnabled() { - return this.authSection.fields.enabled && (!this.isTargetAPI || this.isAPISecurityEnabled); + return this.isAuthFieldSupport && this.authSection.fields.enabled; }, isSubmitBlocked() { return !this.form.state || (this.isAuthEnabled && !this.authSection.state); @@ -191,7 +194,7 @@ export default { profileName, targetUrl, targetType, - ...((!this.isTargetAPI || this.isAPISecurityEnabled) && { + ...(this.isAuthFieldSupport && { auth: this.serializedAuthFields, }), ...(excludedUrls && { @@ -391,7 +394,7 @@ export default { </gl-form-group> <dast-site-auth-section - v-if="!isTargetAPI || isAPISecurityEnabled" + v-if="isAuthFieldSupport" v-model="authSection" :is-target-api="isTargetAPI" :disabled="isPolicyProfile" -- GitLab From a5aa4fc1d104d08624f40dddb73e9cb55cf8a815 Mon Sep 17 00:00:00 2001 From: Adam Hegyi <ahegyi@gitlab.com> Date: Thu, 25 Aug 2022 16:40:07 +0200 Subject: [PATCH 049/169] Expose dates where DORA data is missing This change updates the DORA charts within the Insights feature to expose dates with nil value where metrics were not recorded. Changelog: added EE: true --- .../insights/executors/dora_executor.rb | 44 ++++-- .../gitlab/insights/reducers/dora_reducer.rb | 53 ------- .../insights/executors/dora_executor_spec.rb | 40 +++++- ee/spec/lib/gitlab/insights/loader_spec.rb | 5 +- .../insights/reducers/dora_reducer_spec.rb | 93 ------------ lib/gitlab/analytics/date_filler.rb | 112 +++++++++++++++ spec/lib/gitlab/analytics/date_filler_spec.rb | 136 ++++++++++++++++++ 7 files changed, 320 insertions(+), 163 deletions(-) delete mode 100644 ee/lib/gitlab/insights/reducers/dora_reducer.rb delete mode 100644 ee/spec/lib/gitlab/insights/reducers/dora_reducer_spec.rb create mode 100644 lib/gitlab/analytics/date_filler.rb create mode 100644 spec/lib/gitlab/analytics/date_filler_spec.rb diff --git a/ee/lib/gitlab/insights/executors/dora_executor.rb b/ee/lib/gitlab/insights/executors/dora_executor.rb index d2d461a5fb4834..f7881261e53109 100644 --- a/ee/lib/gitlab/insights/executors/dora_executor.rb +++ b/ee/lib/gitlab/insights/executors/dora_executor.rb @@ -5,6 +5,13 @@ module Insights module Executors class DoraExecutor DoraExecutorError = Class.new(StandardError) + FORMATTERS = { + 'day' => '%d %b %y', + 'month' => '%B %Y' + }.freeze + DEFAULT_VALUES = { + 'deployment_frequency' => 0 + }.freeze def initialize(query_params:, current_user:, insights_entity:, projects: {}, chart_type:) @query_params = query_params @@ -23,14 +30,36 @@ def execute raise(DoraExecutorError, result[:message]) if result[:status] == :error - reduced_data = Gitlab::Insights::Reducers::DoraReducer.reduce(result[:data], period: group_by, metric: metric) - serializer.present(reduced_data) + serializer.present(format_data(result[:data])) end private attr_reader :query_params, :current_user, :insights_entity, :projects, :chart_type + def format_data(data) + input = data.each_with_object({}) { |item, hash| hash[item['date']] = format_value(item['value']) } + + Gitlab::Analytics::DateFiller.new(input, + from: start_date, + to: Date.today, + period: group_by.to_sym, + default_value: DEFAULT_VALUES[metric], + date_formatter: -> (date) { date.strftime(FORMATTERS[group_by]) } + ).fill + end + + def format_value(value) + case metric + when 'lead_time_for_changes', 'time_to_restore_service' + value ? value.fdiv(1.day).round(1) : nil + when 'change_failure_rate' + value ? (value * 100).round(2) : 0 + else + value + end + end + def dora_api_params params = { interval: dora_interval, @@ -62,9 +91,9 @@ def metric def start_date case group_by when 'day' - period_limit.days.ago.to_date + (period_limit - 1).days.ago.to_date when 'month' - period_limit.months.ago.to_date + (period_limit - 1).months.ago.to_date end end @@ -88,12 +117,11 @@ def group_by end def serializer - case chart_type - when 'bar', 'line' - Gitlab::Insights::Serializers::Chartjs::BarSerializer - else + unless %w[bar line].include?(chart_type) raise DoraExecutor::DoraExecutorError, "Unsupported chart type is given: #{chart_type}" end + + Gitlab::Insights::Serializers::Chartjs::BarSerializer end end end diff --git a/ee/lib/gitlab/insights/reducers/dora_reducer.rb b/ee/lib/gitlab/insights/reducers/dora_reducer.rb deleted file mode 100644 index 9791fb24491e06..00000000000000 --- a/ee/lib/gitlab/insights/reducers/dora_reducer.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Insights - module Reducers - class DoraReducer < BaseReducer - def initialize(data, period:, metric:) - @data = data - @period = period - @metric = metric - end - - def reduce - data.each_with_object({}) do |item, hash| - hash[format_date(item['date'])] = format_value(item['value']) - end - end - - private - - attr_reader :data, :period, :metric - - def format_date(date) - Date.parse(date).strftime(period_format) - end - - def period_format - case period - when 'day' - '%d %b %y' - when 'month' - '%B %Y' - else - raise Gitlab::Insights::Executors::DoraExecutor::DoraExecutorError, "Unknown period is given: #{period}" - end - end - - def format_value(value) - case metric - when 'lead_time_for_changes', 'time_to_restore_service' - value ? value.fdiv(1.day).round(1) : nil - when 'deployment_frequency' - value - when 'change_failure_rate' - value ? (value * 100).round(2) : 0 - else - raise Gitlab::Insights::Executors::DoraExecutor::DoraExecutorError, "Unknown metric is given: #{period}" - end - end - end - end - end -end diff --git a/ee/spec/lib/gitlab/insights/executors/dora_executor_spec.rb b/ee/spec/lib/gitlab/insights/executors/dora_executor_spec.rb index 270e99db520126..e9226890eb22c0 100644 --- a/ee/spec/lib/gitlab/insights/executors/dora_executor_spec.rb +++ b/ee/spec/lib/gitlab/insights/executors/dora_executor_spec.rb @@ -43,21 +43,27 @@ create(:dora_daily_metrics, deployment_frequency: 5, + lead_time_for_changes_in_seconds: 100, environment: environment1, date: date1) create(:dora_daily_metrics, deployment_frequency: 20, + lead_time_for_changes_in_seconds: 10000, + incidents_count: 5, environment: environment2, date: date1) create(:dora_daily_metrics, deployment_frequency: 50, + lead_time_for_changes_in_seconds: 20000, + incidents_count: 15, environment: environment1, date: date2) create(:dora_daily_metrics, deployment_frequency: 100, + lead_time_for_changes_in_seconds: 40000, environment: environment3, date: date2) end @@ -84,7 +90,27 @@ let(:insights_entity) { group } it_behaves_like 'serialized_data examples' do - let(:expected_result) { [50, 25] } + let(:expected_result) { [0, 50, 0, 0, 25] } + end + + context 'when requesting the lead_time_for_changes metric' do + before do + query_params[:metric] = 'lead_time_for_changes' + end + + it_behaves_like 'serialized_data examples' do + let(:expected_result) { [nil, 0.2, nil, nil, 0.1] } + end + end + + context 'when requesting the change_failure_rate metric' do + before do + query_params[:metric] = 'change_failure_rate' + end + + it_behaves_like 'serialized_data examples' do + let(:expected_result) { [nil, 30, nil, nil, 20] } + end end context 'when filtering environment tiers' do @@ -93,7 +119,7 @@ end it_behaves_like 'serialized_data examples' do - let(:expected_result) { [150, 25] } + let(:expected_result) { [0, 150, 0, 0, 25] } end end @@ -102,7 +128,7 @@ let(:projects) { { only: [project2.id] } } it_behaves_like 'serialized_data examples' do - let(:expected_result) { [20] } + let(:expected_result) { [0, 0, 0, 0, 20] } end end @@ -110,7 +136,7 @@ let(:projects) { { only: [project2.full_path] } } it_behaves_like 'serialized_data examples' do - let(:expected_result) { [20] } + let(:expected_result) { [0, 0, 0, 0, 20] } end end end @@ -142,7 +168,7 @@ let(:insights_entity) { project1 } it_behaves_like 'serialized_data examples' do - let(:expected_result) { [50, 5] } + let(:expected_result) { [0, 50, 0, 0, 5] } end context 'when filtering projects' do @@ -150,7 +176,7 @@ let(:projects) { { only: [project1.id] } } it_behaves_like 'serialized_data examples' do - let(:expected_result) { [50, 5] } + let(:expected_result) { [0, 50, 0, 0, 5] } end end @@ -159,7 +185,7 @@ # ignores the filter it_behaves_like 'serialized_data examples' do - let(:expected_result) { [50, 5] } + let(:expected_result) { [0, 50, 0, 0, 5] } end end end diff --git a/ee/spec/lib/gitlab/insights/loader_spec.rb b/ee/spec/lib/gitlab/insights/loader_spec.rb index 5efc4e278b25d8..ebf35999d6fe43 100644 --- a/ee/spec/lib/gitlab/insights/loader_spec.rb +++ b/ee/spec/lib/gitlab/insights/loader_spec.rb @@ -73,7 +73,8 @@ let(:data_source_params) do { metric: 'time_to_restore_service', - group_by: 'day' + group_by: 'day', + period_limit: 3 } end @@ -97,7 +98,7 @@ end it 'returns the serialized data' do - expect(serialized_data['datasets'].first['data']).to eq([2]) + expect(serialized_data['datasets'].first['data']).to eq([nil, nil, 2]) end end end diff --git a/ee/spec/lib/gitlab/insights/reducers/dora_reducer_spec.rb b/ee/spec/lib/gitlab/insights/reducers/dora_reducer_spec.rb deleted file mode 100644 index 9eb7550bd9f804..00000000000000 --- a/ee/spec/lib/gitlab/insights/reducers/dora_reducer_spec.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Insights::Reducers::DoraReducer do - context 'when metric=change_failure_rate' do - it 'converts to percentage' do - data = [ - { 'value' => 0.5, 'date' => '2020-01-01' }, - { 'value' => 0.1, 'date' => '2020-01-02' } - ] - - result = described_class - .reduce(data, period: 'day', metric: 'change_failure_rate') - .to_a - - expect(result).to eq({ '01 Jan 20' => 50, '02 Jan 20' => 10 }.to_a) - end - end - - context 'when metric=deployment_frequency' do - it 'uses the value as is' do - data = [ - { 'value' => 100, 'date' => '2020-01-01' }, - { 'value' => 20, 'date' => '2020-02-01' } - ] - - result = described_class - .reduce(data, period: 'month', metric: 'deployment_frequency') - .to_a - - expect(result).to eq({ 'January 2020' => 100, 'February 2020' => 20 }.to_a) - end - end - - context 'when metric=lead_time_for_changes' do - it 'converts from seconds to days' do - data = [ - { 'value' => 86400, 'date' => '2020-01-01' }, - { 'value' => 43200, 'date' => '2020-01-02' }, - { 'value' => nil, 'date' => '2020-01-03' } - ] - - result = described_class - .reduce(data, period: 'day', metric: 'lead_time_for_changes') - .to_a - - expect(result).to match({ '01 Jan 20' => 1, '02 Jan 20' => 0.5, '03 Jan 20' => nil }.to_a) - end - end - - context 'when metric=time_to_restore_service' do - it 'converts from seconds to days' do - data = [ - { 'value' => 86400, 'date' => '2020-01-01' }, - { 'value' => 43200, 'date' => '2020-01-02' }, - { 'value' => nil, 'date' => '2020-01-03' } - ] - - result = described_class - .reduce(data, period: 'day', metric: 'time_to_restore_service') - .to_a - - expect(result).to eq({ '01 Jan 20' => 1, '02 Jan 20' => 0.5, '03 Jan 20' => nil }.to_a) - end - end - - context 'when unknown metric is given' do - it 'raises error' do - data = [ - { 'value' => 86400, 'date' => '2020-01-01' }, - { 'value' => 43200, 'date' => '2020-01-02' } - ] - - expect do - described_class.reduce(data, period: 'day', metric: 'unknown') - end.to raise_error /Unknown metric is given/ - end - end - - context 'when unknown period is given' do - it 'raises error' do - data = [ - { 'value' => 86400, 'date' => '2020-01-01' }, - { 'value' => 43200, 'date' => '2020-01-02' } - ] - - expect do - described_class.reduce(data, period: 'unknown', metric: 'time_to_restore_service') - end.to raise_error /Unknown period is given/ - end - end -end diff --git a/lib/gitlab/analytics/date_filler.rb b/lib/gitlab/analytics/date_filler.rb new file mode 100644 index 00000000000000..aa3db9f3635c2c --- /dev/null +++ b/lib/gitlab/analytics/date_filler.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + # This class generates a date => value hash without gaps in the data points. + # + # Simple usage: + # + # > # We have the following data for the last 5 day: + # > input = { 3.days.ago.to_date => 10, Date.today => 5 } + # + # > # Format this data, so we can chart the complete date range: + # > Gitlab::Analytics::DateFiller.new(input, from: 4.days.ago, to: Date.today, default_value: 0).fill + # > { + # > Sun, 28 Aug 2022=>0, + # > Mon, 29 Aug 2022=>10, + # > Tue, 30 Aug 2022=>0, + # > Wed, 31 Aug 2022=>0, + # > Thu, 01 Sep 2022=>5 + # > } + # + # Parameters: + # + # **input** + # A Hash containing data for the series or the chart. The key is a Date object + # or an object which can be converted to Date. + # + # **from** + # Start date of the range + # + # **to** + # End date of the range + # + # **period** + # Specifies the period in wich the dates should be generated. Options: + # + # - :day, generate date-value pair for each day in the given period + # - :week, generate date-value pair for each week (beginning of the week date) + # - :month, generate date-value pair for each week (beginning of the month date) + # + # Note: the Date objects in the `input` should follow the same pattern (beginning of ...) + # + # **default_value** + # + # Which value use when the `input` Hash does not contain data for the given day. + # + # **date_formatter** + # + # How to format the dates in the resulting hash. + class DateFiller + DEFAULT_DATE_FORMATTER = -> (date) { date } + PERIOD_STEPS = { + day: 1.day, + week: 1.week, + month: 1.month + }.freeze + + def initialize( + input, + from:, + to:, + period: :day, + default_value: nil, + date_formatter: DEFAULT_DATE_FORMATTER) + @input = input.transform_keys(&:to_date) + @from = from.to_date + @to = to.to_date + @period = period + @default_value = default_value + @date_formatter = date_formatter + end + + def fill + data = {} + + current_date = from + loop do + transformed_date = transform_date(current_date) + break if transformed_date > to + + formatted_date = date_formatter.call(transformed_date) + + value = input.delete(transformed_date) + data[formatted_date] = value.nil? ? default_value : value + + current_date = (current_date + PERIOD_STEPS.fetch(period)).to_date + end + + raise "Input contains values which doesn't fall under the given period!" if input.any? + + data + end + + private + + attr_reader :input, :from, :to, :period, :default_value, :date_formatter + + def transform_date(date) + case period + when :day + date.beginning_of_day.to_date + when :week + date.beginning_of_week.to_date + when :month + date.beginning_of_month.to_date + else + raise "Unknown period given: #{period}" + end + end + end + end +end diff --git a/spec/lib/gitlab/analytics/date_filler_spec.rb b/spec/lib/gitlab/analytics/date_filler_spec.rb new file mode 100644 index 00000000000000..3f547f667f2692 --- /dev/null +++ b/spec/lib/gitlab/analytics/date_filler_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true +require 'fast_spec_helper' + +RSpec.describe Gitlab::Analytics::DateFiller do + let(:default_value) { 0 } + let(:formatter) { Gitlab::Analytics::DateFiller::DEFAULT_DATE_FORMATTER } + + subject(:filler_result) do + described_class.new(data, + from: from, + to: to, + period: period, + default_value: default_value, + date_formatter: formatter).fill.to_a + end + + context 'when unknown period is given' do + it 'raises error' do + input = { 3.days.ago.to_date => 10, Date.today => 5 } + + expect do + described_class.new(input, from: 4.days.ago, to: Date.today, period: :unknown).fill + end.to raise_error(/Unknown period given/) + end + end + + context 'when period=:day' do + let(:from) { Date.new(2021, 5, 25) } + let(:to) { Date.new(2021, 6, 5) } + let(:period) { :day } + + let(:expected_result) do + { + Date.new(2021, 5, 25) => 1, + Date.new(2021, 5, 26) => default_value, + Date.new(2021, 5, 27) => default_value, + Date.new(2021, 5, 28) => default_value, + Date.new(2021, 5, 29) => default_value, + Date.new(2021, 5, 30) => default_value, + Date.new(2021, 5, 31) => default_value, + Date.new(2021, 6, 1) => default_value, + Date.new(2021, 6, 2) => default_value, + Date.new(2021, 6, 3) => 10, + Date.new(2021, 6, 4) => default_value, + Date.new(2021, 6, 5) => default_value + } + end + + let(:data) do + { + Date.new(2021, 6, 3) => 10, # deliberatly not sorted + Date.new(2021, 5, 27) => nil, + Date.new(2021, 5, 25) => 1 + } + end + + it { is_expected.to eq(expected_result.to_a) } + + context 'when a custom default value is given' do + let(:default_value) { 'MISSING' } + + it do + is_expected.to eq(expected_result.to_a) + end + end + + context 'when a custom date formatter is given' do + let(:formatter) { -> (date) { date.to_s } } + + it do + expected_result.transform_keys!(&:to_s) + + is_expected.to eq(expected_result.to_a) + end + end + + context 'when the data contains dates outside of the requested period' do + before do + data[Date.new(2022, 6, 1)] = 5 + end + + it 'raises error' do + expect { filler_result }.to raise_error(/Input contains values which doesn't/) + end + end + end + + context 'when period=:week' do + let(:from) { Date.new(2021, 5, 16) } + let(:to) { Date.new(2021, 6, 7) } + let(:period) { :week } + let(:data) do + { + Date.new(2021, 5, 24) => nil, + Date.new(2021, 6, 7) => 10 + } + end + + let(:expected_result) do + { + Date.new(2021, 5, 10) => 0, + Date.new(2021, 5, 17) => 0, + Date.new(2021, 5, 24) => 0, + Date.new(2021, 5, 31) => 0, + Date.new(2021, 6, 7) => 10 + } + end + + it do + is_expected.to eq(expected_result.to_a) + end + end + + context 'when period=:month' do + let(:from) { Date.new(2021, 5, 1) } + let(:to) { Date.new(2021, 7, 1) } + let(:period) { :month } + let(:data) do + { + Date.new(2021, 5, 1) => 100 + } + end + + let(:expected_result) do + { + Date.new(2021, 5, 1) => 100, + Date.new(2021, 6, 1) => 0, + Date.new(2021, 7, 1) => 0 + } + end + + it do + is_expected.to eq(expected_result.to_a) + end + end +end -- GitLab From 067e69b4c2b0334c53f136cb93959d5851032a95 Mon Sep 17 00:00:00 2001 From: Sofia Vistas <svistas@gitlab.com> Date: Mon, 5 Sep 2022 11:58:48 +0300 Subject: [PATCH 050/169] Quarantine MR creation from fork test --- .../merge_request/merge_merge_request_from_fork_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index d198d79c5fe001..0f65647e1687a1 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -17,7 +17,11 @@ module QA merge_request.fork.remove_via_api! end - it 'can merge feature branch fork to mainline', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347818' do + it 'can merge feature branch fork to mainline', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347818', quarantine: { + only: { subdomain: :production }, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/372258' + } do merge_request.visit! Page::MergeRequest::Show.perform do |merge_request| -- GitLab From 3ed1e8f26991401ec745dae195dd342c3475ceff Mon Sep 17 00:00:00 2001 From: Marc Shaw <mshaw@gitlab.com> Date: Fri, 2 Sep 2022 12:45:15 +0200 Subject: [PATCH 051/169] Invalidate the merge request cache when assignee/reviewer changes MR: gitlab.com/gitlab-org/gitlab/-/merge_requests/96860 Issue: gitlab.com/gitlab-org/gitlab/-/issues/364264 Changelog: fixed --- lib/api/merge_requests.rb | 12 +++++++++++- spec/requests/api/merge_requests_spec.rb | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index a8f58e91067e61..ed94fb75a2ef85 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -212,7 +212,17 @@ def recheck_mergeability_of(merge_requests:) recheck_mergeability_of(merge_requests: merge_requests) unless options[:skip_merge_status_recheck] - present_cached merge_requests, expires_in: 8.hours, cache_context: -> (mr) { "#{current_user&.cache_key}:#{mr.merge_status}" }, **options + present_cached merge_requests, + expires_in: 8.hours, + cache_context: -> (mr) do + [ + current_user&.cache_key, + mr.merge_status, + mr.merge_request_assignees.map(&:cache_key), + mr.merge_request_reviewers.map(&:cache_key) + ].join(":") + end, + **options end desc 'Create a merge request' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 17cd86e73a1793..5a0a6c44b9c9f0 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1022,6 +1022,22 @@ it_behaves_like 'a non-cached MergeRequest api request', 1 end + context 'when the assignees change' do + before do + merge_request.assignees << create(:user) + end + + it_behaves_like 'a non-cached MergeRequest api request', 1 + end + + context 'when the reviewers change' do + before do + merge_request.reviewers << create(:user) + end + + it_behaves_like 'a non-cached MergeRequest api request', 1 + end + context 'when another user requests' do before do sign_in(user2) -- GitLab From ef9d352391252bb674ce71ebe4469f77a6e4f411 Mon Sep 17 00:00:00 2001 From: Vasilii Iakliushin <viakliushin@gitlab.com> Date: Mon, 5 Sep 2022 14:17:59 +0200 Subject: [PATCH 052/169] Minor refactoring for Projects::DestroyService Contributes to https://gitlab.com/gitlab-org/gitlab/-/issues/372526 Apply suggestions after the code review --- app/services/projects/destroy_service.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 2a58ee7a868c65..f1525ed9763982 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -160,10 +160,9 @@ def log_destroy_event # # rubocop: disable CodeReuse/ActiveRecord def destroy_mr_diff_relations! - mr_batch_size = 100 delete_batch_size = 1000 - project.merge_requests.each_batch(column: :iid, of: mr_batch_size) do |relation_ids| + project.merge_requests.each_batch(column: :iid, of: BATCH_SIZE) do |relation_ids| [MergeRequestDiffCommit, MergeRequestDiffFile].each do |model| loop do inner_query = model @@ -184,13 +183,12 @@ def destroy_mr_diff_relations! # rubocop: disable CodeReuse/ActiveRecord def destroy_merge_request_diffs! - mr_batch_size = 100 delete_batch_size = 1000 - project.merge_requests.each_batch(column: :iid, of: mr_batch_size) do |relation_ids| + project.merge_requests.each_batch(column: :iid, of: BATCH_SIZE) do |relation| loop do deleted_rows = MergeRequestDiff - .where(merge_request_id: relation_ids) + .where(merge_request: relation) .limit(delete_batch_size) .delete_all -- GitLab From 016d0e9c76d456e55680756c4bc94f104470b229 Mon Sep 17 00:00:00 2001 From: Roy Zwambag <rzwambag@gitlab.com> Date: Mon, 5 Sep 2022 12:35:34 +0000 Subject: [PATCH 053/169] Freeze the text variable string This fixes Uploads Rewriter spec in Ruby 3 since interpolated strings are not frozen by default anymore. --- spec/lib/gitlab/gfm/uploads_rewriter_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb index 763e6f1b5f443d..a16f96a7d115a9 100644 --- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -19,7 +19,7 @@ end let(:text) do - "Text and #{image_uploader.markdown_link} and #{zip_uploader.markdown_link}" + "Text and #{image_uploader.markdown_link} and #{zip_uploader.markdown_link}".freeze # rubocop:disable Style/RedundantFreeze end def referenced_files(text, project) -- GitLab From dbbd5c2836dba915afab79a155c9f23b16e501e0 Mon Sep 17 00:00:00 2001 From: Rajendra Kadam <rkadam@gitlab.com> Date: Mon, 5 Sep 2022 13:11:25 +0000 Subject: [PATCH 054/169] Update timeline event docs --- .../incident_management/incidents.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md index c1a4c1eb93ee07..b12e44398658ad 100644 --- a/doc/operations/incident_management/incidents.md +++ b/doc/operations/incident_management/incidents.md @@ -231,6 +231,10 @@ To view the event timeline of an incident: #### Create a timeline event +You can create a timeline event in many ways in GitLab. + +##### Using the form + Create a timeline event manually using the form. Prerequisites: @@ -247,6 +251,24 @@ To create a timeline event: 1. Complete the required fields. 1. Select **Save** or **Save and add another event**. +##### From a comment on the incident + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344058) in GitLab 15.4. + +Prerequisites: + +- You must have at least the Developer role for the project. + +To create a timeline event from a comment on the incident: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Monitor > Incidents**. +1. Select an incident. +1. Create a comment or choose an existing comment. +1. On the comment you want to add, select **Add comment to incident timeline** (**{clock}**). + +The comment is shown on the incident timeline as a timeline event. + #### Delete a timeline event You can also delete timeline events. -- GitLab From 41baa4cf23239d0ae1643d394864e4c21f87cb6b Mon Sep 17 00:00:00 2001 From: Jan Provaznik <jprovaznik@gitlab.com> Date: Mon, 5 Sep 2022 15:29:30 +0200 Subject: [PATCH 055/169] Removed forgotten debug line --- ee/app/workers/epics/update_cached_metadata_worker.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/ee/app/workers/epics/update_cached_metadata_worker.rb b/ee/app/workers/epics/update_cached_metadata_worker.rb index 28c661dfc5501c..ac9e9458c7c474 100644 --- a/ee/app/workers/epics/update_cached_metadata_worker.rb +++ b/ee/app/workers/epics/update_cached_metadata_worker.rb @@ -12,7 +12,6 @@ class UpdateCachedMetadataWorker feature_category :portfolio_management def perform(ids) - pp ids return unless Feature.enabled?(:cache_issue_sums) ::Epic.id_in(ids).find_each do |epic| -- GitLab From e9b2c5fb4d3f83e31afe3878f49022ce1ce8350a Mon Sep 17 00:00:00 2001 From: Amy Qualls <aqualls@gitlab.com> Date: Mon, 5 Sep 2022 13:35:17 +0000 Subject: [PATCH 056/169] Alphabetize items in tables Two sets of alphabetical order per table. First, the required parameters in order. Then the optional parameters in order. --- doc/api/merge_requests.md | 442 +++++++++++++++++++------------------- 1 file changed, 219 insertions(+), 223 deletions(-) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 66a9a19e6914de..1df140979ae6ee 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -6,8 +6,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Merge requests API **(FREE)** -> - `reference` was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20354) in GitLab 12.10 in favour of `references`. -> - `reviewer_username` and `reviewer_id` were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. > - `draft` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63473) as a replacement for `work_in_progress` in GitLab 14.0. > - `merge_user` was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/349031) as an eventual replacement for `merged_by` in GitLab 14.7. @@ -40,38 +38,38 @@ GET /merge_requests?search=foo&in=title Parameters: -| Attribute | Type | Required | Description | -| ------------------------------- | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | -| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged`. | -| `order_by` | string | no | Return requests ordered by `created_at`, `title`, or `updated_at` fields. Default is `created_at`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331625) in GitLab 14.8.| -| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc`. | -| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | -| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request. | -| `labels` | string | no | Return merge requests matching a comma-separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. Predefined names are case-insensitive. | -| `with_labels_details` | boolean | no | If `true`, response returns more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) in GitLab 12.7. | -| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) in GitLab 13.0. | -| `created_after` | datetime | no | Return merge requests created on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | -| `created_before` | datetime | no | Return merge requests created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | -| `updated_after` | datetime | no | Return merge requests updated on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | -| `updated_before` | datetime | no | Return merge requests updated on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | -| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`. | -| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. | -| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 12.10. | +| Attribute | Type | Required | Description | +| ------------------------------- | -------------- | -------- | ----------- | +| `approved_by_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`. Maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | +| `approver_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have specified all the users with the given `id` as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | | `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. | -| `approver_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | -| `approved_by_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | -| `reviewer_id` | integer | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given user `id`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_username`. | -| `reviewer_username` | string | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given `username`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. | -| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. | -| `source_branch` | string | no | Return merge requests with the given source branch. | -| `target_branch` | string | no | Return merge requests with the given target branch. | -| `search` | string | no | Search merge requests against their `title` and `description`. | +| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. | +| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. | +| `created_after` | datetime | no | Return merge requests created on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `created_before` | datetime | no | Return merge requests created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `deployed_after` | datetime | no | Return merge requests deployed after the given date/time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `deployed_before` | datetime | no | Return merge requests deployed before the given date/time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `environment` | string | no | Returns merge requests deployed to the given environment. | | `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description`. | -| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* draft merge requests, `no` to return *non-draft* merge requests. | +| `labels` | string | no | Return merge requests matching a comma-separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. Predefined names are case-insensitive. | +| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | +| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. | | `not` | Hash | no | Return merge requests that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `reviewer_id`, `reviewer_username`, `my_reaction_emoji`. | -| `environment` | string | no | Returns merge requests deployed to the given environment. | -| `deployed_before` | datetime | no | Return merge requests deployed before the given date/time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | -| `deployed_after` | datetime | no | Return merge requests deployed after the given date/time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | +| `order_by` | string | no | Return requests ordered by `created_at`, `title`, or `updated_at` fields. Default is `created_at`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331625) in GitLab 14.8.| +| `reviewer_id` | integer | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given user `id`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_username`. | +| `reviewer_username` | string | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given `username`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. | +| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`. | +| `search` | string | no | Search merge requests against their `title` and `description`. | +| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc`. | +| `source_branch` | string | no | Return merge requests with the given source branch. | +| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged`. | +| `target_branch` | string | no | Return merge requests with the given target branch. | +| `updated_after` | datetime | no | Return merge requests updated on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `updated_before` | datetime | no | Return merge requests updated on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request. | +| `with_labels_details` | boolean | no | If `true`, response returns more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. | +| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) in GitLab 13.0. | +| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* draft merge requests, `no` to return *non-draft* merge requests. | ```json [ @@ -241,37 +239,37 @@ are the same. In the case of a merge request from a fork, Parameters: -| Attribute | Type | Required | Description | -| ------------------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `iids[]` | integer array | no | Return the request having the given `iid`. | -| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged`. | -| `order_by` | string | no | Return requests ordered by `created_at`, `title` or `updated_at` fields. Default is `created_at`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331625) in GitLab 14.8. | -| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc`. | -| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | -| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request. | -| `labels` | string | no | Return merge requests matching a comma-separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. Predefined names are case-insensitive. | -| `with_labels_details` | boolean | no | If `true`, response returns more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) in GitLab 12.7. | -| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) in GitLab 13.0. | -| `created_after` | datetime | no | Return merge requests created on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | -| `created_before` | datetime | no | Return merge requests created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | -| `updated_after` | datetime | no | Return merge requests updated on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | -| `updated_before` | datetime | no | Return merge requests updated on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | -| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me`, or `all`. | -| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. | -| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 12.10. | +| Attribute | Type | Required | Description | +| ------------------------------- | -------------- | -------- | ----------- | +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `approved_by_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`, with a maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | +| `approver_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have specified all the users with the given `id` as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | | `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. | -| `approver_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | -| `approved_by_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | +| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. | +| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. | +| `created_after` | datetime | no | Return merge requests created on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `created_before` | datetime | no | Return merge requests created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `environment` | string | no | Returns merge requests deployed to the given environment. | +| `iids[]` | integer array | no | Return the request having the given `iid`. | +| `labels` | string | no | Return merge requests matching a comma-separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. Predefined names are case-insensitive. | +| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | +| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. | +| `not` | Hash | no | Return merge requests that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `reviewer_id`, `reviewer_username`, `my_reaction_emoji`. | +| `order_by` | string | no | Return requests ordered by `created_at`, `title` or `updated_at` fields. Default is `created_at`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331625) in GitLab 14.8. | | `reviewer_id` | integer | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given user `id`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_username`. | | `reviewer_username` | string | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given `username`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. | -| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. | -| `source_branch` | string | no | Return merge requests with the given source branch. | -| `target_branch` | string | no | Return merge requests with the given target branch. | -| `search` | string | no | Search merge requests against their `title` and `description`. | +| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me`, or `all`. | +| `search` | string | no | Search merge requests against their `title` and `description`. | +| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc`. | +| `source_branch` | string | no | Return merge requests with the given source branch. | +| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged`. | +| `target_branch` | string | no | Return merge requests with the given target branch. | +| `updated_after` | datetime | no | Return merge requests updated on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `updated_before` | datetime | no | Return merge requests updated on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request. | | `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* draft merge requests, `no` to return *non-draft* merge requests. | -| `not` | Hash | no | Return merge requests that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `reviewer_id`, `reviewer_username`, `my_reaction_emoji`. | -| `environment` | string | no | Returns merge requests deployed to the given environment. | +| `with_labels_details` | boolean | no | If `true`, response returns more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. | +| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) in GitLab 13.0. | ```json [ @@ -429,36 +427,36 @@ GET /groups/:id/merge_requests?my_reaction_emoji=star Parameters: -| Attribute | Type | Required | Description | -| ------------------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | -| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged`. | -| `order_by` | string | no | Return merge requests ordered by `created_at`, `title` or `updated_at` fields. Default is `created_at`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331625) in GitLab 14.8. | -| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc`. | -| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | -| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request. | -| `labels` | string | no | Return merge requests matching a comma-separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. Predefined names are case-insensitive. | -| `with_labels_details` | boolean | no | If `true`, response returns more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) in GitLab 12.7. | -| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) in GitLab 13.0. | +| Attribute | Type | Required | Description | +| ------------------------------- | -------------- | -------- | ----------- | +| `id` | integer or string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `approved_by_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`, with a maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | +| `approved_by_usernames` **(PREMIUM)** | string array | no | Returns merge requests which have been approved by all the users with the given `username`, with a maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | +| `approver_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | +| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. | +| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. | +| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. | | `created_after` | datetime | no | Return merge requests created on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | | `created_before` | datetime | no | Return merge requests created on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | -| `updated_after` | datetime | no | Return merge requests updated on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | -| `updated_before` | datetime | no | Return merge requests updated on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | -| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. | -| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. | -| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 12.10. | -| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. | -| `approver_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. | -| `approved_by_ids` **(PREMIUM)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | -| `approved_by_usernames` **(PREMIUM)** | string array | no | Returns merge requests which have been approved by all the users with the given `username`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. | -| `reviewer_id` | integer | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given user `id`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_username`. | -| `reviewer_username` | string | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given `username`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. | +| `labels` | string | no | Return merge requests matching a comma-separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. Predefined names are case-insensitive. | +| `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. | -| `source_branch` | string | no | Return merge requests with the given source branch. | -| `target_branch` | string | no | Return merge requests with the given target branch. | -| `search` | string | no | Search merge requests against their `title` and `description`. | -| `non_archived` | boolean | no | Return merge requests from non archived projects only. Default is true. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23809) in GitLab 12.8)_. | +| `non_archived` | boolean | no | Return merge requests from non archived projects only. Default is `true`. | | `not` | Hash | no | Return merge requests that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `reviewer_id`, `reviewer_username`, `my_reaction_emoji`. | +| `order_by` | string | no | Return merge requests ordered by `created_at`, `title` or `updated_at` fields. Default is `created_at`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331625) in GitLab 14.8. | +| `reviewer_id` | integer | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given user `id`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_username`. | +| `reviewer_username` | string | no | Returns merge requests which have the user as a [reviewer](../user/project/merge_requests/getting_started.md#reviewer) with the given `username`. `None` returns merge requests with no reviewers. `Any` returns merge requests with any reviewer. Mutually exclusive with `reviewer_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. | +| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. | +| `search` | string | no | Search merge requests against their `title` and `description`. | +| `source_branch` | string | no | Return merge requests with the given source branch. | +| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc`. | +| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged`. | +| `target_branch` | string | no | Return merge requests with the given target branch. | +| `updated_after` | datetime | no | Return merge requests updated on or after the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `updated_before` | datetime | no | Return merge requests updated on or before the given time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request. | +| `with_labels_details` | boolean | no | If `true`, response returns more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. | +| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) in GitLab 13.0. | ```json [ @@ -610,13 +608,13 @@ GET /projects/:id/merge_requests/:merge_request_iid Parameters: -| Attribute | Type | Required | Description | -|----------------------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | -| `render_html` | boolean | no | If `true` response includes rendered HTML for title and description. | -| `include_diverged_commits_count` | boolean | no | If `true` response includes the commits behind the target branch. | -| `include_rebase_in_progress` | boolean | no | If `true` response includes whether a rebase operation is in progress. | +| Attribute | Type | Required | Description | +|----------------------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| `include_diverged_commits_count` | boolean | no | If `true`, response includes the commits behind the target branch. | +| `include_rebase_in_progress` | boolean | no | If `true`, response includes whether a rebase operation is in progress. | +| `render_html` | boolean | no | If `true`, response includes rendered HTML for title and description. | ```json { @@ -799,10 +797,10 @@ GET /projects/:id/merge_requests/:merge_request_iid/participants Parameters: -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```json [ @@ -835,10 +833,10 @@ GET /projects/:id/merge_requests/:merge_request_iid/reviewers Parameters: -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| | `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```json [ @@ -879,10 +877,10 @@ GET /projects/:id/merge_requests/:merge_request_iid/commits Parameters: -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```json [ @@ -926,11 +924,11 @@ GET /projects/:id/merge_requests/:merge_request_iid/changes Parameters: -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | -| `access_raw_diffs` | boolean | no | Retrieve change diffs via Gitaly. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| `access_raw_diffs` | boolean | no | Retrieve change diffs via Gitaly. | ```json { @@ -1048,10 +1046,10 @@ GET /projects/:id/merge_requests/:merge_request_iid/pipelines Parameters: -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```json [ @@ -1066,8 +1064,6 @@ Parameters: ## Create MR Pipeline -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/31722) in GitLab 12.3. - Create a new [pipeline for a merge request](../ci/pipelines/merge_request_pipelines.md). A pipeline created via this endpoint doesn't run a regular branch/tag pipeline. It requires `.gitlab-ci.yml` to be configured with `only: [merge_requests]` to create jobs. @@ -1084,10 +1080,10 @@ POST /projects/:id/merge_requests/:merge_request_iid/pipelines Parameters: -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```json { @@ -1136,24 +1132,24 @@ Creates a new merge request. POST /projects/:id/merge_requests ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user | -| `source_branch` | string | yes | The source branch. | -| `target_branch` | string | yes | The target branch. | -| `title` | string | yes | Title of MR. | -| `assignee_id` | integer | no | Assignee user ID. | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user | +| `source_branch` | string | yes | The source branch. | +| `target_branch` | string | yes | The target branch. | +| `title` | string | yes | Title of MR. | +| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch. | +| `allow_maintainer_to_push` | boolean | no | Alias of `allow_collaboration`. | +| `approvals_before_merge` **(PREMIUM)** | integer | no | Number of approvals required before this can be merged (see below). | +| `assignee_id` | integer | no | Assignee user ID. | | `assignee_ids` | integer array | no | The ID of the users to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. | -| `reviewer_ids` | integer array | no | The ID of the users added as a reviewer to the MR. If set to `0` or left empty, no reviewers are added. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. | -| `description` | string | no | Description of MR. Limited to 1,048,576 characters. | -| `target_project_id` | integer | no | The target project (numeric ID). | -| `labels` | string | no | Labels for MR as a comma-separated list. | -| `milestone_id` | integer | no | The global ID of a milestone. | +| `description` | string | no | Description of the merge request. Limited to 1,048,576 characters. | +| `labels` | string | no | Labels for the merge request, as a comma-separated list. | +| `milestone_id` | integer | no | The global ID of a milestone. | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging. | -| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch. | -| `allow_maintainer_to_push` | boolean | no | Alias of `allow_collaboration`. | -| `approvals_before_merge` **(PREMIUM)** | integer | no | Number of approvals required before this can be merged (see below). | -| `squash` | boolean | no | Squash commits into a single commit when merging. | +| `reviewer_ids` | integer array | no | The ID of the users added as a reviewer to the merge request. If set to `0` or left empty, no reviewers are added. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. | +| `squash` | boolean | no | Squash commits into a single commit when merging. | +| `target_project_id` | integer | no | Numeric ID of the target project. | If `approvals_before_merge` is not provided, it inherits the value from the target project. If provided, the following conditions must hold for it to take effect: @@ -1304,26 +1300,26 @@ Updates an existing merge request. You can change the target branch, title, or e PUT /projects/:id/merge_requests/:merge_request_iid ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The ID of a merge request. | -| `target_branch` | string | no | The target branch. | -| `title` | string | no | Title of MR. | -| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. | -| `assignee_ids` | integer array | no | The ID of the users to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. | -| `reviewer_ids` | integer array | no | The ID of the users set as a reviewer to the MR. Set the value to `0` or provide an empty value to unset all reviewers. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. | -| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The ID of a merge request. | +| `add_labels` | string | no | Comma-separated label names to add to a merge request. | +| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch. | +| `allow_maintainer_to_push` | boolean | no | Alias of `allow_collaboration`. | +| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. | +| `assignee_ids` | integer array | no | The ID of the users to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. | +| `description` | string | no | Description of the merge request. Limited to 1,048,576 characters. | +| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. | | `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. | -| `add_labels` | string | no | Comma-separated label names to add to a merge request. | -| `remove_labels` | string | no | Comma-separated label names to remove from a merge request. | -| `description` | string | no | Description of MR. Limited to 1,048,576 characters. | -| `state_event` | string | no | New state (close/reopen). | +| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| +| `remove_labels` | string | no | Comma-separated label names to remove from a merge request. | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging. | +| `reviewer_ids` | integer array | no | The ID of the users set as a reviewer to the merge request. Set the value to `0` or provide an empty value to unset all reviewers. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. | | `squash` | boolean | no | Squash commits into a single commit when merging. | -| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. | -| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch. | -| `allow_maintainer_to_push` | boolean | no | Alias of `allow_collaboration`. | +| `state_event` | string | no | New state (close/reopen). | +| `target_branch` | string | no | The target branch. | +| `title` | string | no | Title of MR. | Must include at least one non-required attribute from above. @@ -1485,10 +1481,10 @@ Only for administrators and project owners. Deletes the merge request in questio DELETE /projects/:id/merge_requests/:merge_request_iid ``` -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```shell curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/merge_requests/85" @@ -1504,16 +1500,16 @@ PUT /projects/:id/merge_requests/:merge_request_iid/merge Parameters: -| Attribute | Type | Required | Description | -|--------------------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | -| `merge_commit_message` | string | no | Custom merge commit message. | -| `squash_commit_message` | string | no | Custom squash commit message. | -| `squash` | boolean | no | If `true` the commits are squashed into a single commit on merge. | -| `should_remove_source_branch` | boolean | no | If `true` removes the source branch. | -| `merge_when_pipeline_succeeds` | boolean | no | If `true` the MR is merged when the pipeline succeeds. | -| `sha` | string | no | If present, then this SHA must match the HEAD of the source branch, otherwise the merge fails. | +| Attribute | Type | Required | Description | +|--------------------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| `merge_commit_message` | string | no | Custom merge commit message. | +| `merge_when_pipeline_succeeds` | boolean | no | If `true`, the merge request is merged when the pipeline succeeds. | +| `sha` | string | no | If present, then this SHA must match the HEAD of the source branch, otherwise the merge fails. | +| `should_remove_source_branch` | boolean | no | If `true`, removes the source branch. | +| `squash_commit_message` | string | no | Custom squash commit message. | +| `squash` | boolean | no | If `true`, the commits are squashed into a single commit on merge. | ```json { @@ -1665,12 +1661,12 @@ the `approvals_before_merge` parameter: This API returns specific HTTP status codes on failure: -| HTTP Status | Message | Reason | -|:------------|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------| -| `401` | `Unauthorized` | This user does not have permission to accept this merge request. | -| `405` | `Method Not Allowed` | The merge request is not able to be merged. | -| `409` | `SHA does not match HEAD of source branch` | The provided `sha` parameter does not match the HEAD of the source. | -| `422` | `Branch cannot be merged` | The merge request failed to merge. | +| HTTP Status | Message | Reason | +|:------------|---------|--------| +| `401` | `Unauthorized` | This user does not have permission to accept this merge request. | +| `405` | `Method Not Allowed` | The merge request is not able to be merged. | +| `409` | `SHA does not match HEAD of source branch` | The provided `sha` parameter does not match the HEAD of the source. | +| `422` | `Branch cannot be merged` | The merge request failed to merge. | For additional important notes on response data, read [Single merge request response notes](#single-merge-request-response-notes). @@ -1695,10 +1691,10 @@ GET /projects/:id/merge_requests/:merge_request_iid/merge_ref Parameters: -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```json { @@ -1722,10 +1718,10 @@ POST /projects/:id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_ Parameters: -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```json { @@ -1889,11 +1885,11 @@ you receive a `403 Forbidden` response. PUT /projects/:id/merge_requests/:merge_request_iid/rebase ``` -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | -| `skip_ci` | boolean | no | Set to `true` to skip creating a CI pipeline. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| `skip_ci` | boolean | no | Set to `true` to skip creating a CI pipeline. | ```shell curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/76/merge_requests/1/rebase" @@ -1952,10 +1948,10 @@ Get all the issues that would be closed by merging the provided merge request. GET /projects/:id/merge_requests/:merge_request_iid/closes_issues ``` -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/76/merge_requests/1/closes_issues" @@ -2028,10 +2024,10 @@ status code `HTTP 304 Not Modified` is returned. POST /projects/:id/merge_requests/:merge_request_iid/subscribe ``` -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/17/subscribe" @@ -2198,10 +2194,10 @@ not subscribed to the merge request, the status code `HTTP 304 Not Modified` is POST /projects/:id/merge_requests/:merge_request_iid/unsubscribe ``` -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/17/unsubscribe" @@ -2368,10 +2364,10 @@ status code `HTTP 304 Not Modified` is returned. POST /projects/:id/merge_requests/:merge_request_iid/todo ``` -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/27/todo" @@ -2532,8 +2528,8 @@ Example response: | SHA field | Purpose | |--------------------|-------------------------------------------------------------------------------------| -| `head_commit_sha` | The HEAD commit of the source branch. | | `base_commit_sha` | The merge-base commit SHA between the source branch and the target branches. | +| `head_commit_sha` | The HEAD commit of the source branch. | | `start_commit_sha` | The HEAD commit SHA of the target branch when this version of the diff was created. | ## Get a single MR diff version @@ -2613,11 +2609,11 @@ Sets an estimated time of work for this merge request. POST /projects/:id/merge_requests/:merge_request_iid/time_estimate ``` -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | -| `duration` | string | yes | The duration in human format, such as `3h30m`. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| `duration` | string | yes | The duration in human format, such as `3h30m`. | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_estimate?duration=3h30m" @@ -2642,10 +2638,10 @@ Resets the estimated time for this merge request to 0 seconds. POST /projects/:id/merge_requests/:merge_request_iid/reset_time_estimate ``` -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of a project's merge request. | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_time_estimate" @@ -2670,12 +2666,12 @@ Adds spent time for this merge request. POST /projects/:id/merge_requests/:merge_request_iid/add_spent_time ``` -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | -| `duration` | string | yes | The duration in human format, such as `3h30m` | -| `summary` | string | no | A summary of how the time was spent. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| `duration` | string | yes | The duration in human format, such as `3h30m` | +| `summary` | string | no | A summary of how the time was spent. | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/93/add_spent_time?duration=1h" @@ -2700,10 +2696,10 @@ Resets the total spent time for this merge request to 0 seconds. POST /projects/:id/merge_requests/:merge_request_iid/reset_spent_time ``` -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of a project's merge_request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of a project's merge request. | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/93/reset_spent_time" @@ -2726,10 +2722,10 @@ Example response: GET /projects/:id/merge_requests/:merge_request_iid/time_stats ``` -| Attribute | Type | Required | Description | -|---------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `merge_request_iid` | integer | yes | The internal ID of the merge request. | +| Attribute | Type | Required | Description | +|---------------------|----------------|----------|-------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/93/time_stats" -- GitLab From 3911bcbf3c90400537ed41caaf6605f80dbf416d Mon Sep 17 00:00:00 2001 From: Imre Farkas <ifarkas@gitlab.com> Date: Sat, 6 Aug 2022 09:54:58 +0200 Subject: [PATCH 057/169] Add Users::MigrateRecordsToGhostUserService The new service extracts parts of the existing Users::DestroyService and Users::MigrateToGhostUserService. This will be used by a cron job doing interruptible batched migration / deletion of users and associated records with limited execution time. --- .../migrate_records_to_ghost_user_service.rb | 107 ++++++++ .../migrate_records_to_ghost_user_service.rb | 65 +++++ ...rate_records_to_ghost_user_service_spec.rb | 98 +++++++ ...rate_records_to_ghost_user_service_spec.rb | 258 ++++++++++++++++++ ...s_to_ghost_user_service_shared_examples.rb | 39 +++ 5 files changed, 567 insertions(+) create mode 100644 app/services/users/migrate_records_to_ghost_user_service.rb create mode 100644 ee/app/services/ee/users/migrate_records_to_ghost_user_service.rb create mode 100644 ee/spec/services/ee/users/migrate_records_to_ghost_user_service_spec.rb create mode 100644 spec/services/users/migrate_records_to_ghost_user_service_spec.rb create mode 100644 spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb diff --git a/app/services/users/migrate_records_to_ghost_user_service.rb b/app/services/users/migrate_records_to_ghost_user_service.rb new file mode 100644 index 00000000000000..6e2f478ea72f4e --- /dev/null +++ b/app/services/users/migrate_records_to_ghost_user_service.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +# When a user is destroyed, some of their associated records are +# moved to a "Ghost User", to prevent these associated records from +# being destroyed. +# +# For example, all the issues/MRs a user has created are _not_ destroyed +# when the user is destroyed. +module Users + class MigrateRecordsToGhostUserService + extend ActiveSupport::Concern + + DestroyError = Class.new(StandardError) + + attr_reader :ghost_user, :user, :initiator_user, :hard_delete + + def initialize(user, initiator_user) + @user = user + @initiator_user = initiator_user + @ghost_user = User.ghost + end + + def execute(hard_delete: false) + @hard_delete = hard_delete + + migrate_records + post_migrate_records + end + + private + + def migrate_records + return if hard_delete + + migrate_issues + migrate_merge_requests + migrate_notes + migrate_abuse_reports + migrate_award_emoji + migrate_snippets + migrate_reviews + end + + def post_migrate_records + delete_snippets + + # Rails attempts to load all related records into memory before + # destroying: https://github.com/rails/rails/issues/22510 + # This ensures we delete records in batches. + user.destroy_dependent_associations_in_batches(exclude: [:snippets]) + user.nullify_dependent_associations_in_batches + + # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing + user_data = user.destroy + user.namespace.destroy + + user_data + end + + def delete_snippets + response = Snippets::BulkDestroyService.new(initiator_user, user.snippets).execute(skip_authorization: true) + raise DestroyError, response.message if response.error? + end + + def migrate_issues + batched_migrate(Issue, :author_id) + batched_migrate(Issue, :last_edited_by_id) + end + + def migrate_merge_requests + batched_migrate(MergeRequest, :author_id) + batched_migrate(MergeRequest, :merge_user_id) + end + + def migrate_notes + batched_migrate(Note, :author_id) + end + + def migrate_abuse_reports + user.reported_abuse_reports.update_all(reporter_id: ghost_user.id) + end + + def migrate_award_emoji + user.award_emoji.update_all(user_id: ghost_user.id) + end + + def migrate_snippets + snippets = user.snippets.only_project_snippets + snippets.update_all(author_id: ghost_user.id) + end + + def migrate_reviews + batched_migrate(Review, :author_id) + end + + # rubocop:disable CodeReuse/ActiveRecord + def batched_migrate(base_scope, column, batch_size: 50) + loop do + update_count = base_scope.where(column => user.id).limit(batch_size).update_all(column => ghost_user.id) + break if update_count == 0 + end + end + # rubocop:enable CodeReuse/ActiveRecord + end +end + +Users::MigrateRecordsToGhostUserService.prepend_mod_with('Users::MigrateRecordsToGhostUserService') diff --git a/ee/app/services/ee/users/migrate_records_to_ghost_user_service.rb b/ee/app/services/ee/users/migrate_records_to_ghost_user_service.rb new file mode 100644 index 00000000000000..c9b9034216b817 --- /dev/null +++ b/ee/app/services/ee/users/migrate_records_to_ghost_user_service.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module EE + module Users + module MigrateRecordsToGhostUserService + extend ::Gitlab::Utils::Override + + private + + override :migrate_records + def migrate_records + # these should always be ghosted + migrate_resource_iteration_events + + return super if hard_delete + + migrate_epics + migrate_requirements_management_requirements + migrate_vulnerabilities_feedback + migrate_vulnerabilities + migrate_vulnerabilities_external_issue_links + super + end + + override :post_migrate_records + def post_migrate_records + log_audit_event(user) if super.try(:destroyed?) + end + + def migrate_epics + batched_migrate(::Epic, :author_id) + batched_migrate(::Epic, :last_edited_by_id) + end + + def migrate_requirements_management_requirements + user.requirements.update_all(author_id: ghost_user.id) + end + + def migrate_vulnerabilities_feedback + batched_migrate(Vulnerabilities::Feedback, :author_id) + batched_migrate(Vulnerabilities::Feedback, :comment_author_id) + end + + def migrate_vulnerabilities + batched_migrate(::Vulnerability, :author_id) + end + + def migrate_vulnerabilities_external_issue_links + batched_migrate(Vulnerabilities::ExternalIssueLink, :author_id) + end + + def migrate_resource_iteration_events + batched_migrate(ResourceIterationEvent, :user_id) + end + + def log_audit_event(user) + ::AuditEventService.new( + initiator_user, + user, + action: :destroy + ).for_user.security_event + end + end + end +end diff --git a/ee/spec/services/ee/users/migrate_records_to_ghost_user_service_spec.rb b/ee/spec/services/ee/users/migrate_records_to_ghost_user_service_spec.rb new file mode 100644 index 00000000000000..338482a664ebd7 --- /dev/null +++ b/ee/spec/services/ee/users/migrate_records_to_ghost_user_service_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::MigrateRecordsToGhostUserService do + let!(:user) { create(:user) } + let(:service) { described_class.new(user, admin) } + + let_it_be(:admin) { create(:admin) } + + context "when migrating a user's associated records to the ghost user" do + context 'for epics' do + context 'when deleted user is present as both author and edited_user' do + include_examples 'migrating records to the ghost user', Epic, [:author, :last_edited_by] do + let(:created_record) do + create(:epic, group: create(:group), author: user, last_edited_by: user) + end + end + end + + context 'when deleted user is present only as edited_user' do + include_examples 'migrating records to the ghost user', Epic, [:last_edited_by] do + let(:created_record) { create(:epic, group: create(:group), author: create(:user), last_edited_by: user) } + end + end + end + + context 'for vulnerability_feedback author' do + include_examples 'migrating records to the ghost user', Vulnerabilities::Feedback, [:author] do + let(:created_record) { create(:vulnerability_feedback, author: user) } + end + end + + context 'for vulnerability_feedback comment author' do + include_examples 'migrating records to the ghost user', Vulnerabilities::Feedback, [:comment_author] do + let(:created_record) { create(:vulnerability_feedback, comment_author: user) } + end + end + + context 'for vulnerability author' do + include_examples 'migrating records to the ghost user', Vulnerability, [:author] do + let(:created_record) { create(:vulnerability, author: user) } + end + end + + context 'for vulnerability_external_issue_link author' do + include_examples 'migrating records to the ghost user', Vulnerabilities::ExternalIssueLink, [:author] do + let(:created_record) { create(:vulnerabilities_external_issue_link, author: user) } + end + end + + context 'for requirements' do + include_examples 'migrating records to the ghost user', RequirementsManagement::Requirement, [:author] do + let(:created_record) { create(:requirement, author: user) } + end + end + + context 'for resource_iteration_events' do + let(:always_ghost) { true } + + include_examples 'migrating records to the ghost user', ResourceIterationEvent, [:user] do + let(:created_record) do + create(:resource_iteration_event, issue: create(:issue), + user: user, + iteration: create(:iteration)) + end + end + end + end + + context 'on post-migrate cleanups' do + subject(:operation) { service.execute } + + describe 'audit events' do + include_examples 'audit event logging' do + let(:fail_condition!) do + expect(user).to receive(:destroy).and_return(user) + expect(user).to receive(:destroyed?).and_return(false) + end + + let(:attributes) do + { + author_id: admin.id, + entity_id: user.id, + entity_type: 'User', + details: { + remove: 'user', + author_name: admin.name, + target_id: user.id, + target_type: 'User', + target_details: user.full_path + } + } + end + end + end + end +end diff --git a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb new file mode 100644 index 00000000000000..04310b977c01a5 --- /dev/null +++ b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb @@ -0,0 +1,258 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::MigrateRecordsToGhostUserService do + let!(:user) { create(:user) } + let(:service) { described_class.new(user, admin) } + + let_it_be(:admin) { create(:admin) } + let_it_be(:project) { create(:project, :repository) } + + context "when migrating a user's associated records to the ghost user" do + context 'for issues' do + context 'when deleted user is present as both author and edited_user' do + include_examples 'migrating records to the ghost user', Issue, [:author, :last_edited_by] do + let(:created_record) do + create(:issue, project: project, author: user, last_edited_by: user) + end + end + end + + context 'when deleted user is present only as edited_user' do + include_examples 'migrating records to the ghost user', Issue, [:last_edited_by] do + let(:created_record) { create(:issue, project: project, author: create(:user), last_edited_by: user) } + end + end + + context "when deleted user is the assignee" do + let!(:issue) { create(:issue, project: project, assignees: [user]) } + + it 'migrates the issue so that it is "Unassigned"' do + service.execute + + migrated_issue = Issue.find_by_id(issue.id) + expect(migrated_issue).to be_present + expect(migrated_issue.assignees).to be_empty + end + end + end + + context 'for merge requests' do + context 'when deleted user is present as both author and merge_user' do + include_examples 'migrating records to the ghost user', MergeRequest, [:author, :merge_user] do + let(:created_record) do + create(:merge_request, source_project: project, + author: user, + merge_user: user, + target_branch: "first") + end + end + end + + context 'when deleted user is present only as both merge_user' do + include_examples 'migrating records to the ghost user', MergeRequest, [:merge_user] do + let(:created_record) do + create(:merge_request, source_project: project, + merge_user: user, + target_branch: "first") + end + end + end + + context "when deleted user is the assignee" do + let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) } + + it 'migrates the merge request so that it is "Unassigned"' do + service.execute + + migrated_merge_request = MergeRequest.find_by_id(merge_request.id) + expect(migrated_merge_request).to be_present + expect(migrated_merge_request.assignees).to be_empty + end + end + end + + context 'for notes' do + include_examples 'migrating records to the ghost user', Note do + let(:created_record) { create(:note, project: project, author: user) } + end + end + + context 'for abuse reports' do + include_examples 'migrating records to the ghost user', AbuseReport do + let(:created_record) { create(:abuse_report, reporter: user, user: create(:user)) } + end + end + + context 'for award emoji' do + include_examples 'migrating records to the ghost user', AwardEmoji, [:user] do + let(:created_record) { create(:award_emoji, user: user) } + + context "when the awardable already has an award emoji of the same name assigned to the ghost user" do + let(:awardable) { create(:issue) } + let!(:existing_award_emoji) { create(:award_emoji, user: User.ghost, name: "thumbsup", awardable: awardable) } + let!(:award_emoji) { create(:award_emoji, user: user, name: "thumbsup", awardable: awardable) } + + it "migrates the award emoji regardless" do + service.execute + + migrated_record = AwardEmoji.find_by_id(award_emoji.id) + + expect(migrated_record.user).to eq(User.ghost) + end + + it "does not leave the migrated award emoji in an invalid state" do + service.execute + + migrated_record = AwardEmoji.find_by_id(award_emoji.id) + + expect(migrated_record).to be_valid + end + end + end + end + + context 'for snippets' do + include_examples 'migrating records to the ghost user', Snippet do + let(:created_record) { create(:snippet, project: project, author: user) } + end + end + + context 'for reviews' do + include_examples 'migrating records to the ghost user', Review, [:author] do + let(:created_record) { create(:review, author: user) } + end + end + end + + context 'on post-migrate cleanups' do + it 'destroys the user and personal namespace' do + namespace = user.namespace + + allow(user).to receive(:destroy).and_call_original + + service.execute + + expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'deletes user associations in batches' do + expect(user).to receive(:destroy_dependent_associations_in_batches) + + service.execute + end + + context 'for batched nullify' do + it 'nullifies related associations in batches' do + expect(user).to receive(:nullify_dependent_associations_in_batches).and_call_original + + service.execute + end + + it 'nullifies last_updated_issues, closed_issues, resource_label_events' do + issue = create(:issue, closed_by: user, updated_by: user) + resource_label_event = create(:resource_label_event, user: user) + + service.execute + + issue.reload + resource_label_event.reload + + expect(issue.closed_by).to be_nil + expect(issue.updated_by).to be_nil + expect(resource_label_event.user).to be_nil + end + end + + context 'for snippets' do + let(:gitlab_shell) { Gitlab::Shell.new } + + it 'does not include snippets when deleting in batches' do + expect(user).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:snippets] }) + + service.execute + end + + it 'calls the bulk snippet destroy service for the user personal snippets' do + repo1 = create(:personal_snippet, :repository, author: user).snippet_repository + repo2 = create(:project_snippet, :repository, project: project, author: user).snippet_repository + + aggregate_failures do + expect(gitlab_shell.repository_exists?(repo1.shard_name, "#{repo1.disk_path}.git")).to be(true) + expect(gitlab_shell.repository_exists?(repo2.shard_name, "#{repo2.disk_path}.git")).to be(true) + end + + # Call made when destroying user personal projects + expect(Snippets::BulkDestroyService).not_to( + receive(:new).with(admin, project.snippets).and_call_original) + + # Call to remove user personal snippets and for + # project snippets where projects are not user personal + # ones + expect(Snippets::BulkDestroyService).to( + receive(:new).with(admin, user.snippets.only_personal_snippets).and_call_original) + + service.execute + + aggregate_failures do + expect(gitlab_shell.repository_exists?(repo1.shard_name, "#{repo1.disk_path}.git")).to be(false) + expect(gitlab_shell.repository_exists?(repo2.shard_name, "#{repo2.disk_path}.git")).to be(true) + end + end + + it 'calls the bulk snippet destroy service with hard delete option if it is present' do + # this avoids getting into Projects::DestroyService as it would + # call Snippets::BulkDestroyService first! + allow(user).to receive(:personal_projects).and_return([]) + + expect_next_instance_of(Snippets::BulkDestroyService) do |bulk_destroy_service| + expect(bulk_destroy_service).to receive(:execute).with({ skip_authorization: true }).and_call_original + end + + service.execute(hard_delete: true) + end + + it 'does not delete project snippets that the user is the author of' do + repo = create(:project_snippet, :repository, author: user).snippet_repository + + service.execute + + expect(gitlab_shell.repository_exists?(repo.shard_name, "#{repo.disk_path}.git")).to be(true) + expect(User.ghost.snippets).to include(repo.snippet) + end + + context 'when an error is raised deleting snippets' do + it 'does not delete user' do + snippet = create(:personal_snippet, :repository, author: user) + + bulk_service = double + allow(Snippets::BulkDestroyService).to receive(:new).and_call_original + allow(Snippets::BulkDestroyService).to receive(:new).with(admin, user.snippets).and_return(bulk_service) + allow(bulk_service).to receive(:execute).and_return(ServiceResponse.error(message: 'foo')) + + aggregate_failures do + expect { service.execute }.to( + raise_error(Users::MigrateRecordsToGhostUserService::DestroyError, 'foo' )) + expect(snippet.reload).not_to be_nil + expect( + gitlab_shell.repository_exists?(snippet.repository_storage, + "#{snippet.disk_path}.git") + ).to be(true) + end + end + end + end + + context 'when hard_delete option is given' do + it 'will not ghost certain records' do + issue = create(:issue, author: user) + + service.execute(hard_delete: true) + + expect(Issue).not_to exist(issue.id) + end + end + end +end diff --git a/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb b/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb new file mode 100644 index 00000000000000..eb03f0888b9568 --- /dev/null +++ b/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'migrating records to the ghost user' do |record_class, fields| + record_class_name = record_class.to_s.titleize.downcase + + let(:project) do + case record_class + when MergeRequest + create(:project, :repository) + else + create(:project) + end + end + + before do + project.add_developer(user) + end + + context "for a #{record_class_name} the user has created" do + let!(:record) { created_record } + let(:migrated_fields) { fields || [:author] } + + it "does not delete the #{record_class_name}" do + service.execute + + expect(record_class.find_by_id(record.id)).to be_present + end + + it 'migrates all associated fields to the "Ghost user"' do + service.execute + + migrated_record = record_class.find_by_id(record.id) + + migrated_fields.each do |field| + expect(migrated_record.public_send(field)).to eq(User.ghost) + end + end + end +end -- GitLab From afbad48c0b89e9036d9198dd13f2f40a7cd56604 Mon Sep 17 00:00:00 2001 From: Marcel Amirault <mamirault@gitlab.com> Date: Mon, 5 Sep 2022 13:51:22 +0000 Subject: [PATCH 058/169] Move parent-child pipelines to downstream page Part 3 of combining multi-project pipelines and parent-child pipelines together. --- doc/api/job_artifacts.md | 4 +- doc/api/jobs.md | 2 +- doc/api/pipelines.md | 4 +- doc/ci/jobs/job_control.md | 2 +- doc/ci/pipelines/downstream_pipelines.md | 211 ++++++++++++++++- doc/ci/pipelines/index.md | 4 +- doc/ci/pipelines/job_artifacts.md | 2 +- doc/ci/pipelines/parent_child_pipelines.md | 224 +----------------- doc/ci/pipelines/pipeline_architectures.md | 2 +- doc/ci/pipelines/pipeline_efficiency.md | 2 +- doc/ci/resource_groups/index.md | 2 +- doc/ci/troubleshooting.md | 6 +- doc/ci/yaml/index.md | 12 +- .../coverage_fuzzing/index.md | 2 +- .../policies/scan-execution-policies.md | 2 +- doc/user/project/settings/index.md | 2 +- 16 files changed, 240 insertions(+), 243 deletions(-) diff --git a/doc/api/job_artifacts.md b/doc/api/job_artifacts.md index 31da0638d23935..d1bd40b91b98eb 100644 --- a/doc/api/job_artifacts.md +++ b/doc/api/job_artifacts.md @@ -70,7 +70,7 @@ is the same as [getting the job's artifacts](#get-job-artifacts), but by defining the job's name instead of its ID. NOTE: -If a pipeline is [parent of other child pipelines](../ci/pipelines/parent_child_pipelines.md), artifacts +If a pipeline is [parent of other child pipelines](../ci/pipelines/downstream_pipelines.md#parent-child-pipelines), artifacts are searched in hierarchical order from parent to child. For example, if both parent and child pipelines have a job with the same name, the artifact from the parent pipeline is returned. @@ -175,7 +175,7 @@ The artifact file provides more detail than what is available in the [CSV export](../user/application_security/vulnerability_report/index.md#export-vulnerability-details). In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201784) and later, artifacts -for [parent and child pipelines](../ci/pipelines/parent_child_pipelines.md) are searched in hierarchical +for [parent and child pipelines](../ci/pipelines/downstream_pipelines.md#parent-child-pipelines) are searched in hierarchical order from parent to child. For example, if both parent and child pipelines have a job with the same name, the artifact from the parent pipeline is returned. diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 3173b8f8e70dc8..1548045d7c23f5 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -303,7 +303,7 @@ Example of response ``` In GitLab 13.3 and later, this endpoint [returns data for any pipeline](pipelines.md#get-a-single-pipeline) -including [child pipelines](../ci/pipelines/parent_child_pipelines.md). +including [child pipelines](../ci/pipelines/downstream_pipelines.md#parent-child-pipelines). In GitLab 13.5 and later, this endpoint does not return retried jobs in the response by default. Additionally, jobs are sorted by ID in descending order (newest first). diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index 2e601f6e24a7bf..23c55cfb1770b9 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -80,7 +80,7 @@ Example of response Get one pipeline from a project. -You can also get a single [child pipeline](../ci/pipelines/parent_child_pipelines.md). +You can also get a single [child pipeline](../ci/pipelines/downstream_pipelines.md#parent-child-pipelines). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36494) in GitLab 13.3. ```plaintext @@ -427,7 +427,7 @@ related objects, such as builds, logs, artifacts, and triggers. **This action cannot be undone.** Deleting a pipeline does not automatically delete its -[child pipelines](../ci/pipelines/parent_child_pipelines.md). +[child pipelines](../ci/pipelines/downstream_pipelines.md#parent-child-pipelines). See the [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/39503) for details. diff --git a/doc/ci/jobs/job_control.md b/doc/ci/jobs/job_control.md index 217d12e4c267f2..ca8755550fe539 100644 --- a/doc/ci/jobs/job_control.md +++ b/doc/ci/jobs/job_control.md @@ -243,7 +243,7 @@ check the value of the `$CI_PIPELINE_SOURCE` variable: | `external` | When you use CI services other than GitLab. | | `external_pull_request_event` | When an external pull request on GitHub is created or updated. See [Pipelines for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). | | `merge_request_event` | For pipelines created when a merge request is created or updated. Required to enable [merge request pipelines](../pipelines/merge_request_pipelines.md), [merged results pipelines](../pipelines/merged_results_pipelines.md), and [merge trains](../pipelines/merge_trains.md). | -| `parent_pipeline` | For pipelines triggered by a [parent/child pipeline](../pipelines/parent_child_pipelines.md) with `rules`. Use this pipeline source in the child pipeline configuration so that it can be triggered by the parent pipeline. | +| `parent_pipeline` | For pipelines triggered by a [parent/child pipeline](../pipelines/downstream_pipelines.md#parent-child-pipelines) with `rules`. Use this pipeline source in the child pipeline configuration so that it can be triggered by the parent pipeline. | | `pipeline` | For [multi-project pipelines](../pipelines/downstream_pipelines.md#multi-project-pipelines) created by [using the API with `CI_JOB_TOKEN`](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api), or the [`trigger`](../yaml/index.md#trigger) keyword. | | `push` | For pipelines triggered by a `git push` event, including for branches and tags. | | `schedule` | For [scheduled pipelines](../pipelines/schedules.md). | diff --git a/doc/ci/pipelines/downstream_pipelines.md b/doc/ci/pipelines/downstream_pipelines.md index 4fc7adfaae3c19..8554a725bde7ea 100644 --- a/doc/ci/pipelines/downstream_pipelines.md +++ b/doc/ci/pipelines/downstream_pipelines.md @@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w A downstream pipeline is any GitLab CI/CD pipeline triggered by another pipeline. A downstream pipeline can be either: -- A [parent-child pipeline](parent_child_pipelines.md), which is a downstream pipeline triggered +- A [parent-child pipeline](downstream_pipelines.md#parent-child-pipelines), which is a downstream pipeline triggered in the same project as the first pipeline. - A [multi-project pipeline](#multi-project-pipelines), which is a downstream pipeline triggered in a different project than the first pipeline. @@ -172,6 +172,197 @@ When using: - [`only/except`](../yaml/index.md#only--except) to control job behavior, use the `pipelines` keyword. +## Parent-child pipelines + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16094) in GitLab 12.7. + +As pipelines grow more complex, a few related problems start to emerge: + +- The staged structure, where all steps in a stage must be completed before the first + job in next stage begins, causes arbitrary waits, slowing things down. +- Configuration for the single global pipeline becomes very long and complicated, + making it hard to manage. +- Imports with [`include`](../yaml/index.md#include) increase the complexity of the configuration, and create the potential + for namespace collisions where jobs are unintentionally duplicated. +- Pipeline UX can become unwieldy with so many jobs and stages to work with. + +Additionally, sometimes the behavior of a pipeline needs to be more dynamic. The ability +to choose to start sub-pipelines (or not) is a powerful ability, especially if the +YAML is dynamically generated. + + + +Similarly to [multi-project pipelines](#multi-project-pipelines), a pipeline can trigger a +set of concurrently running downstream child pipelines, but in the same project: + +- Child pipelines still execute each of their jobs according to a stage sequence, but + would be free to continue forward through their stages without waiting for unrelated + jobs in the parent pipeline to finish. +- The configuration is split up into smaller child pipeline configurations. Each child pipeline contains only relevant steps which are + easier to understand. This reduces the cognitive load to understand the overall configuration. +- Imports are done at the child pipeline level, reducing the likelihood of collisions. + +Child pipelines work well with other GitLab CI/CD features: + +- Use [`rules: changes`](../yaml/index.md#ruleschanges) to trigger pipelines only when + certain files change. This is useful for monorepos, for example. +- Since the parent pipeline in `.gitlab-ci.yml` and the child pipeline run as normal + pipelines, they can have their own behaviors and sequencing in relation to triggers. + +See the [`trigger`](../yaml/index.md#trigger) keyword documentation for full details on how to +include the child pipeline configuration. + +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +For an overview, see [Parent-Child Pipelines feature demo](https://youtu.be/n8KpBSqZNbk). + +NOTE: +The artifact containing the generated YAML file must not be larger than 5MB. + +### Trigger a parent-child pipeline + +The simplest case is [triggering a child pipeline](../yaml/index.md#trigger) using a +local YAML file to define the pipeline configuration. In this case, the parent pipeline +triggers the child pipeline, and continues without waiting: + +```yaml +microservice_a: + trigger: + include: path/to/microservice_a.yml +``` + +You can include multiple files when defining a child pipeline. The child pipeline's +configuration is composed of all configuration files merged together: + +```yaml +microservice_a: + trigger: + include: + - local: path/to/microservice_a.yml + - template: Security/SAST.gitlab-ci.yml +``` + +In [GitLab 13.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/205157), +you can use [`include:file`](../yaml/index.md#includefile) to trigger child pipelines +with a configuration file in a different project: + +```yaml +microservice_a: + trigger: + include: + - project: 'my-group/my-pipeline-library' + ref: 'main' + file: '/path/to/child-pipeline.yml' +``` + +The maximum number of entries that are accepted for `trigger:include` is three. + +### Merge request child pipelines + +To trigger a child pipeline as a [merge request pipeline](merge_request_pipelines.md) we need to: + +- Set the trigger job to run on merge requests: + +```yaml +# parent .gitlab-ci.yml +microservice_a: + trigger: + include: path/to/microservice_a.yml + rules: + - if: $CI_MERGE_REQUEST_ID +``` + +- Configure the child pipeline by either: + + - Setting all jobs in the child pipeline to evaluate in the context of a merge request: + + ```yaml + # child path/to/microservice_a.yml + workflow: + rules: + - if: $CI_MERGE_REQUEST_ID + + job1: + script: ... + + job2: + script: ... + ``` + + - Alternatively, setting the rule per job. For example, to create only `job1` in + the context of merge request pipelines: + + ```yaml + # child path/to/microservice_a.yml + job1: + script: ... + rules: + - if: $CI_MERGE_REQUEST_ID + + job2: + script: ... + ``` + +### Dynamic child pipelines + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35632) in GitLab 12.9. + +Instead of running a child pipeline from a static YAML file, you can define a job that runs +your own script to generate a YAML file, which is then used to trigger a child pipeline. + +This technique can be very powerful in generating pipelines targeting content that changed or to +build a matrix of targets and architectures. + +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +For an overview, see [Create child pipelines using dynamically generated configurations](https://youtu.be/nMdfus2JWHM). + +We also have an example project using +[Dynamic Child Pipelines with Jsonnet](https://gitlab.com/gitlab-org/project-templates/jsonnet) +which shows how to use a data templating language to generate your `.gitlab-ci.yml` at runtime. +You could use a similar process for other templating languages like +[Dhall](https://dhall-lang.org/) or [ytt](https://get-ytt.io/). + +The artifact path is parsed by GitLab, not the runner, so the path must match the +syntax for the OS running GitLab. If GitLab is running on Linux but using a Windows +runner for testing, the path separator for the trigger job would be `/`. Other CI/CD +configuration for jobs, like scripts, that use the Windows runner would use `\`. + +For example, to trigger a child pipeline from a dynamically generated configuration file: + +```yaml +generate-config: + stage: build + script: generate-ci-config > generated-config.yml + artifacts: + paths: + - generated-config.yml + +child-pipeline: + stage: test + trigger: + include: + - artifact: generated-config.yml + job: generate-config +``` + +The `generated-config.yml` is extracted from the artifacts and used as the configuration +for triggering the child pipeline. + +In GitLab 12.9, the child pipeline could fail to be created in certain cases, causing the parent pipeline to fail. +This is [resolved](https://gitlab.com/gitlab-org/gitlab/-/issues/209070) in GitLab 12.10. + +### Nested child pipelines + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29651) in GitLab 13.4. +> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/243747) in GitLab 13.5. + +Parent and child pipelines were introduced with a maximum depth of one level of child +pipelines, which was later increased to two. A parent pipeline can trigger many child +pipelines, and these child pipelines can trigger their own child pipelines. It's not +possible to trigger another level of child pipelines. + +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +For an overview, see [Nested Dynamic Pipelines](https://youtu.be/C5j3ju9je2M). + ## View a downstream pipeline In the [pipeline graph view](index.md#view-full-pipeline-graph), downstream pipelines display @@ -203,7 +394,11 @@ To cancel a downstream pipeline that is still running, select **Cancel** (**{can > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8. You can mirror the pipeline status from the triggered pipeline to the source trigger job -by using [`strategy: depend`](../yaml/index.md#triggerstrategy). For example: +by using [`strategy: depend`](../yaml/index.md#triggerstrategy): + +::Tabs + +:::TabTitle Multi-Project pipeline ```yaml trigger_job: @@ -212,6 +407,18 @@ trigger_job: strategy: depend ``` +:::TabTitle Parent-child pipeline + +```yaml +trigger_job: + trigger: + include: + - local: path/to/child-pipeline.yml + strategy: depend +``` + +::EndTabs + ### View multi-project pipelines in pipeline graphs **(PREMIUM)** When you trigger a multi-project pipeline, the downstream pipeline displays diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md index 59ad47765b1c46..1e2b68901b37d2 100644 --- a/doc/ci/pipelines/index.md +++ b/doc/ci/pipelines/index.md @@ -57,7 +57,7 @@ Pipelines can be configured in many different ways: already been merged into the target branch. - [Merge trains](../pipelines/merge_trains.md) use merged results pipelines to queue merges one after the other. -- [Parent-child pipelines](parent_child_pipelines.md) break down complex pipelines +- [Parent-child pipelines](downstream_pipelines.md#parent-child-pipelines) break down complex pipelines into one parent pipeline that can trigger multiple child sub-pipelines, which all run in the same project and with the same SHA. This pipeline architecture is commonly used for mono-repos. - [Multi-project pipelines](downstream_pipelines.md#multi-project-pipelines) combine pipelines for different projects together. @@ -254,7 +254,7 @@ page, then selecting **Delete**.  Deleting a pipeline does not automatically delete its -[child pipelines](parent_child_pipelines.md). +[child pipelines](downstream_pipelines.md#parent-child-pipelines). See the [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/39503) for details. diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md index 00c898a7e0e59d..c6520f259c3217 100644 --- a/doc/ci/pipelines/job_artifacts.md +++ b/doc/ci/pipelines/job_artifacts.md @@ -305,7 +305,7 @@ the artifact. ## How searching for job artifacts works In [GitLab 13.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/201784), artifacts -for [parent and child pipelines](parent_child_pipelines.md) are searched in hierarchical +for [parent and child pipelines](downstream_pipelines.md#parent-child-pipelines) are searched in hierarchical order from parent to child. For example, if both parent and child pipelines have a job with the same name, the job artifact from the parent pipeline is returned. diff --git a/doc/ci/pipelines/parent_child_pipelines.md b/doc/ci/pipelines/parent_child_pipelines.md index 56c51d7b119b04..be8ed8ba6d79e5 100644 --- a/doc/ci/pipelines/parent_child_pipelines.md +++ b/doc/ci/pipelines/parent_child_pipelines.md @@ -1,221 +1,11 @@ --- -stage: Verify -group: Pipeline Authoring -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments -type: reference +redirect_to: 'downstream_pipelines.md' +remove_date: '2022-12-05' --- -# Parent-child pipelines **(FREE)** +This document was moved to [another location](downstream_pipelines.md). -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16094) in GitLab 12.7. - -As pipelines grow more complex, a few related problems start to emerge: - -- The staged structure, where all steps in a stage must be completed before the first - job in next stage begins, causes arbitrary waits, slowing things down. -- Configuration for the single global pipeline becomes very long and complicated, - making it hard to manage. -- Imports with [`include`](../yaml/index.md#include) increase the complexity of the configuration, and create the potential - for namespace collisions where jobs are unintentionally duplicated. -- Pipeline UX can become unwieldy with so many jobs and stages to work with. - -Additionally, sometimes the behavior of a pipeline needs to be more dynamic. The ability -to choose to start sub-pipelines (or not) is a powerful ability, especially if the -YAML is dynamically generated. - - - -Similarly to [multi-project pipelines](downstream_pipelines.md#multi-project-pipelines), a pipeline can trigger a -set of concurrently running [downstream](downstream_pipelines.md) child pipelines, but in the same project: - -- Child pipelines still execute each of their jobs according to a stage sequence, but - would be free to continue forward through their stages without waiting for unrelated - jobs in the parent pipeline to finish. -- The configuration is split up into smaller child pipeline configurations. Each child pipeline contains only relevant steps which are - easier to understand. This reduces the cognitive load to understand the overall configuration. -- Imports are done at the child pipeline level, reducing the likelihood of collisions. - -Child pipelines work well with other GitLab CI/CD features: - -- Use [`rules: changes`](../yaml/index.md#ruleschanges) to trigger pipelines only when - certain files change. This is useful for monorepos, for example. -- Since the parent pipeline in `.gitlab-ci.yml` and the child pipeline run as normal - pipelines, they can have their own behaviors and sequencing in relation to triggers. - -See the [`trigger`](../yaml/index.md#trigger) keyword documentation for full details on how to -include the child pipeline configuration. - -<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> -For an overview, see [Parent-Child Pipelines feature demo](https://youtu.be/n8KpBSqZNbk). - -NOTE: -The artifact containing the generated YAML file must not be larger than 5MB. - -## Examples - -The simplest case is [triggering a child pipeline](../yaml/index.md#trigger) using a -local YAML file to define the pipeline configuration. In this case, the parent pipeline -triggers the child pipeline, and continues without waiting: - -```yaml -microservice_a: - trigger: - include: path/to/microservice_a.yml -``` - -You can include multiple files when defining a child pipeline. The child pipeline's -configuration is composed of all configuration files merged together: - -```yaml -microservice_a: - trigger: - include: - - local: path/to/microservice_a.yml - - template: Security/SAST.gitlab-ci.yml -``` - -In [GitLab 13.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/205157), -you can use [`include:file`](../yaml/index.md#includefile) to trigger child pipelines -with a configuration file in a different project: - -```yaml -microservice_a: - trigger: - include: - - project: 'my-group/my-pipeline-library' - ref: 'main' - file: '/path/to/child-pipeline.yml' -``` - -The maximum number of entries that are accepted for `trigger:include` is three. - -Similar to [multi-project pipelines](downstream_pipelines.md#multi-project-pipelines), we can set the -parent pipeline to [depend on the status](downstream_pipelines.md#mirror-the-status-of-a-downstream-pipeline-in-the-trigger-job) -of the child pipeline upon completion: - -```yaml -microservice_a: - trigger: - include: - - local: path/to/microservice_a.yml - - template: Security/SAST.gitlab-ci.yml - strategy: depend -``` - -## Merge request child pipelines - -To trigger a child pipeline as a [merge request pipeline](merge_request_pipelines.md) we need to: - -- Set the trigger job to run on merge requests: - -```yaml -# parent .gitlab-ci.yml -microservice_a: - trigger: - include: path/to/microservice_a.yml - rules: - - if: $CI_MERGE_REQUEST_ID -``` - -- Configure the child pipeline by either: - - - Setting all jobs in the child pipeline to evaluate in the context of a merge request: - - ```yaml - # child path/to/microservice_a.yml - workflow: - rules: - - if: $CI_MERGE_REQUEST_ID - - job1: - script: ... - - job2: - script: ... - ``` - - - Alternatively, setting the rule per job. For example, to create only `job1` in - the context of merge request pipelines: - - ```yaml - # child path/to/microservice_a.yml - job1: - script: ... - rules: - - if: $CI_MERGE_REQUEST_ID - - job2: - script: ... - ``` - -## Dynamic child pipelines - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35632) in GitLab 12.9. - -Instead of running a child pipeline from a static YAML file, you can define a job that runs -your own script to generate a YAML file, which is then used to trigger a child pipeline. - -This technique can be very powerful in generating pipelines targeting content that changed or to -build a matrix of targets and architectures. - -<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> -For an overview, see [Create child pipelines using dynamically generated configurations](https://youtu.be/nMdfus2JWHM). - -We also have an example project using -[Dynamic Child Pipelines with Jsonnet](https://gitlab.com/gitlab-org/project-templates/jsonnet) -which shows how to use a data templating language to generate your `.gitlab-ci.yml` at runtime. -You could use a similar process for other templating languages like -[Dhall](https://dhall-lang.org/) or [ytt](https://get-ytt.io/). - -The artifact path is parsed by GitLab, not the runner, so the path must match the -syntax for the OS running GitLab. If GitLab is running on Linux but using a Windows -runner for testing, the path separator for the trigger job would be `/`. Other CI/CD -configuration for jobs, like scripts, that use the Windows runner would use `\`. - -In GitLab 12.9, the child pipeline could fail to be created in certain cases, causing the parent pipeline to fail. -This is [resolved](https://gitlab.com/gitlab-org/gitlab/-/issues/209070) in GitLab 12.10. - -### Dynamic child pipeline example - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35632) in GitLab 12.9. - -You can trigger a child pipeline from a [dynamically generated configuration file](../pipelines/parent_child_pipelines.md#dynamic-child-pipelines): - -```yaml -generate-config: - stage: build - script: generate-ci-config > generated-config.yml - artifacts: - paths: - - generated-config.yml - -child-pipeline: - stage: test - trigger: - include: - - artifact: generated-config.yml - job: generate-config -``` - -The `generated-config.yml` is extracted from the artifacts and used as the configuration -for triggering the child pipeline. - -## Nested child pipelines - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29651) in GitLab 13.4. -> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/243747) in GitLab 13.5. - -Parent and child pipelines were introduced with a maximum depth of one level of child -pipelines, which was later increased to two. A parent pipeline can trigger many child -pipelines, and these child pipelines can trigger their own child pipelines. It's not -possible to trigger another level of child pipelines. - -<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> -For an overview, see [Nested Dynamic Pipelines](https://youtu.be/C5j3ju9je2M). - -## Pass CI/CD variables to a child pipeline - -You can pass variables to a downstream pipeline: - -- [By using the `variables` keyword](downstream_pipelines.md#pass-yaml-defined-cicd-variables). -- [By using dotenv variable inheritance](downstream_pipelines.md#pass-dotenv-variables-created-in-a-job). +<!-- This redirect file can be deleted after <2022-12-05>. --> +<!-- Redirects that point to other docs in the same project expire in three months. --> +<!-- Redirects that point to docs in a different project or site (link is not relative and starts with `https:`) expire in one year. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> diff --git a/doc/ci/pipelines/pipeline_architectures.md b/doc/ci/pipelines/pipeline_architectures.md index 3ff22a169008c0..4058df6ec1ba79 100644 --- a/doc/ci/pipelines/pipeline_architectures.md +++ b/doc/ci/pipelines/pipeline_architectures.md @@ -162,7 +162,7 @@ deploy_b: ## Child / Parent Pipelines In the examples above, it's clear we've got two types of things that could be built independently. -This is an ideal case for using [Child / Parent Pipelines](parent_child_pipelines.md)) via +This is an ideal case for using [Child / Parent Pipelines](downstream_pipelines.md#parent-child-pipelines)) via the [`trigger` keyword](../yaml/index.md#trigger). It separates out the configuration into multiple files, keeping things very simple. You can also combine this with: diff --git a/doc/ci/pipelines/pipeline_efficiency.md b/doc/ci/pipelines/pipeline_efficiency.md index ad43895d7ef5ee..72711f9b9dd82a 100644 --- a/doc/ci/pipelines/pipeline_efficiency.md +++ b/doc/ci/pipelines/pipeline_efficiency.md @@ -187,7 +187,7 @@ shouldn't run, saving pipeline resources. In a basic configuration, jobs always wait for all other jobs in earlier stages to complete before running. This is the simplest configuration, but it's also the slowest in most cases. [Directed Acyclic Graphs](../directed_acyclic_graph/index.md) and -[parent/child pipelines](parent_child_pipelines.md) are more flexible and can +[parent/child pipelines](downstream_pipelines.md#parent-child-pipelines) are more flexible and can be more efficient, but can also make pipelines harder to understand and analyze. ### Caching diff --git a/doc/ci/resource_groups/index.md b/doc/ci/resource_groups/index.md index c215ef412a11f4..dff52a742a8494 100644 --- a/doc/ci/resource_groups/index.md +++ b/doc/ci/resource_groups/index.md @@ -210,7 +210,7 @@ Read more how you can use GitLab for [safe deployments](../environments/deployme Because [`oldest_first` process mode](#process-modes) enforces the jobs to be executed in a pipeline order, there is a case that it doesn't work well with the other CI features. -For example, when you run [a child pipeline](../pipelines/parent_child_pipelines.md) +For example, when you run [a child pipeline](../pipelines/downstream_pipelines.md#parent-child-pipelines) that requires the same resource group with the parent pipeline, a dead lock could happen. Here is an example of a _bad_ setup: diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md index 34bd0602ca55cd..33dc77c45a9d82 100644 --- a/doc/ci/troubleshooting.md +++ b/doc/ci/troubleshooting.md @@ -89,9 +89,9 @@ if you are using that type: - [Multi-project pipelines](pipelines/downstream_pipelines.md#multi-project-pipelines): Have your pipeline trigger a pipeline in a different project. -- [Parent/child pipelines](pipelines/parent_child_pipelines.md): Have your main pipeline trigger +- [Parent/child pipelines](pipelines/downstream_pipelines.md#parent-child-pipelines): Have your main pipeline trigger and run separate pipelines in the same project. You can also - [dynamically generate the child pipeline's configuration](pipelines/parent_child_pipelines.md#dynamic-child-pipelines) + [dynamically generate the child pipeline's configuration](pipelines/downstream_pipelines.md#dynamic-child-pipelines) at runtime. - [Merge request pipelines](pipelines/merge_request_pipelines.md): Run a pipeline in the context of a merge request. @@ -316,7 +316,7 @@ To reduce the configuration size, you can: [merged YAML](pipeline_editor/index.md#view-expanded-configuration) tab. Look for duplicated configuration that can be removed or simplified. - Move long or repeated `script` sections into standalone scripts in the project. -- Use [parent and child pipelines](pipelines/parent_child_pipelines.md) to move some +- Use [parent and child pipelines](pipelines/downstream_pipelines.md#parent-child-pipelines) to move some work to jobs in an independent child pipeline. On a self-managed instance, you can [increase the size limits](../administration/instance_limits.md#maximum-size-and-depth-of-cicd-configuration-yaml-files). diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index a5d881b6864d5a..a8154a7ae4ef6c 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -1384,7 +1384,7 @@ In this example: for the coverage number. - If there are multiple coverage numbers found in the matched fragment, the first number is used. - Leading zeros are removed. -- Coverage output from [child pipelines](../pipelines/parent_child_pipelines.md) +- Coverage output from [child pipelines](../pipelines/downstream_pipelines.md#parent-child-pipelines) is not recorded or displayed. Check [the related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/280818) for more details. @@ -2283,14 +2283,14 @@ build_job: **Related topics**: -- To download artifacts between [parent-child pipelines](../pipelines/parent_child_pipelines.md), +- To download artifacts between [parent-child pipelines](../pipelines/downstream_pipelines.md#parent-child-pipelines), use [`needs:pipeline:job`](#needspipelinejob). #### `needs:pipeline:job` > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255983) in GitLab 13.7. -A [child pipeline](../pipelines/parent_child_pipelines.md) can download artifacts from a job in +A [child pipeline](../pipelines/downstream_pipelines.md#parent-child-pipelines) can download artifacts from a job in its parent pipeline or another child pipeline in the same parent-child pipeline hierarchy. **Keyword type**: Job keyword. You can use it only as part of a job. @@ -3881,7 +3881,7 @@ Use `trigger` to declare that a job is a "trigger job" which starts a [downstream pipeline](../pipelines/downstream_pipelines.md) that is either: - [A multi-project pipeline](../pipelines/downstream_pipelines.md#multi-project-pipelines). -- [A child pipeline](../pipelines/parent_child_pipelines.md). +- [A child pipeline](../pipelines/downstream_pipelines.md#parent-child-pipelines). Trigger jobs can use only a limited set of the GitLab CI/CD configuration keywords. The keywords available for use in trigger jobs are: @@ -3942,7 +3942,7 @@ trigger_job: **Related topics**: - [Multi-project pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-from-a-job-in-your-gitlab-ciyml-file). -- [Child pipeline configuration examples](../pipelines/parent_child_pipelines.md#examples). +- [Child pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-parent-child-pipeline). - To run a pipeline for a specific branch, tag, or commit, you can use a [trigger token](../triggers/index.md) to authenticate with the [pipeline triggers API](../../api/pipeline_triggers.md). The trigger token is different than the `trigger` keyword. @@ -3987,7 +3987,7 @@ successfully complete before starting. > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/355572) in GitLab 15.1. [Feature flag `ci_trigger_forward_variables`](https://gitlab.com/gitlab-org/gitlab/-/issues/355572) removed. Use `trigger:forward` to specify what to forward to the downstream pipeline. You can control -what is forwarded to both [parent-child pipelines](../pipelines/parent_child_pipelines.md) +what is forwarded to both [parent-child pipelines](../pipelines/downstream_pipelines.md#parent-child-pipelines) and [multi-project pipelines](../pipelines/downstream_pipelines.md#multi-project-pipelines). **Possible inputs**: diff --git a/doc/user/application_security/coverage_fuzzing/index.md b/doc/user/application_security/coverage_fuzzing/index.md index 0297dc161e3804..b7f3d273e326c3 100644 --- a/doc/user/application_security/coverage_fuzzing/index.md +++ b/doc/user/application_security/coverage_fuzzing/index.md @@ -277,7 +277,7 @@ For a complete example, read the [Go coverage-guided fuzzing example](https://gi It's also possible to run the coverage-guided fuzzing jobs longer and without blocking your main pipeline. This configuration uses the GitLab -[parent-child pipelines](../../../ci/pipelines/parent_child_pipelines.md). +[parent-child pipelines](../../../ci/pipelines/downstream_pipelines.md#parent-child-pipelines). The suggested workflow in this scenario is to have long-running, asynchronous fuzzing jobs on the main or development branch, and short synchronous fuzzing jobs on all other branches and MRs. This diff --git a/doc/user/application_security/policies/scan-execution-policies.md b/doc/user/application_security/policies/scan-execution-policies.md index c253b8a1092207..0ca38fe5f484cd 100644 --- a/doc/user/application_security/policies/scan-execution-policies.md +++ b/doc/user/application_security/policies/scan-execution-policies.md @@ -128,7 +128,7 @@ Note the following: - A container scanning and cluster image scanning scans configured for the `pipeline` rule type ignores the cluster defined in the `clusters` object. They use predefined CI/CD variables defined for your project. Cluster selection with the `clusters` object is supported for the `schedule` rule type. A cluster with a name provided in the `clusters` object must be created and configured for the project. -- The SAST scan uses the default template and runs in a [child pipeline](../../../ci/pipelines/parent_child_pipelines.md). +- The SAST scan uses the default template and runs in a [child pipeline](../../../ci/pipelines/downstream_pipelines.md#parent-child-pipelines). ## Example security policies project diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index ec408081d68a0a..900e2bd1ca9714 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -214,7 +214,7 @@ Compliance pipelines start on the run of _every_ pipeline in a relevant project. triggers a child pipeline, the compliance pipeline runs first. This can trigger the parent pipeline, instead of the child pipeline. Therefore, in projects with compliance frameworks, we recommend replacing -[parent-child pipelines](../../../ci/pipelines/parent_child_pipelines.md) with the following: +[parent-child pipelines](../../../ci/pipelines/downstream_pipelines.md#parent-child-pipelines) with the following: - Direct [`include`](../../../ci/yaml/index.md#include) statements that provide the parent pipeline with child pipeline configuration. - Child pipelines placed in another project that are run using the [trigger API](../../../ci/triggers/index.md) rather than the parent-child -- GitLab From 5eca99c054c0ca14a10261354a84504091cd674e Mon Sep 17 00:00:00 2001 From: Lee Tickett <lee@tickett.net> Date: Mon, 5 Sep 2022 15:14:22 +0000 Subject: [PATCH 059/169] Add JobArtifactsDestroy GraphQL mutation Changelog: added --- .../mutations/ci/job/artifacts_destroy.rb | 38 +++++++++++++ app/graphql/types/mutation_type.rb | 1 + app/policies/ci/build_policy.rb | 2 + doc/api/graphql/reference/index.md | 20 +++++++ .../graphql/mutations/ci/job/destroy_spec.rb | 54 +++++++++++++++++++ 5 files changed, 115 insertions(+) create mode 100644 app/graphql/mutations/ci/job/artifacts_destroy.rb create mode 100644 spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb diff --git a/app/graphql/mutations/ci/job/artifacts_destroy.rb b/app/graphql/mutations/ci/job/artifacts_destroy.rb new file mode 100644 index 00000000000000..c27ab9c4d89bdc --- /dev/null +++ b/app/graphql/mutations/ci/job/artifacts_destroy.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module Job + class ArtifactsDestroy < Base + graphql_name 'JobArtifactsDestroy' + + authorize :destroy_artifacts + + field :job, + Types::Ci::JobType, + null: true, + description: 'Job with artifacts to be deleted.' + + field :destroyed_artifacts_count, + GraphQL::Types::Int, + null: false, + description: 'Number of artifacts deleted.' + + def find_object(id: ) + GlobalID::Locator.locate(id) + end + + def resolve(id:) + job = authorized_find!(id: id) + + result = ::Ci::JobArtifacts::DestroyBatchService.new(job.job_artifacts, pick_up_at: Time.current).execute + { + job: job, + destroyed_artifacts_count: result[:destroyed_artifacts_count], + errors: Array(result[:errors]) + } + end + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index e1806e5b19acc8..3cd7d612bc0a07 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -120,6 +120,7 @@ class MutationType < BaseObject milestone: '15.0' } mount_mutation Mutations::Ci::ProjectCiCdSettingsUpdate + mount_mutation Mutations::Ci::Job::ArtifactsDestroy mount_mutation Mutations::Ci::Job::Play mount_mutation Mutations::Ci::Job::Retry mount_mutation Mutations::Ci::Job::Cancel diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index f377ff85b5e3e4..459ebed97913e6 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -2,6 +2,8 @@ module Ci class BuildPolicy < CommitStatusPolicy + delegate { @subject.project } + condition(:protected_ref) do access = ::Gitlab::UserAccess.new(@user, container: @subject.project) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 089c6e0d766c7f..dc41bac1b890e9 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -3463,6 +3463,26 @@ Input type: `JiraImportUsersInput` | <a id="mutationjiraimportuserserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationjiraimportusersjirausers"></a>`jiraUsers` | [`[JiraUser!]`](#jirauser) | Users returned from Jira, matched by email and name if possible. | +### `Mutation.jobArtifactsDestroy` + +Input type: `JobArtifactsDestroyInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationjobartifactsdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationjobartifactsdestroyid"></a>`id` | [`CiBuildID!`](#cibuildid) | ID of the job to mutate. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationjobartifactsdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationjobartifactsdestroydestroyedartifactscount"></a>`destroyedArtifactsCount` | [`Int!`](#int) | Number of artifacts deleted. | +| <a id="mutationjobartifactsdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationjobartifactsdestroyjob"></a>`job` | [`CiJob`](#cijob) | Job with artifacts to be deleted. | + ### `Mutation.jobCancel` Input type: `JobCancelInput` diff --git a/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb new file mode 100644 index 00000000000000..5855eb6bb51ea8 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/job/destroy_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'JobArtifactsDestroy' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:job) { create(:ci_build) } + + let(:mutation) do + variables = { + id: job.to_global_id.to_s + } + graphql_mutation(:job_artifacts_destroy, variables, <<~FIELDS) + job { + name + } + destroyedArtifactsCount + errors + FIELDS + end + + before do + create(:ci_job_artifact, :archive, job: job) + create(:ci_job_artifact, :junit, job: job) + end + + it 'returns an error if the user is not allowed to destroy the job artifacts' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + expect(job.reload.job_artifacts.count).to be(2) + end + + it 'destroys the job artifacts and returns the expected data' do + job.project.add_maintainer(user) + expected_data = { + 'jobArtifactsDestroy' => { + 'errors' => [], + 'destroyedArtifactsCount' => 2, + 'job' => { + 'name' => job.name + } + } + } + + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_data).to eq(expected_data) + expect(job.reload.job_artifacts.count).to be(0) + end +end -- GitLab From b666446e02304ea2b6630e05d150a749b5a4a875 Mon Sep 17 00:00:00 2001 From: Philip Becker <philip.becker@katalon.com> Date: Mon, 5 Sep 2022 15:37:03 +0000 Subject: [PATCH 060/169] Add new Katalon ci/cd template for partnership program Changelog: added MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86484 --- ..._templates_total_unique_counts_monthly.yml | 1 + ...1145023_p_ci_templates_katalon_monthly.yml | 25 +++++++ ...i_templates_total_unique_counts_weekly.yml | 1 + ...31145014_p_ci_templates_katalon_weekly.yml | 25 +++++++ lib/gitlab/ci/templates/Katalon.gitlab-ci.yml | 65 +++++++++++++++++++ .../known_events/ci_templates.yml | 4 ++ .../templates/katalon_gitlab_ci_yaml_spec.rb | 52 +++++++++++++++ 7 files changed, 173 insertions(+) create mode 100644 config/metrics/counts_28d/20220531145023_p_ci_templates_katalon_monthly.yml create mode 100644 config/metrics/counts_7d/20220531145014_p_ci_templates_katalon_weekly.yml create mode 100644 lib/gitlab/ci/templates/Katalon.gitlab-ci.yml create mode 100644 spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb diff --git a/config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml b/config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml index a2a97eb7477208..6f32243c8f8969 100755 --- a/config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml +++ b/config/metrics/counts_28d/20210216184559_ci_templates_total_unique_counts_monthly.yml @@ -172,6 +172,7 @@ options: - p_ci_templates_liquibase - p_ci_templates_matlab - p_ci_templates_themekit + - p_ci_templates_katalon distribution: - ce - ee diff --git a/config/metrics/counts_28d/20220531145023_p_ci_templates_katalon_monthly.yml b/config/metrics/counts_28d/20220531145023_p_ci_templates_katalon_monthly.yml new file mode 100644 index 00000000000000..abbc30a5d109b3 --- /dev/null +++ b/config/metrics/counts_28d/20220531145023_p_ci_templates_katalon_monthly.yml @@ -0,0 +1,25 @@ +--- +key_path: redis_hll_counters.ci_templates.p_ci_templates_katalon_monthly +description: 'Monthly counts of times users have executed katalon_tests jobs' +product_section: 'ops' +product_stage: 'analytics' +product_group: 'pipeline_authoring' +product_category: 'pipeline_authoring' +value_type: number +status: active +milestone: "15.4" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86484 +time_frame: 28d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - p_ci_templates_katalon +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml b/config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml index aa25ab379b9e91..faaf5be63a0972 100755 --- a/config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml +++ b/config/metrics/counts_7d/20210216184557_ci_templates_total_unique_counts_weekly.yml @@ -172,6 +172,7 @@ options: - p_ci_templates_liquibase - p_ci_templates_matlab - p_ci_templates_themekit + - p_ci_templates_katalon distribution: - ce - ee diff --git a/config/metrics/counts_7d/20220531145014_p_ci_templates_katalon_weekly.yml b/config/metrics/counts_7d/20220531145014_p_ci_templates_katalon_weekly.yml new file mode 100644 index 00000000000000..f668646eacb217 --- /dev/null +++ b/config/metrics/counts_7d/20220531145014_p_ci_templates_katalon_weekly.yml @@ -0,0 +1,25 @@ +--- +key_path: redis_hll_counters.ci_templates.p_ci_templates_katalon_weekly +description: 'Weekly counts of times users have executed katalon_tests jobs' +product_section: 'ops' +product_stage: 'analytics' +product_group: 'pipeline_authoring' +product_category: 'pipeline_authoring' +value_type: number +status: active +milestone: "15.4" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86484 +time_frame: 7d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - p_ci_templates_katalon +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/lib/gitlab/ci/templates/Katalon.gitlab-ci.yml b/lib/gitlab/ci/templates/Katalon.gitlab-ci.yml new file mode 100644 index 00000000000000..c8939c8f5a2d19 --- /dev/null +++ b/lib/gitlab/ci/templates/Katalon.gitlab-ci.yml @@ -0,0 +1,65 @@ +# This template is provided and maintained by Katalon, an official Technology Partner with GitLab. +# +# Use this template to run a Katalon Studio test from this repository. +# You can: +# - Copy and paste this template into a new `.gitlab-ci.yml` file. +# - Add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword. +# +# In either case, you must also select which job you want to run, `.katalon_tests` +# or `.katalon_tests_with_artifacts` (see configuration below), and add that configuration +# to a new job with `extends:`. For example: +# +# Katalon-tests: +# extends: +# - .katalon_tests_with_artifacts +# +# Requirements: +# - A Katalon Studio project with the content saved in the root GitLab repository folder. +# - An active KRE license. +# - A valid Katalon API key. +# +# CI/CD variables, set in the project CI/CD settings: +# - KATALON_TEST_SUITE_PATH: The default path is `Test Suites/<Your Test Suite Name>`. +# Defines which test suite to run. +# - KATALON_API_KEY: The Katalon API key. +# - KATALON_PROJECT_DIR: Optional. Add if the project is in another location. +# - KATALON_ORG_ID: Optional. Add if you are part of multiple Katalon orgs. +# Set to the Org ID that has KRE licenses assigned. For more info on the Org ID, +# see https://support.katalon.com/hc/en-us/articles/4724459179545-How-to-get-Organization-ID- + +.katalon_tests: + # Use the latest version of the Katalon Runtime Engine. You can also use other versions of the + # Katalon Runtime Engine by specifying another tag, for example `katalonstudio/katalon:8.1.2` + # or `katalonstudio/katalon:8.3.0`. + image: 'katalonstudio/katalon' + services: + - docker:dind + variables: + # Specify the Katalon Studio project directory. By default, it is stored under the root project folder. + KATALON_PROJECT_DIR: $CI_PROJECT_DIR + + # The following bash script has two different versions, one if you set the KATALON_ORG_ID + # CI/CD variable, and the other if you did not set it. If you have more than one org in + # admin.katalon.com you must set the KATALON_ORG_ID variable with an ORG ID or + # the Katalon Test Suite fails to run. + # + # You can update or add additional `katalonc` commands below. To see all of the arguments + # `katalonc` supports, go to https://docs.katalon.com/katalon-studio/docs/console-mode-execution.html + script: + - |- + if [[ $KATALON_ORG_ID == "" ]]; then + katalonc.sh -projectPath=$KATALON_PROJECT_DIR -apiKey=$KATALON_API_KEY -browserType="Chrome" -retry=0 -statusDelay=20 -testSuitePath="$KATALON_TEST_SUITE_PATH" -reportFolder=Reports/ + else + katalonc.sh -projectPath=$KATALON_PROJECT_DIR -apiKey=$KATALON_API_KEY -browserType="Chrome" -retry=0 -statusDelay=20 -orgID=$KATALON_ORG_ID -testSuitePath="$KATALON_TEST_SUITE_PATH" -reportFolder=Reports/ + fi + +# Upload the artifacts and make the junit report accessible under the Pipeline Tests +.katalon_tests_with_artifacts: + extends: .katalon_tests + artifacts: + when: always + paths: + - Reports/ + reports: + junit: + Reports/*/*/*/*.xml diff --git a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml index a8f1bab1f208fd..c4074f70d91801 100644 --- a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml +++ b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml @@ -231,6 +231,10 @@ category: ci_templates redis_slot: ci_templates aggregation: weekly +- name: p_ci_templates_katalon + category: ci_templates + redis_slot: ci_templates + aggregation: weekly - name: p_ci_templates_mono category: ci_templates redis_slot: ci_templates diff --git a/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000000..5a62324da7414d --- /dev/null +++ b/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Katalon.gitlab-ci.yml' do + subject(:template) do + <<~YAML + include: + - template: 'Katalon.gitlab-ci.yml' + + katalon_tests_placeholder: + extends: .katalon_tests + stage: test + script: + - echo "katalon tests" + + katalon_tests_with_artifacts_placeholder: + extends: .katalon_tests_with_artifacts + stage: test + script: + - echo "katalon tests with artifacts" + YAML + end + + describe 'the created pipeline' do + let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:user) { project.first_owner } + + let(:service) { Ci::CreatePipelineService.new(project, user, ref: 'master' ) } + let(:pipeline) { service.execute!(:push).payload } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template) + end + + it 'create katalon tests jobs' do + expect(build_names).to match_array(%w[katalon_tests_placeholder katalon_tests_with_artifacts_placeholder]) + + expect(pipeline.builds.find_by(name: 'katalon_tests_placeholder').options).to include( + image: { name: 'katalonstudio/katalon' }, + services: [{ name: 'docker:dind' }] + ) + + expect(pipeline.builds.find_by(name: 'katalon_tests_with_artifacts_placeholder').options).to include( + image: { name: 'katalonstudio/katalon' }, + services: [{ name: 'docker:dind' }], + artifacts: { when: 'always', paths: ['Reports/'], reports: { junit: ['Reports/*/*/*/*.xml'] } } + ) + end + end +end -- GitLab From e0a6c25c05147119934793cc2e90b8f13b0a2e8e Mon Sep 17 00:00:00 2001 From: Arran Walker <ajwalker@gitlab.com> Date: Mon, 5 Sep 2022 15:46:21 +0100 Subject: [PATCH 061/169] workhorse: Improve perf for LSIF hover entries with markdown lsif-go and other tools commonly output a 'markdown' hover entry, rather than specifying a language alongside a fragment of code. Unfortunately, the lexer library we use to process code fragments enters a very slow code path if no language, or an unknown language, is specified. The reason for the slow path is because if no language is immediately matched against an index, it starts to use filepath matching, using the language name as either a filename or as a filename extension. It's very unlikely that the language provided will be a filename, and if it is a common extension, this will most likely already be registered and indexed as an alias. This change bails early if no language is provided, and guards against the slow filepath matches. A benchmark has been added to help detect such a scenario. --- .../lsif_transformer/parser/code_hover.go | 28 +++++++++++ .../parser/code_hover_test.go | 48 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/workhorse/internal/lsif_transformer/parser/code_hover.go b/workhorse/internal/lsif_transformer/parser/code_hover.go index 25550cce29e3bf..ab3ab291432f92 100644 --- a/workhorse/internal/lsif_transformer/parser/code_hover.go +++ b/workhorse/internal/lsif_transformer/parser/code_hover.go @@ -28,6 +28,16 @@ type truncatableString struct { Truncated bool } +// supportedLexerLanguages is used for a fast lookup to ensure the language +// is supported by the lexer library. +var supportedLexerLanguages = map[string]struct{}{} + +func init() { + for _, name := range lexers.Names(true) { + supportedLexerLanguages[name] = struct{}{} + } +} + func (ts *truncatableString) UnmarshalText(b []byte) error { s := 0 for i := 0; s < len(b); i++ { @@ -93,6 +103,24 @@ func newCodeHover(content json.RawMessage) (*codeHover, error) { } func (c *codeHover) setTokens() { + // fastpath: bail early if no language specified + if c.Language == "" { + return + } + + // fastpath: lexer.Get() will first match against indexed languages by + // name and alias, and then fallback to a very slow filepath match. We + // avoid this slow path by first checking against languages we know to + // be within the index, and bailing if not found. + // + // Not case-folding immediately is done intentionally. These two lookups + // mirror the behaviour of lexer.Get(). + if _, ok := supportedLexerLanguages[c.Language]; !ok { + if _, ok := supportedLexerLanguages[strings.ToLower(c.Language)]; !ok { + return + } + } + lexer := lexers.Get(c.Language) if lexer == nil { return diff --git a/workhorse/internal/lsif_transformer/parser/code_hover_test.go b/workhorse/internal/lsif_transformer/parser/code_hover_test.go index c09636b2f7627e..7dc9e126ae7eb1 100644 --- a/workhorse/internal/lsif_transformer/parser/code_hover_test.go +++ b/workhorse/internal/lsif_transformer/parser/code_hover_test.go @@ -55,6 +55,14 @@ func TestHighlight(t *testing.T) { {{Class: "k", Value: "end"}}, }, }, + { + name: "ruby by file extension", + language: "rb", + value: `print hello`, + want: [][]token{ + {{Value: "print hello"}}, + }, + }, { name: "unknown/malicious language is passed", language: "<lang> alert(1); </lang>", @@ -116,3 +124,43 @@ func TestTruncatingMultiByteChars(t *testing.T) { symbolSize := 3 require.Equal(t, value[0:maxValueSize*symbolSize], c.TruncatedValue.Value) } + +func BenchmarkHighlight(b *testing.B) { + type entry struct { + Language string `json:"language"` + Value string `json:"value"` + } + + tests := []entry{ + { + Language: "go", + Value: "func main()", + }, + { + Language: "ruby", + Value: "def read(line)", + }, + { + Language: "", + Value: "<html><head>foobar</head></html>", + }, + { + Language: "zzz", + Value: "def read(line)", + }, + } + + for _, tc := range tests { + b.Run("lang:"+tc.Language, func(b *testing.B) { + raw, err := json.Marshal(tc) + require.NoError(b, err) + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _, err := newCodeHovers(raw) + require.NoError(b, err) + } + }) + } +} -- GitLab From ae7ebb99689de946da3f4c7d757acd9cb81ce99a Mon Sep 17 00:00:00 2001 From: Mireya Andres <mandres@gitlab.com> Date: Mon, 5 Sep 2022 16:54:16 +0000 Subject: [PATCH 062/169] Set feature flag and create GraphQL app for Run Pipeline form This serves as the foundation for migrating the Run Pipeline form to use GraphQL endpoints. For now we are just setting the feature flag and creating a copy GraphQL app which we will develop in future MRs. --- .../components/legacy_pipeline_new_form.vue | 490 ++++++++++++++++++ app/assets/javascripts/pipeline_new/index.js | 75 ++- .../projects/pipelines_controller.rb | 1 + .../development/run_pipeline_graphql.yml | 8 + qa/qa/page/project/pipeline/new.rb | 2 +- ...late_new_pipeline_vars_with_params_spec.rb | 42 +- .../pipelines/legacy_pipelines_spec.rb | 1 + .../projects/pipelines/pipelines_spec.rb | 61 ++- .../legacy_pipeline_new_form_spec.js | 456 ++++++++++++++++ 9 files changed, 1100 insertions(+), 36 deletions(-) create mode 100644 app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue create mode 100644 config/feature_flags/development/run_pipeline_graphql.yml create mode 100644 spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js diff --git a/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue new file mode 100644 index 00000000000000..529ec4897b47f9 --- /dev/null +++ b/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue @@ -0,0 +1,490 @@ +<script> +import { + GlAlert, + GlIcon, + GlButton, + GlDropdown, + GlDropdownItem, + GlForm, + GlFormGroup, + GlFormInput, + GlFormTextarea, + GlLink, + GlSprintf, + GlLoadingIcon, + GlSafeHtmlDirective as SafeHtml, +} from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { uniqueId } from 'lodash'; +import Vue from 'vue'; +import axios from '~/lib/utils/axios_utils'; +import { backOff } from '~/lib/utils/common_utils'; +import httpStatusCodes from '~/lib/utils/http_status'; +import { redirectTo } from '~/lib/utils/url_utility'; +import { s__, __, n__ } from '~/locale'; +import { + VARIABLE_TYPE, + FILE_TYPE, + CONFIG_VARIABLES_TIMEOUT, + CC_VALIDATION_REQUIRED_ERROR, +} from '../constants'; +import filterVariables from '../utils/filter_variables'; +import RefsDropdown from './refs_dropdown.vue'; + +const i18n = { + variablesDescription: s__( + 'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.', + ), + defaultError: __('Something went wrong on our end. Please try again.'), + refsLoadingErrorTitle: s__('Pipeline|Branches or tags could not be loaded.'), + submitErrorTitle: s__('Pipeline|Pipeline cannot be run.'), + warningTitle: __('The form contains the following warning:'), + maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'), + removeVariableLabel: s__('CiVariables|Remove variable'), +}; + +export default { + typeOptions: { + [VARIABLE_TYPE]: __('Variable'), + [FILE_TYPE]: __('File'), + }, + i18n, + formElementClasses: 'gl-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0', + // this height value is used inline on the textarea to match the input field height + // it's used to prevent the overwrite if 'gl-h-7' or 'gl-h-7!' were used + textAreaStyle: { height: '32px' }, + components: { + GlAlert, + GlIcon, + GlButton, + GlDropdown, + GlDropdownItem, + GlForm, + GlFormGroup, + GlFormInput, + GlFormTextarea, + GlLink, + GlSprintf, + GlLoadingIcon, + RefsDropdown, + CcValidationRequiredAlert: () => + import('ee_component/billings/components/cc_validation_required_alert.vue'), + }, + directives: { SafeHtml }, + props: { + pipelinesPath: { + type: String, + required: true, + }, + configVariablesPath: { + type: String, + required: true, + }, + defaultBranch: { + type: String, + required: true, + }, + projectId: { + type: String, + required: true, + }, + settingsLink: { + type: String, + required: true, + }, + fileParams: { + type: Object, + required: false, + default: () => ({}), + }, + refParam: { + type: String, + required: false, + default: '', + }, + variableParams: { + type: Object, + required: false, + default: () => ({}), + }, + maxWarnings: { + type: Number, + required: true, + }, + }, + data() { + return { + refValue: { + shortName: this.refParam, + }, + form: {}, + errorTitle: null, + error: null, + warnings: [], + totalWarnings: 0, + isWarningDismissed: false, + isLoading: false, + submitted: false, + ccAlertDismissed: false, + }; + }, + computed: { + overMaxWarningsLimit() { + return this.totalWarnings > this.maxWarnings; + }, + warningsSummary() { + return n__('%d warning found:', '%d warnings found:', this.warnings.length); + }, + summaryMessage() { + return this.overMaxWarningsLimit ? i18n.maxWarningsSummary : this.warningsSummary; + }, + shouldShowWarning() { + return this.warnings.length > 0 && !this.isWarningDismissed; + }, + refShortName() { + return this.refValue.shortName; + }, + refFullName() { + return this.refValue.fullName; + }, + variables() { + return this.form[this.refFullName]?.variables ?? []; + }, + descriptions() { + return this.form[this.refFullName]?.descriptions ?? {}; + }, + ccRequiredError() { + return this.error === CC_VALIDATION_REQUIRED_ERROR && !this.ccAlertDismissed; + }, + }, + watch: { + refValue() { + this.loadConfigVariablesForm(); + }, + }, + created() { + // this is needed until we add support for ref type in url query strings + // ensure default branch is called with full ref on load + // https://gitlab.com/gitlab-org/gitlab/-/issues/287815 + if (this.refValue.shortName === this.defaultBranch) { + this.refValue.fullName = `refs/heads/${this.refValue.shortName}`; + } + + this.loadConfigVariablesForm(); + }, + methods: { + addEmptyVariable(refValue) { + const { variables } = this.form[refValue]; + + const lastVar = variables[variables.length - 1]; + if (lastVar?.key === '' && lastVar?.value === '') { + return; + } + + variables.push({ + uniqueId: uniqueId(`var-${refValue}`), + variable_type: VARIABLE_TYPE, + key: '', + value: '', + }); + }, + setVariable(refValue, type, key, value) { + const { variables } = this.form[refValue]; + + const variable = variables.find((v) => v.key === key); + if (variable) { + variable.type = type; + variable.value = value; + } else { + variables.push({ + uniqueId: uniqueId(`var-${refValue}`), + key, + value, + variable_type: type, + }); + } + }, + setVariableType(key, type) { + const { variables } = this.form[this.refFullName]; + const variable = variables.find((v) => v.key === key); + variable.variable_type = type; + }, + setVariableParams(refValue, type, paramsObj) { + Object.entries(paramsObj).forEach(([key, value]) => { + this.setVariable(refValue, type, key, value); + }); + }, + removeVariable(index) { + this.variables.splice(index, 1); + }, + canRemove(index) { + return index < this.variables.length - 1; + }, + loadConfigVariablesForm() { + // Skip when variables already cached in `form` + if (this.form[this.refFullName]) { + return; + } + + this.fetchConfigVariables(this.refFullName || this.refShortName) + .then(({ descriptions, params }) => { + Vue.set(this.form, this.refFullName, { + variables: [], + descriptions, + }); + + // Add default variables from yml + this.setVariableParams(this.refFullName, VARIABLE_TYPE, params); + }) + .catch(() => { + Vue.set(this.form, this.refFullName, { + variables: [], + descriptions: {}, + }); + }) + .finally(() => { + // Add/update variables, e.g. from query string + if (this.variableParams) { + this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams); + } + if (this.fileParams) { + this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams); + } + + // Adds empty var at the end of the form + this.addEmptyVariable(this.refFullName); + }); + }, + fetchConfigVariables(refValue) { + this.isLoading = true; + + return backOff((next, stop) => { + axios + .get(this.configVariablesPath, { + params: { + sha: refValue, + }, + }) + .then(({ data, status }) => { + if (status === httpStatusCodes.NO_CONTENT) { + next(); + } else { + this.isLoading = false; + stop(data); + } + }) + .catch((error) => { + stop(error); + }); + }, CONFIG_VARIABLES_TIMEOUT) + .then((data) => { + const params = {}; + const descriptions = {}; + + Object.entries(data).forEach(([key, { value, description }]) => { + if (description) { + params[key] = value; + descriptions[key] = description; + } + }); + + return { params, descriptions }; + }) + .catch((error) => { + this.isLoading = false; + + Sentry.captureException(error); + + return { params: {}, descriptions: {} }; + }); + }, + createPipeline() { + this.submitted = true; + this.ccAlertDismissed = false; + + return axios + .post(this.pipelinesPath, { + // send shortName as fall back for query params + // https://gitlab.com/gitlab-org/gitlab/-/issues/287815 + ref: this.refValue.fullName || this.refShortName, + variables_attributes: filterVariables(this.variables), + }) + .then(({ data }) => { + redirectTo(`${this.pipelinesPath}/${data.id}`); + }) + .catch((err) => { + // always re-enable submit button + this.submitted = false; + + const { + errors = [], + warnings = [], + total_warnings: totalWarnings = 0, + } = err.response.data; + const [error] = errors; + + this.reportError({ + title: i18n.submitErrorTitle, + error, + warnings, + totalWarnings, + }); + }); + }, + onRefsLoadingError(error) { + this.reportError({ title: i18n.refsLoadingErrorTitle }); + + Sentry.captureException(error); + }, + reportError({ title = null, error = i18n.defaultError, warnings = [], totalWarnings = 0 }) { + this.errorTitle = title; + this.error = error; + this.warnings = warnings; + this.totalWarnings = totalWarnings; + }, + dismissError() { + this.ccAlertDismissed = true; + this.error = null; + }, + }, +}; +</script> + +<template> + <gl-form @submit.prevent="createPipeline"> + <cc-validation-required-alert v-if="ccRequiredError" class="gl-pb-5" @dismiss="dismissError" /> + <gl-alert + v-else-if="error" + :title="errorTitle" + :dismissible="false" + variant="danger" + class="gl-mb-4" + data-testid="run-pipeline-error-alert" + > + <span v-safe-html="error"></span> + </gl-alert> + <gl-alert + v-if="shouldShowWarning" + :title="$options.i18n.warningTitle" + variant="warning" + class="gl-mb-4" + data-testid="run-pipeline-warning-alert" + @dismiss="isWarningDismissed = true" + > + <details> + <summary> + <gl-sprintf :message="summaryMessage"> + <template #total> + {{ totalWarnings }} + </template> + <template #warningsDisplayed> + {{ maxWarnings }} + </template> + </gl-sprintf> + </summary> + <p + v-for="(warning, index) in warnings" + :key="`warning-${index}`" + data-testid="run-pipeline-warning" + > + {{ warning }} + </p> + </details> + </gl-alert> + <gl-form-group :label="s__('Pipeline|Run for branch name or tag')"> + <refs-dropdown v-model="refValue" @loadingError="onRefsLoadingError" /> + </gl-form-group> + + <gl-loading-icon v-if="isLoading" class="gl-mb-5" size="lg" /> + + <gl-form-group v-else :label="s__('Pipeline|Variables')"> + <div + v-for="(variable, index) in variables" + :key="variable.uniqueId" + class="gl-mb-3 gl-ml-n3 gl-pb-2" + data-testid="ci-variable-row" + data-qa-selector="ci_variable_row_container" + > + <div + class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row" + > + <gl-dropdown + :text="$options.typeOptions[variable.variable_type]" + :class="$options.formElementClasses" + data-testid="pipeline-form-ci-variable-type" + > + <gl-dropdown-item + v-for="type in Object.keys($options.typeOptions)" + :key="type" + @click="setVariableType(variable.key, type)" + > + {{ $options.typeOptions[type] }} + </gl-dropdown-item> + </gl-dropdown> + <gl-form-input + v-model="variable.key" + :placeholder="s__('CiVariables|Input variable key')" + :class="$options.formElementClasses" + data-testid="pipeline-form-ci-variable-key" + data-qa-selector="ci_variable_key_field" + @change="addEmptyVariable(refFullName)" + /> + <gl-form-textarea + v-model="variable.value" + :placeholder="s__('CiVariables|Input variable value')" + class="gl-mb-3" + :style="$options.textAreaStyle" + :no-resize="false" + data-testid="pipeline-form-ci-variable-value" + data-qa-selector="ci_variable_value_field" + /> + + <template v-if="variables.length > 1"> + <gl-button + v-if="canRemove(index)" + class="gl-md-ml-3 gl-mb-3" + data-testid="remove-ci-variable-row" + variant="danger" + category="secondary" + :aria-label="$options.i18n.removeVariableLabel" + @click="removeVariable(index)" + > + <gl-icon class="gl-mr-0! gl-display-none gl-md-display-block" name="clear" /> + <span class="gl-md-display-none">{{ $options.i18n.removeVariableLabel }}</span> + </gl-button> + <gl-button + v-else + class="gl-md-ml-3 gl-mb-3 gl-display-none gl-md-display-block gl-visibility-hidden" + icon="clear" + :aria-label="$options.i18n.removeVariableLabel" + /> + </template> + </div> + <div v-if="descriptions[variable.key]" class="gl-text-gray-500 gl-mb-3"> + {{ descriptions[variable.key] }} + </div> + </div> + + <template #description + ><gl-sprintf :message="$options.i18n.variablesDescription"> + <template #link="{ content }"> + <gl-link :href="settingsLink">{{ content }}</gl-link> + </template> + </gl-sprintf></template + > + </gl-form-group> + <div class="gl-pt-5 gl-display-flex"> + <gl-button + type="submit" + category="primary" + variant="confirm" + class="js-no-auto-disable gl-mr-3" + data-qa-selector="run_pipeline_button" + data-testid="run_pipeline_button" + :disabled="submitted" + >{{ s__('Pipeline|Run pipeline') }}</gl-button + > + <gl-button :href="pipelinesPath">{{ __('Cancel') }}</gl-button> + </div> + </gl-form> +</template> diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js index 927eeb5e144aeb..e3f363f4ada4e4 100644 --- a/app/assets/javascripts/pipeline_new/index.js +++ b/app/assets/javascripts/pipeline_new/index.js @@ -1,27 +1,72 @@ import Vue from 'vue'; +import LegacyPipelineNewForm from './components/legacy_pipeline_new_form.vue'; import PipelineNewForm from './components/pipeline_new_form.vue'; -export default () => { - const el = document.getElementById('js-new-pipeline'); +const mountLegacyPipelineNewForm = (el) => { const { // provide/inject projectRefsEndpoint, // props - projectId, - pipelinesPath, configVariablesPath, defaultBranch, + fileParam, + maxWarnings, + pipelinesPath, + projectId, refParam, + settingsLink, varParam, + } = el.dataset; + + const variableParams = JSON.parse(varParam); + const fileParams = JSON.parse(fileParam); + + return new Vue({ + el, + provide: { + projectRefsEndpoint, + }, + render(createElement) { + return createElement(LegacyPipelineNewForm, { + props: { + configVariablesPath, + defaultBranch, + fileParams, + maxWarnings: Number(maxWarnings), + pipelinesPath, + projectId, + refParam, + settingsLink, + variableParams, + }, + }); + }, + }); +}; + +const mountPipelineNewForm = (el) => { + const { + // provide/inject + projectRefsEndpoint, + + // props + configVariablesPath, + defaultBranch, fileParam, - settingsLink, maxWarnings, + pipelinesPath, + projectId, + refParam, + settingsLink, + varParam, } = el.dataset; const variableParams = JSON.parse(varParam); const fileParams = JSON.parse(fileParam); + // TODO: add apolloProvider + return new Vue({ el, provide: { @@ -30,17 +75,27 @@ export default () => { render(createElement) { return createElement(PipelineNewForm, { props: { - projectId, - pipelinesPath, configVariablesPath, defaultBranch, - refParam, - variableParams, fileParams, - settingsLink, maxWarnings: Number(maxWarnings), + pipelinesPath, + projectId, + refParam, + settingsLink, + variableParams, }, }); }, }); }; + +export default () => { + const el = document.getElementById('js-new-pipeline'); + + if (gon.features?.runPipelineGraphql) { + mountPipelineNewForm(el); + } else { + mountLegacyPipelineNewForm(el); + } +}; diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index c582d3f728573f..51c3ed4e65bff6 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -26,6 +26,7 @@ class Projects::PipelinesController < Projects::ApplicationController before_action do push_frontend_feature_flag(:pipeline_tabs_vue, @project) + push_frontend_feature_flag(:run_pipeline_graphql, @project) end # Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596 diff --git a/config/feature_flags/development/run_pipeline_graphql.yml b/config/feature_flags/development/run_pipeline_graphql.yml new file mode 100644 index 00000000000000..78d8afbbee5114 --- /dev/null +++ b/config/feature_flags/development/run_pipeline_graphql.yml @@ -0,0 +1,8 @@ +--- +name: run_pipeline_graphql +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96633 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372310 +milestone: '15.4' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/qa/qa/page/project/pipeline/new.rb b/qa/qa/page/project/pipeline/new.rb index 6cf5c3b11343c5..742fcad5c073c1 100644 --- a/qa/qa/page/project/pipeline/new.rb +++ b/qa/qa/page/project/pipeline/new.rb @@ -5,7 +5,7 @@ module Page module Project module Pipeline class New < QA::Page::Base - view 'app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue' do + view 'app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue' do element :run_pipeline_button, required: true element :ci_variable_row_container element :ci_variable_key_field diff --git a/spec/features/populate_new_pipeline_vars_with_params_spec.rb b/spec/features/populate_new_pipeline_vars_with_params_spec.rb index 744543d12523fe..75fa8561235e7c 100644 --- a/spec/features/populate_new_pipeline_vars_with_params_spec.rb +++ b/spec/features/populate_new_pipeline_vars_with_params_spec.rb @@ -7,24 +7,42 @@ let(:project) { create(:project) } let(:page_path) { new_project_pipeline_path(project) } - before do - sign_in(user) - project.add_maintainer(user) + shared_examples 'form pre-filled with URL params' do + before do + sign_in(user) + project.add_maintainer(user) - visit "#{page_path}?var[key1]=value1&file_var[key2]=value2" + visit "#{page_path}?var[key1]=value1&file_var[key2]=value2" + end + + it "var[key1]=value1 populates env_var variable correctly" do + page.within(all("[data-testid='ci-variable-row']")[0]) do + expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key1') + expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value1') + end + end + + it "file_var[key2]=value2 populates file variable correctly" do + page.within(all("[data-testid='ci-variable-row']")[1]) do + expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key2') + expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value2') + end + end end - it "var[key1]=value1 populates env_var variable correctly" do - page.within(all("[data-testid='ci-variable-row']")[0]) do - expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key1') - expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value1') + context 'when feature flag is disabled' do + before do + stub_feature_flags(run_pipeline_graphql: false) end + + it_behaves_like 'form pre-filled with URL params' end - it "file_var[key2]=value2 populates file variable correctly" do - page.within(all("[data-testid='ci-variable-row']")[1]) do - expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key2') - expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value2') + context 'when feature flag is enabled' do + before do + stub_feature_flags(run_pipeline_graphql: true) end + + it_behaves_like 'form pre-filled with URL params' end end diff --git a/spec/features/projects/pipelines/legacy_pipelines_spec.rb b/spec/features/projects/pipelines/legacy_pipelines_spec.rb index c903fe60fdb641..2b3a6569c56158 100644 --- a/spec/features/projects/pipelines/legacy_pipelines_spec.rb +++ b/spec/features/projects/pipelines/legacy_pipelines_spec.rb @@ -674,6 +674,7 @@ def create_build(stage, stage_idx, name, status) let(:project) { create(:project, :repository) } before do + stub_feature_flags(run_pipeline_graphql: false) visit new_project_pipeline_path(project) end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index d4f588135344a2..d5705d1da04d3c 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -656,19 +656,7 @@ def create_build(stage, stage_idx, name, status) describe 'POST /:project/-/pipelines' do let(:project) { create(:project, :repository) } - before do - visit new_project_pipeline_path(project) - end - - context 'for valid commit', :js do - before do - click_button project.default_branch - wait_for_requests - - find('p', text: 'master').click - wait_for_requests - end - + shared_examples 'run pipeline form with gitlab-ci.yml' do context 'with gitlab-ci.yml', :js do before do stub_ci_pipeline_to_return_yaml_file @@ -702,7 +690,9 @@ def create_build(stage, stage_idx, name, status) end end end + end + shared_examples 'run pipeline form without gitlab-ci.yml' do context 'without gitlab-ci.yml' do before do click_on 'Run pipeline' @@ -722,6 +712,51 @@ def create_build(stage, stage_idx, name, status) end end end + + # Run Pipeline form with REST endpoints + # TODO: Clean up tests when run_pipeline_graphql is enabled + context 'with feature flag disabled' do + before do + stub_feature_flags(run_pipeline_graphql: false) + visit new_project_pipeline_path(project) + end + + context 'for valid commit', :js do + before do + click_button project.default_branch + wait_for_requests + + find('p', text: 'master').click + wait_for_requests + end + + it_behaves_like 'run pipeline form with gitlab-ci.yml' + + it_behaves_like 'run pipeline form without gitlab-ci.yml' + end + end + + # Run Pipeline form with GraphQL + context 'with feature flag enabled' do + before do + stub_feature_flags(run_pipeline_graphql: true) + visit new_project_pipeline_path(project) + end + + context 'for valid commit', :js do + before do + click_button project.default_branch + wait_for_requests + + find('p', text: 'master').click + wait_for_requests + end + + it_behaves_like 'run pipeline form with gitlab-ci.yml' + + it_behaves_like 'run pipeline form without gitlab-ci.yml' + end + end end describe 'Reset runner caches' do diff --git a/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js new file mode 100644 index 00000000000000..f2d2575c5fb6f4 --- /dev/null +++ b/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js @@ -0,0 +1,456 @@ +import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; +import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue'; +import { TEST_HOST } from 'helpers/test_constants'; +import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; +import httpStatusCodes from '~/lib/utils/http_status'; +import { redirectTo } from '~/lib/utils/url_utility'; +import LegacyPipelineNewForm from '~/pipeline_new/components/legacy_pipeline_new_form.vue'; +import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue'; +import { + mockQueryParams, + mockPostParams, + mockProjectId, + mockError, + mockRefs, + mockCreditCardValidationRequiredError, +} from '../mock_data'; + +jest.mock('~/lib/utils/url_utility', () => ({ + redirectTo: jest.fn(), +})); + +const projectRefsEndpoint = '/root/project/refs'; +const pipelinesPath = '/root/project/-/pipelines'; +const configVariablesPath = '/root/project/-/pipelines/config_variables'; +const newPipelinePostResponse = { id: 1 }; +const defaultBranch = 'main'; + +describe('Pipeline New Form', () => { + let wrapper; + let mock; + let dummySubmitEvent; + + const findForm = () => wrapper.find(GlForm); + const findRefsDropdown = () => wrapper.findComponent(RefsDropdown); + const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]'); + const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]'); + const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]'); + const findDropdowns = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-type"]'); + const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]'); + const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]'); + const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]'); + const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]'); + const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf); + const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]'); + const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert); + const getFormPostParams = () => JSON.parse(mock.history.post[0].data); + + const selectBranch = (branch) => { + // Select a branch in the dropdown + findRefsDropdown().vm.$emit('input', { + shortName: branch, + fullName: `refs/heads/${branch}`, + }); + }; + + const createComponent = (props = {}, method = shallowMount) => { + wrapper = method(LegacyPipelineNewForm, { + provide: { + projectRefsEndpoint, + }, + propsData: { + projectId: mockProjectId, + pipelinesPath, + configVariablesPath, + defaultBranch, + refParam: defaultBranch, + settingsLink: '', + maxWarnings: 25, + ...props, + }, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {}); + mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs); + + dummySubmitEvent = { + preventDefault: jest.fn(), + }; + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + mock.restore(); + }); + + describe('Form', () => { + beforeEach(async () => { + createComponent(mockQueryParams, mount); + + mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse); + + await waitForPromises(); + }); + + it('displays the correct values for the provided query params', async () => { + expect(findDropdowns().at(0).props('text')).toBe('Variable'); + expect(findDropdowns().at(1).props('text')).toBe('File'); + expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' }); + expect(findVariableRows()).toHaveLength(3); + }); + + it('displays a variable from provided query params', () => { + expect(findKeyInputs().at(0).element.value).toBe('test_var'); + expect(findValueInputs().at(0).element.value).toBe('test_var_val'); + }); + + it('displays an empty variable for the user to fill out', async () => { + expect(findKeyInputs().at(2).element.value).toBe(''); + expect(findValueInputs().at(2).element.value).toBe(''); + expect(findDropdowns().at(2).props('text')).toBe('Variable'); + }); + + it('does not display remove icon for last row', () => { + expect(findRemoveIcons()).toHaveLength(2); + }); + + it('removes ci variable row on remove icon button click', async () => { + findRemoveIcons().at(1).trigger('click'); + + await nextTick(); + + expect(findVariableRows()).toHaveLength(2); + }); + + it('creates blank variable on input change event', async () => { + const input = findKeyInputs().at(2); + input.element.value = 'test_var_2'; + input.trigger('change'); + + await nextTick(); + + expect(findVariableRows()).toHaveLength(4); + expect(findKeyInputs().at(3).element.value).toBe(''); + expect(findValueInputs().at(3).element.value).toBe(''); + }); + }); + + describe('Pipeline creation', () => { + beforeEach(async () => { + mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse); + + await waitForPromises(); + }); + + it('does not submit the native HTML form', async () => { + createComponent(); + + findForm().vm.$emit('submit', dummySubmitEvent); + + expect(dummySubmitEvent.preventDefault).toHaveBeenCalled(); + }); + + it('disables the submit button immediately after submitting', async () => { + createComponent(); + + expect(findSubmitButton().props('disabled')).toBe(false); + + findForm().vm.$emit('submit', dummySubmitEvent); + await waitForPromises(); + + expect(findSubmitButton().props('disabled')).toBe(true); + }); + + it('creates pipeline with full ref and variables', async () => { + createComponent(); + + findForm().vm.$emit('submit', dummySubmitEvent); + await waitForPromises(); + + expect(getFormPostParams().ref).toEqual(`refs/heads/${defaultBranch}`); + expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); + }); + + it('creates a pipeline with short ref and variables from the query params', async () => { + createComponent(mockQueryParams); + + await waitForPromises(); + + findForm().vm.$emit('submit', dummySubmitEvent); + + await waitForPromises(); + + expect(getFormPostParams()).toEqual(mockPostParams); + expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); + }); + }); + + describe('When the ref has been changed', () => { + beforeEach(async () => { + createComponent({}, mount); + + await waitForPromises(); + }); + it('variables persist between ref changes', async () => { + selectBranch('main'); + + await waitForPromises(); + + const mainInput = findKeyInputs().at(0); + mainInput.element.value = 'build_var'; + mainInput.trigger('change'); + + await nextTick(); + + selectBranch('branch-1'); + + await waitForPromises(); + + const branchOneInput = findKeyInputs().at(0); + branchOneInput.element.value = 'deploy_var'; + branchOneInput.trigger('change'); + + await nextTick(); + + selectBranch('main'); + + await waitForPromises(); + + expect(findKeyInputs().at(0).element.value).toBe('build_var'); + expect(findVariableRows().length).toBe(2); + + selectBranch('branch-1'); + + await waitForPromises(); + + expect(findKeyInputs().at(0).element.value).toBe('deploy_var'); + expect(findVariableRows().length).toBe(2); + }); + }); + + describe('when yml defines a variable', () => { + const mockYmlKey = 'yml_var'; + const mockYmlValue = 'yml_var_val'; + const mockYmlMultiLineValue = `A value + with multiple + lines`; + const mockYmlDesc = 'A var from yml.'; + + it('loading icon is shown when content is requested and hidden when received', async () => { + createComponent(mockQueryParams, mount); + + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { + [mockYmlKey]: { + value: mockYmlValue, + description: mockYmlDesc, + }, + }); + + expect(findLoadingIcon().exists()).toBe(true); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + + it('multi-line strings are added to the value field without removing line breaks', async () => { + createComponent(mockQueryParams, mount); + + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { + [mockYmlKey]: { + value: mockYmlMultiLineValue, + description: mockYmlDesc, + }, + }); + + await waitForPromises(); + + expect(findValueInputs().at(0).element.value).toBe(mockYmlMultiLineValue); + }); + + describe('with description', () => { + beforeEach(async () => { + createComponent(mockQueryParams, mount); + + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { + [mockYmlKey]: { + value: mockYmlValue, + description: mockYmlDesc, + }, + }); + + await waitForPromises(); + }); + + it('displays all the variables', async () => { + expect(findVariableRows()).toHaveLength(4); + }); + + it('displays a variable from yml', () => { + expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey); + expect(findValueInputs().at(0).element.value).toBe(mockYmlValue); + }); + + it('displays a variable from provided query params', () => { + expect(findKeyInputs().at(1).element.value).toBe('test_var'); + expect(findValueInputs().at(1).element.value).toBe('test_var_val'); + }); + + it('adds a description to the first variable from yml', () => { + expect(findVariableRows().at(0).text()).toContain(mockYmlDesc); + }); + + it('removes the description when a variable key changes', async () => { + findKeyInputs().at(0).element.value = 'yml_var_modified'; + findKeyInputs().at(0).trigger('change'); + + await nextTick(); + + expect(findVariableRows().at(0).text()).not.toContain(mockYmlDesc); + }); + }); + + describe('without description', () => { + beforeEach(async () => { + createComponent(mockQueryParams, mount); + + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { + [mockYmlKey]: { + value: mockYmlValue, + description: null, + }, + yml_var2: { + value: 'yml_var2_val', + }, + yml_var3: { + description: '', + }, + }); + + await waitForPromises(); + }); + + it('displays all the variables', async () => { + expect(findVariableRows()).toHaveLength(3); + }); + }); + }); + + describe('Form errors and warnings', () => { + beforeEach(() => { + createComponent(); + }); + + describe('when the refs cannot be loaded', () => { + beforeEach(() => { + mock + .onGet(projectRefsEndpoint, { params: { search: '' } }) + .reply(httpStatusCodes.INTERNAL_SERVER_ERROR); + + findRefsDropdown().vm.$emit('loadingError'); + }); + + it('shows both an error alert', () => { + expect(findErrorAlert().exists()).toBe(true); + expect(findWarningAlert().exists()).toBe(false); + }); + }); + + describe('when the error response can be handled', () => { + beforeEach(async () => { + mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError); + + findForm().vm.$emit('submit', dummySubmitEvent); + + await waitForPromises(); + }); + + it('shows both error and warning', () => { + expect(findErrorAlert().exists()).toBe(true); + expect(findWarningAlert().exists()).toBe(true); + }); + + it('shows the correct error', () => { + expect(findErrorAlert().text()).toBe(mockError.errors[0]); + }); + + it('shows the correct warning title', () => { + const { length } = mockError.warnings; + + expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`); + }); + + it('shows the correct amount of warnings', () => { + expect(findWarnings()).toHaveLength(mockError.warnings.length); + }); + + it('re-enables the submit button', () => { + expect(findSubmitButton().props('disabled')).toBe(false); + }); + + it('does not show the credit card validation required alert', () => { + expect(findCCAlert().exists()).toBe(false); + }); + + describe('when the error response is credit card validation required', () => { + beforeEach(async () => { + mock + .onPost(pipelinesPath) + .reply(httpStatusCodes.BAD_REQUEST, mockCreditCardValidationRequiredError); + + window.gon = { + subscriptions_url: TEST_HOST, + payment_form_url: TEST_HOST, + }; + + findForm().vm.$emit('submit', dummySubmitEvent); + + await waitForPromises(); + }); + + it('shows credit card validation required alert', () => { + expect(findErrorAlert().exists()).toBe(false); + expect(findCCAlert().exists()).toBe(true); + }); + + it('clears error and hides the alert on dismiss', async () => { + expect(findCCAlert().exists()).toBe(true); + expect(wrapper.vm.$data.error).toBe(mockCreditCardValidationRequiredError.errors[0]); + + findCCAlert().vm.$emit('dismiss'); + + await nextTick(); + + expect(findCCAlert().exists()).toBe(false); + expect(wrapper.vm.$data.error).toBe(null); + }); + }); + }); + + describe('when the error response cannot be handled', () => { + beforeEach(async () => { + mock + .onPost(pipelinesPath) + .reply(httpStatusCodes.INTERNAL_SERVER_ERROR, 'something went wrong'); + + findForm().vm.$emit('submit', dummySubmitEvent); + + await waitForPromises(); + }); + + it('re-enables the submit button', () => { + expect(findSubmitButton().props('disabled')).toBe(false); + }); + }); + }); +}); -- GitLab From 05911b54f516564b36ffd1111c72e4dfcb351f58 Mon Sep 17 00:00:00 2001 From: Dan Mizzi-Harris <dmizzi-harris@gitlab.com> Date: Mon, 5 Sep 2022 18:50:23 +0000 Subject: [PATCH 063/169] Adds information about moved service desk issues --- doc/user/project/service_desk.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md index f32035102bb0f7..544ee2d8617910 100644 --- a/doc/user/project/service_desk.md +++ b/doc/user/project/service_desk.md @@ -358,9 +358,23 @@ to everyone who can view the project. Behind the scenes, Service Desk works by the special Support Bot user creating issues. This user does not count toward the license limit count. +### Moving a Service Desk issue + +Service Desk issues can be moved like any other issue in GitLab. + +You can move a Service Desk issue the same way you +[move a regular issue](issues/managing_issues.md#move-an-issue) in GitLab. + +If a Service Desk issue is moved to a different project the customer who created the issue stops receiving emails. + ## Troubleshooting Service Desk ### Emails to Service Desk do not create issues Your emails might be ignored because they contain one of the [email headers that GitLab ignores](../../administration/incoming_email.md#rejected-headers). + +### Responses to a Service Desk issue do not generate emails + +Your issue might have been moved to a different project. +Moved Service Desk issues do not retain email participants. -- GitLab From b91e55166e7069956106594be51f9f6adf21e4cf Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Mon, 5 Sep 2022 16:06:21 -0300 Subject: [PATCH 064/169] Improve error message while validating config/database.yml In https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89696 we introduced additional handling of an error when doing an upsert on a read-only database. When a user encounters this scenario, they are presented with a message stating "ERROR: cannot execute INSERT in a read-only transaction". The verbiage is confusing to users because it implies it is ERROR severity, but it should be a WARNING since this does not break anything or halt any processes. Changelog: other --- lib/tasks/gitlab/db/validate_config.rake | 2 +- spec/tasks/gitlab/db/validate_config_rake_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tasks/gitlab/db/validate_config.rake b/lib/tasks/gitlab/db/validate_config.rake index 2a3a54b5351c31..bf9ebc56486d86 100644 --- a/lib/tasks/gitlab/db/validate_config.rake +++ b/lib/tasks/gitlab/db/validate_config.rake @@ -144,7 +144,7 @@ namespace :gitlab do rescue ActiveRecord::StatementInvalid => err raise unless err.cause.is_a?(PG::ReadOnlySqlTransaction) - warn "WARNING: Could not write to the database #{db_config.name}: #{err.message}" + warn "WARNING: Could not write to the database #{db_config.name}: cannot execute UPSERT in a read-only transaction" end def get_db_identifier(db_config) diff --git a/spec/tasks/gitlab/db/validate_config_rake_spec.rb b/spec/tasks/gitlab/db/validate_config_rake_spec.rb index ad15c7f0d1cbb5..1d47c94aa779b0 100644 --- a/spec/tasks/gitlab/db/validate_config_rake_spec.rb +++ b/spec/tasks/gitlab/db/validate_config_rake_spec.rb @@ -216,7 +216,7 @@ let(:exception) { ActiveRecord::StatementInvalid.new("READONLY") } before do - allow(exception).to receive(:cause).and_return(PG::ReadOnlySqlTransaction.new("cannot execute INSERT in a read-only transaction")) + allow(exception).to receive(:cause).and_return(PG::ReadOnlySqlTransaction.new("cannot execute UPSERT in a read-only transaction")) allow(ActiveRecord::InternalMetadata).to receive(:upsert).at_least(:once).and_raise(exception) end -- GitLab From bc4446098aefdc3904ab8f5aa7b6166f8ef041e8 Mon Sep 17 00:00:00 2001 From: Lucas Zampieri <lzampier@redhat.com> Date: Mon, 5 Sep 2022 19:11:03 +0000 Subject: [PATCH 065/169] Added reset_approvalls REST API This adds an endpoint to MRs, `reset_approvals`, in which a bot user can clear all approvals. Changelog: added Signed-off-by: Lucas Zampieri <lzampier@redhat.com> --- doc/api/merge_requests.md | 20 +++++++ lib/api/merge_requests.rb | 13 +++++ spec/requests/api/merge_requests_spec.rb | 66 ++++++++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 66a9a19e6914de..e8322657a50e3e 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -1940,6 +1940,26 @@ If the rebase operation fails, the response includes the following: } ``` +## Reset approvals of a merge request + +Clear all approvals of merge request. + +Available only for [bot users](../user/project/settings/project_access_tokens.md#bot-users-for-projects) +based on project or group tokens. Users without bot permissions receive a `401 Unauthorized` response. + +```plaintext +PUT /projects/:id/merge_requests/:merge_request_iid/reset_approvals +``` + +| Attribute | Type | Required | Description | +|---------------------|-------------------|----------|-----------------------------------------------------------------------------------------------------------------| +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `merge_request_iid` | integer | yes | The internal ID of the merge request. | + +```shell +curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/76/merge_requests/1/reset_approvals" +``` + ## Comments on merge requests Comments are done via the [notes](notes.md) resource. diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index a8f58e91067e61..9e64d9515479ac 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -544,6 +544,19 @@ def recheck_mergeability_of(merge_requests:) render_api_error!(e.message, 409) end + desc 'Remove merge request approvals' do + detail 'This feature was added in GitLab 15.4' + end + put ':id/merge_requests/:merge_request_iid/reset_approvals', feature_category: :code_review, urgency: :low do + merge_request = find_project_merge_request(params[:merge_request_iid]) + + unauthorized! unless current_user.bot? && merge_request.can_be_approved_by?(current_user) + + merge_request.approvals.delete_all + + status :accepted + end + desc 'List issues that will be closed on merge' do success Entities::MRNote end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 17cd86e73a1793..9bd1aef9961d5e 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -9,6 +9,7 @@ let_it_be(:user) { create(:user) } let_it_be(:user2) { create(:user) } let_it_be(:admin) { create(:user, :admin) } + let_it_be(:bot) { create(:user, :project_bot) } let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) } let(:milestone1) { create(:milestone, title: '0.9', project: project) } @@ -3514,6 +3515,71 @@ end end + describe 'PUT :id/merge_requests/:merge_request_iid/reset_approvals' do + before do + merge_request.approvals.create!(user: user2) + create(:project_member, :maintainer, user: bot, source: project) + end + + context 'when reset_approvals can be performed' do + it 'clears approvals of the merge_request' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot) + + merge_request.reload + expect(response).to have_gitlab_http_status(:accepted) + expect(merge_request.approvals).to be_empty + end + + it 'for users with bot role' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot) + + expect(response).to have_gitlab_http_status(:accepted) + end + + context 'for users with non-bot roles' do + let(:human_user) { create(:user) } + + [:add_owner, :add_maintainer, :add_developer, :add_guest].each do |role_method| + it 'returns 401' do + project.send(role_method, human_user) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", human_user) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + context 'for bot-users from external namespaces' do + let_it_be(:external_bot) { create(:user, :project_bot) } + + context 'external group bot-user' do + before do + create(:group_member, :maintainer, user: external_bot, source: create(:group)) + end + + it 'returns 401' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'external project bot-user' do + before do + create(:project_member, :maintainer, user: external_bot, source: create(:project)) + end + + it 'returns 401' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + end + end + describe 'Time tracking' do let!(:issuable) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) } -- GitLab From 9049d0b7ad46d859a9e9410bdc454328c099694e Mon Sep 17 00:00:00 2001 From: Alejandro Guerrero <argdealba@gitlab.com> Date: Mon, 5 Sep 2022 19:28:24 +0000 Subject: [PATCH 066/169] Update security vulnerabilities link --- .../components/pipeline/pipeline_security_dashboard.vue | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ee/app/assets/javascripts/security_dashboard/components/pipeline/pipeline_security_dashboard.vue b/ee/app/assets/javascripts/security_dashboard/components/pipeline/pipeline_security_dashboard.vue index eb1574f76ac59d..3a2692c9c1170b 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/pipeline/pipeline_security_dashboard.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/pipeline/pipeline_security_dashboard.vue @@ -127,8 +127,7 @@ export default { `SecurityReports|Results show vulnerabilities introduced by the merge request, in addition to existing vulnerabilities from the latest successful pipeline in your project's default branch.`, ), pageDescriptionHelpLink: helpPagePath( - 'user/application_security/security_dashboard/index.html', - { anchor: 'view-vulnerabilities-in-a-pipeline' }, + 'user/application_security/vulnerability_report/pipeline.html', ), }, }; -- GitLab From c636019e30e108fd91dd073576589ec3f355dec1 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara <ealcantara@gitlab.com> Date: Wed, 31 Aug 2022 07:56:16 -0400 Subject: [PATCH 067/169] Support parsing table of contents in gfm parser Implement a plugin to parse GitLab Flavored Markdown table of contents in the client-side GLFM markdown parser. This MR also provides two renderers for table of contents. One of the renderers converts the table of contents into a section element. The other creates a tableofcontents element suitable for usage in the Content Editor --- app/assets/javascripts/lib/gfm/index.js | 25 +- .../glfm_mdast_to_hast_handlers.js | 1 + .../glfm_table_of_contents.js | 69 ++++ package.json | 2 + spec/frontend/lib/gfm/index_spec.js | 376 ++++++++++-------- yarn.lock | 2 +- 6 files changed, 300 insertions(+), 175 deletions(-) create mode 100644 app/assets/javascripts/lib/gfm/mdast_to_hast_handlers/glfm_mdast_to_hast_handlers.js create mode 100644 app/assets/javascripts/lib/gfm/parser_extensions/glfm_table_of_contents.js diff --git a/app/assets/javascripts/lib/gfm/index.js b/app/assets/javascripts/lib/gfm/index.js index eaf653e9924174..d2cc343042f305 100644 --- a/app/assets/javascripts/lib/gfm/index.js +++ b/app/assets/javascripts/lib/gfm/index.js @@ -6,6 +6,8 @@ import remarkFrontmatter from 'remark-frontmatter'; import remarkGfm from 'remark-gfm'; import remarkRehype, { all } from 'remark-rehype'; import rehypeRaw from 'rehype-raw'; +import glfmTableOfContents from './parser_extensions/glfm_table_of_contents'; +import * as glfmMdastToHastHandlers from './mdast_to_hast_handlers/glfm_mdast_to_hast_handlers'; const skipFrontmatterHandler = (language) => (h, node) => h(node.position, 'frontmatter', { language }, [{ type: 'text', value: node.value }]); @@ -65,23 +67,18 @@ const skipRenderingHandlers = { all(h, node), ); }, + tableOfContents: (h, node) => h(node.position, 'tableOfContents'), toml: skipFrontmatterHandler('toml'), yaml: skipFrontmatterHandler('yaml'), json: skipFrontmatterHandler('json'), }; -const createParser = ({ skipRendering = [] }) => { +const createParser = () => { return unified() .use(remarkParse) .use(remarkGfm) .use(remarkFrontmatter, ['yaml', 'toml', { type: 'json', marker: ';' }]) - .use(remarkRehype, { - allowDangerousHtml: true, - handlers: { - ...pick(skipRenderingHandlers, skipRendering), - }, - }) - .use(rehypeRaw); + .use(glfmTableOfContents); }; const compilerFactory = (renderer) => @@ -99,17 +96,25 @@ const compilerFactory = (renderer) => * tree in any desired representation * * @param {String} params.markdown Markdown to parse - * @param {(tree: MDast -> any)} params.renderer A function that accepts mdast + * @param {Function} params.renderer A function that accepts mdast * AST tree and returns an object of any type that represents the result of * rendering the tree. See the references below to for more information * about MDast. * * MDastTree documentation https://github.com/syntax-tree/mdast - * @returns {Promise<any>} Returns a promise with the result of rendering + * @returns {Promise} Returns a promise with the result of rendering * the MDast tree */ export const render = async ({ markdown, renderer, skipRendering = [] }) => { const { result } = await createParser({ skipRendering }) + .use(remarkRehype, { + allowDangerousHtml: true, + handlers: { + ...glfmMdastToHastHandlers, + ...pick(skipRenderingHandlers, skipRendering), + }, + }) + .use(rehypeRaw) .use(compilerFactory(renderer)) .process(markdown); diff --git a/app/assets/javascripts/lib/gfm/mdast_to_hast_handlers/glfm_mdast_to_hast_handlers.js b/app/assets/javascripts/lib/gfm/mdast_to_hast_handlers/glfm_mdast_to_hast_handlers.js new file mode 100644 index 00000000000000..91b09e694050b5 --- /dev/null +++ b/app/assets/javascripts/lib/gfm/mdast_to_hast_handlers/glfm_mdast_to_hast_handlers.js @@ -0,0 +1 @@ +export const tableOfContents = (h, node) => h(node.position, 'nav'); diff --git a/app/assets/javascripts/lib/gfm/parser_extensions/glfm_table_of_contents.js b/app/assets/javascripts/lib/gfm/parser_extensions/glfm_table_of_contents.js new file mode 100644 index 00000000000000..d0a9ebfcf09d08 --- /dev/null +++ b/app/assets/javascripts/lib/gfm/parser_extensions/glfm_table_of_contents.js @@ -0,0 +1,69 @@ +import { last } from 'lodash'; +import { u } from 'unist-builder'; +import { is } from 'unist-util-is'; +import { visitParents, SKIP, CONTINUE } from 'unist-util-visit-parents'; + +const isTOCTextNode = (node) => is(node, { type: 'text', value: 'TOC' }); + +/* + * Detects table of contents declaration with syntax [[_TOC_]] + */ +const isTableOfContentsDoubleSquareBracketSyntax = ({ children }) => { + if (children.length !== 3) { + return false; + } + + const [firstChild, middleChild, lastChild] = children; + + return ( + is(firstChild, ({ type, value }) => type === 'text' && value.trim() === '[[') && + is( + middleChild, + ({ type, children: emChildren }) => + type === 'emphasis' && emChildren.length === 1 && isTOCTextNode(emChildren[0]), + ) && + is(lastChild, { type: 'text', value: ']]' }) + ); +}; + +/* + * Detects table of contents declaration with syntax [TOC] + */ +const isTableOfContentsSingleSquareBracketSyntax = ({ children }) => { + if (children.length !== 1) { + return false; + } + + const [firstChild] = children; + + return is(firstChild, ({ type, value }) => type === 'text' && value.trim() === '[TOC]'); +}; + +const isTableOfContentsNode = (node) => + node.type === 'paragraph' && + (isTableOfContentsDoubleSquareBracketSyntax(node) || + isTableOfContentsSingleSquareBracketSyntax(node)); + +export default () => { + return (tree) => { + visitParents(tree, (node, ancestors) => { + const parent = last(ancestors); + + if (!parent) { + return CONTINUE; + } + + if (isTableOfContentsNode(node)) { + const index = parent.children.indexOf(node); + + parent.children[index] = u('tableOfContents', { + position: node.position, + }); + } + + return SKIP; + }); + + return tree; + }; +}; diff --git a/package.json b/package.json index d8c5eb4749e2c6..2071edfcb1dc91 100644 --- a/package.json +++ b/package.json @@ -172,6 +172,8 @@ "three": "^0.143.0", "timeago.js": "^4.0.2", "unified": "^10.1.2", + "unist-builder": "^3.0.0", + "unist-util-is": "^5.1.1", "unist-util-visit-parents": "^5.1.0", "url-loader": "^4.1.1", "uuid": "8.1.0", diff --git a/spec/frontend/lib/gfm/index_spec.js b/spec/frontend/lib/gfm/index_spec.js index f53f809b7996a9..7c383ae68a48d3 100644 --- a/spec/frontend/lib/gfm/index_spec.js +++ b/spec/frontend/lib/gfm/index_spec.js @@ -24,12 +24,6 @@ describe('gfm', () => { }; describe('render', () => { - it('processes Commonmark and provides an ast to the renderer function', async () => { - const result = await markdownToAST('This is text'); - - expect(result.type).toBe('root'); - }); - it('transforms raw HTML into individual nodes in the AST', async () => { const result = await markdownToAST('<strong>This is bold text</strong>'); @@ -46,216 +40,270 @@ describe('gfm', () => { ); }); - it('returns the result of executing the renderer function', async () => { - const rendered = { value: 'rendered tree' }; + describe('with custom renderer', () => { + it('processes Commonmark and provides an ast to the renderer function', async () => { + const result = await markdownToAST('This is text'); - const result = await render({ - markdown: '<strong>This is bold text</strong>', - renderer: () => { - return rendered; - }, + expect(result.type).toBe('root'); }); - expect(result).toEqual(rendered); + it('returns the result of executing the renderer function', async () => { + const rendered = { value: 'rendered tree' }; + + const result = await render({ + markdown: '<strong>This is bold text</strong>', + renderer: () => { + return rendered; + }, + }); + + expect(result).toEqual(rendered); + }); }); - describe('when skipping the rendering of footnote reference and definition nodes', () => { - it('transforms footnotes into footnotedefinition and footnotereference tags', async () => { - const result = await markdownToAST( - `footnote reference [^footnote] + describe('footnote references and footnote definitions', () => { + describe('when skipping the rendering of footnote reference and definition nodes', () => { + it('transforms footnotes into footnotedefinition and footnotereference tags', async () => { + const result = await markdownToAST( + `footnote reference [^footnote] [^footnote]: Footnote definition`, - ['footnoteReference', 'footnoteDefinition'], - ); + ['footnoteReference', 'footnoteDefinition'], + ); - expectInRoot( - result, - expect.objectContaining({ - children: expect.arrayContaining([ - expect.objectContaining({ - type: 'element', - tagName: 'footnotereference', - properties: { - identifier: 'footnote', - label: 'footnote', - }, - }), - ]), - }), - ); + expectInRoot( + result, + expect.objectContaining({ + children: expect.arrayContaining([ + expect.objectContaining({ + type: 'element', + tagName: 'footnotereference', + properties: { + identifier: 'footnote', + label: 'footnote', + }, + }), + ]), + }), + ); - expectInRoot( - result, - expect.objectContaining({ - tagName: 'footnotedefinition', - properties: { - identifier: 'footnote', - label: 'footnote', - }, - }), - ); + expectInRoot( + result, + expect.objectContaining({ + tagName: 'footnotedefinition', + properties: { + identifier: 'footnote', + label: 'footnote', + }, + }), + ); + }); }); }); - describe('when skipping the rendering of code blocks', () => { - it('transforms code nodes into codeblock html tags', async () => { - const result = await markdownToAST( - ` + describe('code blocks', () => { + describe('when skipping the rendering of code blocks', () => { + it('transforms code nodes into codeblock html tags', async () => { + const result = await markdownToAST( + ` \`\`\`javascript console.log('Hola'); \`\`\`\ `, - ['code'], - ); + ['code'], + ); - expectInRoot( - result, - expect.objectContaining({ - tagName: 'codeblock', - properties: { - language: 'javascript', - }, - }), - ); + expectInRoot( + result, + expect.objectContaining({ + tagName: 'codeblock', + properties: { + language: 'javascript', + }, + }), + ); + }); }); }); - describe('when skipping the rendering of reference definitions', () => { - it('transforms code nodes into codeblock html tags', async () => { - const result = await markdownToAST( - ` + describe('reference definitions', () => { + describe('when skipping the rendering of reference definitions', () => { + it('transforms code nodes into codeblock html tags', async () => { + const result = await markdownToAST( + ` [gitlab][gitlab] [gitlab]: https://gitlab.com "GitLab" `, - ['definition'], - ); + ['definition'], + ); - expectInRoot( - result, - expect.objectContaining({ - type: 'element', - tagName: 'referencedefinition', - properties: { - identifier: 'gitlab', - title: 'GitLab', - url: 'https://gitlab.com', - }, - children: [ - { - type: 'text', - value: '[gitlab]: https://gitlab.com "GitLab"', + expectInRoot( + result, + expect.objectContaining({ + type: 'element', + tagName: 'referencedefinition', + properties: { + identifier: 'gitlab', + title: 'GitLab', + url: 'https://gitlab.com', }, - ], - }), - ); + children: [ + { + type: 'text', + value: '[gitlab]: https://gitlab.com "GitLab"', + }, + ], + }), + ); + }); }); }); - describe('when skipping the rendering of link and image references', () => { - it('transforms linkReference and imageReference nodes into html tags', async () => { - const result = await markdownToAST( - ` + describe('link and image references', () => { + describe('when skipping the rendering of link and image references', () => { + it('transforms linkReference and imageReference nodes into html tags', async () => { + const result = await markdownToAST( + ` [gitlab][gitlab] and ![GitLab Logo][gitlab-logo] [gitlab]: https://gitlab.com "GitLab" [gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo" `, - ['linkReference', 'imageReference'], - ); + ['linkReference', 'imageReference'], + ); - expectInRoot( - result, - expect.objectContaining({ - tagName: 'p', - children: expect.arrayContaining([ - expect.objectContaining({ - type: 'element', - tagName: 'a', - properties: expect.objectContaining({ - href: 'https://gitlab.com', - isReference: 'true', - identifier: 'gitlab', - title: 'GitLab', + expectInRoot( + result, + expect.objectContaining({ + tagName: 'p', + children: expect.arrayContaining([ + expect.objectContaining({ + type: 'element', + tagName: 'a', + properties: expect.objectContaining({ + href: 'https://gitlab.com', + isReference: 'true', + identifier: 'gitlab', + title: 'GitLab', + }), }), - }), - expect.objectContaining({ - type: 'element', - tagName: 'img', - properties: expect.objectContaining({ - src: 'https://gitlab.com/gitlab-logo.png', - isReference: 'true', - identifier: 'gitlab-logo', - title: 'GitLab Logo', - alt: 'GitLab Logo', + expect.objectContaining({ + type: 'element', + tagName: 'img', + properties: expect.objectContaining({ + src: 'https://gitlab.com/gitlab-logo.png', + isReference: 'true', + identifier: 'gitlab-logo', + title: 'GitLab Logo', + alt: 'GitLab Logo', + }), }), - }), - ]), - }), - ); - }); + ]), + }), + ); + }); - it('normalizes the urls extracted from the reference definitions', async () => { - const result = await markdownToAST( - ` + it('normalizes the urls extracted from the reference definitions', async () => { + const result = await markdownToAST( + ` [gitlab][gitlab] and ![GitLab Logo][gitlab] [gitlab]: /url\\bar*baz `, - ['linkReference', 'imageReference'], - ); + ['linkReference', 'imageReference'], + ); + + expectInRoot( + result, + expect.objectContaining({ + tagName: 'p', + children: expect.arrayContaining([ + expect.objectContaining({ + type: 'element', + tagName: 'a', + properties: expect.objectContaining({ + href: '/url%5Cbar*baz', + }), + }), + expect.objectContaining({ + type: 'element', + tagName: 'img', + properties: expect.objectContaining({ + src: '/url%5Cbar*baz', + }), + }), + ]), + }), + ); + }); + }); + }); + + describe('frontmatter', () => { + describe('when skipping the rendering of frontmatter types', () => { + it.each` + type | input + ${'yaml'} | ${'---\ntitle: page\n---'} + ${'toml'} | ${'+++\ntitle: page\n+++'} + ${'json'} | ${';;;\ntitle: page\n;;;'} + `('transforms $type nodes into frontmatter html tags', async ({ input, type }) => { + const result = await markdownToAST(input, [type]); + + expectInRoot( + result, + expect.objectContaining({ + type: 'element', + tagName: 'frontmatter', + properties: { + language: type, + }, + children: [ + { + type: 'text', + value: 'title: page', + }, + ], + }), + ); + }); + }); + }); + + describe('table of contents', () => { + it.each` + markdown + ${'[[_TOC_]]'} + ${' [[_TOC_]]'} + ${'[[_TOC_]] '} + ${'[TOC]'} + ${' [TOC]'} + ${'[TOC] '} + `('parses $markdown and produces a table of contents section', async ({ markdown }) => { + const result = await markdownToAST(markdown); expectInRoot( result, expect.objectContaining({ - tagName: 'p', - children: expect.arrayContaining([ - expect.objectContaining({ - type: 'element', - tagName: 'a', - properties: expect.objectContaining({ - href: '/url%5Cbar*baz', - }), - }), - expect.objectContaining({ - type: 'element', - tagName: 'img', - properties: expect.objectContaining({ - src: '/url%5Cbar*baz', - }), - }), - ]), + type: 'element', + tagName: 'nav', }), ); }); }); - }); - describe('when skipping the rendering of frontmatter types', () => { - it.each` - type | input - ${'yaml'} | ${'---\ntitle: page\n---'} - ${'toml'} | ${'+++\ntitle: page\n+++'} - ${'json'} | ${';;;\ntitle: page\n;;;'} - `('transforms $type nodes into frontmatter html tags', async ({ input, type }) => { - const result = await markdownToAST(input, [type]); + describe('when skipping the rendering of table of contents', () => { + it('transforms table of contents nodes into html tableofcontents tags', async () => { + const result = await markdownToAST('[[_TOC_]]', ['tableOfContents']); - expectInRoot( - result, - expect.objectContaining({ - type: 'element', - tagName: 'frontmatter', - properties: { - language: type, - }, - children: [ - { - type: 'text', - value: 'title: page', - }, - ], - }), - ); + expectInRoot( + result, + expect.objectContaining({ + type: 'element', + tagName: 'tableofcontents', + }), + ); + }); }); }); }); diff --git a/yarn.lock b/yarn.lock index 7b3291b9c79096..ece1ad1b658df1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11652,7 +11652,7 @@ unist-util-generated@^2.0.0: resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113" integrity sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw== -unist-util-is@^5.0.0: +unist-util-is@^5.0.0, unist-util-is@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.1.1.tgz#e8aece0b102fa9bc097b0fef8f870c496d4a6236" integrity sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ== -- GitLab From 7d2efdae4f8c156fb4be56ff1f791791bf30516d Mon Sep 17 00:00:00 2001 From: Enrique Alcantara <ealcantara@gitlab.com> Date: Thu, 1 Sep 2022 09:06:31 -0400 Subject: [PATCH 068/169] Code review feedback Revert change that moves rehype plugin inclusion to the render method --- app/assets/javascripts/lib/gfm/index.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/lib/gfm/index.js b/app/assets/javascripts/lib/gfm/index.js index d2cc343042f305..d282d3759f8ca2 100644 --- a/app/assets/javascripts/lib/gfm/index.js +++ b/app/assets/javascripts/lib/gfm/index.js @@ -73,12 +73,20 @@ const skipRenderingHandlers = { json: skipFrontmatterHandler('json'), }; -const createParser = () => { +const createParser = ({ skipRendering }) => { return unified() .use(remarkParse) .use(remarkGfm) .use(remarkFrontmatter, ['yaml', 'toml', { type: 'json', marker: ';' }]) - .use(glfmTableOfContents); + .use(glfmTableOfContents) + .use(remarkRehype, { + allowDangerousHtml: true, + handlers: { + ...glfmMdastToHastHandlers, + ...pick(skipRenderingHandlers, skipRendering), + }, + }) + .use(rehypeRaw); }; const compilerFactory = (renderer) => @@ -107,14 +115,6 @@ const compilerFactory = (renderer) => */ export const render = async ({ markdown, renderer, skipRendering = [] }) => { const { result } = await createParser({ skipRendering }) - .use(remarkRehype, { - allowDangerousHtml: true, - handlers: { - ...glfmMdastToHastHandlers, - ...pick(skipRenderingHandlers, skipRendering), - }, - }) - .use(rehypeRaw) .use(compilerFactory(renderer)) .process(markdown); -- GitLab From e36df64aaa1bb27d8c82a8bd57098433ce7885e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thiago=20Figueir=C3=B3?= <tfigueiro@gitlab.com> Date: Fri, 2 Sep 2022 13:40:00 +1000 Subject: [PATCH 069/169] Fix Layout/HashAlignment offenses --- .rubocop_todo/layout/hash_alignment.yml | 15 -------- .../group/object_builder_spec.rb | 4 +- .../group/relation_tree_restorer_spec.rb | 18 ++++----- .../project/relation_tree_restorer_spec.rb | 18 ++++----- .../sample/relation_tree_restorer_spec.rb | 18 ++++----- spec/lib/gitlab/import_sources_spec.rb | 20 +++++----- .../lib/gitlab/instrumentation_helper_spec.rb | 14 +++---- spec/lib/gitlab/jira/middleware_spec.rb | 2 +- .../active_record/extension_spec.rb | 4 +- .../importers/prometheus_metrics_spec.rb | 30 +++++++-------- .../dashboard/validator/errors_spec.rb | 38 +++++++++---------- .../metrics/dashboard/validator_spec.rb | 24 ++++++------ .../metrics/requests_rack_middleware_spec.rb | 10 ++--- .../metrics/subscribers/action_view_spec.rb | 2 +- .../metrics/subscribers/active_record_spec.rb | 6 +-- .../subscribers/load_balancing_spec.rb | 4 +- 16 files changed, 105 insertions(+), 122 deletions(-) diff --git a/.rubocop_todo/layout/hash_alignment.yml b/.rubocop_todo/layout/hash_alignment.yml index f15ea0d250641a..a6857e2eb2711b 100644 --- a/.rubocop_todo/layout/hash_alignment.yml +++ b/.rubocop_todo/layout/hash_alignment.yml @@ -4,21 +4,6 @@ Layout/HashAlignment: Exclude: - 'ee/spec/lib/ee/gitlab/usage_data_spec.rb' - 'spec/controllers/projects/merge_requests_controller_spec.rb' - - 'spec/lib/gitlab/import_export/group/object_builder_spec.rb' - - 'spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb' - - 'spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb' - - 'spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb' - - 'spec/lib/gitlab/import_sources_spec.rb' - - 'spec/lib/gitlab/instrumentation_helper_spec.rb' - - 'spec/lib/gitlab/jira/middleware_spec.rb' - - 'spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb' - - 'spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb' - - 'spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb' - - 'spec/lib/gitlab/metrics/dashboard/validator_spec.rb' - - 'spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb' - - 'spec/lib/gitlab/metrics/subscribers/action_view_spec.rb' - - 'spec/lib/gitlab/metrics/subscribers/active_record_spec.rb' - - 'spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb' - 'spec/routing/project_routing_spec.rb' - 'spec/serializers/ci/lint/job_entity_spec.rb' - 'spec/serializers/container_repository_entity_spec.rb' diff --git a/spec/lib/gitlab/import_export/group/object_builder_spec.rb b/spec/lib/gitlab/import_export/group/object_builder_spec.rb index 09f40199b31c44..25d9858dd4cbcb 100644 --- a/spec/lib/gitlab/import_export/group/object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/group/object_builder_spec.rb @@ -6,9 +6,9 @@ let(:group) { create(:group) } let(:base_attributes) do { - 'title' => 'title', + 'title' => 'title', 'description' => 'description', - 'group' => group + 'group' => group } end diff --git a/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb index 2f1e2dd2db4bb5..5e84284a060834 100644 --- a/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb @@ -33,15 +33,15 @@ let(:relation_tree_restorer) do described_class.new( - user: user, - shared: shared, - relation_reader: relation_reader, - object_builder: Gitlab::ImportExport::Group::ObjectBuilder, - members_mapper: members_mapper, - relation_factory: Gitlab::ImportExport::Group::RelationFactory, - reader: reader, - importable: importable, - importable_path: nil, + user: user, + shared: shared, + relation_reader: relation_reader, + object_builder: Gitlab::ImportExport::Group::ObjectBuilder, + members_mapper: members_mapper, + relation_factory: Gitlab::ImportExport::Group::RelationFactory, + reader: reader, + importable: importable, + importable_path: nil, importable_attributes: attributes ) end diff --git a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb index b7b652005e90a5..ac646087a95c21 100644 --- a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb @@ -21,15 +21,15 @@ let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } let(:relation_tree_restorer) do described_class.new( - user: user, - shared: shared, - relation_reader: relation_reader, - object_builder: Gitlab::ImportExport::Project::ObjectBuilder, - members_mapper: members_mapper, - relation_factory: Gitlab::ImportExport::Project::RelationFactory, - reader: reader, - importable: importable, - importable_path: 'project', + user: user, + shared: shared, + relation_reader: relation_reader, + object_builder: Gitlab::ImportExport::Project::ObjectBuilder, + members_mapper: members_mapper, + relation_factory: Gitlab::ImportExport::Project::RelationFactory, + reader: reader, + importable: importable, + importable_path: 'project', importable_attributes: attributes ) end diff --git a/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb index 3dab84af7443aa..d1fe9b80062d7c 100644 --- a/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb @@ -21,15 +21,15 @@ let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) } let(:sample_data_relation_tree_restorer) do described_class.new( - user: user, - shared: shared, - relation_reader: relation_reader, - object_builder: Gitlab::ImportExport::Project::ObjectBuilder, - members_mapper: members_mapper, - relation_factory: Gitlab::ImportExport::Project::Sample::RelationFactory, - reader: reader, - importable: importable, - importable_path: 'project', + user: user, + shared: shared, + relation_reader: relation_reader, + object_builder: Gitlab::ImportExport::Project::ObjectBuilder, + members_mapper: members_mapper, + relation_factory: Gitlab::ImportExport::Project::Sample::RelationFactory, + reader: reader, + importable: importable, + importable_path: 'project', importable_attributes: attributes ) end diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb index f42a109aa3a659..41ffcece221a63 100644 --- a/spec/lib/gitlab/import_sources_spec.rb +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -7,17 +7,17 @@ it 'returns a hash' do expected = { - 'GitHub' => 'github', - 'Bitbucket Cloud' => 'bitbucket', - 'Bitbucket Server' => 'bitbucket_server', - 'GitLab.com' => 'gitlab', - 'Google Code' => 'google_code', - 'FogBugz' => 'fogbugz', + 'GitHub' => 'github', + 'Bitbucket Cloud' => 'bitbucket', + 'Bitbucket Server' => 'bitbucket_server', + 'GitLab.com' => 'gitlab', + 'Google Code' => 'google_code', + 'FogBugz' => 'fogbugz', 'Repository by URL' => 'git', - 'GitLab export' => 'gitlab_project', - 'Gitea' => 'gitea', - 'Manifest file' => 'manifest', - 'Phabricator' => 'phabricator' + 'GitLab export' => 'gitlab_project', + 'Gitea' => 'gitea', + 'Manifest file' => 'manifest', + 'Phabricator' => 'phabricator' } expect(described_class.options).to eq(expected) diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index 4fa9079144de2a..d5ff39767c47f2 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -140,13 +140,13 @@ subject expect(payload).to include(db_replica_count: 0, - db_replica_cached_count: 0, - db_primary_count: 0, - db_primary_cached_count: 0, - db_primary_wal_count: 0, - db_replica_wal_count: 0, - db_primary_wal_cached_count: 0, - db_replica_wal_cached_count: 0) + db_replica_cached_count: 0, + db_primary_count: 0, + db_primary_cached_count: 0, + db_primary_wal_count: 0, + db_replica_wal_count: 0, + db_primary_wal_cached_count: 0, + db_replica_wal_cached_count: 0) end context 'when replica caught up search was made' do diff --git a/spec/lib/gitlab/jira/middleware_spec.rb b/spec/lib/gitlab/jira/middleware_spec.rb index 1d1f736853418f..09cf67d0657be4 100644 --- a/spec/lib/gitlab/jira/middleware_spec.rb +++ b/spec/lib/gitlab/jira/middleware_spec.rb @@ -24,7 +24,7 @@ describe '#call' do it 'adjusts HTTP_AUTHORIZATION env when request from Jira DVCS user agent' do expect(app).to receive(:call).with({ 'HTTP_USER_AGENT' => jira_user_agent, - 'HTTP_AUTHORIZATION' => 'Bearer hash-123' }) + 'HTTP_AUTHORIZATION' => 'Bearer hash-123' }) middleware.call('HTTP_USER_AGENT' => jira_user_agent, 'HTTP_AUTHORIZATION' => 'token hash-123') end diff --git a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb index 81910773dfa14b..57f2b1cfd966e7 100644 --- a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb +++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb @@ -174,8 +174,8 @@ expect(thing).to receive(:update_columns) .with({ "title_html" => updated_html, - "description_html" => "", - "cached_markdown_version" => cache_version }) + "description_html" => "", + "cached_markdown_version" => cache_version }) thing.refresh_markdown_cache! end diff --git a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb index c15e717b126ee6..bc6cd3837584a0 100644 --- a/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/importers/prometheus_metrics_spec.rb @@ -24,13 +24,13 @@ context 'with existing metrics' do let(:existing_metric_attributes) do { - project: project, - identifier: 'metric_b', - title: 'overwrite', - y_label: 'overwrite', - query: 'overwrite', - unit: 'overwrite', - legend: 'overwrite', + project: project, + identifier: 'metric_b', + title: 'overwrite', + y_label: 'overwrite', + query: 'overwrite', + unit: 'overwrite', + legend: 'overwrite', dashboard_path: dashboard_path } end @@ -43,11 +43,11 @@ subject.execute expect(existing_metric.reload.attributes.with_indifferent_access).to include({ - title: 'Super Chart B', + title: 'Super Chart B', y_label: 'y_label', - query: 'query', - unit: 'unit', - legend: 'Legend Label' + query: 'query', + unit: 'unit', + legend: 'Legend Label' }) end @@ -69,11 +69,11 @@ subject.execute expect(existing_metric.reload.attributes.with_indifferent_access).to include({ - title: 'Super Chart B', + title: 'Super Chart B', y_label: 'y_label', - query: 'query', - unit: 'unit', - legend: 'Legend Label' + query: 'query', + unit: 'unit', + legend: 'Legend Label' }) end diff --git a/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb index fdbba6c31b5930..a50c2a506cb012 100644 --- a/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb @@ -17,11 +17,11 @@ let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => 'schema', - 'details' => details + 'type' => type, + 'schema' => 'schema', + 'details' => details } end @@ -72,10 +72,10 @@ let(:type) { 'pattern' } let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'pattern' => 'aa.*' } + 'type' => type, + 'schema' => { 'pattern' => 'aa.*' } } end @@ -86,10 +86,10 @@ let(:type) { 'format' } let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'format' => 'date-time' } + 'type' => type, + 'schema' => { 'format' => 'date-time' } } end @@ -100,10 +100,10 @@ let(:type) { 'const' } let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'const' => 'one' } + 'type' => type, + 'schema' => { 'const' => 'one' } } end @@ -114,10 +114,10 @@ let(:type) { 'enum' } let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => { 'enum' => %w(one two) } + 'type' => type, + 'schema' => { 'enum' => %w(one two) } } end @@ -128,10 +128,10 @@ let(:type) { 'unknown' } let(:error_hash) do { - 'data' => 'property_name', + 'data' => 'property_name', 'data_pointer' => pointer, - 'type' => type, - 'schema' => 'schema' + 'type' => type, + 'schema' => 'schema' } end diff --git a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb index eb67ea2b7da584..aaa9daf8feebd9 100644 --- a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb @@ -33,9 +33,9 @@ context 'with metric identifier present in current dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'test/path.yml', - project: project + project: project ) end @@ -45,9 +45,9 @@ context 'with metric identifier present in another dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'some/other/dashboard/path.yml', - project: project + project: project ) end @@ -94,9 +94,9 @@ context 'with metric identifier present in current dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'test/path.yml', - project: project + project: project ) end @@ -106,9 +106,9 @@ context 'with metric identifier present in another dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'some/other/dashboard/path.yml', - project: project + project: project ) end @@ -166,9 +166,9 @@ context 'with metric identifier present in current dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'test/path.yml', - project: project + project: project ) end @@ -178,9 +178,9 @@ context 'with metric identifier present in another dashboard' do before do create(:prometheus_metric, - identifier: 'metric_a1', + identifier: 'metric_a1', dashboard_path: 'some/other/dashboard/path.yml', - project: project + project: project ) end diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb index 3396de9b12c6e9..ed78548ef62502 100644 --- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb @@ -194,9 +194,8 @@ let(:endpoint) do route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)') - double(:endpoint, route: route, - options: { for: api_handler, path: [":id/archive"] }, - namespace: "/projects") + double(:endpoint, + route: route, options: { for: api_handler, path: [":id/archive"] }, namespace: "/projects") end let(:env) { { 'api.endpoint' => endpoint, 'REQUEST_METHOD' => 'GET' } } @@ -256,9 +255,8 @@ context 'Grape API without expected duration' do let(:endpoint) do route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)') - double(:endpoint, route: route, - options: { for: api_handler, path: [":id/archive"] }, - namespace: "/projects") + double(:endpoint, + route: route, options: { for: api_handler, path: [":id/archive"] }, namespace: "/projects") end let(:env) { { 'api.endpoint' => endpoint, 'REQUEST_METHOD' => 'GET' } } diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb index adbc474343f6f6..67cd863075884d 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -12,7 +12,7 @@ root = Rails.root.to_s double(:event, duration: 2.1, - payload: { identifier: "#{root}/app/views/x.html.haml" }) + payload: { identifier: "#{root}/app/views/x.html.haml" }) end before do diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 28c3ef229ab38d..005c1ae2d0aa52 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -137,7 +137,7 @@ :event, name: 'transaction.active_record', duration: 230, - payload: { connection: connection } + payload: { connection: connection } ) end @@ -213,7 +213,7 @@ :event, name: 'sql.active_record', duration: 2, - payload: payload + payload: payload ) end @@ -278,7 +278,7 @@ def sql(query, comments: true) :event, name: 'sql.active_record', duration: 2, - payload: payload + payload: payload ) end diff --git a/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb b/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb index bc6effd0438ec8..7f7efaffd9e8c5 100644 --- a/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb @@ -15,7 +15,7 @@ double( :event, name: 'load_balancing.caught_up_replica_pick', - payload: payload + payload: payload ) end @@ -37,7 +37,7 @@ double( :event, name: 'load_balancing.web_transaction_completed', - payload: {} + payload: {} ) end -- GitLab From 3836367a37ac234863f4a01190c26655ed3146fe Mon Sep 17 00:00:00 2001 From: Enrique Alcantara <ealcantara@gitlab.com> Date: Mon, 5 Sep 2022 16:04:36 -0400 Subject: [PATCH 070/169] Code review feedback Rename parser_extensions dir to glfm_extensions Create constants for table of contents tokens Remove unist-util-is dependency --- app/assets/javascripts/lib/gfm/constants.js | 10 +++ .../gfm/glfm_extensions/table_of_contents.js | 85 +++++++++++++++++++ app/assets/javascripts/lib/gfm/index.js | 2 +- .../glfm_table_of_contents.js | 69 --------------- package.json | 1 - yarn.lock | 2 +- 6 files changed, 97 insertions(+), 72 deletions(-) create mode 100644 app/assets/javascripts/lib/gfm/constants.js create mode 100644 app/assets/javascripts/lib/gfm/glfm_extensions/table_of_contents.js delete mode 100644 app/assets/javascripts/lib/gfm/parser_extensions/glfm_table_of_contents.js diff --git a/app/assets/javascripts/lib/gfm/constants.js b/app/assets/javascripts/lib/gfm/constants.js new file mode 100644 index 00000000000000..eaabeb2a76764a --- /dev/null +++ b/app/assets/javascripts/lib/gfm/constants.js @@ -0,0 +1,10 @@ +export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN = '[['; +export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN = 'TOC'; +export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN = ']]'; +export const TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN = '[TOC]'; + +export const MDAST_TEXT_NODE = 'text'; +export const MDAST_EMPHASIS_NODE = 'emphasis'; +export const MDAST_PARAGRAPH_NODE = 'paragraph'; + +export const GLFM_TABLE_OF_CONTENTS_NODE = 'tableOfContents'; diff --git a/app/assets/javascripts/lib/gfm/glfm_extensions/table_of_contents.js b/app/assets/javascripts/lib/gfm/glfm_extensions/table_of_contents.js new file mode 100644 index 00000000000000..4d2484a657ab58 --- /dev/null +++ b/app/assets/javascripts/lib/gfm/glfm_extensions/table_of_contents.js @@ -0,0 +1,85 @@ +import { first, last } from 'lodash'; +import { u } from 'unist-builder'; +import { visitParents, SKIP, CONTINUE } from 'unist-util-visit-parents'; +import { + TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN, + TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN, + TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN, + TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN, + MDAST_TEXT_NODE, + MDAST_EMPHASIS_NODE, + MDAST_PARAGRAPH_NODE, + GLFM_TABLE_OF_CONTENTS_NODE, +} from '../constants'; + +const isTOCTextNode = ({ type, value }) => + type === MDAST_TEXT_NODE && value === TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN; + +const isTOCEmphasisNode = ({ type, children }) => + type === MDAST_EMPHASIS_NODE && children.length === 1 && isTOCTextNode(first(children)); + +const isTOCDoubleSquareBracketOpenTokenTextNode = ({ type, value }) => + type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN; + +const isTOCDoubleSquareBracketCloseTokenTextNode = ({ type, value }) => + type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN; + +/* + * Detects table of contents declaration with syntax [[_TOC_]] + */ +const isTableOfContentsDoubleSquareBracketSyntax = ({ children }) => { + if (children.length !== 3) { + return false; + } + + const [firstChild, middleChild, lastChild] = children; + + return ( + isTOCDoubleSquareBracketOpenTokenTextNode(firstChild) && + isTOCEmphasisNode(middleChild) && + isTOCDoubleSquareBracketCloseTokenTextNode(lastChild) + ); +}; + +/* + * Detects table of contents declaration with syntax [TOC] + */ +const isTableOfContentsSingleSquareBracketSyntax = ({ children }) => { + if (children.length !== 1) { + return false; + } + + const [firstChild] = children; + const { type, value } = firstChild; + + return type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN; +}; + +const isTableOfContentsNode = (node) => + node.type === MDAST_PARAGRAPH_NODE && + (isTableOfContentsDoubleSquareBracketSyntax(node) || + isTableOfContentsSingleSquareBracketSyntax(node)); + +export default () => { + return (tree) => { + visitParents(tree, (node, ancestors) => { + const parent = last(ancestors); + + if (!parent) { + return CONTINUE; + } + + if (isTableOfContentsNode(node)) { + const index = parent.children.indexOf(node); + + parent.children[index] = u(GLFM_TABLE_OF_CONTENTS_NODE, { + position: node.position, + }); + } + + return SKIP; + }); + + return tree; + }; +}; diff --git a/app/assets/javascripts/lib/gfm/index.js b/app/assets/javascripts/lib/gfm/index.js index d282d3759f8ca2..fad73f93c1a9e6 100644 --- a/app/assets/javascripts/lib/gfm/index.js +++ b/app/assets/javascripts/lib/gfm/index.js @@ -6,7 +6,7 @@ import remarkFrontmatter from 'remark-frontmatter'; import remarkGfm from 'remark-gfm'; import remarkRehype, { all } from 'remark-rehype'; import rehypeRaw from 'rehype-raw'; -import glfmTableOfContents from './parser_extensions/glfm_table_of_contents'; +import glfmTableOfContents from './glfm_extensions/table_of_contents'; import * as glfmMdastToHastHandlers from './mdast_to_hast_handlers/glfm_mdast_to_hast_handlers'; const skipFrontmatterHandler = (language) => (h, node) => diff --git a/app/assets/javascripts/lib/gfm/parser_extensions/glfm_table_of_contents.js b/app/assets/javascripts/lib/gfm/parser_extensions/glfm_table_of_contents.js deleted file mode 100644 index d0a9ebfcf09d08..00000000000000 --- a/app/assets/javascripts/lib/gfm/parser_extensions/glfm_table_of_contents.js +++ /dev/null @@ -1,69 +0,0 @@ -import { last } from 'lodash'; -import { u } from 'unist-builder'; -import { is } from 'unist-util-is'; -import { visitParents, SKIP, CONTINUE } from 'unist-util-visit-parents'; - -const isTOCTextNode = (node) => is(node, { type: 'text', value: 'TOC' }); - -/* - * Detects table of contents declaration with syntax [[_TOC_]] - */ -const isTableOfContentsDoubleSquareBracketSyntax = ({ children }) => { - if (children.length !== 3) { - return false; - } - - const [firstChild, middleChild, lastChild] = children; - - return ( - is(firstChild, ({ type, value }) => type === 'text' && value.trim() === '[[') && - is( - middleChild, - ({ type, children: emChildren }) => - type === 'emphasis' && emChildren.length === 1 && isTOCTextNode(emChildren[0]), - ) && - is(lastChild, { type: 'text', value: ']]' }) - ); -}; - -/* - * Detects table of contents declaration with syntax [TOC] - */ -const isTableOfContentsSingleSquareBracketSyntax = ({ children }) => { - if (children.length !== 1) { - return false; - } - - const [firstChild] = children; - - return is(firstChild, ({ type, value }) => type === 'text' && value.trim() === '[TOC]'); -}; - -const isTableOfContentsNode = (node) => - node.type === 'paragraph' && - (isTableOfContentsDoubleSquareBracketSyntax(node) || - isTableOfContentsSingleSquareBracketSyntax(node)); - -export default () => { - return (tree) => { - visitParents(tree, (node, ancestors) => { - const parent = last(ancestors); - - if (!parent) { - return CONTINUE; - } - - if (isTableOfContentsNode(node)) { - const index = parent.children.indexOf(node); - - parent.children[index] = u('tableOfContents', { - position: node.position, - }); - } - - return SKIP; - }); - - return tree; - }; -}; diff --git a/package.json b/package.json index 2071edfcb1dc91..f5546a8f775924 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,6 @@ "timeago.js": "^4.0.2", "unified": "^10.1.2", "unist-builder": "^3.0.0", - "unist-util-is": "^5.1.1", "unist-util-visit-parents": "^5.1.0", "url-loader": "^4.1.1", "uuid": "8.1.0", diff --git a/yarn.lock b/yarn.lock index ece1ad1b658df1..7b3291b9c79096 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11652,7 +11652,7 @@ unist-util-generated@^2.0.0: resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113" integrity sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw== -unist-util-is@^5.0.0, unist-util-is@^5.1.1: +unist-util-is@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.1.1.tgz#e8aece0b102fa9bc097b0fef8f870c496d4a6236" integrity sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ== -- GitLab From e6c17621ba53cb508a56f719120d06f27733cbdd Mon Sep 17 00:00:00 2001 From: Dylan Griffith <dyl.griffith@gmail.com> Date: Mon, 5 Sep 2022 16:14:17 -0500 Subject: [PATCH 071/169] Clarify migration docs can add not null in single MR for small tables This previous documentation suggested you needed to split the changes across multiple releases but this is only necessary for large tables where you need a background migraiton for cleanup. Otherwise it should be safe to use a single merge request with 3 separate `post_migrate` migrations. --- doc/development/database/not_null_constraints.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/development/database/not_null_constraints.md b/doc/development/database/not_null_constraints.md index 9b3d017b09fbaa..72921d4b567b2d 100644 --- a/doc/development/database/not_null_constraints.md +++ b/doc/development/database/not_null_constraints.md @@ -53,8 +53,11 @@ end ## Add a `NOT NULL` constraint to an existing column -Adding `NOT NULL` to existing database columns requires multiple steps split into at least two -different releases: +Adding `NOT NULL` to existing database columns usually requires multiple steps split into at least two +different releases. If your table is sufficiently small, that you don't need to +use a background migration, then you can include all these in the same merge +request, but it's still recommended to use separate migrations to reduce +transaction durations. The steps required are: 1. Release `N.M` (current release) -- GitLab From fd0c9526a98e1d9ea9b7278fc45b8b051a5295f4 Mon Sep 17 00:00:00 2001 From: "Karthik (PK)" <parkourkarthik@gmail.com> Date: Mon, 5 Sep 2022 21:46:34 +0000 Subject: [PATCH 072/169] Update packages registries menu as sentence case The packages and registries is updated as per the punctuation guidelines Changelog: changed --- .../packages_and_registries/show.html.haml | 4 ++-- .../cleanup_tags.html.haml | 4 ++-- .../packages_and_registries/show.html.haml | 4 ++-- doc/ci/cloud_deployment/ecs/deploy_to_aws_ecs.md | 2 +- doc/ci/migration/jenkins.md | 2 +- .../packages/new_format_development.md | 2 +- doc/topics/build_your_application.md | 2 +- doc/user/packages/composer_repository/index.md | 4 ++-- doc/user/packages/conan_repository/index.md | 2 +- doc/user/packages/container_registry/index.md | 12 ++++++------ .../reduce_container_registry_storage.md | 2 +- doc/user/packages/dependency_proxy/index.md | 6 +++--- .../reduce_dependency_proxy_storage.md | 2 +- doc/user/packages/generic_packages/index.md | 2 +- .../packages/infrastructure_registry/index.md | 8 ++++---- doc/user/packages/maven_repository/index.md | 6 +++--- doc/user/packages/npm_registry/index.md | 2 +- doc/user/packages/package_registry/index.md | 6 +++--- .../reduce_package_registry_storage.md | 6 +++--- doc/user/packages/pypi_repository/index.md | 2 +- doc/user/packages/rubygems_registry/index.md | 2 +- ee/spec/features/groups/navbar_spec.rb | 2 +- .../groups/menus/packages_registries_menu.rb | 2 +- lib/sidebars/groups/menus/settings_menu.rb | 2 +- .../projects/menus/packages_registries_menu.rb | 2 +- lib/sidebars/projects/menus/settings_menu.rb | 2 +- locale/gitlab.pot | 8 ++++---- qa/qa/page/group/menu.rb | 6 +++--- qa/qa/page/project/sub_menus/packages.rb | 4 ++-- spec/features/groups/navbar_spec.rb | 2 +- .../settings/packages_and_registries_spec.rb | 10 +++++----- .../settings/user_searches_in_settings_spec.rb | 2 +- spec/features/projects/navbar_spec.rb | 2 +- .../registry_settings_cleanup_tags_spec.rb | 2 +- .../projects/settings/registry_settings_spec.rb | 2 +- .../sidebars/groups/menus/settings_menu_spec.rb | 2 +- .../projects/menus/settings_menu_spec.rb | 2 +- spec/support/helpers/navbar_structure_helper.rb | 10 +++++----- .../shared_contexts/navbar_structure_context.rb | 4 ++-- .../layouts/nav/sidebar/_group.html.haml_spec.rb | 6 +++--- .../nav/sidebar/_project.html.haml_spec.rb | 16 ++++++++-------- 41 files changed, 85 insertions(+), 85 deletions(-) diff --git a/app/views/groups/settings/packages_and_registries/show.html.haml b/app/views/groups/settings/packages_and_registries/show.html.haml index e414a59c55d980..2861e696e31a4e 100644 --- a/app/views/groups/settings/packages_and_registries/show.html.haml +++ b/app/views/groups/settings/packages_and_registries/show.html.haml @@ -1,5 +1,5 @@ -- breadcrumb_title _('Package & registry settings') -- page_title _('Package & registry settings') +- breadcrumb_title _('Package and registry settings') +- page_title _('Package and registry settings') - @content_class = 'limit-container-width' unless fluid_layout %section#js-packages-and-registries-settings{ data: { group_path: @group.full_path, diff --git a/app/views/projects/settings/packages_and_registries/cleanup_tags.html.haml b/app/views/projects/settings/packages_and_registries/cleanup_tags.html.haml index 9994ea0c912d05..5244587c16dce5 100644 --- a/app/views/projects/settings/packages_and_registries/cleanup_tags.html.haml +++ b/app/views/projects/settings/packages_and_registries/cleanup_tags.html.haml @@ -1,6 +1,6 @@ -- add_to_breadcrumbs _('Package & registry settings'), project_settings_packages_and_registries_path(@project) +- add_to_breadcrumbs _('Package and registry settings'), project_settings_packages_and_registries_path(@project) - breadcrumb_title s_('ContainerRegistry|Clean up image tags') -- page_title s_('ContainerRegistry|Clean up image tags'), _('Package & registry settings') +- page_title s_('ContainerRegistry|Clean up image tags'), _('Package and registry settings') - @content_class = 'limit-container-width' unless fluid_layout #js-registry-settings-cleanup-image-tags{ data: cleanup_settings_data } diff --git a/app/views/projects/settings/packages_and_registries/show.html.haml b/app/views/projects/settings/packages_and_registries/show.html.haml index 4a8c48be7ad7c8..e0ac07f5f31b5a 100644 --- a/app/views/projects/settings/packages_and_registries/show.html.haml +++ b/app/views/projects/settings/packages_and_registries/show.html.haml @@ -1,5 +1,5 @@ -- breadcrumb_title _('Package & registry settings') -- page_title _('Package & registry settings') +- breadcrumb_title _('Package and registry settings') +- page_title _('Package and registry settings') - @content_class = 'limit-container-width' unless fluid_layout #js-registry-settings{ data: settings_data } diff --git a/doc/ci/cloud_deployment/ecs/deploy_to_aws_ecs.md b/doc/ci/cloud_deployment/ecs/deploy_to_aws_ecs.md index aea7b492d4e160..2d1c3f927e230a 100644 --- a/doc/ci/cloud_deployment/ecs/deploy_to_aws_ecs.md +++ b/doc/ci/cloud_deployment/ecs/deploy_to_aws_ecs.md @@ -79,7 +79,7 @@ and [Container Registry](../../../user/packages/container_registry/index.md).  -1. Visit **Packages & Registries > Container Registry**. Make sure the application image has been +1. Visit **Packages and registries > Container Registry**. Make sure the application image has been pushed.  diff --git a/doc/ci/migration/jenkins.md b/doc/ci/migration/jenkins.md index c59116ea8ed149..35c5a7e56c87ac 100644 --- a/doc/ci/migration/jenkins.md +++ b/doc/ci/migration/jenkins.md @@ -190,7 +190,7 @@ pdf: Additionally, we have package management features like built-in container and package registries that you can leverage. You can see the complete list of packaging features in the -[Packages & Registries](../../user/packages/index.md) documentation. +[Packages and registries](../../user/packages/index.md) documentation. ## Integrated features diff --git a/doc/development/packages/new_format_development.md b/doc/development/packages/new_format_development.md index 24cdc8c1f8ecdc..73a2b2f1f8190d 100644 --- a/doc/development/packages/new_format_development.md +++ b/doc/development/packages/new_format_development.md @@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w This document guides you through adding support to GitLab for a new a [package management system](../../administration/packages/index.md). -See the already supported formats in the [Packages & Registries documentation](../../user/packages/index.md) +See the already supported formats in the [Packages and registries documentation](../../user/packages/index.md) It is possible to add a new format with only backend changes. This guide is superficial and does not cover the way the code should be written. diff --git a/doc/topics/build_your_application.md b/doc/topics/build_your_application.md index d7097e55052ade..d48b838327191a 100644 --- a/doc/topics/build_your_application.md +++ b/doc/topics/build_your_application.md @@ -12,5 +12,5 @@ code, and use CI/CD to generate your application. Include packages in your app a - [Repositories](../user/project/repository/index.md) - [Merge requests](../user/project/merge_requests/index.md) - [CI/CD](../ci/index.md) -- [Packages & Registries](../user/packages/index.md) +- [Packages and registries](../user/packages/index.md) - [Application infrastructure](../user/infrastructure/index.md) diff --git a/doc/user/packages/composer_repository/index.md b/doc/user/packages/composer_repository/index.md index 4fc55d18253536..53116b12b3932f 100644 --- a/doc/user/packages/composer_repository/index.md +++ b/doc/user/packages/composer_repository/index.md @@ -127,7 +127,7 @@ To publish the package with a deploy token: - `<tag>` is the Git tag name of the version you want to publish. To publish a branch, use `branch=<branch>` instead of `tag=<tag>`. -You can view the published package by going to **Packages & Registries > Package Registry** and +You can view the published package by going to **Packages and registries > Package Registry** and selecting the **Composer** tab. ## Publish a Composer package by using CI/CD @@ -149,7 +149,7 @@ You can publish a Composer package to the Package Registry as part of your CI/CD 1. Run the pipeline. -To view the published package, go to **Packages & Registries > Package Registry** and select the **Composer** tab. +To view the published package, go to **Packages and registries > Package Registry** and select the **Composer** tab. ### Use a CI/CD template diff --git a/doc/user/packages/conan_repository/index.md b/doc/user/packages/conan_repository/index.md index 7260dbb616c70b..549923cb3a83ff 100644 --- a/doc/user/packages/conan_repository/index.md +++ b/doc/user/packages/conan_repository/index.md @@ -389,7 +389,7 @@ There are two ways to remove a Conan package from the GitLab Package Registry. - From the GitLab user interface: - Go to your project's **Packages & Registries > Package Registry**. Remove the + Go to your project's **Packages and registries > Package Registry**. Remove the package by selecting **Remove repository** (**{remove}**). ## Search for Conan packages in the Package Registry diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index a203de2ed2c5e8..a079baaf12c8fa 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -26,7 +26,7 @@ Registry for your GitLab instance, visit the You can view the Container Registry for a project or group. 1. Go to your project or group. -1. Go to **Packages & Registries > Container Registry**. +1. Go to **Packages and registries > Container Registry**. You can search, sort, filter, and [delete](#delete-images-from-within-gitlab) containers on this page. You can share a filtered view by copying the URL from your browser. @@ -40,7 +40,7 @@ If a project is public, so is the Container Registry. You can view a list of tags associated with a given container image: 1. Go to your project or group. -1. Go to **Packages & Registries > Container Registry**. +1. Go to **Packages and registries > Container Registry**. 1. Select the container image you are interested in. This brings up the Container Registry **Tag Details** page. You can view details about each tag, @@ -55,7 +55,7 @@ tags on this page. You can share a filtered view by copying the URL from your br To download and run a container image hosted in the GitLab Container Registry: 1. Copy the link to your container image: - - Go to your project or group's **Packages & Registries > Container Registry** + - Go to your project or group's **Packages and registries > Container Registry** and find the image you want. - Next to the image name, select **Copy**. @@ -139,7 +139,7 @@ To build and push to the Container Registry: docker push registry.example.com/group/project/image ``` -To view these commands, go to your project's **Packages & Registries > Container Registry**. +To view these commands, go to your project's **Packages and registries > Container Registry**. ## Build and push by using GitLab CI/CD @@ -394,7 +394,7 @@ images), are automatically scheduled for deletion after 24 hours if left unrefer To delete images from within GitLab: -1. Navigate to your project's or group's **Packages & Registries > Container Registry**. +1. Navigate to your project's or group's **Packages and registries > Container Registry**. 1. From the **Container Registry** page, you can select what you want to delete, by either: @@ -508,7 +508,7 @@ You can, however, remove the Container Registry for a project: and disable **Container Registry**. 1. Select **Save changes**. -The **Packages & Registries > Container Registry** entry is removed from the project's sidebar. +The **Packages and registries > Container Registry** entry is removed from the project's sidebar. ## Change visibility of the Container Registry diff --git a/doc/user/packages/container_registry/reduce_container_registry_storage.md b/doc/user/packages/container_registry/reduce_container_registry_storage.md index f889f4836d9c30..590c5c14be483a 100644 --- a/doc/user/packages/container_registry/reduce_container_registry_storage.md +++ b/doc/user/packages/container_registry/reduce_container_registry_storage.md @@ -113,7 +113,7 @@ You can create a cleanup policy in [the API](#use-the-cleanup-policy-api) or the To create a cleanup policy in the UI: -1. For your project, go to **Settings > Packages & Registries**. +1. For your project, go to **Settings > Packages and registries**. 1. Expand the **Clean up image tags** section. 1. Complete the fields. diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md index ea9435de12acee..3aaca00dd4a1bb 100644 --- a/doc/user/packages/dependency_proxy/index.md +++ b/doc/user/packages/dependency_proxy/index.md @@ -38,7 +38,7 @@ For a list of planned additions, view the To enable or turn off the Dependency Proxy for a group: 1. On the top bar, select **Menu > Groups** and find your group. -1. On the left sidebar, select **Settings > Packages & Registries**. +1. On the left sidebar, select **Settings > Packages and registries**. 1. Expand the **Dependency Proxy** section. 1. To enable the proxy, turn on **Enable Proxy**. To turn it off, turn the toggle off. @@ -51,7 +51,7 @@ for the entire GitLab instance. To view the Dependency Proxy: 1. On the top bar, select **Menu > Groups** and find your group. -1. On the left sidebar, select **Packages & Registries > Dependency Proxy**. +1. On the left sidebar, select **Packages and registries > Dependency Proxy**. The Dependency Proxy is not available for projects. @@ -176,7 +176,7 @@ You can also use [custom CI/CD variables](../../../ci/variables/index.md#custom- To store a Docker image in Dependency Proxy storage: 1. On the top bar, select **Menu > Groups** and find your group. -1. On the left sidebar, select **Packages & Registries > Dependency Proxy**. +1. On the left sidebar, select **Packages and registries > Dependency Proxy**. 1. Copy the **Dependency Proxy image prefix**. 1. Use one of these commands. In these examples, the image is `alpine:latest`. 1. You can also pull images by digest to specify exactly which version of an image to pull. diff --git a/doc/user/packages/dependency_proxy/reduce_dependency_proxy_storage.md b/doc/user/packages/dependency_proxy/reduce_dependency_proxy_storage.md index 839684da875f96..fecf60feeef579 100644 --- a/doc/user/packages/dependency_proxy/reduce_dependency_proxy_storage.md +++ b/doc/user/packages/dependency_proxy/reduce_dependency_proxy_storage.md @@ -33,7 +33,7 @@ image or tag from Docker Hub. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340777) in GitLab 14.6 You can enable an automatic time-to-live (TTL) policy for the Dependency Proxy from the user -interface. To do this, navigate to your group's **Settings > Packages & Registries > Dependency Proxy** +interface. To do this, navigate to your group's **Settings > Packages and registries > Dependency Proxy** and enable the setting to automatically clear items from the cache after 90 days. ### Enable cleanup policies with GraphQL diff --git a/doc/user/packages/generic_packages/index.md b/doc/user/packages/generic_packages/index.md index eb8cdd395171f9..312a2c119d6812 100644 --- a/doc/user/packages/generic_packages/index.md +++ b/doc/user/packages/generic_packages/index.md @@ -123,7 +123,7 @@ or the UI. In the UI: -1. For your group, go to **Settings > Packages & Registries**. +1. For your group, go to **Settings > Packages and registries**. 1. Expand the **Package Registry** section. 1. Turn on the **Reject duplicates** toggle. 1. Optional. To allow some duplicate packages, in the **Exceptions** box enter a regex pattern that diff --git a/doc/user/packages/infrastructure_registry/index.md b/doc/user/packages/infrastructure_registry/index.md index e6a179c9d121a6..48cc7b9dea9922 100644 --- a/doc/user/packages/infrastructure_registry/index.md +++ b/doc/user/packages/infrastructure_registry/index.md @@ -18,7 +18,7 @@ projects. To view packages within your project: 1. Go to the project. -1. Go to **Packages & Registries > Infrastructure Registry**. +1. Go to **Packages and registries > Infrastructure Registry**. You can search, sort, and filter packages on this page. @@ -49,7 +49,7 @@ You can see the pipeline that published the package as well as the commit and th To download a package: -1. Go to **Packages & Registries > Infrastructure Registry**. +1. Go to **Packages and registries > Infrastructure Registry**. 1. Select the name of the package you want to download. 1. In the **Activity** section, select the name of the package you want to download. @@ -64,7 +64,7 @@ You can delete packages by using [the API](../../../api/packages.md#delete-a-pro To delete a package in the UI, from your project: -1. Go to **Packages & Registries > Infrastructure Registry**. +1. Go to **Packages and registries > Infrastructure Registry**. 1. Find the name of the package you want to delete. 1. Select **Delete**. @@ -75,7 +75,7 @@ The package is permanently deleted. The Infrastructure Registry is automatically enabled. For self-managed instances, a GitLab administrator can -[disable](../../../administration/packages/index.md) **Packages & Registries**, +[disable](../../../administration/packages/index.md) **Packages and registries**, which removes this menu item from the sidebar. You can also remove the Infrastructure Registry for a specific project: diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md index eaa04404a1fdb2..47fb1431981c62 100644 --- a/doc/user/packages/maven_repository/index.md +++ b/doc/user/packages/maven_repository/index.md @@ -605,7 +605,7 @@ To publish a package by using Gradle: gradle publish ``` -Now navigate to your project's **Packages & Registries** page and view the published artifacts. +Now navigate to your project's **Packages and registries** page and view the published artifacts. ### Publishing a package with the same name or version @@ -624,7 +624,7 @@ To prevent users from publishing duplicate Maven packages, you can use the [Grap In the UI: -1. For your group, go to **Settings > Packages & Registries**. +1. For your group, go to **Settings > Packages and registries**. 1. Expand the **Package Registry** section. 1. Turn on the **Reject duplicates** toggle. 1. Optional. To allow some duplicate packages, in the **Exceptions** box, enter a regex pattern that matches the names and/or versions of packages you want to allow. @@ -699,7 +699,7 @@ dependencies { ## Remove a package -For your project, go to **Packages & Registries > Package Registry**. +For your project, go to **Packages and registries > Package Registry**. To remove a package, select the red trash icon or, from the package details, the **Delete** button. diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md index 28de06b2d8abd4..09b1aae95346bc 100644 --- a/doc/user/packages/npm_registry/index.md +++ b/doc/user/packages/npm_registry/index.md @@ -284,7 +284,7 @@ To upload an npm package to your project, run this command: npm publish ``` -To view the package, go to your project's **Packages & Registries**. +To view the package, go to your project's **Packages and registries**. You can also define `"publishConfig"` for your project in `package.json`. For example: diff --git a/doc/user/packages/package_registry/index.md b/doc/user/packages/package_registry/index.md index 7748780d0e41c5..fe19c5495361cf 100644 --- a/doc/user/packages/package_registry/index.md +++ b/doc/user/packages/package_registry/index.md @@ -26,7 +26,7 @@ Learn how to use the GitLab Package Registry to build your own custom package wo You can view packages for your project or group. 1. Go to the project or group. -1. Go to **Packages & Registries > Package Registry**. +1. Go to **Packages and registries > Package Registry**. You can search, sort, and filter packages on this page. You can share your search results by copying and pasting the URL from your browser. @@ -99,7 +99,7 @@ For information on reducing your storage use for the Package Registry, see The Package Registry is automatically enabled. If you are using a self-managed instance of GitLab, your administrator can remove -the menu item, **Packages & Registries**, from the GitLab sidebar. For more information, +the menu item, **Packages and registries**, from the GitLab sidebar. For more information, see the [administration documentation](../../../administration/packages/index.md). You can also remove the Package Registry for your project specifically: @@ -109,7 +109,7 @@ You can also remove the Package Registry for your project specifically: **Packages** feature. 1. Select **Save changes**. -The **Packages & Registries > Package Registry** entry is removed from the sidebar. +The **Packages and registries > Package Registry** entry is removed from the sidebar. ## Package Registry visibility permissions diff --git a/doc/user/packages/package_registry/reduce_package_registry_storage.md b/doc/user/packages/package_registry/reduce_package_registry_storage.md index cd7dd062f60dc3..34b2f732c4857c 100644 --- a/doc/user/packages/package_registry/reduce_package_registry_storage.md +++ b/doc/user/packages/package_registry/reduce_package_registry_storage.md @@ -29,7 +29,7 @@ You can delete packages by using [the API](../../../api/packages.md#delete-a-pro To delete a package in the UI, from your group or project: -1. Go to **Packages & Registries > Package Registry**. +1. Go to **Packages and registries > Package Registry**. 1. Find the name of the package you want to delete. 1. Select **Delete**. @@ -43,7 +43,7 @@ You can delete packages by using [the API](../../../api/packages.md#delete-a-pac To delete package files in the UI, from your group or project: -1. Go to **Packages & Registries > Package Registry**. +1. Go to **Packages and registries > Package Registry**. 1. Find the name of the package you want to delete. 1. Select the package to view additional details. 1. Find the name of the file you would like to delete. @@ -62,7 +62,7 @@ A cleanup policy defines a set of rules that, applied to a project, defines whic By default, the packages cleanup policy is disabled. To enable it: -1. Go to your project **Settings > Packages & Registries**. +1. Go to your project **Settings > Packages and registries**. 1. Expand **Manage storage used by package assets**. 1. Set the rules appropriately. diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md index b8996dc2963f35..7166d6127070c6 100644 --- a/doc/user/packages/pypi_repository/index.md +++ b/doc/user/packages/pypi_repository/index.md @@ -309,7 +309,7 @@ Uploading mypypipackage-0.0.1.tar.gz 100%|███████████████████████████████████████████████████████████████████████████████████████████| 4.24k/4.24k [00:00<00:00, 11.0kB/s] ``` -To view the published package, go to your project's **Packages & Registries** +To view the published package, go to your project's **Packages and registries** page. If you didn't use a `.pypirc` file to define your repository source, you can diff --git a/doc/user/packages/rubygems_registry/index.md b/doc/user/packages/rubygems_registry/index.md index 05113d0bc10e38..682a3e2ecf18aa 100644 --- a/doc/user/packages/rubygems_registry/index.md +++ b/doc/user/packages/rubygems_registry/index.md @@ -130,7 +130,7 @@ Pushing gem to https://gitlab.example.com/api/v4/projects/1/packages/rubygems... {"message":"201 Created"} ``` -To view the published gem, go to your project's **Packages & Registries** page. Gems pushed to +To view the published gem, go to your project's **Packages and registries** page. Gems pushed to GitLab aren't displayed in your project's Packages UI immediately. It can take up to 10 minutes to process a gem. diff --git a/ee/spec/features/groups/navbar_spec.rb b/ee/spec/features/groups/navbar_spec.rb index 94f0e07e671e34..e026d6e9b86181 100644 --- a/ee/spec/features/groups/navbar_spec.rb +++ b/ee/spec/features/groups/navbar_spec.rb @@ -116,7 +116,7 @@ insert_after_sub_nav_item( _('Package Registry'), - within: _('Packages & Registries'), + within: _('Packages and registries'), new_sub_nav_item_name: _('Container Registry') ) diff --git a/lib/sidebars/groups/menus/packages_registries_menu.rb b/lib/sidebars/groups/menus/packages_registries_menu.rb index fda90406e0ab6b..2077a46a7872da 100644 --- a/lib/sidebars/groups/menus/packages_registries_menu.rb +++ b/lib/sidebars/groups/menus/packages_registries_menu.rb @@ -15,7 +15,7 @@ def configure_menu_items override :title def title - _('Packages & Registries') + _('Packages and registries') end override :sprite_icon diff --git a/lib/sidebars/groups/menus/settings_menu.rb b/lib/sidebars/groups/menus/settings_menu.rb index 9d27f09146fc54..df170670aab016 100644 --- a/lib/sidebars/groups/menus/settings_menu.rb +++ b/lib/sidebars/groups/menus/settings_menu.rb @@ -117,7 +117,7 @@ def packages_and_registries_menu_item end ::Sidebars::MenuItem.new( - title: _('Packages & Registries'), + title: _('Packages and registries'), link: group_settings_packages_and_registries_path(context.group), active_routes: { controller: :packages_and_registries }, item_id: :packages_and_registries diff --git a/lib/sidebars/projects/menus/packages_registries_menu.rb b/lib/sidebars/projects/menus/packages_registries_menu.rb index e4d4441c6875eb..8cc588f2f8b59c 100644 --- a/lib/sidebars/projects/menus/packages_registries_menu.rb +++ b/lib/sidebars/projects/menus/packages_registries_menu.rb @@ -15,7 +15,7 @@ def configure_menu_items override :title def title - _('Packages & Registries') + _('Packages and registries') end override :sprite_icon diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb index 85931e63ebccdb..23be751ff1029d 100644 --- a/lib/sidebars/projects/menus/settings_menu.rb +++ b/lib/sidebars/projects/menus/settings_menu.rb @@ -109,7 +109,7 @@ def packages_and_registries_menu_item end ::Sidebars::MenuItem.new( - title: _('Packages & Registries'), + title: _('Packages and registries'), link: project_settings_packages_and_registries_path(context.project), active_routes: { path: 'packages_and_registries#show' }, item_id: :packages_and_registries diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e30601d30117b2..7b73a86a5228b3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -27734,9 +27734,6 @@ msgstr "" msgid "PQL|Thank you for reaching out! Our sales team will get back to you soon." msgstr "" -msgid "Package & registry settings" -msgstr "" - msgid "Package Registry" msgstr "" @@ -27749,6 +27746,9 @@ msgstr "" msgid "Package already exists" msgstr "" +msgid "Package and registry settings" +msgstr "" + msgid "Package deleted successfully" msgstr "" @@ -28181,7 +28181,7 @@ msgstr "" msgid "PackageRegistry|published by %{author}" msgstr "" -msgid "Packages & Registries" +msgid "Packages and registries" msgstr "" msgid "Page not found" diff --git a/qa/qa/page/group/menu.rb b/qa/qa/page/group/menu.rb index 783fbd259297a0..de065ca187de53 100644 --- a/qa/qa/page/group/menu.rb +++ b/qa/qa/page/group/menu.rb @@ -47,7 +47,7 @@ def go_to_milestones def go_to_package_settings hover_group_settings do within_submenu do - click_element(:sidebar_menu_item_link, menu_item: 'Packages & Registries') + click_element(:sidebar_menu_item_link, menu_item: 'Packages and registries') end end end @@ -122,8 +122,8 @@ def hover_subgroup_information def hover_group_packages within_sidebar do - scroll_to_element(:sidebar_menu_link, menu_item: 'Packages & Registries') - find_element(:sidebar_menu_link, menu_item: 'Packages & Registries').hover + scroll_to_element(:sidebar_menu_link, menu_item: 'Packages and registries') + find_element(:sidebar_menu_link, menu_item: 'Packages and registries').hover yield end diff --git a/qa/qa/page/project/sub_menus/packages.rb b/qa/qa/page/project/sub_menus/packages.rb index f2084a094def04..9600540c5bc3d8 100644 --- a/qa/qa/page/project/sub_menus/packages.rb +++ b/qa/qa/page/project/sub_menus/packages.rb @@ -35,8 +35,8 @@ def go_to_infrastructure_registry def hover_registry within_sidebar do - scroll_to_element(:sidebar_menu_link, menu_item: 'Packages & Registries') - find_element(:sidebar_menu_link, menu_item: 'Packages & Registries').hover + scroll_to_element(:sidebar_menu_link, menu_item: 'Packages and registries') + find_element(:sidebar_menu_link, menu_item: 'Packages and registries').hover yield end diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb index c2dde1ba3fdce1..56ef1ff8354ca1 100644 --- a/spec/features/groups/navbar_spec.rb +++ b/spec/features/groups/navbar_spec.rb @@ -49,7 +49,7 @@ if Gitlab.ee? insert_customer_relations_nav(_('Analytics')) else - insert_customer_relations_nav(_('Packages & Registries')) + insert_customer_relations_nav(_('Packages and registries')) end visit group_path(group) diff --git a/spec/features/groups/settings/packages_and_registries_spec.rb b/spec/features/groups/settings/packages_and_registries_spec.rb index 81305c777aa8d7..7f3f5775559ba4 100644 --- a/spec/features/groups/settings/packages_and_registries_spec.rb +++ b/spec/features/groups/settings/packages_and_registries_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Group Packages & Registries settings' do +RSpec.describe 'Group Package and registry settings' do include WaitForRequests let(:user) { create(:user) } @@ -25,7 +25,7 @@ settings_menu = find_settings_menu - expect(settings_menu).not_to have_content 'Packages & Registries' + expect(settings_menu).not_to have_content 'Packages and registries' end it 'renders 404 when navigating to page' do @@ -40,20 +40,20 @@ visit group_path(group) settings_menu = find_settings_menu - expect(settings_menu).to have_content 'Packages & Registries' + expect(settings_menu).to have_content 'Packages and registries' end it 'has a page title set' do visit_settings_page - expect(page).to have_title _('Package & registry settings') + expect(page).to have_title _('Package and registry settings') end it 'sidebar menu is open' do visit_settings_page sidebar = find('.nav-sidebar') - expect(sidebar).to have_link _('Packages & Registries') + expect(sidebar).to have_link _('Packages and registries') end it 'has a Duplicate packages section', :js do diff --git a/spec/features/groups/settings/user_searches_in_settings_spec.rb b/spec/features/groups/settings/user_searches_in_settings_spec.rb index 998c3d2ca3fae1..fe0dd7cec9ab12 100644 --- a/spec/features/groups/settings/user_searches_in_settings_spec.rb +++ b/spec/features/groups/settings/user_searches_in_settings_spec.rb @@ -43,7 +43,7 @@ it_behaves_like 'can search settings', 'Variables', 'Auto DevOps' end - context 'in Packages & Registries page' do + context 'in Packages and registries page' do before do visit group_settings_packages_and_registries_path(group) end diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb index e07a5d09405595..20fee84392cfba 100644 --- a/spec/features/projects/navbar_spec.rb +++ b/spec/features/projects/navbar_spec.rb @@ -49,7 +49,7 @@ stub_config(pages: { enabled: true }) insert_after_sub_nav_item( - _('Packages & Registries'), + _('Packages and registries'), within: _('Settings'), new_sub_nav_item_name: _('Pages') ) diff --git a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb index 5a50b3de772d93..b3b99833d5d69e 100644 --- a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb +++ b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Project > Settings > Packages & Registries > Container registry tag expiration policy' do +RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy' do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) } diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb index 1fb46c669e7053..5d252a86de73ab 100644 --- a/spec/features/projects/settings/registry_settings_spec.rb +++ b/spec/features/projects/settings/registry_settings_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Project > Settings > Packages & Registries > Container registry tag expiration policy', :js do +RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy', :js do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) } diff --git a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb index 252da8ea69961c..4e3c639672b144 100644 --- a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb +++ b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb @@ -80,7 +80,7 @@ it_behaves_like 'access rights checks' end - describe 'Packages & Registries' do + describe 'Packages and registries' do let(:item_id) { :packages_and_registries } before do diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb index f41f7a01d88afd..904b9f041b1d59 100644 --- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb @@ -133,7 +133,7 @@ end end - describe 'Packages & Registries' do + describe 'Packages and registries' do let(:item_id) { :packages_and_registries } let(:packages_enabled) { false } diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb index e11548d0b75ef1..3d51c022b399ed 100644 --- a/spec/support/helpers/navbar_structure_helper.rb +++ b/spec/support/helpers/navbar_structure_helper.rb @@ -34,7 +34,7 @@ def insert_package_nav(within) insert_after_nav_item( within, new_nav_item: { - nav_item: _('Packages & Registries'), + nav_item: _('Packages and registries'), nav_sub_items: [_('Package Registry')] } ) @@ -56,7 +56,7 @@ def insert_customer_relations_nav(within) def insert_container_nav insert_after_sub_nav_item( _('Package Registry'), - within: _('Packages & Registries'), + within: _('Packages and registries'), new_sub_nav_item_name: _('Container Registry') ) end @@ -64,7 +64,7 @@ def insert_container_nav def insert_dependency_proxy_nav insert_after_sub_nav_item( _('Package Registry'), - within: _('Packages & Registries'), + within: _('Packages and registries'), new_sub_nav_item_name: _('Dependency Proxy') ) end @@ -72,7 +72,7 @@ def insert_dependency_proxy_nav def insert_infrastructure_registry_nav insert_after_sub_nav_item( _('Package Registry'), - within: _('Packages & Registries'), + within: _('Packages and registries'), new_sub_nav_item_name: _('Infrastructure Registry') ) end @@ -80,7 +80,7 @@ def insert_infrastructure_registry_nav def insert_harbor_registry_nav(within) insert_after_sub_nav_item( within, - within: _('Packages & Registries'), + within: _('Packages and registries'), new_sub_nav_item_name: _('Harbor Registry') ) end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 0b36a2e615a0bd..6543fc327d2976 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -110,7 +110,7 @@ _('Access Tokens'), _('Repository'), _('CI/CD'), - _('Packages & Registries'), + _('Packages and registries'), _('Monitor'), s_('UsageQuota|Usage Quotas') ] @@ -139,7 +139,7 @@ _('Repository'), _('CI/CD'), _('Applications'), - _('Packages & Registries') + _('Packages and registries') ] } end diff --git a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb index 428e9cc84909bb..472a2f3cb34050 100644 --- a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb @@ -109,7 +109,7 @@ end end - describe 'Packages & Registries' do + describe 'Packages and registries' do it 'has a link to the package registry page' do stub_config(packages: { enabled: true }) @@ -178,10 +178,10 @@ expect(rendered).to have_link('Applications', href: group_settings_applications_path(group)) end - it 'has a link to the Package & Registries settings page' do + it 'has a link to the Package and registry settings page' do render - expect(rendered).to have_link('Packages & Registries', href: group_settings_packages_and_registries_path(group)) + expect(rendered).to have_link('Packages and registries', href: group_settings_packages_and_registries_path(group)) end end end diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index 9ae3f814679199..f5cd56792703d8 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -559,7 +559,7 @@ it 'top level navigation link is visible and points to package registry page' do render - expect(rendered).to have_link('Packages & Registries', href: project_packages_path(project)) + expect(rendered).to have_link('Packages and registries', href: project_packages_path(project)) end describe 'Packages Registry' do @@ -908,7 +908,7 @@ end end - describe 'Packages & Registries' do + describe 'Packages and registries' do let(:packages_enabled) { false } before do @@ -919,20 +919,20 @@ context 'when registry is enabled' do let(:registry_enabled) { true } - it 'has a link to the Packages & Registries settings' do + it 'has a link to the Package and registry settings' do render - expect(rendered).to have_link('Packages & Registries', href: project_settings_packages_and_registries_path(project)) + expect(rendered).to have_link('Packages and registries', href: project_settings_packages_and_registries_path(project)) end end context 'when registry is not enabled' do let(:registry_enabled) { false } - it 'does not have a link to the Packages & Registries settings' do + it 'does not have a link to the Package and registry settings' do render - expect(rendered).not_to have_link('Packages & Registries', href: project_settings_packages_and_registries_path(project)) + expect(rendered).not_to have_link('Packages and registries', href: project_settings_packages_and_registries_path(project)) end end @@ -940,10 +940,10 @@ let(:registry_enabled) { false } let(:packages_enabled) { true } - it 'has a link to the Packages & Registries settings' do + it 'has a link to the Package and registry settings' do render - expect(rendered).to have_link('Packages & Registries', href: project_settings_packages_and_registries_path(project)) + expect(rendered).to have_link('Packages and registries', href: project_settings_packages_and_registries_path(project)) end end end -- GitLab From 5f4088d656d0b63112ec52f4f50ad21d94b22cc7 Mon Sep 17 00:00:00 2001 From: Krasimir Angelov <kangelov@gitlab.com> Date: Tue, 6 Sep 2022 10:25:20 +1200 Subject: [PATCH 073/169] Fix migration type With https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95881 a new background migration was scheduled in a regular Rails migration. This results in teh workers trying to execute the migration job while the job class is not yet available, as teh deployment is not finished. This move the scheduling migration under `db/post_migrate` to fif the problem. See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95881#note_1087932448. Changelog: changed --- .../20220901035725_schedule_destroy_invalid_project_members.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename db/{migrate => post_migrate}/20220901035725_schedule_destroy_invalid_project_members.rb (100%) diff --git a/db/migrate/20220901035725_schedule_destroy_invalid_project_members.rb b/db/post_migrate/20220901035725_schedule_destroy_invalid_project_members.rb similarity index 100% rename from db/migrate/20220901035725_schedule_destroy_invalid_project_members.rb rename to db/post_migrate/20220901035725_schedule_destroy_invalid_project_members.rb -- GitLab From 379d683631fe16c17a8e79d55af9a1ff2f60a782 Mon Sep 17 00:00:00 2001 From: Florie Guibert <fguibert@gitlab.com> Date: Thu, 1 Sep 2022 16:30:30 +1000 Subject: [PATCH 074/169] Add autosave on design notes Changelog: added --- .../design_notes/design_discussion.vue | 5 +++ .../components/design_notes/design_note.vue | 5 +++ .../design_notes/design_reply_form.vue | 40 +++++++++++++++++-- .../design_management/pages/design/index.vue | 1 + locale/gitlab.pot | 3 ++ .../design_notes/design_note_spec.js | 1 + .../design_notes/design_reply_form_spec.js | 37 +++++++++++++++++ 7 files changed, 89 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue index ac00af2ab346cb..124780df8a5228 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue @@ -174,6 +174,7 @@ export default { this.$emit('open-form', this.discussion.id); this.isFormRendered = true; }, + toggleResolvedStatus() { this.isResolving = true; @@ -234,6 +235,7 @@ export default { :note="firstNote" :markdown-preview-path="markdownPreviewPath" :is-resolving="isResolving" + :noteable-id="noteableId" :class="{ 'gl-bg-blue-50': isDiscussionActive }" @error="$emit('update-note-error', $event)" > @@ -276,6 +278,7 @@ export default { :note="note" :markdown-preview-path="markdownPreviewPath" :is-resolving="isResolving" + :noteable-id="noteableId" :class="{ 'gl-bg-blue-50': isDiscussionActive }" @error="$emit('update-note-error', $event)" /> @@ -307,6 +310,8 @@ export default { v-model="discussionComment" :is-saving="loading" :markdown-preview-path="markdownPreviewPath" + :noteable-id="noteableId" + :discussion-id="discussion.id" @submit-form="mutate" @cancel-form="hideForm" > diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue index 5fb5989e11ab5c..e629f74ba020a4 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue @@ -45,6 +45,10 @@ export default { required: false, default: '', }, + noteableId: { + type: String, + required: true, + }, }, data() { return { @@ -160,6 +164,7 @@ export default { :is-saving="loading" :markdown-preview-path="markdownPreviewPath" :is-new-comment="false" + :noteable-id="noteableId" class="gl-mt-5" @submit-form="mutate" @cancel-form="hideForm" diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue index 1b6458668f5e3f..4faeba3983b8c3 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue @@ -1,7 +1,11 @@ <script> import { GlButton, GlModal } from '@gitlab/ui'; +import $ from 'jquery'; import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; +import Autosave from '~/autosave'; +import { isLoggedIn } from '~/lib/utils/common_utils'; +import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; export default { @@ -30,10 +34,20 @@ export default { required: false, default: true, }, + noteableId: { + type: String, + required: true, + }, + discussionId: { + type: String, + required: false, + default: 'new', + }, }, data() { return { formText: this.value, + isLoggedIn: isLoggedIn(), }; }, computed: { @@ -64,13 +78,19 @@ export default { markdownDocsPath() { return helpPagePath('user/markdown'); }, + shortDiscussionId() { + return isGid(this.discussionId) ? getIdFromGraphQLId(this.discussionId) : this.discussionId; + }, }, mounted() { this.focusInput(); }, methods: { submitForm() { - if (this.hasValue) this.$emit('submit-form'); + if (this.hasValue) { + this.$emit('submit-form'); + this.autosaveDiscussion.reset(); + } }, cancelComment() { if (this.hasValue && this.formText !== this.value) { @@ -79,8 +99,22 @@ export default { this.$emit('cancel-form'); } }, + confirmCancelCommentModal() { + this.$emit('cancel-form'); + this.autosaveDiscussion.reset(); + }, focusInput() { this.$refs.textarea.focus(); + this.initAutosaveComment(); + }, + initAutosaveComment() { + if (this.isLoggedIn) { + this.autosaveDiscussion = new Autosave($(this.$refs.textarea), [ + s__('DesignManagement|Discussion'), + getIdFromGraphQLId(this.noteableId), + this.shortDiscussionId, + ]); + } }, }, }; @@ -124,7 +158,7 @@ export default { type="submit" data-track-action="click_button" data-qa-selector="save_comment_button" - @click="$emit('submit-form')" + @click="submitForm" > {{ buttonText }} </gl-button> @@ -144,7 +178,7 @@ export default { :ok-title="modalSettings.okTitle" :cancel-title="modalSettings.cancelTitle" modal-id="cancel-comment-modal" - @ok="$emit('cancel-form')" + @ok="confirmCancelCommentModal" >{{ modalSettings.content }} </gl-modal> </form> diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 1825ce7f0923fb..228ad637b9e7f0 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -418,6 +418,7 @@ export default { v-model="comment" :is-saving="loading" :markdown-preview-path="markdownPreviewPath" + :noteable-id="design.id" @submit-form="mutate" @cancel-form="closeCommentForm" /> </apollo-mutation diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 289145817e0dcc..22195bcd848000 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13345,6 +13345,9 @@ msgstr "" msgid "DesignManagement|Discard comment" msgstr "" +msgid "DesignManagement|Discussion" +msgstr "" + msgid "DesignManagement|Download design" msgstr "" diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js index 28833b4af5c8d7..df511586c10f5f 100644 --- a/spec/frontend/design_management/components/design_notes/design_note_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js @@ -43,6 +43,7 @@ describe('Design note component', () => { wrapper = shallowMountExtended(DesignNote, { propsData: { note: {}, + noteableId: 'gid://gitlab/DesignManagement::Design/6', ...props, }, data() { diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js index f7ce742b933762..e6ff83ac7545dc 100644 --- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import Autosave from '~/autosave'; import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; const showModal = jest.fn(); @@ -24,6 +25,7 @@ describe('Design reply form component', () => { propsData: { value: '', isSaving: false, + noteableId: 'gid://gitlab/DesignManagement::Design/6', ...props, }, stubs: { GlModal }, @@ -31,6 +33,10 @@ describe('Design reply form component', () => { }); } + beforeEach(() => { + window.gon.current_user_id = 1; + }); + afterEach(() => { wrapper.destroy(); }); @@ -66,6 +72,25 @@ describe('Design reply form component', () => { expect(findSubmitButton().html()).toMatchSnapshot(); }); + it.each` + discussionId | shortDiscussionId + ${undefined} | ${'new'} + ${'gid://gitlab/DiffDiscussion/123'} | ${123} + `( + 'initializes autosave support on discussion with proper key', + async ({ discussionId, shortDiscussionId }) => { + createComponent({ discussionId }); + await nextTick(); + + // We discourage testing `wrapper.vm` properties but + // since `autosave` library instantiates on component + // there's no other way to test whether instantiation + // happened correctly or not. + expect(wrapper.vm.autosaveDiscussion).toBeInstanceOf(Autosave); + expect(wrapper.vm.autosaveDiscussion.key).toBe(`autosave/Discussion/6/${shortDiscussionId}`); + }, + ); + describe('when form has no text', () => { beforeEach(() => { createComponent({ @@ -120,28 +145,37 @@ describe('Design reply form component', () => { }); it('emits submitForm event on Comment button click', async () => { + const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + findSubmitButton().vm.$emit('click'); await nextTick(); expect(wrapper.emitted('submit-form')).toBeTruthy(); + expect(autosaveResetSpy).toHaveBeenCalled(); }); it('emits submitForm event on textarea ctrl+enter keydown', async () => { + const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + findTextarea().trigger('keydown.enter', { ctrlKey: true, }); await nextTick(); expect(wrapper.emitted('submit-form')).toBeTruthy(); + expect(autosaveResetSpy).toHaveBeenCalled(); }); it('emits submitForm event on textarea meta+enter keydown', async () => { + const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + findTextarea().trigger('keydown.enter', { metaKey: true, }); await nextTick(); expect(wrapper.emitted('submit-form')).toBeTruthy(); + expect(autosaveResetSpy).toHaveBeenCalled(); }); it('emits input event on changing textarea content', async () => { @@ -180,10 +214,13 @@ describe('Design reply form component', () => { }); it('emits cancelForm event on modal Ok button click', () => { + const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + findTextarea().trigger('keyup.esc'); findModal().vm.$emit('ok'); expect(wrapper.emitted('cancel-form')).toBeTruthy(); + expect(autosaveResetSpy).toHaveBeenCalled(); }); }); }); -- GitLab From f7f3f586a1b10c008e4420a6eafc6dfdfd43263f Mon Sep 17 00:00:00 2001 From: Florie Guibert <fguibert@gitlab.com> Date: Tue, 6 Sep 2022 09:43:45 +1000 Subject: [PATCH 075/169] Add autosave on design notes Review feedback --- .../components/design_notes/design_reply_form_spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js index e6ff83ac7545dc..e36f5c79e3eaaf 100644 --- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js @@ -14,6 +14,7 @@ const GlModal = { describe('Design reply form component', () => { let wrapper; + let originalGon; const findTextarea = () => wrapper.find('textarea'); const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' }); @@ -34,11 +35,13 @@ describe('Design reply form component', () => { } beforeEach(() => { + originalGon = window.gon; window.gon.current_user_id = 1; }); afterEach(() => { wrapper.destroy(); + window.gon = originalGon; }); it('textarea has focus after component mount', () => { -- GitLab From 9979eabc7a06ee45ef4a2214cbacdec2edddb4cb Mon Sep 17 00:00:00 2001 From: Ezekiel Kigbo <ekigbo@gitlab.com> Date: Thu, 16 Jun 2022 19:04:20 +1000 Subject: [PATCH 076/169] Add group label seeding for VSA projects --- .../30_customizable_cycle_analytics.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ee/db/fixtures/development/30_customizable_cycle_analytics.rb b/ee/db/fixtures/development/30_customizable_cycle_analytics.rb index fbf7b771be5420..0e0267332a1e3d 100644 --- a/ee/db/fixtures/development/30_customizable_cycle_analytics.rb +++ b/ee/db/fixtures/development/30_customizable_cycle_analytics.rb @@ -8,6 +8,7 @@ class Gitlab::Seeder::CustomizableCycleAnalytics ONE_WEEK_IN_HOURS = 168 ISSUE_COUNT = 15 MERGE_REQUEST_COUNT = 10 + GROUP_LABEL_COUNT = 10 def initialize(project) @project = project @@ -20,6 +21,7 @@ def seed! Sidekiq::Testing.inline! do create_stages! + seed_group_labels! seed_issue_based_stages! seed_issue_label_based_stages! @@ -84,6 +86,17 @@ def create_stages! end end + def seed_group_labels! + GROUP_LABEL_COUNT.times do + label_title = FFaker::Product.brand + label_color = ::Gitlab::Color.color_for(label_title).to_s + + Labels::CreateService + .new(title: label_title, color: label_color) + .execute(group: @group) + end + end + def seed_issue_based_stages! # issue created - issue closed issues.pop(5).each do |issue| @@ -180,9 +193,11 @@ def merge_requests Gitlab::Seeder.quiet do flag = 'SEED_CUSTOMIZABLE_CYCLE_ANALYTICS' + project_id = ENV['VSA_SEED_PROJECT_ID'] + projects = project_id ? [Project.find(project_id)] : Project.find_each if ENV[flag] - Project.find_each do |project| + projects.each do |project| next unless project.group # This seed naively assumes that every project has a repository, and every # repository has a `master` branch, which may be the case for a pristine -- GitLab From 1cfc0c2e368de4b8182c89742b893408f351d647 Mon Sep 17 00:00:00 2001 From: Alejandro Guerrero <argdealba@gitlab.com> Date: Tue, 6 Sep 2022 00:00:59 +0000 Subject: [PATCH 077/169] Fix typos in protected environment list --- .../protected_environments/_environments_list.html.haml | 2 +- .../protected_environments/_group_environments_list.html.haml | 2 +- locale/gitlab.pot | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ee/app/views/projects/protected_environments/_environments_list.html.haml b/ee/app/views/projects/protected_environments/_environments_list.html.haml index 1cf637cab35f54..53abd3a6ea6888 100644 --- a/ee/app/views/projects/protected_environments/_environments_list.html.haml +++ b/ee/app/views/projects/protected_environments/_environments_list.html.haml @@ -1,7 +1,7 @@ .protected-branches-list.js-protected-environments-list - if @protected_environments.empty? %p.settings-message.text-center - = s_('ProtectedEnvironment|No environments in this project are projected.') + = s_('ProtectedEnvironment|No environments in this project are protected.') - else .flash-container %table.table.table-bordered diff --git a/ee/app/views/projects/protected_environments/_group_environments_list.html.haml b/ee/app/views/projects/protected_environments/_group_environments_list.html.haml index 937459115b7c6f..d9fcb847802bb0 100644 --- a/ee/app/views/projects/protected_environments/_group_environments_list.html.haml +++ b/ee/app/views/projects/protected_environments/_group_environments_list.html.haml @@ -2,7 +2,7 @@ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_url('ci/environments/protected_environments.md', anchor: 'group-level-protected-environments') } - link_end = '</a>'.html_safe %h5= s_('ProtectedEnvironment|Environments protected upstream') - %p= s_('ProtectedEnvironment|All environments specivied with the deployment tiers below are protected by a parent group. %{link_start}Learn More%{link_end}.').html_safe % { link_start: link_start, link_end: link_end } + %p= s_('ProtectedEnvironment|All environments specified with the deployment tiers below are protected by a parent group. %{link_start}Learn More%{link_end}.').html_safe % { link_start: link_start, link_end: link_end } .group-protected-branches-list .flash-container diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2b23af4a038303..6809ded0d17da1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -31873,7 +31873,7 @@ msgstr "" msgid "ProtectedEnvironment|%{environment_name} will be writable for developers. Are you sure?" msgstr "" -msgid "ProtectedEnvironment|All environments specivied with the deployment tiers below are protected by a parent group. %{link_start}Learn More%{link_end}." +msgid "ProtectedEnvironment|All environments specified with the deployment tiers below are protected by a parent group. %{link_start}Learn More%{link_end}." msgstr "" msgid "ProtectedEnvironment|Allowed to deploy" @@ -31891,7 +31891,7 @@ msgstr "" msgid "ProtectedEnvironment|Failed to load details for this group." msgstr "" -msgid "ProtectedEnvironment|No environments in this project are projected." +msgid "ProtectedEnvironment|No environments in this project are protected." msgstr "" msgid "ProtectedEnvironment|Only specified groups can execute deployments in protected environments." -- GitLab From 48f1670e66309217eea01ebb3e102ba37123c13a Mon Sep 17 00:00:00 2001 From: Andrejs Cunskis <acunskis@gitlab.com> Date: Tue, 6 Sep 2022 00:09:32 +0000 Subject: [PATCH 078/169] Unify dynamic setup for review-app and package-and-test pipelines Extend minimal job Set common variables Fix dependent job name Reduce verbose logging of full yml --- .gitlab/ci/_skip.yml | 11 ++++++ .../ci/package-and-test/main.gitlab-ci.yml | 1 + .../ci/package-and-test/skip.gitlab-ci.yml | 14 ------- .../package-and-test/variables.gitlab-ci.yml | 10 +++++ .gitlab/ci/qa.gitlab-ci.yml | 36 ++---------------- .gitlab/ci/review-apps/skip-qa.gitlab-ci.yml | 13 ------- .gitlab/ci/review.gitlab-ci.yml | 28 ++------------ .gitlab/ci/setup.gitlab-ci.yml | 20 ++++++++++ qa/tasks/ci.rake | 20 ++++------ scripts/generate-e2e-pipeline | 38 ++++++++++++------- 10 files changed, 82 insertions(+), 109 deletions(-) create mode 100644 .gitlab/ci/_skip.yml delete mode 100644 .gitlab/ci/package-and-test/skip.gitlab-ci.yml create mode 100644 .gitlab/ci/package-and-test/variables.gitlab-ci.yml delete mode 100644 .gitlab/ci/review-apps/skip-qa.gitlab-ci.yml diff --git a/.gitlab/ci/_skip.yml b/.gitlab/ci/_skip.yml new file mode 100644 index 00000000000000..27a3ff5b836f75 --- /dev/null +++ b/.gitlab/ci/_skip.yml @@ -0,0 +1,11 @@ +# no-op pipeline template for skipping whole child pipeline execution + +no-op: + image: ${GITLAB_DEPENDENCY_PROXY}alpine:latest + stage: test + variables: + GIT_STRATEGY: none + script: + - echo "${SKIP_MESSAGE:-no-op run, nothing will be executed!}" + rules: + - when: always diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index b4e6db74f7caa1..d1280c43903dba 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -3,6 +3,7 @@ include: - local: .gitlab/ci/global.gitlab-ci.yml - local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml + - local: .gitlab/ci/package-and-test/variables.gitlab-ci.yml - project: gitlab-org/quality/pipeline-common ref: 1.2.1 file: diff --git a/.gitlab/ci/package-and-test/skip.gitlab-ci.yml b/.gitlab/ci/package-and-test/skip.gitlab-ci.yml deleted file mode 100644 index 2675d804a5e75f..00000000000000 --- a/.gitlab/ci/package-and-test/skip.gitlab-ci.yml +++ /dev/null @@ -1,14 +0,0 @@ -# no-op pipeline triggered on quarantine only changes - -stages: - - qa - -no-op: - image: ${GITLAB_DEPENDENCY_PROXY}alpine:latest - stage: qa - variables: - GIT_STRATEGY: none - script: - - echo "Skipping E2E tests because the MR includes only quarantine changes" - rules: - - when: always diff --git a/.gitlab/ci/package-and-test/variables.gitlab-ci.yml b/.gitlab/ci/package-and-test/variables.gitlab-ci.yml new file mode 100644 index 00000000000000..545a494eee3566 --- /dev/null +++ b/.gitlab/ci/package-and-test/variables.gitlab-ci.yml @@ -0,0 +1,10 @@ +# Default variables for package-and-test + +variables: + RELEASE: "gitlab/gitlab-ee:nightly" + SKIP_REPORT_IN_ISSUES: "true" + OMNIBUS_GITLAB_CACHE_UPDATE: "false" + COLORIZED_LOGS: "true" + QA_LOG_LEVEL: "info" + QA_TESTS: "" + QA_FEATURE_FLAGS: "" diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 58d2c2417af67c..6134b201c23852 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -66,48 +66,18 @@ qa:update-qa-cache: script: - echo "Cache has been updated and ready to be uploaded." -populate-e2e-test-vars: - extends: - - .qa-job-base - - .qa:rules:determine-e2e-tests - stage: prepare - variables: - ENV_FILE: $CI_PROJECT_DIR/qa_tests_vars.env - COLORIZED_LOGS: "true" - script: - - bundle exec rake "ci:detect_changes[$ENV_FILE]" - artifacts: - expire_in: 1 day - reports: - dotenv: $ENV_FILE - -e2e-test-pipeline-generate: - extends: - - .qa:rules:determine-e2e-tests - stage: prepare - when: on_success - needs: - - populate-e2e-test-vars - variables: - PIPELINE_YML: package-and-test.yml - script: - - scripts/generate-e2e-pipeline $PIPELINE_YML - artifacts: - expire_in: 1 day - paths: - - $PIPELINE_YML - e2e:package-and-test: extends: - .qa:rules:package-and-test stage: qa - when: on_success needs: - build-assets-image - build-qa-image - e2e-test-pipeline-generate + variables: + SKIP_MESSAGE: Skipping package-and-test due to mr containing only quarantine changes! trigger: strategy: depend include: - - artifact: package-and-test.yml + - artifact: package-and-test-pipeline.yml job: e2e-test-pipeline-generate diff --git a/.gitlab/ci/review-apps/skip-qa.gitlab-ci.yml b/.gitlab/ci/review-apps/skip-qa.gitlab-ci.yml deleted file mode 100644 index 1305673a4d8bf6..00000000000000 --- a/.gitlab/ci/review-apps/skip-qa.gitlab-ci.yml +++ /dev/null @@ -1,13 +0,0 @@ -stages: - - review - -include: - - local: .gitlab/ci/global.gitlab-ci.yml - - local: .gitlab/ci/rules.gitlab-ci.yml - -no-op: - extends: - - .review:rules:start-review-app-pipeline - stage: review - script: - - echo "Skip Review App because the MR includes only quarantine changes" diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 41b4145dfd8f9c..967f8c8215854a 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -23,34 +23,13 @@ review-cleanup: - ruby -rrubygems scripts/review_apps/automated_cleanup.rb - gcp_cleanup -review-app-pipeline-generate: - image: ${GITLAB_DEPENDENCY_PROXY}ruby:${RUBY_VERSION} - stage: prepare - extends: - - .review:rules:start-review-app-pipeline - needs: - - populate-e2e-test-vars - script: - - | - if [ "$QA_SKIP_ALL_TESTS" == "true" ]; then - echo "Skip Review App because the MR includes only quarantine changes" - cp .gitlab/ci/review-apps/skip-qa.gitlab-ci.yml review-app-pipeline.yml - else - echo "Review App will use the full pipeline" - cp .gitlab/ci/review-apps/main.gitlab-ci.yml review-app-pipeline.yml - fi - artifacts: - expire_in: 7d - paths: - - review-app-pipeline.yml - start-review-app-pipeline: extends: - .review:rules:start-review-app-pipeline resource_group: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # CI_ENVIRONMENT_SLUG is not available here and we want this to be the same as the environment stage: review needs: - - review-app-pipeline-generate + - job: e2e-test-pipeline-generate - job: build-assets-image artifacts: false # These variables are set in the pipeline schedules. @@ -59,11 +38,12 @@ start-review-app-pipeline: variables: SCHEDULE_TYPE: $SCHEDULE_TYPE DAST_RUN: $DAST_RUN + SKIP_MESSAGE: Skipping review-app due to mr containing only quarantine changes! trigger: + strategy: depend include: - artifact: review-app-pipeline.yml - job: review-app-pipeline-generate - strategy: depend + job: e2e-test-pipeline-generate danger-review: extends: diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml index 2631bae0c9a844..437b02e481986c 100644 --- a/.gitlab/ci/setup.gitlab-ci.yml +++ b/.gitlab/ci/setup.gitlab-ci.yml @@ -156,3 +156,23 @@ detect-previous-failed-tests: expire_in: 7d paths: - ${PREVIOUS_FAILED_TESTS_DIR} + +e2e-test-pipeline-generate: + extends: + - .qa-job-base + - .minimal-job + - .qa:rules:determine-e2e-tests + stage: prepare + variables: + ENV_FILE: $CI_PROJECT_DIR/qa_tests_vars.env + OMNIBUS_PIPELINE_YML: package-and-test-pipeline.yml + REVIEW_PIPELINE_YML: review-app-pipeline.yml + COLORIZED_LOGS: "true" + script: + - bundle exec rake "ci:detect_changes[$ENV_FILE]" + - cd $CI_PROJECT_DIR && scripts/generate-e2e-pipeline + artifacts: + expire_in: 1 day + paths: + - $OMNIBUS_PIPELINE_YML + - $REVIEW_PIPELINE_YML diff --git a/qa/tasks/ci.rake b/qa/tasks/ci.rake index 0c56a84982482d..b59bd77314955a 100644 --- a/qa/tasks/ci.rake +++ b/qa/tasks/ci.rake @@ -27,33 +27,29 @@ namespace :ci do next end - # run all tests when framework changes detected - if qa_changes.framework_changes? + tests = qa_changes.qa_tests + if qa_changes.framework_changes? # run all tests when framework changes detected logger.info(" merge request contains qa framework changes, full test suite will be executed") append_to_file(env_file, <<~TXT) QA_FRAMEWORK_CHANGES=true TXT - end - - # detect if any of the test suites would not execute any tests and populate environment variables - tests = qa_changes.qa_tests - if tests - logger.info(" following changed specs detected: '#{tests}'") + elsif tests + logger.info(" detected following specs to execute: '#{tests}'") else - logger.info(" no specific spec changes detected") + logger.info(" no specific specs to execute detected") end # always check all test suites in case a suite is defined but doesn't have any runnable specs suites = QA::Tools::Ci::NonEmptySuites.new(tests).fetch append_to_file(env_file, <<~TXT) - QA_TESTS=#{tests} - QA_SUITES=#{suites} + QA_TESTS='#{tests}' + QA_SUITES='#{suites}' TXT # check if mr contains feature flag changes feature_flags = QA::Tools::Ci::FfChanges.new(diff).fetch append_to_file(env_file, <<~TXT) - QA_FEATURE_FLAGS=#{feature_flags} + QA_FEATURE_FLAGS='#{feature_flags}' TXT end end diff --git a/scripts/generate-e2e-pipeline b/scripts/generate-e2e-pipeline index b6519eff621619..697c4371d3b5dc 100755 --- a/scripts/generate-e2e-pipeline +++ b/scripts/generate-e2e-pipeline @@ -5,21 +5,21 @@ set -e # Script to generate e2e test child pipeline # This is required because environment variables that are generated dynamically are not picked up by rules in child pipelines -pipeline_yml="${1:-package-and-test.yml}" +source $ENV_FILE + +echo "Generating child pipeline yml definitions for review-app and package-and-test child pipelines" if [ "$QA_SKIP_ALL_TESTS" == "true" ]; then - echo "Generated no-op child pipeline due to QA_SKIP_ALL_TESTS set to 'true'" - cp .gitlab/ci/package-and-test/skip.gitlab-ci.yml $pipeline_yml + skip_pipeline=".gitlab/ci/_skip.yml" + + echo "Using ${skip_pipeline} due to QA_SKIP_ALL_TESTS set to 'true'" + cp $skip_pipeline "$OMNIBUS_PIPELINE_YML" + cp $skip_pipeline "$REVIEW_PIPELINE_YML" exit fi -variables=$(cat <<YML +common_variables=$(cat <<YML variables: - RELEASE: "${CI_REGISTRY}/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:${CI_COMMIT_SHA}" - SKIP_REPORT_IN_ISSUES: "${SKIP_REPORT_IN_ISSUES:-true}" - OMNIBUS_GITLAB_CACHE_UPDATE: "${OMNIBUS_GITLAB_CACHE_UPDATE:-false}" - COLORIZED_LOGS: "true" - QA_LOG_LEVEL: "info" QA_TESTS: "$QA_TESTS" QA_FEATURE_FLAGS: "${QA_FEATURE_FLAGS}" QA_FRAMEWORK_CHANGES: "${QA_FRAMEWORK_CHANGES:-false}" @@ -27,8 +27,20 @@ variables: YML ) -echo "$variables" >$pipeline_yml -cat .gitlab/ci/package-and-test/main.gitlab-ci.yml >>$pipeline_yml +echo "Using .gitlab/ci/review-apps/main.gitlab-ci.yml and .gitlab/ci/package-and-test/main.gitlab-ci.yml" + +cp .gitlab/ci/review-apps/main.gitlab-ci.yml "$REVIEW_PIPELINE_YML" +echo "$common_variables" >>"$REVIEW_PIPELINE_YML" +echo "Successfully generated review-app pipeline with following variables section:" +echo -e "$common_variables" -echo "Generated e2e:package-and-test pipeline with following variables section:" -echo "$variables" +omnibus_variables=$(cat <<YML + RELEASE: "${CI_REGISTRY}/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:${CI_COMMIT_SHA}" + OMNIBUS_GITLAB_CACHE_UPDATE: "${OMNIBUS_GITLAB_CACHE_UPDATE:-false}" +YML +) +cp .gitlab/ci/package-and-test/main.gitlab-ci.yml "$OMNIBUS_PIPELINE_YML" +echo "$common_variables" >>"$OMNIBUS_PIPELINE_YML" +echo "$omnibus_variables" >>"$OMNIBUS_PIPELINE_YML" +echo "Successfully generated package-and-test pipeline with following variables section:" +echo -e "${common_variables}\n${omnibus_variables}" -- GitLab From 03587ecb2b3e824bd013d492123cd1ad6ac95498 Mon Sep 17 00:00:00 2001 From: Russell Dickenson <rdickenson@gitlab.com> Date: Tue, 6 Sep 2022 01:17:21 +0000 Subject: [PATCH 079/169] Fix case of DAST UI text Changelog: changed EE: true --- .../scanner_profile_selector.vue | 4 ++-- .../site_profile_selector.vue | 4 ++-- .../dast_profiles/settings/profiles.js | 8 +++---- .../components/dast_profiles_spec.js | 12 +++++----- .../scanner_profile_selector_spec.js.snap | 8 +++---- .../site_profile_selector_spec.js.snap | 8 +++---- locale/gitlab.pot | 24 +++++++++---------- 7 files changed, 34 insertions(+), 34 deletions(-) diff --git a/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_profile_selector/scanner_profile_selector.vue b/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_profile_selector/scanner_profile_selector.vue index 15f3d8b7453eb6..961c1c6d0ba2aa 100644 --- a/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_profile_selector/scanner_profile_selector.vue +++ b/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_profile_selector/scanner_profile_selector.vue @@ -13,13 +13,13 @@ const SCANNER_PROFILE_INFO = helpPagePath('user/application_security/dast/index' export default { name: 'DastScannerProfileSelector', i18n: { - emptyStateHeader: s__('DastProfiles|Scanner Profile'), + emptyStateHeader: s__('DastProfiles|Scanner profile'), emptyStateContentHeader: s__('DastProfiles|No scanner profile selected'), emptyStateContent: s__('DastProfiles|Select a scanner profile to run a DAST scan'), selectProfileButton: s__('DastProfiles|Select scanner profile'), changeProfileButton: s__('DastProfiles|Change scanner profile'), scannerProfileDescription: s__( - 'DastProfiles|Scanner profiles define the configuration details of a security scanner. %{linkStart}Learn more%{linkEnd}.', + 'DastProfiles|A scanner profile defines the configuration details of a security scanner. %{linkStart}Learn more%{linkEnd}.', ), }, SCANNER_PROFILE_INFO, diff --git a/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_profile_selector/site_profile_selector.vue b/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_profile_selector/site_profile_selector.vue index 81b25a248d864e..981e2f9def88a3 100644 --- a/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_profile_selector/site_profile_selector.vue +++ b/ee/app/assets/javascripts/security_configuration/dast_profiles/dast_profile_selector/site_profile_selector.vue @@ -13,13 +13,13 @@ const SCANNER_PROFILE_INFO = helpPagePath('user/application_security/dast/index' export default { name: 'DastSiteProfileSelector', i18n: { - emptyStateHeader: s__('DastProfiles|Site Profile'), + emptyStateHeader: s__('DastProfiles|Site profile'), emptyStateContentHeader: s__('DastProfiles|No site profile selected'), emptyStateContent: s__('DastProfiles|Select a site profile to run a DAST scan'), selectProfileButton: s__('DastProfiles|Select site profile'), changeProfileButton: s__('DastProfiles|Change site profile'), siteProfileDescription: s__( - 'DastProfiles|Site profiles define the attributes and configuration details of your deployed application, website, or API. %{linkStart}Learn more%{linkEnd}.', + 'DastProfiles|A site profile defines the attributes and configuration details of your deployed application, website, or API. %{linkStart}Learn more%{linkEnd}.', ), }, SCANNER_PROFILE_INFO, diff --git a/ee/app/assets/javascripts/security_configuration/dast_profiles/settings/profiles.js b/ee/app/assets/javascripts/security_configuration/dast_profiles/settings/profiles.js index 010717713fb14e..650fd92cd179af 100644 --- a/ee/app/assets/javascripts/security_configuration/dast_profiles/settings/profiles.js +++ b/ee/app/assets/javascripts/security_configuration/dast_profiles/settings/profiles.js @@ -29,8 +29,8 @@ export const getProfileSettings = ({ createNewProfilePaths }) => ({ { label: s__('DastProfiles|Validation status'), key: 'validationStatus' }, ], i18n: { - createNewLinkText: s__('DastProfiles|Site Profile'), - name: s__('DastProfiles|Site Profiles'), + createNewLinkText: s__('DastProfiles|Site profile'), + name: s__('DastProfiles|Site profiles'), errorMessages: { fetchNetworkError: s__( 'DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later.', @@ -63,8 +63,8 @@ export const getProfileSettings = ({ createNewProfilePaths }) => ({ { label: s__('DastProfiles|Scan mode'), key: 'scanType' }, ], i18n: { - createNewLinkText: s__('DastProfiles|Scanner Profile'), - name: s__('DastProfiles|Scanner Profiles'), + createNewLinkText: s__('DastProfiles|Scanner profile'), + name: s__('DastProfiles|Scanner profiles'), errorMessages: { fetchNetworkError: s__( 'DastProfiles|Could not fetch scanner profiles. Please refresh the page, or try again later.', diff --git a/ee/spec/frontend/security_configuration/dast_profiles/components/dast_profiles_spec.js b/ee/spec/frontend/security_configuration/dast_profiles/components/dast_profiles_spec.js index 93d66ed3c8ab87..cb08a19d40ff2e 100644 --- a/ee/spec/frontend/security_configuration/dast_profiles/components/dast_profiles_spec.js +++ b/ee/spec/frontend/security_configuration/dast_profiles/components/dast_profiles_spec.js @@ -103,8 +103,8 @@ describe('EE - DastProfiles', () => { it.each` itemName | href - ${'Site Profile'} | ${TEST_NEW_DAST_SITE_PROFILE_PATH} - ${'Scanner Profile'} | ${TEST_NEW_DAST_SCANNER_PROFILE_PATH} + ${'Site profile'} | ${TEST_NEW_DAST_SITE_PROFILE_PATH} + ${'Scanner profile'} | ${TEST_NEW_DAST_SCANNER_PROFILE_PATH} `('shows a "$itemName" dropdown item that links to $href', ({ itemName, href }) => { createComponent(); @@ -126,8 +126,8 @@ describe('EE - DastProfiles', () => { it.each` tabName | shouldBeSelectedByDefault - ${'Site Profiles'} | ${true} - ${'Scanner Profiles'} | ${false} + ${'Site profiles'} | ${true} + ${'Scanner profiles'} | ${false} `( 'shows a "$tabName" tab which has "selected" set to "$shouldBeSelectedByDefault"', ({ tabName, shouldBeSelectedByDefault }) => { @@ -143,8 +143,8 @@ describe('EE - DastProfiles', () => { describe.each` tabName | index | givenLocationHash - ${'Site Profiles'} | ${0} | ${'#site-profiles'} - ${'Scanner Profiles'} | ${1} | ${'#scanner-profiles'} + ${'Site profiles'} | ${0} | ${'#site-profiles'} + ${'Scanner profiles'} | ${1} | ${'#scanner-profiles'} `('with location hash set to "$givenLocationHash"', ({ tabName, index, givenLocationHash }) => { beforeEach(() => { setWindowLocation(givenLocationHash); diff --git a/ee/spec/frontend/security_configuration/dast_profiles/dast_profile_selector/__snapshots__/scanner_profile_selector_spec.js.snap b/ee/spec/frontend/security_configuration/dast_profiles/dast_profile_selector/__snapshots__/scanner_profile_selector_spec.js.snap index 312800a5f21353..854958c3bc3a8e 100644 --- a/ee/spec/frontend/security_configuration/dast_profiles/dast_profile_selector/__snapshots__/scanner_profile_selector_spec.js.snap +++ b/ee/spec/frontend/security_configuration/dast_profiles/dast_profile_selector/__snapshots__/scanner_profile_selector_spec.js.snap @@ -12,13 +12,13 @@ exports[`ScannerProfileSelector renders properly with no profiles 1`] = ` class="gl-font-lg gl-mt-0 gl-mb-2" > - Scanner Profile + Scanner profile </h4> <p> <gl-sprintf-stub - message="Scanner profiles define the configuration details of a security scanner. %{linkStart}Learn more%{linkEnd}." + message="A scanner profile defines the configuration details of a security scanner. %{linkStart}Learn more%{linkEnd}." /> </p> </div> @@ -54,13 +54,13 @@ exports[`ScannerProfileSelector renders properly with profiles 1`] = ` class="gl-font-lg gl-mt-0 gl-mb-2" > - Scanner Profile + Scanner profile </h4> <p> <gl-sprintf-stub - message="Scanner profiles define the configuration details of a security scanner. %{linkStart}Learn more%{linkEnd}." + message="A scanner profile defines the configuration details of a security scanner. %{linkStart}Learn more%{linkEnd}." /> </p> </div> diff --git a/ee/spec/frontend/security_configuration/dast_profiles/dast_profile_selector/__snapshots__/site_profile_selector_spec.js.snap b/ee/spec/frontend/security_configuration/dast_profiles/dast_profile_selector/__snapshots__/site_profile_selector_spec.js.snap index c66b72d767239e..65acf1d617954b 100644 --- a/ee/spec/frontend/security_configuration/dast_profiles/dast_profile_selector/__snapshots__/site_profile_selector_spec.js.snap +++ b/ee/spec/frontend/security_configuration/dast_profiles/dast_profile_selector/__snapshots__/site_profile_selector_spec.js.snap @@ -12,13 +12,13 @@ exports[`SiteProfileSelector renders properly with no profiles 1`] = ` class="gl-font-lg gl-mt-0 gl-mb-2" > - Site Profile + Site profile </h4> <p> <gl-sprintf-stub - message="Site profiles define the attributes and configuration details of your deployed application, website, or API. %{linkStart}Learn more%{linkEnd}." + message="A site profile defines the attributes and configuration details of your deployed application, website, or API. %{linkStart}Learn more%{linkEnd}." /> </p> </div> @@ -54,13 +54,13 @@ exports[`SiteProfileSelector renders properly with profiles 1`] = ` class="gl-font-lg gl-mt-0 gl-mb-2" > - Site Profile + Site profile </h4> <p> <gl-sprintf-stub - message="Site profiles define the attributes and configuration details of your deployed application, website, or API. %{linkStart}Learn more%{linkEnd}." + message="A site profile defines the attributes and configuration details of your deployed application, website, or API. %{linkStart}Learn more%{linkEnd}." /> </p> </div> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bcfc87fa07845f..a541cb6ffbefdf 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11809,6 +11809,12 @@ msgstr "" msgid "DastProfiles|A passive scan monitors all HTTP messages (requests and responses) sent to the target. An active scan attacks the target to find potential vulnerabilities." msgstr "" +msgid "DastProfiles|A scanner profile defines the configuration details of a security scanner. %{linkStart}Learn more%{linkEnd}." +msgstr "" + +msgid "DastProfiles|A site profile defines the attributes and configuration details of your deployed application, website, or API. %{linkStart}Learn more%{linkEnd}." +msgstr "" + msgid "DastProfiles|AJAX spider" msgstr "" @@ -12007,16 +12013,13 @@ msgstr "" msgid "DastProfiles|Scan mode" msgstr "" -msgid "DastProfiles|Scanner Profile" -msgstr "" - -msgid "DastProfiles|Scanner Profiles" +msgid "DastProfiles|Scanner name" msgstr "" -msgid "DastProfiles|Scanner name" +msgid "DastProfiles|Scanner profile" msgstr "" -msgid "DastProfiles|Scanner profiles define the configuration details of a security scanner. %{linkStart}Learn more%{linkEnd}." +msgid "DastProfiles|Scanner profiles" msgstr "" msgid "DastProfiles|Select a scanner profile to run a DAST scan" @@ -12037,16 +12040,13 @@ msgstr "" msgid "DastProfiles|Show debug messages" msgstr "" -msgid "DastProfiles|Site Profile" -msgstr "" - -msgid "DastProfiles|Site Profiles" +msgid "DastProfiles|Site name" msgstr "" -msgid "DastProfiles|Site name" +msgid "DastProfiles|Site profile" msgstr "" -msgid "DastProfiles|Site profiles define the attributes and configuration details of your deployed application, website, or API. %{linkStart}Learn more%{linkEnd}." +msgid "DastProfiles|Site profiles" msgstr "" msgid "DastProfiles|Site type" -- GitLab From d59bf312e276469e540b19ac8bc1c299007d8ca6 Mon Sep 17 00:00:00 2001 From: Florie Guibert <fguibert@gitlab.com> Date: Tue, 6 Sep 2022 11:28:21 +1000 Subject: [PATCH 080/169] Move requirements.scss to page bundles No user facing changes --- config/application.rb | 1 + .../stylesheets/{pages => page_bundles}/requirements.scss | 6 ++++-- .../requirements_management/requirements/index.html.haml | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) rename ee/app/assets/stylesheets/{pages => page_bundles}/requirements.scss (93%) diff --git a/config/application.rb b/config/application.rb index d28967f29663ad..0e17d39a135c69 100644 --- a/config/application.rb +++ b/config/application.rb @@ -296,6 +296,7 @@ class Application < Rails::Application config.assets.precompile << "page_bundles/projects_edit.css" config.assets.precompile << "page_bundles/reports.css" config.assets.precompile << "page_bundles/roadmap.css" + config.assets.precompile << "page_bundles/requirements.css" config.assets.precompile << "page_bundles/runner_details.css" config.assets.precompile << "page_bundles/security_dashboard.css" config.assets.precompile << "page_bundles/security_discover.css" diff --git a/ee/app/assets/stylesheets/pages/requirements.scss b/ee/app/assets/stylesheets/page_bundles/requirements.scss similarity index 93% rename from ee/app/assets/stylesheets/pages/requirements.scss rename to ee/app/assets/stylesheets/page_bundles/requirements.scss index 53e46c3e1849dc..bdaf0b137ceb4d 100644 --- a/ee/app/assets/stylesheets/pages/requirements.scss +++ b/ee/app/assets/stylesheets/page_bundles/requirements.scss @@ -1,3 +1,5 @@ +@import 'page_bundles/mixins_and_variables_and_functions'; + .requirements-container { .requirement-form-drawer { &.zen-mode { @@ -80,12 +82,12 @@ #export-requirements { .scrollbox { - border: 1px solid $gray-200; + border: 1px solid var(--gray-200, $gray-200); border-radius: $border-radius-default; position: relative; .scrollbox-header { - border-bottom: 1px solid $gray-200; + border-bottom: 1px solid var(--gray-200, $gray-200); } .scrollbox-body { diff --git a/ee/app/views/projects/requirements_management/requirements/index.html.haml b/ee/app/views/projects/requirements_management/requirements/index.html.haml index 1501821eae3edd..dfce47a7cf6c7d 100644 --- a/ee/app/views/projects/requirements_management/requirements/index.html.haml +++ b/ee/app/views/projects/requirements_management/requirements/index.html.haml @@ -1,5 +1,6 @@ - page_title _('Requirements') - add_page_specific_style 'page_bundles/issues_list' +- add_page_specific_style 'page_bundles/requirements' - @content_wrapper_class = 'js-requirements-container-wrapper' - @content_class = 'requirements-container' -- GitLab From fc8a342661df15d0e2bdfec880de200f53e78764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Korbasiewicz?= <lkorbasiewicz@gitlab.com> Date: Tue, 6 Sep 2022 02:50:21 +0000 Subject: [PATCH 081/169] Remove environment from multi-project pipelines trigger with environment not supported https://gitlab.com/groups/gitlab-org/-/epics/8483 --- doc/ci/yaml/index.md | 11 +++++++++++ doc/ci/yaml/script.md | 2 ++ 2 files changed, 13 insertions(+) diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index a8154a7ae4ef6c..b1f4331030aff5 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -607,6 +607,7 @@ job3: stage: deploy script: - deploy_to_staging + environment: staging ``` In this example, `job1` and `job2` run in parallel: @@ -1478,6 +1479,7 @@ test linux: deploy: stage: deploy script: make deploy + environment: production ``` In this example, two jobs have artifacts: `build osx` and `build linux`. When `test osx` is executed, @@ -2120,6 +2122,7 @@ mac:rspec: production: stage: deploy script: echo "Running production..." + environment: production ``` This example creates four paths of execution: @@ -2382,12 +2385,14 @@ deploy-job: - job: test-job2 optional: true - job: test-job1 + environment: production review-job: stage: deploy needs: - job: test-job2 optional: true + environment: review ``` In this example: @@ -2668,6 +2673,7 @@ pages: - public rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + environment: production ``` This example moves all files from the root of the project to the `public/` directory. @@ -2749,6 +2755,7 @@ deploystacks: STACK: [monitoring, backup, app] - PROVIDER: [gcp, vultr] STACK: [data, processing] + environment: $PROVIDER/$STACK ``` The example generates 10 parallel `deploystacks` jobs, each with different values @@ -3718,6 +3725,7 @@ job4: stage: deploy script: - echo "This job deploys the code. It runs when the test stage completes." + environment: production ``` **Additional details**: @@ -4065,6 +4073,7 @@ deploy_job: stage: deploy script: - deploy-script --url $DEPLOY_SITE --path "/" + environment: production deploy_review_job: stage: deploy @@ -4072,6 +4081,7 @@ deploy_review_job: REVIEW_PATH: "/review" script: - deploy-review-script --url $DEPLOY_SITE --path $REVIEW_PATH + environment: production ``` **Additional details**: @@ -4164,6 +4174,7 @@ deploy_job: script: - make deploy when: manual + environment: production cleanup_job: stage: cleanup diff --git a/doc/ci/yaml/script.md b/doc/ci/yaml/script.md index f1cdcf57e64e28..bd8d7f02a1741b 100644 --- a/doc/ci/yaml/script.md +++ b/doc/ci/yaml/script.md @@ -244,6 +244,7 @@ pages-job: stage: deploy script: - curl --header 'PRIVATE-TOKEN: ${PRIVATE_TOKEN}' "https://gitlab.example.com/api/v4/projects" + environment: production ``` The YAML parser thinks the `:` defines a YAML keyword, and outputs the @@ -257,6 +258,7 @@ pages-job: stage: deploy script: - 'curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" "https://gitlab.example.com/api/v4/projects"' + environment: production ``` ### Job does not fail when using `&&` in a script -- GitLab From 6c60ac5975d8e9de77b0a93eb19d8578acdea289 Mon Sep 17 00:00:00 2001 From: Marcin Sedlak-Jakubowski <msedlakjakubowski@gitlab.com> Date: Tue, 6 Sep 2022 03:28:30 +0000 Subject: [PATCH 082/169] Move timeline events doc to a new page No content changes to incident-timeline-events.md except for adjusting headings. --- .../incident_timeline_events.md | 88 ++++++++++++++ .../incident_management/incidents.md | 108 +++--------------- 2 files changed, 102 insertions(+), 94 deletions(-) create mode 100644 doc/operations/incident_management/incident_timeline_events.md diff --git a/doc/operations/incident_management/incident_timeline_events.md b/doc/operations/incident_management/incident_timeline_events.md new file mode 100644 index 00000000000000..c43de2e81e2ce7 --- /dev/null +++ b/doc/operations/incident_management/incident_timeline_events.md @@ -0,0 +1,88 @@ +--- +stage: Monitor +group: Respond +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Timeline events + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344059) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `incident_timeline`. Enabled by default. + +FLAG: +On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `incident_timeline`. +On GitLab.com, this feature is available. + +Incident timelines are an important part of record keeping for incidents. +Timelines can show executives and external viewers what happened during an incident, +and which steps were taken for it to be resolved. + +## View the event timeline + +Incident timeline events are listed in ascending order of the date and time. +They are grouped with dates and are listed in ascending order of the time when they occurred: + + + +To view the event timeline of an incident: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Monitor > Incidents**. +1. Select an incident. +1. Select the **Timeline** tab. + +## Create a timeline event + +You can create a timeline event in many ways in GitLab. + +### Using the form + +Create a timeline event manually using the form. + +Prerequisites: + +- You must have at least the Developer role for the project. + +To create a timeline event: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Monitor > Incidents**. +1. Select an incident. +1. Select the **Timeline** tab. +1. Select **Add new timeline event**. +1. Complete the required fields. +1. Select **Save** or **Save and add another event**. + +### From a comment on the incident + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344058) in GitLab 15.4. + +Prerequisites: + +- You must have at least the Developer role for the project. + +To create a timeline event from a comment on the incident: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Monitor > Incidents**. +1. Select an incident. +1. Create a comment or choose an existing comment. +1. On the comment you want to add, select **Add comment to incident timeline** (**{clock}**). + +The comment is shown on the incident timeline as a timeline event. + +## Delete a timeline event + +You can also delete timeline events. + +Prerequisites: + +- You must have at least the Developer role for the project. + +To delete a timeline event: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Monitor > Incidents**. +1. Select an incident. +1. Select the **Timeline** tab. +1. On the right of a timeline event, select **More actions** (**{ellipsis_v}**) and then select **Delete**. +1. To confirm, select **Delete Event**. diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md index b12e44398658ad..b66f1d3e1f6afc 100644 --- a/doc/operations/incident_management/incidents.md +++ b/doc/operations/incident_management/incidents.md @@ -205,86 +205,10 @@ field populated. ### Timeline events -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344059) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `incident_timeline`. Enabled by default. +Incident timelines give a high-level overview of what happened +during an incident, and the steps that were taken for it to be resolved. -FLAG: -On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `incident_timeline`. -On GitLab.com, this feature is available. - -Incident timelines are an important part of record keeping for incidents. -They give a high-level overview, to executives and external viewers, of what happened during the incident, -and the steps that were taken for it to be resolved. - -#### View the event timeline - -Incident timeline events are listed in ascending order of the date and time. -They are grouped with dates and are listed in ascending order of the time when they occured: - - - -To view the event timeline of an incident: - -1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Monitor > Incidents**. -1. Select an incident. -1. Select the **Timeline** tab. - -#### Create a timeline event - -You can create a timeline event in many ways in GitLab. - -##### Using the form - -Create a timeline event manually using the form. - -Prerequisites: - -- You must have at least the Developer role for the project. - -To create a timeline event: - -1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Monitor > Incidents**. -1. Select an incident. -1. Select the **Timeline** tab. -1. Select **Add new timeline event**. -1. Complete the required fields. -1. Select **Save** or **Save and add another event**. - -##### From a comment on the incident - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344058) in GitLab 15.4. - -Prerequisites: - -- You must have at least the Developer role for the project. - -To create a timeline event from a comment on the incident: - -1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Monitor > Incidents**. -1. Select an incident. -1. Create a comment or choose an existing comment. -1. On the comment you want to add, select **Add comment to incident timeline** (**{clock}**). - -The comment is shown on the incident timeline as a timeline event. - -#### Delete a timeline event - -You can also delete timeline events. - -Prerequisites: - -- You must have at least the Developer role for the project. - -To delete a timeline event: - -1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Monitor > Incidents**. -1. Select an incident. -1. Select the **Timeline** tab. -1. On the right of a timeline event, select **More actions** (**{ellipsis_v}**) and then select **Delete**. -1. To confirm, select **Delete Event**. +Read more about [timeline events](incident_timeline_events.md) and how to enable this feature. ### Recent updates view **(PREMIUM)** @@ -319,32 +243,28 @@ as a column in the Incidents List, and as a field on newly created Incidents. If the incident isn't closed before the SLA period ends, GitLab adds a `missed::SLA` label to the incident. -## Incident actions - -There are different actions available to help triage and respond to incidents. - -### Assign incidents +## Assign incidents Assign incidents to users that are actively responding. Select **Edit** in the right-hand side bar to select or clear assignees. -### Associate a milestone +## Associate a milestone Associate an incident to a milestone by selecting **Edit** next to the milestone feature in the right-hand side bar. -### Change severity +## Change severity See [Incident List](#incident-list) for a full description of the severity levels available. Select **Edit** in the right-hand side bar to change the severity of an incident. You can also change the severity using the [`/severity` quick action](../../user/project/quick_actions.md). -### Add a to-do item +## Add a to-do item Add a to-do for incidents that you want to track in your to-do list. Select **Add a to do** at the top of the right-hand side bar to add a to-do item. -### Change incident status +## Change incident status > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5716) in GitLab 14.9 [with a flag](../../administration/feature_flags.md) named `incident_escalations`. Disabled by default. > - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/345769) in GitLab 14.10. @@ -365,7 +285,7 @@ In GitLab 15.1 and earlier, updating the status of an [incident created from an also updates the alert status. In [GitLab 15.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/356057), the alert status is independent and does not update when the incident status changes. -### Change escalation policy **(PREMIUM)** +## Change escalation policy **(PREMIUM)** > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5716) in GitLab 14.9 [with a flag](../../administration/feature_flags.md) named `incident_escalations`. Disabled by default. > - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/345769) in GitLab 14.10. @@ -384,21 +304,21 @@ In GitLab 15.1 and earlier, the escalation policy for [incidents created from al reflects the alert's escalation policy and cannot be changed. In [GitLab 15.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/356057), the incident escalation policy is independent and can be changed. -### Manage incidents from Slack +## Manage incidents from Slack Slack slash commands allow you to control GitLab and view GitLab content without leaving Slack. Learn how to [set up Slack slash commands](../../user/project/integrations/slack_slash_commands.md) and how to [use the available slash commands](../../integration/slash_commands.md). -### Associate Zoom calls +## Associate Zoom calls GitLab enables you to [associate a Zoom meeting with an issue](../../user/project/issues/associate_zoom_meeting.md) for synchronous communication during incident management. After starting a Zoom call for an incident, you can associate the conference call with an issue. Your team members can join the Zoom call without requesting a link. -### Linked resources +## Linked resources In an incident, you can [links to various resources](linked_resources.md), for example: @@ -407,7 +327,7 @@ for example: - Zoom meeting - Resources for resolving the incidents -### Embed metrics in incidents +## Embed metrics in incidents You can embed metrics anywhere [GitLab Markdown](../../user/markdown.md) is used, such as descriptions, comments on issues, and merge requests. Embedding @@ -420,7 +340,7 @@ You can embed both [GitLab-hosted metrics](../metrics/embed.md) and [Grafana metrics](../metrics/embed_grafana.md) in incidents and issue templates. -### Automatically close incidents via recovery alerts +## Automatically close incidents via recovery alerts > - [Introduced for Prometheus Integrations](https://gitlab.com/gitlab-org/gitlab/-/issues/13401) in GitLab 12.5. > - [Introduced for HTTP Integrations](https://gitlab.com/gitlab-org/gitlab/-/issues/13402) in GitLab 13.4. -- GitLab From 17ca2fc585dc5af8e5abed510979f783c53d270a Mon Sep 17 00:00:00 2001 From: JeremyWuuuuu <jeremyw@jihulab.com> Date: Tue, 6 Sep 2022 11:00:06 +0800 Subject: [PATCH 083/169] Fix: new compare merge request locale issues * Replace raw strings to translated strings. Changelog: fixed --- .../merge_requests/creations/_new_compare.html.haml | 6 +++--- locale/gitlab.pot | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 8cd0d2f9e32f73..17b1e5a757c982 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -8,7 +8,7 @@ .col-lg-6 .card-new-merge-request %h2.gl-font-size-h2 - Source branch + = _('Source branch') .clearfix .merge-request-select.dropdown = f.hidden_field :source_project_id @@ -38,7 +38,7 @@ .col-lg-6 .card-new-merge-request %h2.gl-font-size-h2 - Target branch + = _('Target branch') .clearfix - projects = target_projects(@project) .merge-request-select.dropdown @@ -68,4 +68,4 @@ - if @merge_request.errors.any? = form_errors(@merge_request) - = f.submit 'Compare branches and continue', class: "gl-button btn btn-confirm mr-compare-btn gl-mt-4", data: { qa_selector: "compare_branches_button" } + = f.submit _('Compare branches and continue'), class: "gl-button btn btn-confirm mr-compare-btn gl-mt-4", data: { qa_selector: "compare_branches_button" } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 22cb2a33c05e87..eb4e6239420e38 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9575,6 +9575,9 @@ msgstr "" msgid "Compare Revisions" msgstr "" +msgid "Compare branches and continue" +msgstr "" + msgid "Compare changes" msgstr "" -- GitLab From 9437529ba75341016d8305621c18dda9fde85d74 Mon Sep 17 00:00:00 2001 From: James Reed <jreed@gitlab.com> Date: Tue, 6 Sep 2022 03:42:08 +0000 Subject: [PATCH 084/169] Update Omnibus global server hook guidance based on v15 changes --- doc/administration/server_hooks.md | 22 ++++++++-------------- doc/update/index.md | 4 ++++ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/doc/administration/server_hooks.md b/doc/administration/server_hooks.md index 1a47dc4ccf282d..41b19f7477f199 100644 --- a/doc/administration/server_hooks.md +++ b/doc/administration/server_hooks.md @@ -56,8 +56,7 @@ If the server hook code is properly implemented, it should execute when the Git ## Create global server hooks for all repositories -To create a Git hook that applies to all repositories, set a global server hook. The default global server hook directory -is in the GitLab Shell directory. Any server hook added there applies to all repositories, including: +To create a Git hook that applies to all repositories, set a global server hook. Global server hooks also apply to: - [Project and group wiki](../user/project/wiki/index.md) repositories. Their storage directory names are in the format `<id>.wiki.git`. @@ -66,14 +65,12 @@ is in the GitLab Shell directory. Any server hook added there applies to all rep ### Choose a server hook directory -Before creating a global server hook, you must choose a directory for it. The default global server hook directory: +Before creating a global server hook, you must choose a directory for it. The global server hook directory: -- For Omnibus GitLab installations is usually `/opt/gitlab/embedded/service/gitlab-shell/hooks`. +- For Omnibus GitLab installations, set the directory in `gitlab.rb` under `gitaly['custom_hooks_dir']`. You can use the default suggestion `/var/opt/gitlab/gitaly/custom_hooks` directory + by uncommenting `gitaly['custom_hooks_dir']` to enable it. - For an installation from source is usually `/home/git/gitlab-shell/hooks`. -To use a different directory for global server hooks, set `custom_hooks_dir` in Gitaly configuration: - -- For Omnibus installations, set in `gitlab.rb`. - For source installations, the configuration location depends on the GitLab version. For: - GitLab 13.0 and earlier, set in `gitlab-shell/config.yml`. - GitLab 13.1 and later, set in `gitaly/config.toml` under the `[hooks]` section. However, GitLab honors the @@ -84,18 +81,15 @@ To use a different directory for global server hooks, set `custom_hooks_dir` in To create a global server hook for all repositories: 1. On the GitLab server, go to the configured global server hook directory. -1. In the configured global server hook directory: - - To create a single server hook, create a file with a name that matches the hook type. For example, for a - `pre-receive` server hook, the filename should be `pre-receive` with no extension. - - To create many server hooks, create a directory for the hooks that matches the hook type. For example, for a - `pre-receive` server hook, the directory name should be `pre-receive.d`. Put the files for the hook in that directory. -1. Inside this new directory, add your server hook. Server hooks can be in any programming language. Ensure the +1. In the configured global server hook directory, create a directory for the hooks that matches the hook type. For example, for a `pre-receive` server hook, the directory name should be `pre-receive.d`. +1. Inside this new directory, add your server hooks. Server hooks can be in any programming language. Ensure the [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) at the top reflects the language type. For example, if the script is in Ruby the shebang is probably `#!/usr/bin/env ruby`. 1. Make the hook file executable, ensure that it's owned by the Git user, and ensure it does not match the backup file pattern (`*~`). -If the server hook code is properly implemented, it should execute when the Git hook is next triggered. +If the server hook code is properly implemented, it should execute when the Git hook is next triggered. Hooks are executed in alphabetical order by filename in the hook type +subdirectories. ## Chained server hooks diff --git a/doc/update/index.md b/doc/update/index.md index 4ce90a9a116074..87bb59f57181e1 100644 --- a/doc/update/index.md +++ b/doc/update/index.md @@ -521,6 +521,10 @@ In GitLab 15.3.3, [SAML Group Links](../api/groups.md#saml-group-links) API `acc - The use of encrypted S3 buckets with storage-specific configuration is no longer supported after [removing support for using `background_upload`](removals.md#background-upload-for-object-storage). - The [certificate-based Kubernetes integration (DEPRECATED)](../user/infrastructure/clusters/index.md#certificate-based-kubernetes-integration-deprecated) is disabled by default, but you can be re-enable it through the [`certificate_based_clusters` feature flag](../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags) until GitLab 16.0. - When you use the GitLab Helm Chart project with a custom `serviceAccount`, ensure it has `get` and `list` permissions for the `serviceAccount` and `secret` resources. +- The [`custom_hooks_dir`](../administration/server_hooks.md#create-global-server-hooks-for-all-repositories) setting for configuring global server hooks is now configured in + Gitaly. The previous implementation in GitLab Shell was removed in GitLab 15.0. With this change, global server hooks are stored only inside a subdirectory named after the + hook type. Global server hooks can no longer be a single hook file in the root of the custom hooks directory. For example, you must use `<custom_hooks_dir>/<hook_name>.d/*` rather + than `<custom_hooks_dir>/<hook_name>`. ### 14.10.0 -- GitLab From 15a0917384c732cdfa6c859d8ac9a5c09f17fc0f Mon Sep 17 00:00:00 2001 From: Evan Read <eread@gitlab.com> Date: Tue, 6 Sep 2022 03:46:08 +0000 Subject: [PATCH 085/169] Refine remove group API documentation --- doc/api/groups.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/groups.md b/doc/api/groups.md index 8d3b016e8faf28..aaeb92a4630b3a 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -1116,10 +1116,10 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab Only available to group owners and administrators. -This endpoint either: +This endpoint: -- Removes group, and queues a background job to delete all projects in the group as well. -- Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/33257), on [Premium](https://about.gitlab.com/pricing/) or higher tiers, marks a group for deletion. The deletion happens 7 days later by default, but this can be changed in the [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#deletion-protection). +- On Premium and higher tiers, marks the group for deletion. The deletion happens 7 days later by default, but you can change the retention period in the [instance settings](../user/admin_area/settings/visibility_and_access_controls.md#deletion-protection). +- On Free tier, removes the group immediately and queues a background job to delete all projects in the group. - Deletes a subgroup immediately if the subgroup is marked for deletion (GitLab 15.4 and later). The endpoint does not immediately delete top-level groups. ```plaintext -- GitLab From 788cb9ec0e59d73c19b42467e86b708b9dc760bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20W=C3=A4lter?= <jonas.waelter@noser.com> Date: Tue, 6 Sep 2022 03:49:04 +0000 Subject: [PATCH 086/169] Allow admins to merge topics [API] Changelog: added --- app/controllers/admin/topics_controller.rb | 7 +-- app/services/topics/merge_service.rb | 13 +++- doc/api/topics.md | 42 ++++++++++++- lib/api/topics.rb | 20 ++++++ locale/gitlab.pot | 12 ++++ .../admin/topics_controller_spec.rb | 2 +- spec/requests/api/topics_spec.rb | 62 +++++++++++++++++++ spec/services/topics/merge_service_spec.rb | 10 +-- 8 files changed, 154 insertions(+), 14 deletions(-) diff --git a/app/controllers/admin/topics_controller.rb b/app/controllers/admin/topics_controller.rb index c3b1c6793adb65..e97ead12f71796 100644 --- a/app/controllers/admin/topics_controller.rb +++ b/app/controllers/admin/topics_controller.rb @@ -49,11 +49,8 @@ def merge source_topic = Projects::Topic.find(merge_params[:source_topic_id]) target_topic = Projects::Topic.find(merge_params[:target_topic_id]) - begin - ::Topics::MergeService.new(source_topic, target_topic).execute - rescue ArgumentError => e - return render status: :bad_request, json: { type: :alert, message: e.message } - end + response = ::Topics::MergeService.new(source_topic, target_topic).execute + return render status: :bad_request, json: { type: :alert, message: response.message } if response.error? message = _('Topic %{source_topic} was successfully merged into topic %{target_topic}.') flash[:toast] = message % { source_topic: source_topic.name, target_topic: target_topic.name } diff --git a/app/services/topics/merge_service.rb b/app/services/topics/merge_service.rb index 0d256579fe04d7..58f3d5305b48ef 100644 --- a/app/services/topics/merge_service.rb +++ b/app/services/topics/merge_service.rb @@ -17,14 +17,21 @@ def execute refresh_target_topic_counters delete_source_topic end + + ServiceResponse.success + rescue ArgumentError => e + ServiceResponse.error(message: e.message) + rescue StandardError => e + Gitlab::ErrorTracking.track_exception(e, source_topic_id: source_topic.id, target_topic_id: target_topic.id) + ServiceResponse.error(message: _('Topics could not be merged!')) end private def validate_parameters! - raise ArgumentError, 'The source topic is not a topic.' unless source_topic.is_a?(Projects::Topic) - raise ArgumentError, 'The target topic is not a topic.' unless target_topic.is_a?(Projects::Topic) - raise ArgumentError, 'The source topic and the target topic are identical.' if source_topic == target_topic + raise ArgumentError, _('The source topic is not a topic.') unless source_topic.is_a?(Projects::Topic) + raise ArgumentError, _('The target topic is not a topic.') unless target_topic.is_a?(Projects::Topic) + raise ArgumentError, _('The source topic and the target topic are identical.') if source_topic == target_topic end # rubocop: disable CodeReuse/ActiveRecord diff --git a/doc/api/topics.md b/doc/api/topics.md index ee88a43ff1c412..38d99244bb6ef0 100644 --- a/doc/api/topics.md +++ b/doc/api/topics.md @@ -217,7 +217,7 @@ curl --request PUT \ > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80725) in GitLab 14.9. -You must be an administrator to delete a project. +You must be an administrator to delete a project topic. When you delete a project topic, you also delete the topic assignment for projects. ```plaintext @@ -237,3 +237,43 @@ curl --request DELETE \ --header "PRIVATE-TOKEN: <your_access_token>" \ "https://gitlab.example.com/api/v4/topics/1" ``` + +## Merge topics + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95501) in GitLab 15.4. + +You must be an administrator to merge a source topic into a target topic. +When you merge topics, you delete the source topic and move all assigned projects to the target topic. + +```plaintext +POST /topics/merge +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +| ----------------- | ------- | ---------------------- | -------------------------- | +| `source_topic_id` | integer | **{check-circle}** Yes | ID of source project topic | +| `target_topic_id` | integer | **{check-circle}** Yes | ID of target project topic | + +Example request: + +```shell +curl --request POST \ + --data "source_topic_id=2&target_topic_id=1" \ + --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/topics/merge" +``` + +Example response: + +```json +{ + "id": 1, + "name": "topic1", + "title": "Topic 1", + "description": null, + "total_projects_count": 0, + "avatar_url": null +} +``` diff --git a/lib/api/topics.rb b/lib/api/topics.rb index a08b4c6c1070e7..38cfdc44021adc 100644 --- a/lib/api/topics.rb +++ b/lib/api/topics.rb @@ -94,5 +94,25 @@ class Topics < ::API::Base destroy_conditionally!(topic) end + + desc 'Merge topics' do + detail 'This feature was introduced in GitLab 15.4.' + success Entities::Projects::Topic + end + params do + requires :source_topic_id, type: Integer, desc: 'ID of source project topic' + requires :target_topic_id, type: Integer, desc: 'ID of target project topic' + end + post 'topics/merge' do + authenticated_as_admin! + + source_topic = ::Projects::Topic.find(params[:source_topic_id]) + target_topic = ::Projects::Topic.find(params[:target_topic_id]) + + response = ::Topics::MergeService.new(source_topic, target_topic).execute + render_api_error!(response.message, :bad_request) if response.error? + + present target_topic, with: Entities::Projects::Topic + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bcfc87fa07845f..9d33cbedb36c73 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -39553,6 +39553,12 @@ msgstr "" msgid "The source project of this merge request has been removed." msgstr "" +msgid "The source topic and the target topic are identical." +msgstr "" + +msgid "The source topic is not a topic." +msgstr "" + msgid "The specified tab is invalid, please select another" msgstr "" @@ -39565,6 +39571,9 @@ msgstr "" msgid "The tag name can't be changed for an existing release." msgstr "" +msgid "The target topic is not a topic." +msgstr "" + msgid "The time period in seconds that the maximum requests per project limit applies to." msgstr "" @@ -41270,6 +41279,9 @@ msgstr "" msgid "Topics" msgstr "" +msgid "Topics could not be merged!" +msgstr "" + msgid "Total" msgstr "" diff --git a/spec/controllers/admin/topics_controller_spec.rb b/spec/controllers/admin/topics_controller_spec.rb index 87093e0263bbbf..111fdcc3be6c47 100644 --- a/spec/controllers/admin/topics_controller_spec.rb +++ b/spec/controllers/admin/topics_controller_spec.rb @@ -194,7 +194,7 @@ end it 'renders a 400 error for identical topic ids' do - post :merge, params: { source_topic_id: topic, target_topic_id: topic.id } + post :merge, params: { source_topic_id: topic.id, target_topic_id: topic.id } expect(response).to have_gitlab_http_status(:bad_request) expect { topic.reload }.not_to raise_error diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb index 72221e3fb6a815..1ad6f876fabdfd 100644 --- a/spec/requests/api/topics_spec.rb +++ b/spec/requests/api/topics_spec.rb @@ -317,4 +317,66 @@ end end end + + describe 'POST /topics/merge', :aggregate_failures do + context 'as administrator' do + let_it_be(:api_url) { api('/topics/merge', admin) } + + it 'merge topics' do + post api_url, params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:created) + expect { topic_2.reload }.not_to raise_error + expect { topic_3.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(json_response['id']).to eq(topic_2.id) + expect(json_response['total_projects_count']).to eq(topic_2.total_projects_count) + end + + it 'returns 404 for non existing source topic id' do + post api_url, params: { source_topic_id: non_existing_record_id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 404 for non existing target topic id' do + post api_url, params: { source_topic_id: topic_3.id, target_topic_id: non_existing_record_id } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 400 for identical topic ids' do + post api_url, params: { source_topic_id: topic_2.id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eql('The source topic and the target topic are identical.') + end + + it 'returns 400 if merge failed' do + allow_next_found_instance_of(Projects::Topic) do |topic| + allow(topic).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed) + end + + post api_url, params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eql('Topics could not be merged!') + end + end + + context 'as normal user' do + it 'returns 403 Forbidden' do + post api('/topics/merge', user), params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'as anonymous' do + it 'returns 401 Unauthorized' do + post api('/topics/merge'), params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id } + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end end diff --git a/spec/services/topics/merge_service_spec.rb b/spec/services/topics/merge_service_spec.rb index 971917eb8e9bd4..eef31817aa8d42 100644 --- a/spec/services/topics/merge_service_spec.rb +++ b/spec/services/topics/merge_service_spec.rb @@ -30,7 +30,9 @@ it 'reverts previous changes' do allow(source_topic.reload).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed) - expect { subject }.to raise_error(ActiveRecord::RecordNotDestroyed) + response = subject + expect(response).to be_error + expect(response.message).to eq('Topics could not be merged!') expect(source_topic.projects).to contain_exactly(project_1, project_2, project_4) expect(target_topic.projects).to contain_exactly(project_3, project_4) @@ -50,9 +52,9 @@ with_them do it 'raises correct error' do - expect { subject }.to raise_error(ArgumentError) do |error| - expect(error.message).to eq(expected_message) - end + response = subject + expect(response).to be_error + expect(response.message).to eq(expected_message) end end end -- GitLab From 3efa8222f47c755ef72013ad395d1e590937a28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Korbasiewicz?= <lkorbasiewicz@gitlab.com> Date: Tue, 6 Sep 2022 04:42:22 +0000 Subject: [PATCH 087/169] Updated documentation with environment (batch 2) --- doc/ci/pipelines/pipeline_architectures.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/ci/pipelines/pipeline_architectures.md b/doc/ci/pipelines/pipeline_architectures.md index 3ff22a169008c0..7df59c63367c46 100644 --- a/doc/ci/pipelines/pipeline_architectures.md +++ b/doc/ci/pipelines/pipeline_architectures.md @@ -84,12 +84,14 @@ deploy_a: script: - echo "This job deploys something. It will only run when all jobs in the" - echo "test stage complete." + environment: production deploy_b: stage: deploy script: - echo "This job deploys something else. It will only run when all jobs in the" - echo "test stage complete. It will start at about the same time as deploy_a." + environment: production ``` ## Directed Acyclic Graph Pipelines @@ -151,12 +153,14 @@ deploy_a: script: - echo "Since build_a and test_a run quickly, this deploy job can run much earlier." - echo "It does not need to wait for build_b or test_b." + environment: production deploy_b: stage: deploy needs: [test_b] script: - echo "Since build_b and test_b run slowly, this deploy job will run much later." + environment: production ``` ## Child / Parent Pipelines @@ -237,6 +241,7 @@ deploy_a: needs: [test_a] script: - echo "This job deploys something." + environment: production ``` Example child `b` pipeline configuration, located in `/b/.gitlab-ci.yml`, making @@ -266,6 +271,7 @@ deploy_b: needs: [test_b] script: - echo "This job deploys something else." + environment: production ``` It's also possible to set jobs to run before or after triggering child pipelines, -- GitLab From e925e283a9c4f195ec09d058f7ce901eb5f25bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Korbasiewicz?= <lkorbasiewicz@gitlab.com> Date: Tue, 6 Sep 2022 04:43:20 +0000 Subject: [PATCH 088/169] Updated documentation with environment (batch 5) --- doc/ci/jobs/ci_job_token.md | 1 + doc/ci/jobs/job_control.md | 5 +++++ doc/ci/migration/circleci.md | 3 +++ doc/ci/quick_start/index.md | 1 + 4 files changed, 10 insertions(+) diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md index 93f22da648a42f..72daf3126fc598 100644 --- a/doc/ci/jobs/ci_job_token.md +++ b/doc/ci/jobs/ci_job_token.md @@ -121,6 +121,7 @@ trigger_pipeline: - curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=main "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline" rules: - if: $CI_COMMIT_TAG + environment: production ``` If you use the `CI_PIPELINE_SOURCE` [predefined CI/CD variable](../variables/predefined_variables.md) diff --git a/doc/ci/jobs/job_control.md b/doc/ci/jobs/job_control.md index 217d12e4c267f2..baa4e74d8e5164 100644 --- a/doc/ci/jobs/job_control.md +++ b/doc/ci/jobs/job_control.md @@ -645,6 +645,7 @@ timed rollout 10%: script: echo 'Rolling out 10% ...' when: delayed start_in: 30 minutes + environment: production ``` To stop the active timer of a delayed job, select **Unschedule** (**{time-out}**). @@ -698,6 +699,7 @@ deploystacks: parallel: matrix: - PROVIDER: [aws, ovh, gcp, vultr] + environment: production/$PROVIDER ``` You can also [create a multi-dimensional matrix](../yaml/index.md#parallelmatrix). @@ -722,6 +724,7 @@ deploystacks: STACK: [monitoring, backup] - PROVIDER: [gcp, vultr] STACK: [data] + environment: $PROVIDER/$STACK ``` This example generates 6 parallel `deploystacks` trigger jobs, each with different values @@ -754,6 +757,7 @@ deploystacks: STACK: [data] tags: - ${PROVIDER}-${STACK} + environment: $PROVIDER/$STACK ``` #### Fetch artifacts from a `parallel:matrix` job @@ -784,6 +788,7 @@ deploy: dependencies: - "ruby: [2.7, aws]" script: echo hello + environment: production ``` Quotes around the `dependencies` entry are required. diff --git a/doc/ci/migration/circleci.md b/doc/ci/migration/circleci.md index 7255d9aec82e18..efe11466674fc4 100644 --- a/doc/ci/migration/circleci.md +++ b/doc/ci/migration/circleci.md @@ -136,6 +136,7 @@ job3: job4: stage: deploy script: make deploy + environment: production ``` #### Scheduled run @@ -196,6 +197,7 @@ deploy_prod: script: - echo "Deploy to production server" when: manual + environment: production ``` ### Filter job by branch @@ -222,6 +224,7 @@ deploy: - echo "Deploy job" rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH =~ /^rc-/ + environment: production ``` ### Caching diff --git a/doc/ci/quick_start/index.md b/doc/ci/quick_start/index.md index 0369824c92ef8e..2b44cf3b89855c 100644 --- a/doc/ci/quick_start/index.md +++ b/doc/ci/quick_start/index.md @@ -109,6 +109,7 @@ To create a `.gitlab-ci.yml` file: stage: deploy script: - echo "This job deploys something from the $CI_COMMIT_BRANCH branch." + environment: production ``` `$GITLAB_USER_LOGIN` and `$CI_COMMIT_BRANCH` are -- GitLab From 060e2e542b3271c18a84b6881dab157efbef4a12 Mon Sep 17 00:00:00 2001 From: Andrejs Cunskis <acunskis@gitlab.com> Date: Tue, 30 Aug 2022 12:23:35 +0300 Subject: [PATCH 089/169] Use native trigger for omnibus builds Remove all variable forwarding Do not inherit environment variables in omnibus-trigger Remove CACHE_UPDATE env variable Pass additional variables to omnibus Add missing cache variable Add version environment variables Use dotenv report to populate omnibus env Remove hardcoded version population --- .../ci/package-and-test/main.gitlab-ci.yml | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index d1280c43903dba..bfccdce80eca28 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -35,10 +35,24 @@ stages: RUN_WITH_BUNDLE: "true" # installs and runs gitlab-qa via bundler QA_PATH: qa +.omnibus-env: + variables: + BUILD_ENV: build.env + script: + - | + SECURITY_SOURCES=$([[ ! "$CI_PROJECT_NAMESPACE" =~ ^gitlab-org\/security ]] || echo "true") + echo "SECURITY_SOURCES=${SECURITY_SOURCES:-false}" > $BUILD_ENV + echo "OMNIBUS_GITLAB_CACHE_UPDATE=${OMNIBUS_GITLAB_CACHE_UPDATE:-false}" >> $BUILD_ENV + for version_file in *_VERSION; do echo "$version_file=$(cat $version_file)" >> $BUILD_ENV; done + echo "Built environment file for omnibus build:" + cat $BUILD_ENV + artifacts: + reports: + dotenv: $BUILD_ENV + .update-script: script: - - export CURRENT_VERSION="$(cat ../VERSION)" - - export QA_COMMAND="bundle exec gitlab-qa Test::Omnibus::UpdateFromPrevious $RELEASE $CURRENT_VERSION $UPDATE_TYPE -- $QA_RSPEC_TAGS $RSPEC_REPORT_OPTS" + - export QA_COMMAND="bundle exec gitlab-qa Test::Omnibus::UpdateFromPrevious $RELEASE $GITLAB_VERSION $UPDATE_TYPE -- $QA_RSPEC_TAGS $RSPEC_REPORT_OPTS" - echo "Running - '$QA_COMMAND'" - eval "$QA_COMMAND" @@ -65,16 +79,38 @@ stages: # ========================================== # Prepare stage # ========================================== -trigger-omnibus: +trigger-omnibus-env: extends: - - .ruby-image + - .omnibus-env - .rules:prepare stage: .pre - before_script: - - source scripts/utils.sh - - install_gitlab_gem - script: - - ./scripts/trigger-build.rb omnibus + +trigger-omnibus: + extends: .rules:prepare + stage: .pre + needs: + - trigger-omnibus-env + inherit: + variables: false + variables: + GITALY_SERVER_VERSION: $GITALY_SERVER_VERSION + GITLAB_ELASTICSEARCH_INDEXER_VERSION: $GITLAB_ELASTICSEARCH_INDEXER_VERSION + GITLAB_KAS_VERSION: $GITLAB_KAS_VERSION + GITLAB_METRICS_EXPORTER_VERSION: $GITLAB_METRICS_EXPORTER_VERSION + GITLAB_PAGES_VERSION: $GITLAB_PAGES_VERSION + GITLAB_SHELL_VERSION: $GITLAB_SHELL_VERSION + GITLAB_WORKHORSE_VERSION: $GITLAB_WORKHORSE_VERSION + GITLAB_VERSION: $CI_COMMIT_SHA + IMAGE_TAG: $CI_COMMIT_SHA + TOP_UPSTREAM_SOURCE_PROJECT: $CI_PROJECT_PATH + SECURITY_SOURCES: $SECURITY_SOURCES + CACHE_UPDATE: $OMNIBUS_GITLAB_CACHE_UPDATE + SKIP_QA_DOCKER: "true" + SKIP_QA_TEST: "true" + ee: "true" + trigger: + project: gitlab-org/build/omnibus-gitlab-mirror + strategy: depend download-knapsack-report: extends: -- GitLab From 2d34173c02c27e343e5db087150d24afc774192f Mon Sep 17 00:00:00 2001 From: Andrejs Cunskis <acunskis@gitlab.com> Date: Tue, 6 Sep 2022 08:07:20 +0200 Subject: [PATCH 090/169] Simplify variable creation --- .../ci/package-and-test/variables.gitlab-ci.yml | 2 +- scripts/generate-e2e-pipeline | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.gitlab/ci/package-and-test/variables.gitlab-ci.yml b/.gitlab/ci/package-and-test/variables.gitlab-ci.yml index 545a494eee3566..7475df669479c3 100644 --- a/.gitlab/ci/package-and-test/variables.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/variables.gitlab-ci.yml @@ -1,7 +1,7 @@ # Default variables for package-and-test variables: - RELEASE: "gitlab/gitlab-ee:nightly" + RELEASE: "${CI_REGISTRY}/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:${CI_COMMIT_SHA}" SKIP_REPORT_IN_ISSUES: "true" OMNIBUS_GITLAB_CACHE_UPDATE: "false" COLORIZED_LOGS: "true" diff --git a/scripts/generate-e2e-pipeline b/scripts/generate-e2e-pipeline index 697c4371d3b5dc..8f5046ef32cc1c 100755 --- a/scripts/generate-e2e-pipeline +++ b/scripts/generate-e2e-pipeline @@ -18,8 +18,9 @@ if [ "$QA_SKIP_ALL_TESTS" == "true" ]; then exit fi -common_variables=$(cat <<YML +variables=$(cat <<YML variables: + GITLAB_VERSION: "$(cat VERSION)" QA_TESTS: "$QA_TESTS" QA_FEATURE_FLAGS: "${QA_FEATURE_FLAGS}" QA_FRAMEWORK_CHANGES: "${QA_FRAMEWORK_CHANGES:-false}" @@ -30,17 +31,11 @@ YML echo "Using .gitlab/ci/review-apps/main.gitlab-ci.yml and .gitlab/ci/package-and-test/main.gitlab-ci.yml" cp .gitlab/ci/review-apps/main.gitlab-ci.yml "$REVIEW_PIPELINE_YML" -echo "$common_variables" >>"$REVIEW_PIPELINE_YML" +echo "$variables" >>"$REVIEW_PIPELINE_YML" echo "Successfully generated review-app pipeline with following variables section:" -echo -e "$common_variables" +echo "$variables" -omnibus_variables=$(cat <<YML - RELEASE: "${CI_REGISTRY}/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:${CI_COMMIT_SHA}" - OMNIBUS_GITLAB_CACHE_UPDATE: "${OMNIBUS_GITLAB_CACHE_UPDATE:-false}" -YML -) cp .gitlab/ci/package-and-test/main.gitlab-ci.yml "$OMNIBUS_PIPELINE_YML" -echo "$common_variables" >>"$OMNIBUS_PIPELINE_YML" -echo "$omnibus_variables" >>"$OMNIBUS_PIPELINE_YML" +echo "$variables" >>"$OMNIBUS_PIPELINE_YML" echo "Successfully generated package-and-test pipeline with following variables section:" -echo -e "${common_variables}\n${omnibus_variables}" +echo "$variables" -- GitLab From 9039982866d2c116f8f102ec9a5d28bb23f36cc5 Mon Sep 17 00:00:00 2001 From: Jessie Young <jessieyoung@gitlab.com> Date: Tue, 6 Sep 2022 06:15:45 +0000 Subject: [PATCH 091/169] Add count_user_auth metric * Aggregate count of unique user sign ins across all providers * Unique auth events on a per-login-type were added here: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39747 --- .../20220825232556_count_user_auth.yml | 23 ++++++++++++ .../count_user_auth_metric.rb | 17 +++++++++ .../count_user_auth_metric_spec.rb | 35 +++++++++++++++++++ spec/lib/gitlab/usage_data_metrics_spec.rb | 4 +++ spec/lib/gitlab/usage_data_spec.rb | 18 ++++++++-- 5 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 config/metrics/counts_all/20220825232556_count_user_auth.yml create mode 100644 lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric.rb create mode 100644 spec/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric_spec.rb diff --git a/config/metrics/counts_all/20220825232556_count_user_auth.yml b/config/metrics/counts_all/20220825232556_count_user_auth.yml new file mode 100644 index 00000000000000..623e0dbb7a42fa --- /dev/null +++ b/config/metrics/counts_all/20220825232556_count_user_auth.yml @@ -0,0 +1,23 @@ +--- +key_path: usage_activity_by_stage.manage.count_user_auth +description: Number of unique user logins +product_section: dev +product_stage: manage +product_group: authentication_and_authorization +product_category: system_access +value_type: number +status: active +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96321" +time_frame: all +data_source: database +instrumentation_class: CountUserAuthMetric +data_category: optional +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric.rb new file mode 100644 index 00000000000000..1de93ce6dfa21b --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class CountUserAuthMetric < DatabaseMetric + operation :distinct_count, column: :user_id + + relation do + AuthenticationEvent.success + end + end + end + end + end +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric_spec.rb new file mode 100644 index 00000000000000..2f49c427bd0e87 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_user_auth_metric_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountUserAuthMetric do + context 'with all time frame' do + let(:expected_value) { 2 } + + before do + user = create(:user) + user2 = create(:user) + create(:authentication_event, user: user, provider: :ldapmain, result: :success) + create(:authentication_event, user: user2, provider: :ldapsecondary, result: :success) + create(:authentication_event, user: user2, provider: :group_saml, result: :success) + create(:authentication_event, user: user2, provider: :group_saml, result: :success) + create(:authentication_event, user: user, provider: :group_saml, result: :failed) + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' } + end + + context 'with 28d time frame' do + let(:expected_value) { 1 } + + before do + user = create(:user) + user2 = create(:user) + + create(:authentication_event, created_at: 1.year.ago, user: user, provider: :ldapmain, result: :success) + create(:authentication_event, created_at: 1.week.ago, user: user2, provider: :ldapsecondary, result: :success) + end + + it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', data_source: 'database' } + end +end diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb index 1968523dc4ac1d..ed0eabf1b4db21 100644 --- a/spec/lib/gitlab/usage_data_metrics_spec.rb +++ b/spec/lib/gitlab/usage_data_metrics_spec.rb @@ -37,6 +37,10 @@ expect(subject[:usage_activity_by_stage][:plan]).to include(:issues) end + it 'includes usage_activity_by_stage metrics' do + expect(subject[:usage_activity_by_stage][:manage]).to include(:count_user_auth) + end + it 'includes usage_activity_by_stage_monthly keys' do expect(subject[:usage_activity_by_stage_monthly][:plan]).to include(:issues) end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 3b0cc0fb16b145..f30cc8b6c45b13 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -215,14 +215,28 @@ groups: 2, users_created: 10, omniauth_providers: ['google_oauth2'], - user_auth_by_provider: { 'group_saml' => 2, 'ldap' => 4, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 } + user_auth_by_provider: { + 'group_saml' => 2, + 'ldap' => 4, + 'standard' => 0, + 'two-factor' => 0, + 'two-factor-via-u2f-device' => 0, + "two-factor-via-webauthn-device" => 0 + } ) expect(described_class.usage_activity_by_stage_manage(described_class.monthly_time_range_db_params)).to include( events: be_within(error_rate).percent_of(2), groups: 1, users_created: 6, omniauth_providers: ['google_oauth2'], - user_auth_by_provider: { 'group_saml' => 1, 'ldap' => 2, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 } + user_auth_by_provider: { + 'group_saml' => 1, + 'ldap' => 2, + 'standard' => 0, + 'two-factor' => 0, + 'two-factor-via-u2f-device' => 0, + "two-factor-via-webauthn-device" => 0 + } ) end -- GitLab From 94bda0b8773da17fc2cd0a15bd645ad0776fa129 Mon Sep 17 00:00:00 2001 From: sdejonge <sdejonge@gitlab.com> Date: Tue, 6 Sep 2022 14:43:28 +1000 Subject: [PATCH 092/169] Move profiles/preferences.scss to page bundles --- app/assets/stylesheets/_page_specific_files.scss | 1 - .../{pages => page_bundles}/profiles/preferences.scss | 2 ++ app/views/profiles/preferences/show.html.haml | 1 + config/application.rb | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) rename app/assets/stylesheets/{pages => page_bundles}/profiles/preferences.scss (95%) diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss index 004dc22c9b8183..d0b3f5bda8e93c 100644 --- a/app/assets/stylesheets/_page_specific_files.scss +++ b/app/assets/stylesheets/_page_specific_files.scss @@ -21,7 +21,6 @@ @import './pages/notifications'; @import './pages/pipelines'; @import './pages/profile'; -@import './pages/profiles/preferences'; @import './pages/projects'; @import './pages/prometheus'; @import './pages/registry'; diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/page_bundles/profiles/preferences.scss similarity index 95% rename from app/assets/stylesheets/pages/profiles/preferences.scss rename to app/assets/stylesheets/page_bundles/profiles/preferences.scss index c7d7aacceec534..c9c78a7016364f 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/page_bundles/profiles/preferences.scss @@ -1,3 +1,5 @@ +@import 'page_bundles/mixins_and_variables_and_functions'; + .application-theme { $ui-gray-bg: #303030; $ui-light-gray-bg: #f0f0f0; diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index f8737a4e54a2d1..5f306c6eb487e5 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -1,4 +1,5 @@ - page_title _('Preferences') +- add_page_specific_style 'page_bundles/profiles/preferences' - @content_class = "limit-container-width" unless fluid_layout - user_theme_id = Gitlab::Themes.for_user(@user).id - user_color_schema_id = Gitlab::ColorSchemes.for_user(@user).id diff --git a/config/application.rb b/config/application.rb index d28967f29663ad..05a07983e0a78c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -292,6 +292,7 @@ class Application < Rails::Application config.assets.precompile << "page_bundles/productivity_analytics.css" config.assets.precompile << "page_bundles/profile.css" config.assets.precompile << "page_bundles/profile_two_factor_auth.css" + config.assets.precompile << "page_bundles/profiles/preferences.css" config.assets.precompile << "page_bundles/project.css" config.assets.precompile << "page_bundles/projects_edit.css" config.assets.precompile << "page_bundles/reports.css" -- GitLab From 3f16a3290d1c4200427dab5eacd65c3e1a94c21a Mon Sep 17 00:00:00 2001 From: Sri <srirangan@gmail.com> Date: Wed, 31 Aug 2022 17:29:41 +0200 Subject: [PATCH 093/169] Refactor Cloud Seed Snowplow Events * Use event definitions * By default, track as little as possible * No extra context required * Cover untracked controller methods Fixes: https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/99 --- .../projects/google_cloud/base_controller.rb | 28 +++----- .../google_cloud/configuration_controller.rb | 2 +- .../google_cloud/databases_controller.rb | 9 +-- .../google_cloud/deployments_controller.rb | 12 ++-- .../google_cloud/gcp_regions_controller.rb | 6 +- .../google_cloud/revoke_oauth_controller.rb | 4 +- .../service_accounts_controller.rb | 14 ++-- ...igurationController_error_invalid_user.yml | 26 +++++++ ...roller_error_google_oauth2_not_enabled.yml | 26 +++++++ ...troller_error_feature_flag_not_enabled.yml | 26 +++++++ ...d__ConfigurationController_render_page.yml | 26 +++++++ ...eAccountsController_error_invalid_user.yml | 26 +++++++ ...roller_error_google_oauth2_not_enabled.yml | 26 +++++++ ...troller_error_feature_flag_not_enabled.yml | 26 +++++++ ..._ServiceAccountsController_render_form.yml | 26 +++++++ ...countsController_error_no_gcp_projects.yml | 26 +++++++ ...ountsController_create_service_account.yml | 26 +++++++ ...iceAccountsController_error_google_api.yml | 26 +++++++ ...cpRegionsController_error_invalid_user.yml | 26 +++++++ ...roller_error_google_oauth2_not_enabled.yml | 26 +++++++ ...troller_error_feature_flag_not_enabled.yml | 26 +++++++ ...loud__GcpRegionsController_render_form.yml | 26 +++++++ ..._GcpRegionsController_configure_region.yml | 26 +++++++ ...oud__GcpRegionsController_error_create.yml | 26 +++++++ ...vokeOauthController_error_invalid_user.yml | 26 +++++++ ...roller_error_google_oauth2_not_enabled.yml | 26 +++++++ ...troller_error_feature_flag_not_enabled.yml | 26 +++++++ ...ud__RevokeOauthController_revoke_oauth.yml | 26 +++++++ ...ploymentsController_error_invalid_user.yml | 26 +++++++ ...roller_error_google_oauth2_not_enabled.yml | 26 +++++++ ...troller_error_feature_flag_not_enabled.yml | 26 +++++++ ...oud__DeploymentsController_render_page.yml | 26 +++++++ ...sController_generate_cloudrun_pipeline.yml | 26 +++++++ ...troller_error_enable_cloudrun_services.yml | 26 +++++++ ...oller_error_generate_cloudrun_pipeline.yml | 26 +++++++ ...DeploymentsController_error_google_api.yml | 26 +++++++ ...DatabasesController_error_invalid_user.yml | 26 +++++++ ...roller_error_google_oauth2_not_enabled.yml | 26 +++++++ ...troller_error_feature_flag_not_enabled.yml | 26 +++++++ ...Cloud__DatabasesController_render_page.yml | 26 +++++++ ...tabasesController_render_cloudsql_form.yml | 26 +++++++ ...sesController_create_cloudsql_instance.yml | 26 +++++++ ...troller_error_enable_cloudsql_services.yml | 26 +++++++ ...troller_error_create_cloudsql_instance.yml | 26 +++++++ .../configuration_controller_spec.rb | 39 ++++------ .../google_cloud/databases_controller_spec.rb | 21 +++--- .../deployments_controller_spec.rb | 72 +++++++++++-------- .../gcp_regions_controller_spec.rb | 28 ++++---- .../revoke_oauth_controller_spec.rb | 14 ++-- .../service_accounts_controller_spec.rb | 42 +++++------ 50 files changed, 1096 insertions(+), 157 deletions(-) create mode 100644 config/events/1662373051_Projects__GoogleCloud__ConfigurationController_error_invalid_user.yml create mode 100644 config/events/1662373057_Projects__GoogleCloud__ConfigurationController_error_google_oauth2_not_enabled.yml create mode 100644 config/events/1662373062_Projects__GoogleCloud__ConfigurationController_error_feature_flag_not_enabled.yml create mode 100644 config/events/1662373069_Projects__GoogleCloud__ConfigurationController_render_page.yml create mode 100644 config/events/1662373075_Projects__GoogleCloud__ServiceAccountsController_error_invalid_user.yml create mode 100644 config/events/1662373081_Projects__GoogleCloud__ServiceAccountsController_error_google_oauth2_not_enabled.yml create mode 100644 config/events/1662373087_Projects__GoogleCloud__ServiceAccountsController_error_feature_flag_not_enabled.yml create mode 100644 config/events/1662373092_Projects__GoogleCloud__ServiceAccountsController_render_form.yml create mode 100644 config/events/1662373098_Projects__GoogleCloud__ServiceAccountsController_error_no_gcp_projects.yml create mode 100644 config/events/1662373103_Projects__GoogleCloud__ServiceAccountsController_create_service_account.yml create mode 100644 config/events/1662373109_Projects__GoogleCloud__ServiceAccountsController_error_google_api.yml create mode 100644 config/events/1662373114_Projects__GoogleCloud__GcpRegionsController_error_invalid_user.yml create mode 100644 config/events/1662373120_Projects__GoogleCloud__GcpRegionsController_error_google_oauth2_not_enabled.yml create mode 100644 config/events/1662373125_Projects__GoogleCloud__GcpRegionsController_error_feature_flag_not_enabled.yml create mode 100644 config/events/1662373131_Projects__GoogleCloud__GcpRegionsController_render_form.yml create mode 100644 config/events/1662373136_Projects__GoogleCloud__GcpRegionsController_configure_region.yml create mode 100644 config/events/1662373142_Projects__GoogleCloud__GcpRegionsController_error_create.yml create mode 100644 config/events/1662373147_Projects__GoogleCloud__RevokeOauthController_error_invalid_user.yml create mode 100644 config/events/1662373153_Projects__GoogleCloud__RevokeOauthController_error_google_oauth2_not_enabled.yml create mode 100644 config/events/1662373158_Projects__GoogleCloud__RevokeOauthController_error_feature_flag_not_enabled.yml create mode 100644 config/events/1662373164_Projects__GoogleCloud__RevokeOauthController_revoke_oauth.yml create mode 100644 config/events/1662373170_Projects__GoogleCloud__DeploymentsController_error_invalid_user.yml create mode 100644 config/events/1662373175_Projects__GoogleCloud__DeploymentsController_error_google_oauth2_not_enabled.yml create mode 100644 config/events/1662373181_Projects__GoogleCloud__DeploymentsController_error_feature_flag_not_enabled.yml create mode 100644 config/events/1662373187_Projects__GoogleCloud__DeploymentsController_render_page.yml create mode 100644 config/events/1662373192_Projects__GoogleCloud__DeploymentsController_generate_cloudrun_pipeline.yml create mode 100644 config/events/1662373198_Projects__GoogleCloud__DeploymentsController_error_enable_cloudrun_services.yml create mode 100644 config/events/1662373204_Projects__GoogleCloud__DeploymentsController_error_generate_cloudrun_pipeline.yml create mode 100644 config/events/1662373209_Projects__GoogleCloud__DeploymentsController_error_google_api.yml create mode 100644 config/events/1662373215_Projects__GoogleCloud__DatabasesController_error_invalid_user.yml create mode 100644 config/events/1662373220_Projects__GoogleCloud__DatabasesController_error_google_oauth2_not_enabled.yml create mode 100644 config/events/1662373226_Projects__GoogleCloud__DatabasesController_error_feature_flag_not_enabled.yml create mode 100644 config/events/1662373232_Projects__GoogleCloud__DatabasesController_render_page.yml create mode 100644 config/events/1662373237_Projects__GoogleCloud__DatabasesController_render_cloudsql_form.yml create mode 100644 config/events/1662373243_Projects__GoogleCloud__DatabasesController_create_cloudsql_instance.yml create mode 100644 config/events/1662373249_Projects__GoogleCloud__DatabasesController_error_enable_cloudsql_services.yml create mode 100644 config/events/1662373254_Projects__GoogleCloud__DatabasesController_error_create_cloudsql_instance.yml diff --git a/app/controllers/projects/google_cloud/base_controller.rb b/app/controllers/projects/google_cloud/base_controller.rb index d1eb86c5e4963d..dfb73821b0f7bb 100644 --- a/app/controllers/projects/google_cloud/base_controller.rb +++ b/app/controllers/projects/google_cloud/base_controller.rb @@ -12,7 +12,7 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController def admin_project_google_cloud! unless can?(current_user, :admin_project_google_cloud, project) - track_event('admin_project_google_cloud!', 'error_access_denied', 'invalid_user') + track_event(:error_invalid_user) access_denied! end end @@ -20,11 +20,7 @@ def admin_project_google_cloud! def google_oauth2_enabled! config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2') if config.app_id.blank? || config.app_secret.blank? - track_event( - 'google_oauth2_enabled!', - 'error_access_denied', - { reason: 'google_oauth2_not_configured', config: config } - ) + track_event(:error_google_oauth2_not_enabled) access_denied! 'This GitLab instance not configured for Google Oauth2.' end end @@ -35,7 +31,7 @@ def feature_flag_enabled! enabled_for_project = Feature.enabled?(:incubation_5mp_google_cloud, project) feature_is_enabled = enabled_for_user || enabled_for_group || enabled_for_project unless feature_is_enabled - track_event('feature_flag_enabled!', 'error_access_denied', 'feature_flag_not_enabled') + track_event(:error_feature_flag_not_enabled) access_denied! end end @@ -69,16 +65,14 @@ def expires_at_in_session session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] end - def track_event(action, label, property) - options = { label: label, project: project, user: current_user } - - if property.is_a?(String) - options[:property] = property - else - options[:extra] = property - end - - Gitlab::Tracking.event('Projects::GoogleCloud', action, **options) + def track_event(action, label = nil) + Gitlab::Tracking.event( + self.class.name, + action.to_s, + label: label, + project: project, + user: current_user + ) end def gcp_projects diff --git a/app/controllers/projects/google_cloud/configuration_controller.rb b/app/controllers/projects/google_cloud/configuration_controller.rb index 8d252c35031867..06a6674d5783ad 100644 --- a/app/controllers/projects/google_cloud/configuration_controller.rb +++ b/app/controllers/projects/google_cloud/configuration_controller.rb @@ -16,7 +16,7 @@ def index revokeOauthUrl: revoke_oauth_url } @js_data = js_data.to_json - track_event('configuration#index', 'success', js_data) + track_event(:render_page) end private diff --git a/app/controllers/projects/google_cloud/databases_controller.rb b/app/controllers/projects/google_cloud/databases_controller.rb index fb442f87666619..8f7554f248b705 100644 --- a/app/controllers/projects/google_cloud/databases_controller.rb +++ b/app/controllers/projects/google_cloud/databases_controller.rb @@ -19,7 +19,7 @@ def index } @js_data = js_data.to_json - track_event('databases#index', 'success', nil) + track_event(:render_page) end def new @@ -37,6 +37,7 @@ def new tiers: Projects::GoogleCloud::CloudsqlHelper::TIERS }.to_json + track_event(:render_form) render template: 'projects/google_cloud/databases/cloudsql_form', formats: :html end @@ -46,7 +47,7 @@ def create .execute if enable_response[:status] == :error - track_event('databases#cloudsql_create', 'error_enable_cloudsql_service', enable_response) + track_event(:error_enable_cloudsql_services) flash[:error] = error_message(enable_response[:message]) else permitted_params = params.permit(:gcp_project, :ref, :database_version, :tier) @@ -55,10 +56,10 @@ def create .execute if create_response[:status] == :error - track_event('databases#cloudsql_create', 'error_create_cloudsql_instance', create_response) + track_event(:error_create_cloudsql_instance) flash[:warning] = error_message(create_response[:message]) else - track_event('databases#cloudsql_create', 'success', nil) + track_event(:create_cloudsql_instance, permitted_params.to_s) flash[:notice] = success_message end end diff --git a/app/controllers/projects/google_cloud/deployments_controller.rb b/app/controllers/projects/google_cloud/deployments_controller.rb index 1ac4697a63f692..f6cc8d5eafb204 100644 --- a/app/controllers/projects/google_cloud/deployments_controller.rb +++ b/app/controllers/projects/google_cloud/deployments_controller.rb @@ -12,7 +12,7 @@ def index enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project) } @js_data = js_data.to_json - track_event('deployments#index', 'success', js_data) + track_event(:render_page) end def cloud_run @@ -21,7 +21,7 @@ def cloud_run .new(project, current_user, params).execute if enable_cloud_run_response[:status] == :error - track_event('deployments#cloud_run', 'error_enable_cloud_run', enable_cloud_run_response) + track_event(:error_enable_services) flash[:error] = enable_cloud_run_response[:message] redirect_to project_google_cloud_deployments_path(project) else @@ -30,17 +30,17 @@ def cloud_run .new(project, current_user, params).execute if generate_pipeline_response[:status] == :error - track_event('deployments#cloud_run', 'error_generate_pipeline', generate_pipeline_response) + track_event(:error_generate_cloudrun_pipeline) flash[:error] = 'Failed to generate pipeline' redirect_to project_google_cloud_deployments_path(project) else cloud_run_mr_params = cloud_run_mr_params(generate_pipeline_response[:branch_name]) - track_event('deployments#cloud_run', 'success', cloud_run_mr_params) + track_event(:generate_cloudrun_pipeline) redirect_to project_new_merge_request_path(project, merge_request: cloud_run_mr_params) end end - rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => e - track_event('deployments#cloud_run', 'error_gcp', e) + rescue Google::Apis::Error => e + track_event(:error_google_api) flash[:warning] = _('Google Cloud Error - %{error}') % { error: e } redirect_to project_google_cloud_deployments_path(project) end diff --git a/app/controllers/projects/google_cloud/gcp_regions_controller.rb b/app/controllers/projects/google_cloud/gcp_regions_controller.rb index 39f336248045c3..2f0bc05030f71d 100644 --- a/app/controllers/projects/google_cloud/gcp_regions_controller.rb +++ b/app/controllers/projects/google_cloud/gcp_regions_controller.rb @@ -15,13 +15,13 @@ def index cancelPath: project_google_cloud_configuration_path(project) } @js_data = js_data.to_json - track_event('gcp_regions#index', 'success', js_data) + track_event(:render_form) end def create permitted_params = params.permit(:ref, :gcp_region) - response = GoogleCloud::GcpRegionAddOrReplaceService.new(project).execute(permitted_params[:ref], permitted_params[:gcp_region]) - track_event('gcp_regions#create', 'success', response) + GoogleCloud::GcpRegionAddOrReplaceService.new(project).execute(permitted_params[:ref], permitted_params[:gcp_region]) + track_event(:configure_region) redirect_to project_google_cloud_configuration_path(project), notice: _('GCP region configured') end end diff --git a/app/controllers/projects/google_cloud/revoke_oauth_controller.rb b/app/controllers/projects/google_cloud/revoke_oauth_controller.rb index 1a9a2daf4f2652..dbf9180672211e 100644 --- a/app/controllers/projects/google_cloud/revoke_oauth_controller.rb +++ b/app/controllers/projects/google_cloud/revoke_oauth_controller.rb @@ -9,10 +9,10 @@ def create if response.success? redirect_message = { notice: s_('GoogleCloud|Google OAuth2 token revocation requested') } - track_event('revoke_oauth#create', 'success', response.to_json) + track_event(:revoke_oauth) else redirect_message = { alert: s_('GoogleCloud|Google OAuth2 token revocation request failed') } - track_event('revoke_oauth#create', 'error', response.to_json) + track_event(:error) end session.delete(GoogleApi::CloudPlatform::Client.session_key_for_token) diff --git a/app/controllers/projects/google_cloud/service_accounts_controller.rb b/app/controllers/projects/google_cloud/service_accounts_controller.rb index 7f25054177e585..89d624764df4f7 100644 --- a/app/controllers/projects/google_cloud/service_accounts_controller.rb +++ b/app/controllers/projects/google_cloud/service_accounts_controller.rb @@ -5,7 +5,7 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud:: def index if gcp_projects.empty? - track_event('service_accounts#index', 'error_form', 'no_gcp_projects') + track_event(:error_no_gcp_projects) flash[:warning] = _('No Google Cloud projects - You need at least one Google Cloud project') redirect_to project_google_cloud_configuration_path(project) else @@ -16,10 +16,10 @@ def index } @js_data = js_data.to_json - track_event('service_accounts#index', 'success', js_data) + track_event(:render_form) end - rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => e - track_event('service_accounts#index', 'error_gcp', e) + rescue Google::Apis::Error => e + track_event(:error_google_api) flash[:warning] = _('Google Cloud Error - %{error}') % { error: e } redirect_to project_google_cloud_configuration_path(project) end @@ -35,10 +35,10 @@ def create environment_name: permitted_params[:ref] ).execute - track_event('service_accounts#create', 'success', response) + track_event(:create_service_account) redirect_to project_google_cloud_configuration_path(project), notice: response.message - rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => e - track_event('service_accounts#create', 'error_gcp', e) + rescue Google::Apis::Error => e + track_event(:error_google_api) flash[:warning] = _('Google Cloud Error - %{error}') % { error: e } redirect_to project_google_cloud_configuration_path(project) end diff --git a/config/events/1662373051_Projects__GoogleCloud__ConfigurationController_error_invalid_user.yml b/config/events/1662373051_Projects__GoogleCloud__ConfigurationController_error_invalid_user.yml new file mode 100644 index 00000000000000..5a71e2df485440 --- /dev/null +++ b/config/events/1662373051_Projects__GoogleCloud__ConfigurationController_error_invalid_user.yml @@ -0,0 +1,26 @@ +--- +description: Invalid or unauthorized user +category: Projects::GoogleCloud::ConfigurationController +action: error_invalid_user +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373057_Projects__GoogleCloud__ConfigurationController_error_google_oauth2_not_enabled.yml b/config/events/1662373057_Projects__GoogleCloud__ConfigurationController_error_google_oauth2_not_enabled.yml new file mode 100644 index 00000000000000..483225e0def98c --- /dev/null +++ b/config/events/1662373057_Projects__GoogleCloud__ConfigurationController_error_google_oauth2_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Google OAuth2 not enabled on GitLab instance +category: Projects::GoogleCloud::ConfigurationController +action: error_google_oauth2_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373062_Projects__GoogleCloud__ConfigurationController_error_feature_flag_not_enabled.yml b/config/events/1662373062_Projects__GoogleCloud__ConfigurationController_error_feature_flag_not_enabled.yml new file mode 100644 index 00000000000000..b24a326ab3018b --- /dev/null +++ b/config/events/1662373062_Projects__GoogleCloud__ConfigurationController_error_feature_flag_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Feature flag not enabled on the GitLab instance +category: Projects::GoogleCloud::ConfigurationController +action: error_feature_flag_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373069_Projects__GoogleCloud__ConfigurationController_render_page.yml b/config/events/1662373069_Projects__GoogleCloud__ConfigurationController_render_page.yml new file mode 100644 index 00000000000000..21083a7596bd05 --- /dev/null +++ b/config/events/1662373069_Projects__GoogleCloud__ConfigurationController_render_page.yml @@ -0,0 +1,26 @@ +--- +description: Configuration page rendered +category: Projects::GoogleCloud::ConfigurationController +action: render_page +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373075_Projects__GoogleCloud__ServiceAccountsController_error_invalid_user.yml b/config/events/1662373075_Projects__GoogleCloud__ServiceAccountsController_error_invalid_user.yml new file mode 100644 index 00000000000000..850b8e81c0bed5 --- /dev/null +++ b/config/events/1662373075_Projects__GoogleCloud__ServiceAccountsController_error_invalid_user.yml @@ -0,0 +1,26 @@ +--- +description: Invalid or unauthorized user +category: Projects::GoogleCloud::ServiceAccountsController +action: error_invalid_user +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373081_Projects__GoogleCloud__ServiceAccountsController_error_google_oauth2_not_enabled.yml b/config/events/1662373081_Projects__GoogleCloud__ServiceAccountsController_error_google_oauth2_not_enabled.yml new file mode 100644 index 00000000000000..726ba6af7aab89 --- /dev/null +++ b/config/events/1662373081_Projects__GoogleCloud__ServiceAccountsController_error_google_oauth2_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Google OAuth2 not enabled on GitLab instance +category: Projects::GoogleCloud::ServiceAccountsController +action: error_google_oauth2_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373087_Projects__GoogleCloud__ServiceAccountsController_error_feature_flag_not_enabled.yml b/config/events/1662373087_Projects__GoogleCloud__ServiceAccountsController_error_feature_flag_not_enabled.yml new file mode 100644 index 00000000000000..713e1a355845b9 --- /dev/null +++ b/config/events/1662373087_Projects__GoogleCloud__ServiceAccountsController_error_feature_flag_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Feature flag not enabled on the GitLab instance +category: Projects::GoogleCloud::ServiceAccountsController +action: error_feature_flag_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373092_Projects__GoogleCloud__ServiceAccountsController_render_form.yml b/config/events/1662373092_Projects__GoogleCloud__ServiceAccountsController_render_form.yml new file mode 100644 index 00000000000000..55e0c87dd6cd2c --- /dev/null +++ b/config/events/1662373092_Projects__GoogleCloud__ServiceAccountsController_render_form.yml @@ -0,0 +1,26 @@ +--- +description: Service account form rendered +category: Projects::GoogleCloud::ServiceAccountsController +action: render_form +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373098_Projects__GoogleCloud__ServiceAccountsController_error_no_gcp_projects.yml b/config/events/1662373098_Projects__GoogleCloud__ServiceAccountsController_error_no_gcp_projects.yml new file mode 100644 index 00000000000000..a57df38aa6e02a --- /dev/null +++ b/config/events/1662373098_Projects__GoogleCloud__ServiceAccountsController_error_no_gcp_projects.yml @@ -0,0 +1,26 @@ +--- +description: No GCP projects found for user +category: Projects::GoogleCloud::ServiceAccountsController +action: error_no_gcp_projects +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373103_Projects__GoogleCloud__ServiceAccountsController_create_service_account.yml b/config/events/1662373103_Projects__GoogleCloud__ServiceAccountsController_create_service_account.yml new file mode 100644 index 00000000000000..e147eaea44cb03 --- /dev/null +++ b/config/events/1662373103_Projects__GoogleCloud__ServiceAccountsController_create_service_account.yml @@ -0,0 +1,26 @@ +--- +description: Service account created +category: Projects::GoogleCloud::ServiceAccountsController +action: create_service_account +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373109_Projects__GoogleCloud__ServiceAccountsController_error_google_api.yml b/config/events/1662373109_Projects__GoogleCloud__ServiceAccountsController_error_google_api.yml new file mode 100644 index 00000000000000..f5404c0b318cb6 --- /dev/null +++ b/config/events/1662373109_Projects__GoogleCloud__ServiceAccountsController_error_google_api.yml @@ -0,0 +1,26 @@ +--- +description: Google API error +category: Projects::GoogleCloud::ServiceAccountsController +action: error_google_api +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373114_Projects__GoogleCloud__GcpRegionsController_error_invalid_user.yml b/config/events/1662373114_Projects__GoogleCloud__GcpRegionsController_error_invalid_user.yml new file mode 100644 index 00000000000000..e190dc68e05a97 --- /dev/null +++ b/config/events/1662373114_Projects__GoogleCloud__GcpRegionsController_error_invalid_user.yml @@ -0,0 +1,26 @@ +--- +description: Invalid or unauthorized user +category: Projects::GoogleCloud::GcpRegionsController +action: error_invalid_user +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373120_Projects__GoogleCloud__GcpRegionsController_error_google_oauth2_not_enabled.yml b/config/events/1662373120_Projects__GoogleCloud__GcpRegionsController_error_google_oauth2_not_enabled.yml new file mode 100644 index 00000000000000..4ceb9567a31fce --- /dev/null +++ b/config/events/1662373120_Projects__GoogleCloud__GcpRegionsController_error_google_oauth2_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Google OAuth2 not enabled on GitLab instance +category: Projects::GoogleCloud::GcpRegionsController +action: error_google_oauth2_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373125_Projects__GoogleCloud__GcpRegionsController_error_feature_flag_not_enabled.yml b/config/events/1662373125_Projects__GoogleCloud__GcpRegionsController_error_feature_flag_not_enabled.yml new file mode 100644 index 00000000000000..c7b9c4ac2f6f2e --- /dev/null +++ b/config/events/1662373125_Projects__GoogleCloud__GcpRegionsController_error_feature_flag_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Feature flag not enabled on the GitLab instance +category: Projects::GoogleCloud::GcpRegionsController +action: error_feature_flag_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373131_Projects__GoogleCloud__GcpRegionsController_render_form.yml b/config/events/1662373131_Projects__GoogleCloud__GcpRegionsController_render_form.yml new file mode 100644 index 00000000000000..227e0117e847ef --- /dev/null +++ b/config/events/1662373131_Projects__GoogleCloud__GcpRegionsController_render_form.yml @@ -0,0 +1,26 @@ +--- +description: GCP regions configuration form rendered +category: Projects::GoogleCloud::GcpRegionsController +action: render_form +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373136_Projects__GoogleCloud__GcpRegionsController_configure_region.yml b/config/events/1662373136_Projects__GoogleCloud__GcpRegionsController_configure_region.yml new file mode 100644 index 00000000000000..f301c068188e7d --- /dev/null +++ b/config/events/1662373136_Projects__GoogleCloud__GcpRegionsController_configure_region.yml @@ -0,0 +1,26 @@ +--- +description: GCP region configured +category: Projects::GoogleCloud::GcpRegionsController +action: configure_region +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373142_Projects__GoogleCloud__GcpRegionsController_error_create.yml b/config/events/1662373142_Projects__GoogleCloud__GcpRegionsController_error_create.yml new file mode 100644 index 00000000000000..67bbc1a7465e53 --- /dev/null +++ b/config/events/1662373142_Projects__GoogleCloud__GcpRegionsController_error_create.yml @@ -0,0 +1,26 @@ +--- +description: Failed to configure GCP region +category: Projects::GoogleCloud::GcpRegionsController +action: error_create +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373147_Projects__GoogleCloud__RevokeOauthController_error_invalid_user.yml b/config/events/1662373147_Projects__GoogleCloud__RevokeOauthController_error_invalid_user.yml new file mode 100644 index 00000000000000..a316efda189f5b --- /dev/null +++ b/config/events/1662373147_Projects__GoogleCloud__RevokeOauthController_error_invalid_user.yml @@ -0,0 +1,26 @@ +--- +description: Invalid or unauthorized user +category: Projects::GoogleCloud::RevokeOauthController +action: error_invalid_user +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373153_Projects__GoogleCloud__RevokeOauthController_error_google_oauth2_not_enabled.yml b/config/events/1662373153_Projects__GoogleCloud__RevokeOauthController_error_google_oauth2_not_enabled.yml new file mode 100644 index 00000000000000..fc2bf9a5bcdccc --- /dev/null +++ b/config/events/1662373153_Projects__GoogleCloud__RevokeOauthController_error_google_oauth2_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Google OAuth2 not enabled on GitLab instance +category: Projects::GoogleCloud::RevokeOauthController +action: error_google_oauth2_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373158_Projects__GoogleCloud__RevokeOauthController_error_feature_flag_not_enabled.yml b/config/events/1662373158_Projects__GoogleCloud__RevokeOauthController_error_feature_flag_not_enabled.yml new file mode 100644 index 00000000000000..33fdb94c3d8f19 --- /dev/null +++ b/config/events/1662373158_Projects__GoogleCloud__RevokeOauthController_error_feature_flag_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Feature flag not enabled on the GitLab instance +category: Projects::GoogleCloud::RevokeOauthController +action: error_feature_flag_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373164_Projects__GoogleCloud__RevokeOauthController_revoke_oauth.yml b/config/events/1662373164_Projects__GoogleCloud__RevokeOauthController_revoke_oauth.yml new file mode 100644 index 00000000000000..a621d57271ad18 --- /dev/null +++ b/config/events/1662373164_Projects__GoogleCloud__RevokeOauthController_revoke_oauth.yml @@ -0,0 +1,26 @@ +--- +description: OAuth token revoked +category: Projects::GoogleCloud::RevokeOauthController +action: revoke_oauth +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373170_Projects__GoogleCloud__DeploymentsController_error_invalid_user.yml b/config/events/1662373170_Projects__GoogleCloud__DeploymentsController_error_invalid_user.yml new file mode 100644 index 00000000000000..4543251dd08933 --- /dev/null +++ b/config/events/1662373170_Projects__GoogleCloud__DeploymentsController_error_invalid_user.yml @@ -0,0 +1,26 @@ +--- +description: Invalid or unauthorized user +category: Projects::GoogleCloud::DeploymentsController +action: error_invalid_user +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373175_Projects__GoogleCloud__DeploymentsController_error_google_oauth2_not_enabled.yml b/config/events/1662373175_Projects__GoogleCloud__DeploymentsController_error_google_oauth2_not_enabled.yml new file mode 100644 index 00000000000000..119db94c8283b5 --- /dev/null +++ b/config/events/1662373175_Projects__GoogleCloud__DeploymentsController_error_google_oauth2_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Google OAuth2 not enabled on GitLab instance +category: Projects::GoogleCloud::DeploymentsController +action: error_google_oauth2_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373181_Projects__GoogleCloud__DeploymentsController_error_feature_flag_not_enabled.yml b/config/events/1662373181_Projects__GoogleCloud__DeploymentsController_error_feature_flag_not_enabled.yml new file mode 100644 index 00000000000000..1d4ba496e821d0 --- /dev/null +++ b/config/events/1662373181_Projects__GoogleCloud__DeploymentsController_error_feature_flag_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Feature flag not enabled on the GitLab instance +category: Projects::GoogleCloud::DeploymentsController +action: error_feature_flag_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373187_Projects__GoogleCloud__DeploymentsController_render_page.yml b/config/events/1662373187_Projects__GoogleCloud__DeploymentsController_render_page.yml new file mode 100644 index 00000000000000..0335988d5c5a6d --- /dev/null +++ b/config/events/1662373187_Projects__GoogleCloud__DeploymentsController_render_page.yml @@ -0,0 +1,26 @@ +--- +description: Deployments page rendered +category: Projects::GoogleCloud::DeploymentsController +action: render_page +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373192_Projects__GoogleCloud__DeploymentsController_generate_cloudrun_pipeline.yml b/config/events/1662373192_Projects__GoogleCloud__DeploymentsController_generate_cloudrun_pipeline.yml new file mode 100644 index 00000000000000..8e3920015a25a5 --- /dev/null +++ b/config/events/1662373192_Projects__GoogleCloud__DeploymentsController_generate_cloudrun_pipeline.yml @@ -0,0 +1,26 @@ +--- +description: Cloud Run pipeline generated +category: Projects::GoogleCloud::DeploymentsController +action: generate_cloudrun_pipeline +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373198_Projects__GoogleCloud__DeploymentsController_error_enable_cloudrun_services.yml b/config/events/1662373198_Projects__GoogleCloud__DeploymentsController_error_enable_cloudrun_services.yml new file mode 100644 index 00000000000000..4a3fdd48a0dc1a --- /dev/null +++ b/config/events/1662373198_Projects__GoogleCloud__DeploymentsController_error_enable_cloudrun_services.yml @@ -0,0 +1,26 @@ +--- +description: Failed to enable Cloud Run services +category: Projects::GoogleCloud::DeploymentsController +action: error_enable_cloudrun_services +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373204_Projects__GoogleCloud__DeploymentsController_error_generate_cloudrun_pipeline.yml b/config/events/1662373204_Projects__GoogleCloud__DeploymentsController_error_generate_cloudrun_pipeline.yml new file mode 100644 index 00000000000000..ecf2ef4ae37112 --- /dev/null +++ b/config/events/1662373204_Projects__GoogleCloud__DeploymentsController_error_generate_cloudrun_pipeline.yml @@ -0,0 +1,26 @@ +--- +description: Failed to enable Cloud Run services +category: Projects::GoogleCloud::DeploymentsController +action: error_generate_cloudrun_pipeline +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373209_Projects__GoogleCloud__DeploymentsController_error_google_api.yml b/config/events/1662373209_Projects__GoogleCloud__DeploymentsController_error_google_api.yml new file mode 100644 index 00000000000000..81e7a881b5a529 --- /dev/null +++ b/config/events/1662373209_Projects__GoogleCloud__DeploymentsController_error_google_api.yml @@ -0,0 +1,26 @@ +--- +description: Google API error +category: Projects::GoogleCloud::DeploymentsController +action: error_google_api +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373215_Projects__GoogleCloud__DatabasesController_error_invalid_user.yml b/config/events/1662373215_Projects__GoogleCloud__DatabasesController_error_invalid_user.yml new file mode 100644 index 00000000000000..21734eb875fc6c --- /dev/null +++ b/config/events/1662373215_Projects__GoogleCloud__DatabasesController_error_invalid_user.yml @@ -0,0 +1,26 @@ +--- +description: Invalid or unauthorized user +category: Projects::GoogleCloud::DatabasesController +action: error_invalid_user +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373220_Projects__GoogleCloud__DatabasesController_error_google_oauth2_not_enabled.yml b/config/events/1662373220_Projects__GoogleCloud__DatabasesController_error_google_oauth2_not_enabled.yml new file mode 100644 index 00000000000000..b9a4e3f2c7d0c7 --- /dev/null +++ b/config/events/1662373220_Projects__GoogleCloud__DatabasesController_error_google_oauth2_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Google OAuth2 not enabled on GitLab instance +category: Projects::GoogleCloud::DatabasesController +action: error_google_oauth2_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373226_Projects__GoogleCloud__DatabasesController_error_feature_flag_not_enabled.yml b/config/events/1662373226_Projects__GoogleCloud__DatabasesController_error_feature_flag_not_enabled.yml new file mode 100644 index 00000000000000..04c03b87dd314f --- /dev/null +++ b/config/events/1662373226_Projects__GoogleCloud__DatabasesController_error_feature_flag_not_enabled.yml @@ -0,0 +1,26 @@ +--- +description: Feature flag not enabled on the GitLab instance +category: Projects::GoogleCloud::DatabasesController +action: error_feature_flag_not_enabled +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373232_Projects__GoogleCloud__DatabasesController_render_page.yml b/config/events/1662373232_Projects__GoogleCloud__DatabasesController_render_page.yml new file mode 100644 index 00000000000000..b5bf9853e44653 --- /dev/null +++ b/config/events/1662373232_Projects__GoogleCloud__DatabasesController_render_page.yml @@ -0,0 +1,26 @@ +--- +description: Databases page rendered +category: Projects::GoogleCloud::DatabasesController +action: render_page +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373237_Projects__GoogleCloud__DatabasesController_render_cloudsql_form.yml b/config/events/1662373237_Projects__GoogleCloud__DatabasesController_render_cloudsql_form.yml new file mode 100644 index 00000000000000..5fab18d965d5c1 --- /dev/null +++ b/config/events/1662373237_Projects__GoogleCloud__DatabasesController_render_cloudsql_form.yml @@ -0,0 +1,26 @@ +--- +description: Cloud SQL form rendered +category: Projects::GoogleCloud::DatabasesController +action: render_cloudsql_form +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373243_Projects__GoogleCloud__DatabasesController_create_cloudsql_instance.yml b/config/events/1662373243_Projects__GoogleCloud__DatabasesController_create_cloudsql_instance.yml new file mode 100644 index 00000000000000..3f5a2b5d8bafdb --- /dev/null +++ b/config/events/1662373243_Projects__GoogleCloud__DatabasesController_create_cloudsql_instance.yml @@ -0,0 +1,26 @@ +--- +description: Cloud SQL instance created +category: Projects::GoogleCloud::DatabasesController +action: create_cloudsql_instance +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373249_Projects__GoogleCloud__DatabasesController_error_enable_cloudsql_services.yml b/config/events/1662373249_Projects__GoogleCloud__DatabasesController_error_enable_cloudsql_services.yml new file mode 100644 index 00000000000000..a6fb46df4b80b3 --- /dev/null +++ b/config/events/1662373249_Projects__GoogleCloud__DatabasesController_error_enable_cloudsql_services.yml @@ -0,0 +1,26 @@ +--- +description: Error enabling Cloud SQL services +category: Projects::GoogleCloud::DatabasesController +action: error_enable_cloudsql_services +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/config/events/1662373254_Projects__GoogleCloud__DatabasesController_error_create_cloudsql_instance.yml b/config/events/1662373254_Projects__GoogleCloud__DatabasesController_error_create_cloudsql_instance.yml new file mode 100644 index 00000000000000..df9e28fabf57a3 --- /dev/null +++ b/config/events/1662373254_Projects__GoogleCloud__DatabasesController_error_create_cloudsql_instance.yml @@ -0,0 +1,26 @@ +--- +description: Error creating Cloud SQL instance +category: Projects::GoogleCloud::DatabasesController +action: error_create_cloudsql_instance +label_description: +property_description: +value_description: +extra_properties: +identifiers: +- project +- user +- namespace +product_section: google_cloud +product_stage: configure +product_group: group::incubation +product_category: cloud_seed +milestone: "15.4" +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96683" +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate + diff --git a/spec/requests/projects/google_cloud/configuration_controller_spec.rb b/spec/requests/projects/google_cloud/configuration_controller_spec.rb index cb7f0d76930ab0..41593b8d7a724e 100644 --- a/spec/requests/projects/google_cloud/configuration_controller_spec.rb +++ b/spec/requests/projects/google_cloud/configuration_controller_spec.rb @@ -26,10 +26,9 @@ get url expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::ConfigurationController', + action: 'error_invalid_user', + label: nil, project: project, user: unauthorized_member ) @@ -65,11 +64,9 @@ expect(response).to have_gitlab_http_status(:forbidden) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'google_oauth2_enabled!', - label: 'error_access_denied', - extra: { reason: 'google_oauth2_not_configured', - config: unconfigured_google_oauth2 }, + category: 'Projects::GoogleCloud::ConfigurationController', + action: 'error_google_oauth2_not_enabled', + label: nil, project: project, user: authorized_member ) @@ -90,10 +87,9 @@ expect(response).to have_gitlab_http_status(:not_found) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'feature_flag_enabled!', - label: 'error_access_denied', - property: 'feature_flag_not_enabled', + category: 'Projects::GoogleCloud::ConfigurationController', + action: 'error_feature_flag_not_enabled', + label: nil, project: project, user: authorized_member ) @@ -114,20 +110,9 @@ expect(response).to be_successful expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'configuration#index', - label: 'success', - extra: { - configurationUrl: project_google_cloud_configuration_path(project), - deploymentsUrl: project_google_cloud_deployments_path(project), - databasesUrl: project_google_cloud_databases_path(project), - serviceAccounts: [], - createServiceAccountUrl: project_google_cloud_service_accounts_path(project), - emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'), - configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project), - gcpRegions: [], - revokeOauthUrl: nil - }, + category: 'Projects::GoogleCloud::ConfigurationController', + action: 'render_page', + label: nil, project: project, user: authorized_member ) diff --git a/spec/requests/projects/google_cloud/databases_controller_spec.rb b/spec/requests/projects/google_cloud/databases_controller_spec.rb index cdd0555d52677a..4edef71f32622c 100644 --- a/spec/requests/projects/google_cloud/databases_controller_spec.rb +++ b/spec/requests/projects/google_cloud/databases_controller_spec.rb @@ -105,10 +105,9 @@ expect(response).to redirect_to(project_google_cloud_databases_path(project)) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'databases#cloudsql_create', - label: 'error_enable_cloudsql_service', - extra: { status: :error, message: 'error' }, + category: 'Projects::GoogleCloud::DatabasesController', + action: 'error_enable_cloudsql_services', + label: nil, project: project, user: user ) @@ -133,10 +132,9 @@ expect(response).to redirect_to(project_google_cloud_databases_path(project)) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'databases#cloudsql_create', - label: 'error_create_cloudsql_instance', - extra: { status: :error, message: 'error' }, + category: 'Projects::GoogleCloud::DatabasesController', + action: 'error_create_cloudsql_instance', + label: nil, project: project, user: user ) @@ -156,10 +154,9 @@ expect(response).to redirect_to(project_google_cloud_databases_path(project)) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'databases#cloudsql_create', - label: 'success', - extra: nil, + category: 'Projects::GoogleCloud::DatabasesController', + action: 'create_cloudsql_instance', + label: "{}", project: project, user: user ) diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb index 9e854e015165d6..ad6a3912e0b39c 100644 --- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb +++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb @@ -29,10 +29,9 @@ expect(response).to have_gitlab_http_status(:not_found) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'error_invalid_user', + label: nil, project: project, user: nil ) @@ -48,10 +47,9 @@ expect(response).to have_gitlab_http_status(:not_found) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'error_invalid_user', + label: nil, project: project, user: nil ) @@ -75,6 +73,30 @@ end end + describe 'Authorized GET project/-/google_cloud/deployments', :snowplow do + before do + sign_in(user_maintainer) + + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + allow(client).to receive(:validate_token).and_return(true) + end + end + + it 'renders template' do + get "#{project_google_cloud_deployments_path(project)}" + + expect(response).to render_template(:index) + + expect_snowplow_event( + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'render_page', + label: nil, + project: project, + user: user_maintainer + ) + end + end + describe 'Authorized GET project/-/google_cloud/deployments/cloud_run', :snowplow do let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" } @@ -92,11 +114,9 @@ expect(response).to redirect_to(project_google_cloud_deployments_path(project)) # since GPC_PROJECT_ID is not set, enable cloud run service should return an error expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'deployments#cloud_run', - label: 'error_enable_cloud_run', - extra: { message: 'No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable.', - status: :error }, + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'error_enable_services', + label: nil, project: project, user: user_maintainer ) @@ -113,10 +133,9 @@ expect(response).to redirect_to(project_google_cloud_deployments_path(project)) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'deployments#cloud_run', - label: 'error_gcp', - extra: mock_gcp_error, + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'error_google_api', + label: nil, project: project, user: user_maintainer ) @@ -136,10 +155,9 @@ expect(response).to redirect_to(project_google_cloud_deployments_path(project)) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'deployments#cloud_run', - label: 'error_generate_pipeline', - extra: { status: :error }, + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'error_generate_cloudrun_pipeline', + label: nil, project: project, user: user_maintainer ) @@ -159,15 +177,9 @@ expect(response).to have_gitlab_http_status(:found) expect(response.location).to include(project_new_merge_request_path(project)) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'deployments#cloud_run', - label: 'success', - extra: { "title": "Enable deployments to Cloud Run", - "description": "This merge request includes a Cloud Run deployment job in the pipeline definition (.gitlab-ci.yml).\n\nThe `deploy-to-cloud-run` job:\n* Requires the following environment variables\n * `GCP_PROJECT_ID`\n * `GCP_SERVICE_ACCOUNT_KEY`\n* Job definition can be found at: https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library\n\nThis pipeline definition has been committed to the branch ``.\nYou may modify the pipeline definition further or accept the changes as-is if suitable.\n", - "source_project_id": project.id, - "target_project_id": project.id, - "source_branch": nil, - "target_branch": project.default_branch }, + category: 'Projects::GoogleCloud::DeploymentsController', + action: 'generate_cloudrun_pipeline', + label: nil, project: project, user: user_maintainer ) diff --git a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb index f88273080d5663..e77bcdb40b84c6 100644 --- a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb +++ b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb @@ -13,10 +13,9 @@ it "tracks event" do is_expected.to be(404) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::GcpRegionsController', + action: 'error_invalid_user', + label: nil, project: project, user: nil ) @@ -27,10 +26,9 @@ it "tracks event" do is_expected.to be(404) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::GcpRegionsController', + action: 'error_invalid_user', + label: nil, project: project, user: nil ) @@ -41,10 +39,9 @@ it "tracks event" do is_expected.to be(404) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'feature_flag_enabled!', - label: 'error_access_denied', - property: 'feature_flag_not_enabled', + category: 'Projects::GoogleCloud::GcpRegionsController', + action: 'error_feature_flag_not_enabled', + label: nil, project: project, user: user_maintainer ) @@ -55,10 +52,9 @@ it "tracks event" do is_expected.to be(403) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'google_oauth2_enabled!', - label: 'error_access_denied', - extra: { reason: 'google_oauth2_not_configured', config: config }, + category: 'Projects::GoogleCloud::GcpRegionsController', + action: 'error_google_oauth2_not_enabled', + label: nil, project: project, user: user_maintainer ) diff --git a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb index 36441a184cbba5..9bd8468767d678 100644 --- a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb +++ b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb @@ -50,10 +50,9 @@ expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:notice]).to eq('Google OAuth2 token revocation requested') expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'revoke_oauth#create', - label: 'success', - property: '{}', + category: 'Projects::GoogleCloud::RevokeOauthController', + action: 'revoke_oauth', + label: nil, project: project, user: user ) @@ -73,10 +72,9 @@ expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:alert]).to eq('Google OAuth2 token revocation request failed') expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'revoke_oauth#create', - label: 'error', - property: '{}', + category: 'Projects::GoogleCloud::RevokeOauthController', + action: 'error', + label: nil, project: project, user: user ) diff --git a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb index ae2519855db937..133c6f9153d38e 100644 --- a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb +++ b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb @@ -30,10 +30,9 @@ expect(response).to have_gitlab_http_status(:not_found) expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_invalid_user', + label: nil, project: project, user: nil ) @@ -53,10 +52,9 @@ get url expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_invalid_user', + label: nil, project: project, user: unauthorized_member ) @@ -71,10 +69,9 @@ post url expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'admin_project_google_cloud!', - label: 'error_access_denied', - property: 'invalid_user', + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_invalid_user', + label: nil, project: project, user: unauthorized_member ) @@ -135,10 +132,9 @@ expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:warning]).to eq('No Google Cloud projects - You need at least one Google Cloud project') expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'service_accounts#index', - label: 'error_form', - property: 'no_gcp_projects', + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_no_gcp_projects', + label: nil, project: project, user: authorized_member ) @@ -207,11 +203,10 @@ expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:warning]).to eq('Google Cloud Error - client-error') expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'service_accounts#index', - label: 'error_gcp', - extra: google_client_error, + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_google_api', project: project, + label: nil, user: authorized_member ) end @@ -226,10 +221,9 @@ expect(response).to redirect_to(project_google_cloud_configuration_path(project)) expect(flash[:warning]).to eq('Google Cloud Error - client-error') expect_snowplow_event( - category: 'Projects::GoogleCloud', - action: 'service_accounts#create', - label: 'error_gcp', - extra: google_client_error, + category: 'Projects::GoogleCloud::ServiceAccountsController', + action: 'error_google_api', + label: nil, project: project, user: authorized_member ) -- GitLab From 1680c4971100caeeb086da55cc6c23a0ca7089e4 Mon Sep 17 00:00:00 2001 From: Zhiyuan Lu <1551755561@qq.com> Date: Tue, 6 Sep 2022 08:01:41 +0000 Subject: [PATCH 094/169] Add i18n support for message on todos page --- app/helpers/todos_helper.rb | 10 ++++++++++ app/views/dashboard/todos/index.html.haml | 2 +- config/initializers/1_settings.rb | 1 - config/no_todos_messages.yml | 11 ----------- locale/gitlab.pot | 15 +++++++++++++++ spec/helpers/todos_helper_spec.rb | 15 +++++++++++++++ 6 files changed, 41 insertions(+), 13 deletions(-) delete mode 100644 config/no_todos_messages.yml diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 5977f51cab1044..ecf29c411003b3 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -142,6 +142,16 @@ def todos_filter_empty? todos_filter_params.values.none? end + def no_todos_messages + [ + s_('Todos|Good job! Looks like you don\'t have anything left on your To-Do List'), + s_('Todos|Isn\'t an empty To-Do List beautiful?'), + s_('Todos|Give yourself a pat on the back!'), + s_('Todos|Nothing left to do. High five!'), + s_('Todos|Henceforth, you shall be known as "To-Do Destroyer"') + ] + end + def todos_filter_path(options = {}) without = options.delete(:without) diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 6bfe18fd3b2b89..deb1ac9e3604fe 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -93,7 +93,7 @@ .text-content.gl-text-center - if todos_filter_empty? %h4 - = Gitlab.config.gitlab.no_todos_messages.sample + = no_todos_messages.sample %p = (s_("Todos|Are you looking for things to do? Take a look at %{strongStart}%{openIssuesLinkStart}open issues%{openIssuesLinkEnd}%{strongEnd}, contribute to %{strongStart}%{mergeRequestLinkStart}a merge request%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}, or mention someone in a comment to automatically assign them a new to-do item.") % { strongStart: '<strong>', strongEnd: '</strong>', openIssuesLinkStart: "<a href=\"#{issues_dashboard_path}\">", openIssuesLinkEnd: '</a>', mergeRequestLinkStart: "<a href=\"#{merge_requests_dashboard_path}\">", mergeRequestLinkEnd: '</a>' }).html_safe - else diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b6f97c3ba3010f..83cae631a88f3c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -214,7 +214,6 @@ Settings.gitlab['trusted_proxies'] ||= [] Settings.gitlab['content_security_policy'] ||= {} Settings.gitlab['allowed_hosts'] ||= [] -Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml')) Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil? Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil? Settings.gitlab['max_request_duration_seconds'] ||= 57 diff --git a/config/no_todos_messages.yml b/config/no_todos_messages.yml deleted file mode 100644 index d2076f235fd15a..00000000000000 --- a/config/no_todos_messages.yml +++ /dev/null @@ -1,11 +0,0 @@ -# When the todo list on the user's dashboard becomes empty, a random message -# from the list below will be shown. -# -# If you come up with a fun one, please feel free to contribute it to GitLab! -# https://about.gitlab.com/contributing/ ---- -- Good job! Looks like you don't have anything left on your To-Do List -- Isn't an empty To-Do List beautiful? -- Give yourself a pat on the back! -- Nothing left to do. High five! -- Henceforth, you shall be known as "To-Do Destroyer" diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1192b788a0601f..2b6fda0d86665c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -41124,6 +41124,18 @@ msgstr "" msgid "Todos|Filter by project" msgstr "" +msgid "Todos|Give yourself a pat on the back!" +msgstr "" + +msgid "Todos|Good job! Looks like you don't have anything left on your To-Do List" +msgstr "" + +msgid "Todos|Henceforth, you shall be known as \"To-Do Destroyer\"" +msgstr "" + +msgid "Todos|Isn't an empty To-Do List beautiful?" +msgstr "" + msgid "Todos|It's how you always know what to work on next." msgstr "" @@ -41133,6 +41145,9 @@ msgstr "" msgid "Todos|Nothing is on your to-do list. Nice work!" msgstr "" +msgid "Todos|Nothing left to do. High five!" +msgstr "" + msgid "Todos|Undo mark all as done" msgstr "" diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb index bbabfedc3eef3b..a8945424877d63 100644 --- a/spec/helpers/todos_helper_spec.rb +++ b/spec/helpers/todos_helper_spec.rb @@ -258,6 +258,21 @@ end end + describe '#no_todos_messages' do + context 'when getting todos messsages' do + it 'return these sentences' do + expected_sentences = [ + s_('Todos|Good job! Looks like you don\'t have anything left on your To-Do List'), + s_('Todos|Isn\'t an empty To-Do List beautiful?'), + s_('Todos|Give yourself a pat on the back!'), + s_('Todos|Nothing left to do. High five!'), + s_('Todos|Henceforth, you shall be known as "To-Do Destroyer"') + ] + expect(helper.no_todos_messages).to eq(expected_sentences) + end + end + end + describe '#todo_author_display?' do using RSpec::Parameterized::TableSyntax -- GitLab From 4cc0cd10af4ac564cb24231508bab68f99282c8d Mon Sep 17 00:00:00 2001 From: Furkan Ayhan <furkanayhn@gmail.com> Date: Tue, 6 Sep 2022 08:15:26 +0000 Subject: [PATCH 095/169] Revert "Merge branch '33418-enhance-graphql-job-artifact' into 'master'" This reverts merge request !96422 --- .../graphql/queries/get_jobs.query.graphql | 1 - .../queries/get_pipeline_jobs.query.graphql | 1 - .../fragments/job_artifacts.fragment.graphql | 1 - ..._merge_request_download_paths.query.graphql | 1 - app/graphql/types/ci/job_artifact_type.rb | 9 --------- doc/api/graphql/reference/index.md | 9 --------- spec/frontend/pipelines/mock_data.js | 2 -- .../vue_shared/security_reports/mock_data.js | 18 ------------------ .../graphql/types/ci/job_artifact_type_spec.rb | 2 +- 9 files changed, 1 insertion(+), 43 deletions(-) diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql index 75e1daaafbf3a3..98b51e8c2c4065 100644 --- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql +++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql @@ -12,7 +12,6 @@ query getJobs($fullPath: ID!, $after: String, $first: Int = 30, $statuses: [CiJo nodes { artifacts { nodes { - id downloadPath fileType } diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql index 0ed8f596d3db8f..641ec7a3cf6a3c 100644 --- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql +++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql @@ -12,7 +12,6 @@ query getPipelineJobs($fullPath: ID!, $iid: ID!, $after: String) { nodes { artifacts { nodes { - id downloadPath fileType } diff --git a/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql index 981d01cc81abb7..829b9d9f9d8586 100644 --- a/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql +++ b/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql @@ -6,7 +6,6 @@ fragment JobArtifacts on Pipeline { name artifacts { nodes { - id downloadPath fileType } diff --git a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql index 9c5090cfc28246..2e80db30e9a409 100644 --- a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql +++ b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql @@ -15,7 +15,6 @@ query securityReportDownloadPaths( name artifacts { nodes { - id downloadPath fileType } diff --git a/app/graphql/types/ci/job_artifact_type.rb b/app/graphql/types/ci/job_artifact_type.rb index 6346d50de3a9c3..a6ab445702ced0 100644 --- a/app/graphql/types/ci/job_artifact_type.rb +++ b/app/graphql/types/ci/job_artifact_type.rb @@ -6,9 +6,6 @@ module Ci class JobArtifactType < BaseObject graphql_name 'CiJobArtifact' - field :id, Types::GlobalIDType[::Ci::JobArtifact], null: false, - description: 'ID of the artifact.' - field :download_path, GraphQL::Types::String, null: true, description: "URL for downloading the artifact's file." @@ -19,12 +16,6 @@ class JobArtifactType < BaseObject description: 'File name of the artifact.', method: :filename - field :size, GraphQL::Types::Int, null: false, - description: 'Size of the artifact in bytes.' - - field :expire_at, Types::TimeType, null: true, - description: 'Expiry date of the artifact.' - def download_path ::Gitlab::Routing.url_helpers.download_project_job_artifacts_path( object.project, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f1ed7c5fc04964..edf7661ecb7066 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -10267,11 +10267,8 @@ CI/CD variables for a GitLab instance. | Name | Type | Description | | ---- | ---- | ----------- | | <a id="cijobartifactdownloadpath"></a>`downloadPath` | [`String`](#string) | URL for downloading the artifact's file. | -| <a id="cijobartifactexpireat"></a>`expireAt` | [`Time`](#time) | Expiry date of the artifact. | | <a id="cijobartifactfiletype"></a>`fileType` | [`JobArtifactFileType`](#jobartifactfiletype) | File type of the artifact. | -| <a id="cijobartifactid"></a>`id` | [`CiJobArtifactID!`](#cijobartifactid) | ID of the artifact. | | <a id="cijobartifactname"></a>`name` | [`String`](#string) | File name of the artifact. | -| <a id="cijobartifactsize"></a>`size` | [`Int!`](#int) | Size of the artifact in bytes. | ### `CiJobTokenScopeType` @@ -21288,12 +21285,6 @@ A `CiBuildID` is a global ID. It is encoded as a string. An example `CiBuildID` is: `"gid://gitlab/Ci::Build/1"`. -### `CiJobArtifactID` - -A `CiJobArtifactID` is a global ID. It is encoded as a string. - -An example `CiJobArtifactID` is: `"gid://gitlab/Ci::JobArtifact/1"`. - ### `CiPipelineID` A `CiPipelineID` is a global ID. It is encoded as a string. diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js index 24514d99078c3e..57d1511d8593bd 100644 --- a/spec/frontend/pipelines/mock_data.js +++ b/spec/frontend/pipelines/mock_data.js @@ -528,7 +528,6 @@ export const mockPipelineJobsQueryResponse = { artifacts: { nodes: [ { - id: 'gid://gitlab/Ci::JobArtifact/101', downloadPath: '/root/ci-project/-/jobs/620/artifacts/download?file_type=trace', fileType: 'TRACE', __typename: 'CiJobArtifact', @@ -581,7 +580,6 @@ export const mockPipelineJobsQueryResponse = { artifacts: { nodes: [ { - id: 'gid://gitlab/Ci::JobArtifact/102', downloadPath: '/root/ci-project/-/jobs/619/artifacts/download?file_type=trace', fileType: 'TRACE', __typename: 'CiJobArtifact', diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js index a0e31243365eaa..a9ad675e5383b7 100644 --- a/spec/frontend/vue_shared/security_reports/mock_data.js +++ b/spec/frontend/vue_shared/security_reports/mock_data.js @@ -356,14 +356,12 @@ export const securityReportMergeRequestDownloadPathsQueryResponse = { artifacts: { nodes: [ { - id: 'gid://gitlab/Ci::JobArtifact/101', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=trace', fileType: 'TRACE', __typename: 'CiJobArtifact', }, { - id: 'gid://gitlab/Ci::JobArtifact/102', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection', fileType: 'SECRET_DETECTION', @@ -380,14 +378,12 @@ export const securityReportMergeRequestDownloadPathsQueryResponse = { artifacts: { nodes: [ { - id: 'gid://gitlab/Ci::JobArtifact/103', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=trace', fileType: 'TRACE', __typename: 'CiJobArtifact', }, { - id: 'gid://gitlab/Ci::JobArtifact/104', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast', fileType: 'SAST', @@ -404,14 +400,12 @@ export const securityReportMergeRequestDownloadPathsQueryResponse = { artifacts: { nodes: [ { - id: 'gid://gitlab/Ci::JobArtifact/105', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=trace', fileType: 'TRACE', __typename: 'CiJobArtifact', }, { - id: 'gid://gitlab/Ci::JobArtifact/106', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast', fileType: 'SAST', @@ -428,21 +422,18 @@ export const securityReportMergeRequestDownloadPathsQueryResponse = { artifacts: { nodes: [ { - id: 'gid://gitlab/Ci::JobArtifact/107', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=archive', fileType: 'ARCHIVE', __typename: 'CiJobArtifact', }, { - id: 'gid://gitlab/Ci::JobArtifact/108', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=trace', fileType: 'TRACE', __typename: 'CiJobArtifact', }, { - id: 'gid://gitlab/Ci::JobArtifact/109', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=metadata', fileType: 'METADATA', @@ -477,14 +468,12 @@ export const securityReportPipelineDownloadPathsQueryResponse = { artifacts: { nodes: [ { - id: 'gid://gitlab/Ci::JobArtifact/110', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=trace', fileType: 'TRACE', __typename: 'CiJobArtifact', }, { - id: 'gid://gitlab/Ci::JobArtifact/111', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection', fileType: 'SECRET_DETECTION', @@ -501,14 +490,12 @@ export const securityReportPipelineDownloadPathsQueryResponse = { artifacts: { nodes: [ { - id: 'gid://gitlab/Ci::JobArtifact/112', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=trace', fileType: 'TRACE', __typename: 'CiJobArtifact', }, { - id: 'gid://gitlab/Ci::JobArtifact/113', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast', fileType: 'SAST', @@ -525,14 +512,12 @@ export const securityReportPipelineDownloadPathsQueryResponse = { artifacts: { nodes: [ { - id: 'gid://gitlab/Ci::JobArtifact/114', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=trace', fileType: 'TRACE', __typename: 'CiJobArtifact', }, { - id: 'gid://gitlab/Ci::JobArtifact/115', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast', fileType: 'SAST', @@ -549,21 +534,18 @@ export const securityReportPipelineDownloadPathsQueryResponse = { artifacts: { nodes: [ { - id: 'gid://gitlab/Ci::JobArtifact/116', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=archive', fileType: 'ARCHIVE', __typename: 'CiJobArtifact', }, { - id: 'gid://gitlab/Ci::JobArtifact/117', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=trace', fileType: 'TRACE', __typename: 'CiJobArtifact', }, { - id: 'gid://gitlab/Ci::JobArtifact/118', downloadPath: '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=metadata', fileType: 'METADATA', diff --git a/spec/graphql/types/ci/job_artifact_type_spec.rb b/spec/graphql/types/ci/job_artifact_type_spec.rb index 3e054faf0c9b2b..58b5f9cfcb7d1f 100644 --- a/spec/graphql/types/ci/job_artifact_type_spec.rb +++ b/spec/graphql/types/ci/job_artifact_type_spec.rb @@ -4,7 +4,7 @@ RSpec.describe GitlabSchema.types['CiJobArtifact'] do it 'has the correct fields' do - expected_fields = [:id, :download_path, :file_type, :name, :size, :expire_at] + expected_fields = [:download_path, :file_type, :name] expect(described_class).to have_graphql_fields(*expected_fields) end -- GitLab From 4c6279d8296485d1e1c7e35825247a9e66e74bd7 Mon Sep 17 00:00:00 2001 From: Savas Vedova <svedova@gitlab.com> Date: Tue, 6 Sep 2022 08:30:17 +0000 Subject: [PATCH 096/169] Display content loading error inside content area --- .../components/widget/widget.vue | 41 ++++++++++++++----- .../widget/widget_content_section.vue | 35 ++++++++++++++++ .../widget/widget_content_section_spec.js | 39 ++++++++++++++++++ .../components/widget/widget_spec.js | 2 +- 4 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue create mode 100644 spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue index 5581863591c257..c9fc2dde0bdf99 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue @@ -7,6 +7,7 @@ import Poll from '~/lib/utils/poll'; import StatusIcon from '../extensions/status_icon.vue'; import ActionButtons from '../action_buttons.vue'; import { EXTENSION_ICONS } from '../../constants'; +import ContentSection from './widget_content_section.vue'; const FETCH_TYPE_COLLAPSED = 'collapsed'; const FETCH_TYPE_EXPANDED = 'expanded'; @@ -17,6 +18,7 @@ export default { StatusIcon, GlButton, GlLoadingIcon, + ContentSection, }, directives: { GlTooltip: GlTooltipDirective, @@ -92,15 +94,16 @@ export default { isCollapsed: true, isLoading: false, isLoadingExpandedContent: false, - error: null, + summaryError: null, + contentError: null, }; }, computed: { collapseButtonLabel() { return sprintf(this.isCollapsed ? __('Show details') : __('Hide details')); }, - statusIcon() { - return this.error ? EXTENSION_ICONS.failed : this.statusIconName; + summaryStatusIcon() { + return this.summaryError ? this.$options.failedStatusIcon : this.statusIconName; }, }, watch: { @@ -114,7 +117,7 @@ export default { try { await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED); } catch { - this.error = this.errorText; + this.summaryError = this.errorText; } this.isLoading = false; @@ -130,12 +133,12 @@ export default { }, async fetchExpandedContent() { this.isLoadingExpandedContent = true; - this.error = null; + this.contentError = null; try { await this.fetch(this.fetchExpandedData, FETCH_TYPE_EXPANDED); } catch { - this.error = this.errorText; + this.contentError = this.errorText; // Reset these values so that we allow refetching this.isExpandedForTheFirstTime = true; @@ -178,20 +181,26 @@ export default { }); }, }, + failedStatusIcon: EXTENSION_ICONS.failed, }; </script> <template> <section class="media-section" data-testid="widget-extension"> <div class="media gl-p-5"> - <status-icon :level="1" :name="widgetName" :is-loading="isLoading" :icon-name="statusIcon" /> + <status-icon + :level="1" + :name="widgetName" + :is-loading="isLoading" + :icon-name="summaryStatusIcon" + /> <div class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center" data-testid="widget-extension-top-level" > <div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary"> - <slot v-if="!error" name="summary">{{ isLoading ? loadingText : summary }}</slot> - <span v-else>{{ error }}</span> + <span v-if="summaryError">{{ summaryError }}</span> + <slot v-else name="summary">{{ isLoading ? loadingText : summary }}</slot> </div> <action-buttons v-if="actionButtons.length > 0" @@ -217,14 +226,24 @@ export default { </div> </div> <div - v-if="!isCollapsed" + v-if="!isCollapsed || contentError" class="mr-widget-grouped-section gl-relative" data-testid="widget-extension-collapsed-section" > <div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center"> <gl-loading-icon size="sm" inline /> {{ __('Loading...') }} </div> - <slot v-else name="content">{{ content }}</slot> + <content-section + v-else-if="contentError" + class="report-block-container" + :status-icon-name="$options.failedStatusIcon" + :widget-name="widgetName" + > + {{ contentError }} + </content-section> + <slot v-else name="content"> + {{ content }} + </slot> </div> </section> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue new file mode 100644 index 00000000000000..42fd02f978bdb1 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue @@ -0,0 +1,35 @@ +<script> +import { EXTENSION_ICONS } from '../../constants'; +import StatusIcon from '../extensions/status_icon.vue'; + +export default { + components: { + StatusIcon, + }, + props: { + statusIconName: { + type: String, + default: '', + required: false, + validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).indexOf(value) > -1, + }, + widgetName: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="gl-px-7"> + <div class="gl-pl-4 gl-display-flex"> + <status-icon + v-if="statusIconName" + :level="2" + :name="widgetName" + :icon-name="statusIconName" + /> + <slot name="default"></slot> + </div> + </div> +</template> diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js new file mode 100644 index 00000000000000..c2128d3ff33698 --- /dev/null +++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js @@ -0,0 +1,39 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WidgetContentSection from '~/vue_merge_request_widget/components/widget/widget_content_section.vue'; +import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue'; + +describe('~/vue_merge_request_widget/components/widget/widget_content_section.vue', () => { + let wrapper; + + const findStatusIcon = () => wrapper.findComponent(StatusIcon); + + const createComponent = ({ propsData, slots } = {}) => { + wrapper = shallowMountExtended(WidgetContentSection, { + propsData: { + widgetName: 'MyWidget', + ...propsData, + }, + slots, + }); + }; + + it('does not render the status icon when it is not provided', () => { + createComponent(); + expect(findStatusIcon().exists()).toBe(false); + }); + + it('renders the status icon when provided', () => { + createComponent({ propsData: { statusIconName: 'failed' } }); + expect(findStatusIcon().exists()).toBe(true); + }); + + it('renders the default slot', () => { + createComponent({ + slots: { + default: 'Hello world', + }, + }); + + expect(wrapper.findByText('Hello world').exists()).toBe(true); + }); +}); diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js index b1ed61faf66fbb..b67b5703ad5f21 100644 --- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js @@ -43,7 +43,7 @@ describe('MR Widget', () => { createComponent({ propsData: { fetchCollapsedData } }); await waitForPromises(); expect(fetchCollapsedData).toHaveBeenCalled(); - expect(wrapper.vm.error).toBe(null); + expect(wrapper.vm.summaryError).toBe(null); }); it('sets the error text when fetch method fails', async () => { -- GitLab From 5f04ba20aad7e50d1c6da1f473720993d30e3f02 Mon Sep 17 00:00:00 2001 From: Roman Pertl <roman@pertl.org> Date: Mon, 5 Sep 2022 16:13:09 +0200 Subject: [PATCH 097/169] Fix wrong year in deprecations/15-4-non-expiring-access-tokens --- data/deprecations/15-4-non-expiring-access-tokens.yml | 2 +- doc/update/deprecations.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/deprecations/15-4-non-expiring-access-tokens.yml b/data/deprecations/15-4-non-expiring-access-tokens.yml index 8363e2e88188fe..1f1cfd3e0c0710 100644 --- a/data/deprecations/15-4-non-expiring-access-tokens.yml +++ b/data/deprecations/15-4-non-expiring-access-tokens.yml @@ -2,7 +2,7 @@ announcement_milestone: "15.4" announcement_date: "2022-09-22" removal_milestone: "16.0" - removal_date: "2022-05-22" + removal_date: "2023-05-22" breaking_change: true reporter: hsutor body: | # Do not modify this line, instead modify the lines below. diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index 505b247cbd7d3e..ad565fbc3e7d33 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -51,7 +51,7 @@ sole discretion of GitLab Inc. ### Non-expiring access tokens -Planned removal: GitLab <span class="removal-milestone">16.0</span> (2022-05-22) +Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22) WARNING: This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/). -- GitLab From 1644cb05b7a018436fc5a32e9c04afce9ef4a3e1 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 18 Aug 2022 15:51:27 +0100 Subject: [PATCH 098/169] Moved merge request settings to seperate page Closes https://gitlab.com/gitlab-org/gitlab/-/issues/119451/ --- .../projects/settings/merge_requests/index.js | 10 + .../javascripts/persistent_user_callouts.js | 1 + .../settings/merge_requests_controller.rb | 67 +++++ app/helpers/users/callouts_helper.rb | 5 + app/models/users/callout.rb | 3 +- app/views/projects/edit.html.haml | 24 +- .../settings/merge_requests/show.html.haml | 18 ++ config/routes/project.rb | 1 + doc/api/graphql/reference/index.md | 1 + .../javascripts/pages/projects/edit/index.js | 7 - .../projects/settings/merge_requests/index.js | 9 + .../settings/merge_requests_controller.rb | 78 ++++++ ...merge_request_approvals_settings.html.haml | 4 +- .../merge_requests_controller_spec.rb | 205 ++++++++++++++ .../admin_merge_requests_approvals_spec.rb | 2 +- .../features/projects/audit_events_spec.rb | 2 +- .../disable_merge_trains_setting_spec.rb | 8 +- .../merge_request_approvals_settings_spec.rb | 6 +- .../disable_merge_trains_setting_spec.rb | 130 +++++++++ .../user_manages_approval_settings_spec.rb | 44 +++ .../user_manages_merge_pipelines_spec.rb | 53 ++++ ...er_manages_merge_requests_template_spec.rb | 23 ++ .../user_manages_merge_trains_spec.rb | 48 ++++ .../settings/merge_requests_settings_spec.rb | 14 +- .../user_manages_approval_settings_spec.rb | 2 +- .../user_manages_merge_pipelines_spec.rb | 4 +- ...er_manages_merge_requests_template_spec.rb | 2 +- .../user_manages_merge_trains_spec.rb | 4 +- ee/spec/features/promotion_spec.rb | 6 +- ee/spec/views/projects/edit.html.haml_spec.rb | 26 -- ..._merge_request_approvals.html.haml_spec.rb | 4 +- .../projects/menus/merge_requests_menu.rb | 4 +- lib/sidebars/projects/menus/settings_menu.rb | 12 + locale/gitlab.pot | 6 + qa/qa/ee/page/project/settings/main.rb | 8 +- .../ee/page/project/settings/merge_request.rb | 4 + .../settings/merge_request_approvals.rb | 14 +- qa/qa/flow/merge_request.rb | 3 +- qa/qa/page/project/settings/main.rb | 11 +- qa/qa/page/project/settings/merge_request.rb | 2 +- qa/qa/page/project/sub_menus/settings.rb | 8 + .../rebase_merge_request_spec.rb | 8 +- .../default_merge_request_template_spec.rb | 7 +- ...ssion_not_dropping_merge_trains_mr_spec.rb | 2 +- .../merge_with_code_owner_shared_examples.rb | 8 +- .../merge_requests_controller_spec.rb | 52 ++++ .../settings/merge_requests_settings_spec.rb | 261 ++++++++++++++++++ ...er_manages_merge_requests_settings_spec.rb | 41 +-- .../settings/visibility_settings_spec.rb | 20 +- spec/features/projects_spec.rb | 3 +- .../projects/menus/settings_menu_spec.rb | 6 + .../navbar_structure_context.rb | 1 + spec/views/projects/edit.html.haml_spec.rb | 56 ---- .../merge_requests/show.html.haml_spec.rb | 78 ++++++ 54 files changed, 1217 insertions(+), 209 deletions(-) create mode 100644 app/assets/javascripts/pages/projects/settings/merge_requests/index.js create mode 100644 app/controllers/projects/settings/merge_requests_controller.rb create mode 100644 app/views/projects/settings/merge_requests/show.html.haml create mode 100644 ee/app/assets/javascripts/pages/projects/settings/merge_requests/index.js create mode 100644 ee/app/controllers/ee/projects/settings/merge_requests_controller.rb rename ee/app/views/projects/{ => settings/merge_requests}/_merge_request_approvals_settings.html.haml (79%) create mode 100644 ee/spec/controllers/projects/settings/merge_requests_controller_spec.rb create mode 100644 ee/spec/features/projects/settings/merge_requests/disable_merge_trains_setting_spec.rb create mode 100644 ee/spec/features/projects/settings/merge_requests/user_manages_approval_settings_spec.rb create mode 100644 ee/spec/features/projects/settings/merge_requests/user_manages_merge_pipelines_spec.rb create mode 100644 ee/spec/features/projects/settings/merge_requests/user_manages_merge_requests_template_spec.rb create mode 100644 ee/spec/features/projects/settings/merge_requests/user_manages_merge_trains_spec.rb rename ee/spec/views/projects/{ => settings}/merge_requests/_merge_request_approvals.html.haml_spec.rb (77%) create mode 100644 spec/controllers/projects/settings/merge_requests_controller_spec.rb create mode 100644 spec/features/projects/settings/merge_requests_settings_spec.rb create mode 100644 spec/views/projects/settings/merge_requests/show.html.haml_spec.rb diff --git a/app/assets/javascripts/pages/projects/settings/merge_requests/index.js b/app/assets/javascripts/pages/projects/settings/merge_requests/index.js new file mode 100644 index 00000000000000..739e666644c37f --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/merge_requests/index.js @@ -0,0 +1,10 @@ +import groupsSelect from '~/groups_select'; +import UserCallout from '~/user_callout'; +import UsersSelect from '~/users_select'; + +// eslint-disable-next-line no-new +new UsersSelect(); +groupsSelect(); + +// eslint-disable-next-line no-new +new UserCallout({ className: 'js-mr-approval-callout' }); diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js index 04f0caa1ca3c89..a2b9a3bf8890ea 100644 --- a/app/assets/javascripts/persistent_user_callouts.js +++ b/app/assets/javascripts/persistent_user_callouts.js @@ -18,6 +18,7 @@ const PERSISTENT_USER_CALLOUTS = [ '.js-project-usage-limitations-callout', '.js-namespace-storage-alert', '.js-web-hook-disabled-callout', + '.js-merge-request-settings-callout', ]; const initCallouts = () => { diff --git a/app/controllers/projects/settings/merge_requests_controller.rb b/app/controllers/projects/settings/merge_requests_controller.rb new file mode 100644 index 00000000000000..93e10695767c69 --- /dev/null +++ b/app/controllers/projects/settings/merge_requests_controller.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Projects + module Settings + class MergeRequestsController < Projects::ApplicationController + layout 'project_settings' + + before_action :merge_requests_enabled? + before_action :present_project, only: [:edit] + before_action :authorize_admin_project! + + feature_category :code_review + + def update + result = ::Projects::UpdateService.new(@project, current_user, project_params).execute + + if result[:status] == :success + flash[:notice] = format(_("Project '%{project_name}' was successfully updated."), project_name: @project.name) + redirect_to project_settings_merge_requests_path(@project) + else + # Refresh the repo in case anything changed + @repository = @project.repository.reset + + flash[:alert] = result[:message] + @project.reset + render 'show' + end + end + + private + + def merge_requests_enabled? + render_404 unless @project.merge_requests_enabled? + end + + def project_params + params.require(:project) + .permit(project_params_attributes) + end + + def project_setting_attributes + %i[ + squash_option + allow_editing_commit_messages + mr_default_target_self + ] + end + + def project_params_attributes + [ + :allow_merge_on_skipped_pipeline, + :resolve_outdated_diff_discussions, + :only_allow_merge_if_all_discussions_are_resolved, + :only_allow_merge_if_pipeline_succeeds, + :printing_merge_request_link_enabled, + :remove_source_branch_after_merge, + :merge_method, + :merge_commit_template_or_default, + :squash_commit_template_or_default, + :suggestion_commit_message + ] + [project_setting_attributes: project_setting_attributes] + end + end + end +end + +Projects::Settings::MergeRequestsController.prepend_mod_with('Projects::Settings::MergeRequestsController') diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb index b08de4edb62271..a2c3aa649e6a85 100644 --- a/app/helpers/users/callouts_helper.rb +++ b/app/helpers/users/callouts_helper.rb @@ -10,6 +10,7 @@ module CalloutsHelper REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout' UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout' SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout' + MERGE_REQUEST_SETTINGS_MOVED_CALLOUT = 'merge_request_settings_moved_callout' REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze WEB_HOOK_DISABLED = 'web_hook_disabled' @@ -74,6 +75,10 @@ def web_hook_disabled_dismissed?(project) user_dismissed?(WEB_HOOK_DISABLED, last_failure, project: project) end + def show_merge_request_settings_callout? + !user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT) + end + private def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, project: nil) diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index c9b6b859d45826..03841ee48fabfb 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -60,7 +60,8 @@ class Callout < ApplicationRecord namespace_storage_limit_banner_warning_threshold: 56, # EE-only namespace_storage_limit_banner_alert_threshold: 57, # EE-only namespace_storage_limit_banner_error_threshold: 58, # EE-only - project_quality_summary_feedback: 59 # EE-only + project_quality_summary_feedback: 59, # EE-only + merge_request_settings_moved_callout: 60 } validates :feature_name, diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index a7dd69a9607ae8..fda17284f83332 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -26,23 +26,13 @@ %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe .js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) } -%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests') - = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do - = expanded ? _('Collapse') : _('Expand') - = render_if_exists 'projects/merge_request_settings_description_text' - - .settings-content - = render_if_exists 'shared/promotions/promote_mr_features' - - = gitlab_ui_form_for @project, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f| - %input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' } - = render 'projects/merge_request_settings', form: f - = f.submit _('Save changes'), class: "btn gl-button btn-confirm rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' } - -= render_if_exists 'projects/merge_request_approvals_settings', expanded: expanded - +- if show_merge_request_settings_callout? + %section.settings.expanded + = render Pajamas::AlertComponent.new(variant: :info, + title: _('Merge requests and approvals settings have moved.'), + alert_options: { class: 'js-merge-request-settings-callout gl-my-5', data: { feature_id: Users::CalloutsHelper::MERGE_REQUEST_SETTINGS_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c| + = c.body do + = _('On the left sidebar, select %{merge_requests_link} to view them.').html_safe % { merge_requests_link: link_to('Settings > Merge requests', project_settings_merge_requests_path(@project)).html_safe } %section.settings.no-animate{ class: ('expanded' if expanded), data: { qa_selector: 'badges_settings_content' } } .settings-header diff --git a/app/views/projects/settings/merge_requests/show.html.haml b/app/views/projects/settings/merge_requests/show.html.haml new file mode 100644 index 00000000000000..886e276dea5bc9 --- /dev/null +++ b/app/views/projects/settings/merge_requests/show.html.haml @@ -0,0 +1,18 @@ +- breadcrumb_title _('Merge requests') +- page_title _('Merge requests') +- @content_class = 'limit-container-width' unless fluid_layout + +%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings.expanded{ class: [('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } } + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests') + = render_if_exists 'projects/merge_request_settings_description_text' + + .settings-content + = render_if_exists 'shared/promotions/promote_mr_features' + + = gitlab_ui_form_for @project, url: project_settings_merge_requests_path(@project), html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f| + %input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' } + = render 'projects/merge_request_settings', form: f + = f.submit _('Save changes'), class: "btn gl-button btn-confirm rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' } + += render_if_exists 'projects/settings/merge_requests/merge_request_approvals_settings', expanded: true diff --git a/config/routes/project.rb b/config/routes/project.rb index e1ee0c8f28b942..79ca13e3d8c22f 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -159,6 +159,7 @@ resource :packages_and_registries, only: [:show] do get '/cleanup_image_tags', to: 'packages_and_registries#cleanup_tags' end + resource :merge_requests, only: [:show, :update] end resources :usage_quotas, only: [:index] diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f1ed7c5fc04964..6a423cae00c02d 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -20962,6 +20962,7 @@ Name of the feature that the callout is for. | <a id="usercalloutfeaturenameenumgeo_migrate_hashed_storage"></a>`GEO_MIGRATE_HASHED_STORAGE` | Callout feature name for geo_migrate_hashed_storage. | | <a id="usercalloutfeaturenameenumgke_cluster_integration"></a>`GKE_CLUSTER_INTEGRATION` | Callout feature name for gke_cluster_integration. | | <a id="usercalloutfeaturenameenumgold_trial_billings"></a>`GOLD_TRIAL_BILLINGS` | Callout feature name for gold_trial_billings. | +| <a id="usercalloutfeaturenameenummerge_request_settings_moved_callout"></a>`MERGE_REQUEST_SETTINGS_MOVED_CALLOUT` | Callout feature name for merge_request_settings_moved_callout. | | <a id="usercalloutfeaturenameenummr_experience_survey"></a>`MR_EXPERIENCE_SURVEY` | Callout feature name for mr_experience_survey. | | <a id="usercalloutfeaturenameenumnamespace_storage_limit_banner_alert_threshold"></a>`NAMESPACE_STORAGE_LIMIT_BANNER_ALERT_THRESHOLD` | Callout feature name for namespace_storage_limit_banner_alert_threshold. | | <a id="usercalloutfeaturenameenumnamespace_storage_limit_banner_error_threshold"></a>`NAMESPACE_STORAGE_LIMIT_BANNER_ERROR_THRESHOLD` | Callout feature name for namespace_storage_limit_banner_error_threshold. | diff --git a/ee/app/assets/javascripts/pages/projects/edit/index.js b/ee/app/assets/javascripts/pages/projects/edit/index.js index 97ed5cd6affed5..fdf4d11756a813 100644 --- a/ee/app/assets/javascripts/pages/projects/edit/index.js +++ b/ee/app/assets/javascripts/pages/projects/edit/index.js @@ -1,12 +1,9 @@ /* eslint-disable no-new */ import '~/pages/projects/edit'; -import mountApprovals from 'ee/approvals/mount_project_settings'; -import { initMergeOptionSettings } from 'ee/pages/projects/edit/merge_options'; import { initServicePingSettingsClickTracking } from 'ee/registration_features_discovery_message'; import initProjectAdjournedDeleteButton from 'ee/projects/project_adjourned_delete_button'; import initProjectComplianceFrameworkEmptyState from 'ee/projects/project_compliance_framework_empty_state'; -import mountStatusChecks from 'ee/status_checks/mount'; import groupsSelect from '~/groups_select'; import UserCallout from '~/user_callout'; @@ -14,10 +11,6 @@ groupsSelect(); new UserCallout({ className: 'js-mr-approval-callout' }); -mountApprovals(document.getElementById('js-mr-approvals-settings')); -mountStatusChecks(document.getElementById('js-status-checks-settings')); - initProjectAdjournedDeleteButton(); initProjectComplianceFrameworkEmptyState(); -initMergeOptionSettings(); initServicePingSettingsClickTracking(); diff --git a/ee/app/assets/javascripts/pages/projects/settings/merge_requests/index.js b/ee/app/assets/javascripts/pages/projects/settings/merge_requests/index.js new file mode 100644 index 00000000000000..d025783689b8c4 --- /dev/null +++ b/ee/app/assets/javascripts/pages/projects/settings/merge_requests/index.js @@ -0,0 +1,9 @@ +import '~/pages/projects/settings/merge_requests'; +import mountApprovals from 'ee/approvals/mount_project_settings'; +import { initMergeOptionSettings } from 'ee/pages/projects/edit/merge_options'; +import mountStatusChecks from 'ee/status_checks/mount'; + +mountApprovals(document.getElementById('js-mr-approvals-settings')); +mountStatusChecks(document.getElementById('js-status-checks-settings')); + +initMergeOptionSettings(); diff --git a/ee/app/controllers/ee/projects/settings/merge_requests_controller.rb b/ee/app/controllers/ee/projects/settings/merge_requests_controller.rb new file mode 100644 index 00000000000000..48252a0ccaf6e7 --- /dev/null +++ b/ee/app/controllers/ee/projects/settings/merge_requests_controller.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module EE + module Projects + module Settings + module MergeRequestsController + extend ::Gitlab::Utils::Override + extend ::ActiveSupport::Concern + + private + + override :project_params_attributes + def project_params_attributes + super + project_params_ee + end + + override :project_setting_attributes + def project_setting_attributes + super + [:prevent_merge_without_jira_issue] + end + + def project_params_ee + attrs = %i[ + approvals_before_merge + approver_group_ids + approver_ids + merge_requests_template + reset_approvals_on_push + ci_cd_only + use_custom_template + require_password_to_approve + group_with_project_templates_id + ] + + attrs << %i[merge_pipelines_enabled] if allow_merge_pipelines_params? + attrs << %i[merge_trains_enabled] if allow_merge_trains_params? + + attrs += merge_request_rules_params + + attrs << :auto_rollback_enabled if project&.feature_available?(:auto_rollback) + + attrs + end + + def mirror_params + %i[ + mirror + mirror_trigger_builds + ] + end + + def merge_request_rules_params + attrs = [] + + if can?(current_user, :modify_merge_request_committer_setting, project) + attrs << :merge_requests_disable_committers_approval + end + + if can?(current_user, :modify_approvers_rules, project) + attrs << :disable_overriding_approvers_per_merge_request + end + + attrs << :merge_requests_author_approval if can?(current_user, :modify_merge_request_author_setting, project) + + attrs + end + + def allow_merge_pipelines_params? + project&.feature_available?(:merge_pipelines) + end + + def allow_merge_trains_params? + project&.feature_available?(:merge_trains) + end + end + end + end +end diff --git a/ee/app/views/projects/_merge_request_approvals_settings.html.haml b/ee/app/views/projects/settings/merge_requests/_merge_request_approvals_settings.html.haml similarity index 79% rename from ee/app/views/projects/_merge_request_approvals_settings.html.haml rename to ee/app/views/projects/settings/merge_requests/_merge_request_approvals_settings.html.haml index e8ab89437ff302..007c6162dbc60e 100644 --- a/ee/app/views/projects/_merge_request_approvals_settings.html.haml +++ b/ee/app/views/projects/settings/merge_requests/_merge_request_approvals_settings.html.haml @@ -1,11 +1,9 @@ - return unless @project.feature_available?(:merge_requests, current_user) - return unless @project.feature_available?(:merge_request_approvers, current_user) -%section.settings.merge-requests-feature.no-animate#js-merge-request-approval-settings{ class: [('expanded' if expanded)], data: { qa_selector: 'merge_request_approvals_settings_content' } } +%section.settings.merge-requests-feature.no-animate#js-merge-request-approval-settings.expanded{ data: { qa_selector: 'merge_request_approvals_settings_content' } } .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge request approvals') - = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do - = expanded ? _('Collapse') : _('Expand') %p - duties_link_url = help_page_path('user/compliance/compliance_report/index', anchor: 'separation-of-duties') - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: duties_link_url } diff --git a/ee/spec/controllers/projects/settings/merge_requests_controller_spec.rb b/ee/spec/controllers/projects/settings/merge_requests_controller_spec.rb new file mode 100644 index 00000000000000..f1cb50ba084b84 --- /dev/null +++ b/ee/spec/controllers/projects/settings/merge_requests_controller_spec.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Settings::MergeRequestsController do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:public_project) { create(:project, :public, :repository, namespace: group) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + describe 'PUT #update' do + it 'updates Merge Request Approvers attributes' do + params = { + approvals_before_merge: 50, + approver_group_ids: create(:group).id, + approver_ids: create(:user).id, + reset_approvals_on_push: false + } + + put :update, + params: { + namespace_id: project.namespace, + project_id: project, + project: params + } + + project.reload + + expect(response).to have_gitlab_http_status(:found) + expect(project.approver_groups.pluck(:group_id)).to contain_exactly(params[:approver_group_ids]) + expect(project.approvers.pluck(:user_id)).to contain_exactly(params[:approver_ids]) + end + + it 'updates Issuable Default Templates attributes' do + params = { + merge_requests_template: 'I got tissues' + } + + put :update, + params: { + namespace_id: project.namespace, + project_id: project, + project: params + } + project.reload + + expect(response).to have_gitlab_http_status(:found) + params.each do |param, value| + expect(project.public_send(param)).to eq(value) + end + end + + context 'when merge_pipelines_enabled param is specified' do + let(:params) { { merge_pipelines_enabled: true } } + + let(:request) do + put :update, params: { namespace_id: project.namespace, project_id: project, project: params } + end + + before do + stub_licensed_features(merge_pipelines: true) + end + + it 'updates the attribute' do + request + + expect(project.reload.merge_pipelines_enabled).to be_truthy + end + + context 'when license is not sufficient' do + before do + stub_licensed_features(merge_pipelines: false) + end + + it 'does not update the attribute' do + request + + expect(project.reload.merge_pipelines_enabled).to be_falsy + end + end + end + + context 'when merge_trains_enabled param is specified' do + let(:params) { { merge_trains_enabled: true } } + + let(:request) do + put :update, params: { namespace_id: project.namespace, project_id: project, project: params } + end + + before do + stub_licensed_features(merge_pipelines: true, merge_trains: true) + end + + it 'updates the attribute' do + request + + expect(project.merge_trains_enabled).to be_truthy + end + + context 'when license is not sufficient' do + before do + stub_licensed_features(merge_trains: false) + end + + it 'does not update the attribute' do + request + + expect(project.merge_trains_enabled).to be_falsy + end + end + end + + context 'when auto_rollback_enabled param is specified' do + let(:params) { { auto_rollback_enabled: true } } + + let(:request) do + put :update, params: { namespace_id: project.namespace, project_id: project, project: params } + end + + before do + stub_licensed_features(auto_rollback: true) + end + + it 'updates the attribute' do + request + + expect(project.reload.auto_rollback_enabled).to be_truthy + end + + context 'when license is not sufficient' do + before do + stub_licensed_features(auto_rollback: false) + end + + it 'does not update the attribute' do + request + + expect(project.reload.auto_rollback_enabled).to be_falsy + end + end + end + + describe 'merge request approvers settings' do + shared_examples 'merge request approvers rules' do + using RSpec::Parameterized::TableSyntax + + where(:can_modify, :param_value, :final_value) do + true | true | true + true | false | false + false | true | nil + false | false | nil + end + + with_them do + before do + allow(controller).to receive(:can?).and_call_original + allow(controller).to receive(:can?).with(user, rule_name, project).and_return(can_modify) + end + + it 'updates project if needed' do + put :update, + params: { + namespace_id: project.namespace, + project_id: project, + project: { setting => param_value } + } + + project.reload + + expect(project[setting]).to eq(final_value.nil? ? setting_default_value : final_value) + end + end + end + + describe ':disable_overriding_approvers_per_merge_request' do + it_behaves_like 'merge request approvers rules' do + let(:rule_name) { :modify_approvers_rules } + let(:setting) { :disable_overriding_approvers_per_merge_request } + let(:setting_default_value) { nil } + end + end + + describe ':merge_requests_author_approval' do + it_behaves_like 'merge request approvers rules' do + let(:rule_name) { :modify_merge_request_author_setting } + let(:setting) { :merge_requests_author_approval } + let(:setting_default_value) { false } + end + end + + describe ':merge_requests_disable_committers_approval' do + it_behaves_like 'merge request approvers rules' do + let(:rule_name) { :modify_merge_request_committer_setting } + let(:setting) { :merge_requests_disable_committers_approval } + let(:setting_default_value) { nil } + end + end + end + end +end diff --git a/ee/spec/features/admin/admin_merge_requests_approvals_spec.rb b/ee/spec/features/admin/admin_merge_requests_approvals_spec.rb index 2aead80d295082..a9241fc3dbf737 100644 --- a/ee/spec/features/admin/admin_merge_requests_approvals_spec.rb +++ b/ee/spec/features/admin/admin_merge_requests_approvals_spec.rb @@ -32,7 +32,7 @@ expect(find_field('Prevent approvals by users who add commits')).to be_checked expect(find_field(_('Prevent editing approval rules in projects and merge requests'))).to be_checked - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) page.within('[data-testid="merge-request-approval-settings"]') do expect(find('[data-testid="prevent-author-approval"] > input')).to be_disabled.and be_checked diff --git a/ee/spec/features/projects/audit_events_spec.rb b/ee/spec/features/projects/audit_events_spec.rb index 92c8548219e778..31487d8918a250 100644 --- a/ee/spec/features/projects/audit_events_spec.rb +++ b/ee/spec/features/projects/audit_events_spec.rb @@ -142,7 +142,7 @@ end it "appears in the project's audit events", :js do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) page.within('[data-testid="merge-request-approval-settings"]') do find('[data-testid="prevent-author-approval"] > input').set(false) diff --git a/ee/spec/features/projects/settings/disable_merge_trains_setting_spec.rb b/ee/spec/features/projects/settings/disable_merge_trains_setting_spec.rb index 06702636b52c33..b446cc7871d88f 100644 --- a/ee/spec/features/projects/settings/disable_merge_trains_setting_spec.rb +++ b/ee/spec/features/projects/settings/disable_merge_trains_setting_spec.rb @@ -36,7 +36,7 @@ with_them do before do project.update!(merge_pipelines_enabled: merge_pipelines_setting, merge_trains_enabled: merge_trains_setting) - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) wait_for_requests end @@ -47,7 +47,7 @@ context 'when merge pipelines is enabled' do before do project.update!(merge_pipelines_enabled: true) - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) wait_for_requests end @@ -81,7 +81,7 @@ context 'when merge pipelines is disabled' do before do project.update!(merge_pipelines_enabled: false) - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) wait_for_requests end @@ -105,7 +105,7 @@ context 'when both merge pipelines and merge trains are enabled' do before do project.update!(merge_pipelines_enabled: true, merge_trains_enabled: true) - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) wait_for_requests end diff --git a/ee/spec/features/projects/settings/merge_request_approvals_settings_spec.rb b/ee/spec/features/projects/settings/merge_request_approvals_settings_spec.rb index 467fb5495ed4b6..a056bfbb358956 100644 --- a/ee/spec/features/projects/settings/merge_request_approvals_settings_spec.rb +++ b/ee/spec/features/projects/settings/merge_request_approvals_settings_spec.rb @@ -22,7 +22,7 @@ end it 'adds approver' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) open_modal(text: 'Add approval rule', expand: false) open_approver_select @@ -49,7 +49,7 @@ end it 'adds approver group' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) open_modal(text: 'Add approval rule', expand: false) open_approver_select @@ -78,7 +78,7 @@ end it 'removes approver group' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) expect_avatar(find('.js-members'), rule.approvers) diff --git a/ee/spec/features/projects/settings/merge_requests/disable_merge_trains_setting_spec.rb b/ee/spec/features/projects/settings/merge_requests/disable_merge_trains_setting_spec.rb new file mode 100644 index 00000000000000..b446cc7871d88f --- /dev/null +++ b/ee/spec/features/projects/settings/merge_requests/disable_merge_trains_setting_spec.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Disable Merge Trains Setting', :js do + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + before do + stub_licensed_features(merge_pipelines: true, merge_trains: true) + + project.add_maintainer(user) + sign_in(user) + end + + shared_examples 'loads correct checkbox state' do + it 'merge pipelines checkbox is always enabled' do + expect(find('#project_merge_pipelines_enabled')).not_to be_disabled + end + + it 'merge trains checkbox is enabled only when merge_pipelines_enabled is true' do + expect(find('#project_merge_trains_enabled').disabled?).not_to eq(project.merge_pipelines_enabled) + end + end + + context 'when visiting the project settings page' do + using RSpec::Parameterized::TableSyntax + + where(:merge_pipelines_setting, :merge_trains_setting) do + true | true + true | false + false | true + false | false + end + + with_them do + before do + project.update!(merge_pipelines_enabled: merge_pipelines_setting, merge_trains_enabled: merge_trains_setting) + visit project_settings_merge_requests_path(project) + wait_for_requests + end + + include_examples 'loads correct checkbox state' + end + end + + context 'when merge pipelines is enabled' do + before do + project.update!(merge_pipelines_enabled: true) + visit project_settings_merge_requests_path(project) + wait_for_requests + end + + include_examples 'loads correct checkbox state' + + it "checking merge trains checkbox doesn't affect merge pipelines checkbox" do + check('Enable merge trains') + + expect(find('#project_merge_trains_enabled')).to be_checked + expect(find('#project_merge_pipelines_enabled')).not_to be_disabled + expect(find('#project_merge_pipelines_enabled')).to be_checked + end + + it 'unchecking merge pipelines checkbox disables merge trains checkbox' do + uncheck('Enable merged results pipelines') + + expect(find('#project_merge_pipelines_enabled')).not_to be_checked + expect(find('#project_merge_trains_enabled')).to be_disabled + end + + it 'unchecking merge pipelines checkbox unchecks merge trains checkbox if it was previously checked' do + check('Enable merge trains') + uncheck('Enable merged results pipelines') + + expect(find('#project_merge_pipelines_enabled')).not_to be_checked + expect(find('#project_merge_trains_enabled')).to be_disabled + expect(find('#project_merge_trains_enabled')).not_to be_checked + end + end + + context 'when merge pipelines is disabled' do + before do + project.update!(merge_pipelines_enabled: false) + visit project_settings_merge_requests_path(project) + wait_for_requests + end + + include_examples 'loads correct checkbox state' + + it 'checking merge pipelines checkbox enables merge trains checkbox' do + check('Enable merged results pipelines') + + expect(find('#project_merge_pipelines_enabled')).to be_checked + expect(find('#project_merge_trains_enabled')).not_to be_disabled + end + + it 'checking merge pipelines checkbox should leave merge trains checkbox unchecked' do + check('Enable merged results pipelines') + + expect(find('#project_merge_pipelines_enabled')).to be_checked + expect(find('#project_merge_trains_enabled')).not_to be_checked + end + end + + context 'when both merge pipelines and merge trains are enabled' do + before do + project.update!(merge_pipelines_enabled: true, merge_trains_enabled: true) + visit project_settings_merge_requests_path(project) + wait_for_requests + end + + include_examples 'loads correct checkbox state' + + it 'unchecking merge pipelines checkbox disables and unchecks merge trains checkbox' do + uncheck('Enable merged results pipelines') + + expect(find('#project_merge_pipelines_enabled')).not_to be_checked + expect(find('#project_merge_trains_enabled')).to be_disabled + expect(find('#project_merge_trains_enabled')).not_to be_checked + end + + it "unchecking merge trains checkbox doesn't affect merge pipelines checkbox" do + uncheck('Enable merge trains') + + expect(find('#project_merge_trains_enabled')).not_to be_checked + expect(find('#project_merge_pipelines_enabled')).not_to be_disabled + expect(find('#project_merge_pipelines_enabled')).to be_checked + end + end +end diff --git a/ee/spec/features/projects/settings/merge_requests/user_manages_approval_settings_spec.rb b/ee/spec/features/projects/settings/merge_requests/user_manages_approval_settings_spec.rb new file mode 100644 index 00000000000000..264b0aca57dc55 --- /dev/null +++ b/ee/spec/features/projects/settings/merge_requests/user_manages_approval_settings_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'EE > Projects > Settings > Merge requests > User manages approval rules' do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:path) { edit_project_path(project) } + let(:licensed_features) { {} } + let(:project_features) { {} } + + before do + sign_in(user) + stub_licensed_features(licensed_features) + + project.project_feature.update!(project_features) + + visit project_settings_merge_requests_path(project) + end + + context 'when merge requests is not available' do + let(:project_features) { { merge_requests_access_level: ::ProjectFeature::DISABLED } } + + it 'does not show approval settings' do + expect(page).not_to have_selector('#js-merge-request-approval-settings') + end + end + + context 'when merge requests is available' do + let(:project_features) { { merge_requests_access_level: ::ProjectFeature::ENABLED } } + + it 'shows approval settings' do + expect(page).to have_selector('#js-merge-request-approval-settings') + end + end + + context 'when `code_owner_approval_required` is not available' do + let(:licensed_features) { { code_owner_approval_required: false } } + + it 'does not allow the user to require code owner approval' do + expect(page).not_to have_content('Require approval from code owners') + end + end +end diff --git a/ee/spec/features/projects/settings/merge_requests/user_manages_merge_pipelines_spec.rb b/ee/spec/features/projects/settings/merge_requests/user_manages_merge_pipelines_spec.rb new file mode 100644 index 00000000000000..17d276c0e3add9 --- /dev/null +++ b/ee/spec/features/projects/settings/merge_requests/user_manages_merge_pipelines_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'EE > Projects > Settings > Merge requests > User manages merge pipelines', :js do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + stub_licensed_features(merge_pipelines: true) + + project.add_maintainer(user) + sign_in(user) + end + + it 'sees unchecked merge pipeline checkbox' do + visit project_settings_merge_requests_path(project) + + expect(page.find('#project_merge_pipelines_enabled')).not_to be_checked + end + + context 'when user enabled the checkbox' do + before do + visit project_settings_merge_requests_path(project) + + check('Enable merged results pipelines') + end + + it 'sees enabled merge pipeline checkbox' do + expect(page.find('#project_merge_pipelines_enabled')).to be_checked + end + end + + context 'when license is insufficient' do + before do + stub_licensed_features(merge_pipelines: false) + end + + it 'does not see the checkbox' do + expect(page).not_to have_css('#project_merge_pipelines_enabled') + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(merge_pipelines: false) + end + + it 'does not see the checkbox' do + expect(page).not_to have_css('#project_merge_pipelines_enabled') + end + end +end diff --git a/ee/spec/features/projects/settings/merge_requests/user_manages_merge_requests_template_spec.rb b/ee/spec/features/projects/settings/merge_requests/user_manages_merge_requests_template_spec.rb new file mode 100644 index 00000000000000..efb27b652d6a21 --- /dev/null +++ b/ee/spec/features/projects/settings/merge_requests/user_manages_merge_requests_template_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'EE > Projects > Settings > Merge requests > User manages merge requests template' do + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository, namespace: user.namespace) } + + before do + sign_in(user) + + visit project_settings_merge_requests_path(project) + end + + it 'saves merge request template' do + fill_in 'project_merge_requests_template', with: "This merge request should contain the following." + page.within '#js-merge-request-settings' do + click_button 'Save changes' + end + + expect(find_field('project_merge_requests_template').value).to eq 'This merge request should contain the following.' + end +end diff --git a/ee/spec/features/projects/settings/merge_requests/user_manages_merge_trains_spec.rb b/ee/spec/features/projects/settings/merge_requests/user_manages_merge_trains_spec.rb new file mode 100644 index 00000000000000..20640c08473eb5 --- /dev/null +++ b/ee/spec/features/projects/settings/merge_requests/user_manages_merge_trains_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'EE > Projects > Settings > Merge requests > User manages merge trains', :js do + let_it_be(:project, refind: true) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:merge_pipelines) { true } + let(:merge_trains) { true } + + before do + stub_licensed_features(merge_pipelines: merge_pipelines, merge_trains: merge_trains) + + project.update!(merge_pipelines_enabled: true) + project.add_maintainer(user) + sign_in(user) + + visit project_settings_merge_requests_path(project) + end + + it 'sees unchecked merge trains checkbox' do + wait_for_requests + + expect(page.find('#project_merge_trains_enabled')).not_to be_checked + end + + context 'when user enabled the checkbox' do + before do + wait_for_requests + + check('Enable merge trains') + end + + it 'sees enabled merge trains checkbox' do + expect(page.find('#project_merge_trains_enabled')).to be_checked + end + end + + context 'when license is insufficient' do + let(:merge_pipelines) { false } + let(:merge_trains) { false } + + it 'does not see the checkbox' do + expect(page).not_to have_css('#project_merge_trains_enabled') + end + end +end diff --git a/ee/spec/features/projects/settings/merge_requests_settings_spec.rb b/ee/spec/features/projects/settings/merge_requests_settings_spec.rb index af75345ce41b99..81996ccf82d39b 100644 --- a/ee/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/ee/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -48,7 +48,7 @@ end it 'adds a status check' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) click_button 'Add status check' @@ -68,7 +68,7 @@ let_it_be(:rule) { create(:external_status_check, project: project) } it 'updates the status check' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) expect(find('[data-testid="status-checks-table"]')).to have_content(rule.name) @@ -88,7 +88,7 @@ end it 'removes the status check' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) expect(find('[data-testid="status-checks-table"]')).to have_content(rule.name) @@ -116,13 +116,13 @@ end it 'input to configure merge request template is not shown' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) expect(page).not_to have_selector('#project_merge_requests_template') end it "does not mention the merge request template in the section's description text" do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) expect(page).to have_content('Choose your merge method, options, checks, and squash options.') end @@ -134,13 +134,13 @@ end it 'input to configure merge request template is shown' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) expect(page).to have_selector('#project_merge_requests_template') end it "mentions the merge request template in the section's description text" do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) expect(page).to have_content('Choose the method, options, checks, and squash options for merge requests. You can also set up merge request templates for different actions.') end diff --git a/ee/spec/features/projects/settings/user_manages_approval_settings_spec.rb b/ee/spec/features/projects/settings/user_manages_approval_settings_spec.rb index 810561ac24de6d..58e49dceaa560c 100644 --- a/ee/spec/features/projects/settings/user_manages_approval_settings_spec.rb +++ b/ee/spec/features/projects/settings/user_manages_approval_settings_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'EE > Projects > Settings > User manages approval rule settings' do let(:project) { create(:project) } let(:user) { project.first_owner } - let(:path) { edit_project_path(project) } + let(:path) { project_settings_merge_requests_path(project) } let(:licensed_features) { {} } let(:project_features) { {} } diff --git a/ee/spec/features/projects/settings/user_manages_merge_pipelines_spec.rb b/ee/spec/features/projects/settings/user_manages_merge_pipelines_spec.rb index 10fdded4d4c0b3..c6039ab733b830 100644 --- a/ee/spec/features/projects/settings/user_manages_merge_pipelines_spec.rb +++ b/ee/spec/features/projects/settings/user_manages_merge_pipelines_spec.rb @@ -14,14 +14,14 @@ end it 'sees unchecked merge pipeline checkbox' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) expect(page.find('#project_merge_pipelines_enabled')).not_to be_checked end context 'when user enabled the checkbox' do before do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) check('Enable merged results pipelines') end diff --git a/ee/spec/features/projects/settings/user_manages_merge_requests_template_spec.rb b/ee/spec/features/projects/settings/user_manages_merge_requests_template_spec.rb index b6f79d82c14455..1c6e00f5fe1da8 100644 --- a/ee/spec/features/projects/settings/user_manages_merge_requests_template_spec.rb +++ b/ee/spec/features/projects/settings/user_manages_merge_requests_template_spec.rb @@ -8,7 +8,7 @@ before do sign_in(user) - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) end it 'saves merge request template' do diff --git a/ee/spec/features/projects/settings/user_manages_merge_trains_spec.rb b/ee/spec/features/projects/settings/user_manages_merge_trains_spec.rb index 3ac4881205bf4e..9f4c326d4613d6 100644 --- a/ee/spec/features/projects/settings/user_manages_merge_trains_spec.rb +++ b/ee/spec/features/projects/settings/user_manages_merge_trains_spec.rb @@ -15,7 +15,7 @@ end it 'sees unchecked merge trains checkbox' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) wait_for_requests expect(page.find('#project_merge_trains_enabled')).not_to be_checked @@ -23,7 +23,7 @@ context 'when user enabled the checkbox' do before do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) wait_for_requests check('Enable merge trains') diff --git a/ee/spec/features/promotion_spec.rb b/ee/spec/features/promotion_spec.rb index e4b4e9e819c7ef..b1becf5fc40828 100644 --- a/ee/spec/features/promotion_spec.rb +++ b/ee/spec/features/promotion_spec.rb @@ -22,13 +22,13 @@ end it 'appears in project edit page' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) expect(find('#promote_mr_features')).to have_content 'Improve merge requests' end it 'does not show when cookie is set' do - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) within('#promote_mr_features') do find('.js-close').click @@ -36,7 +36,7 @@ wait_for_requests - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) expect(page).not_to have_selector('#promote_mr_features') end diff --git a/ee/spec/views/projects/edit.html.haml_spec.rb b/ee/spec/views/projects/edit.html.haml_spec.rb index 0463cb4ff4b53e..5ff49699e430d3 100644 --- a/ee/spec/views/projects/edit.html.haml_spec.rb +++ b/ee/spec/views/projects/edit.html.haml_spec.rb @@ -15,32 +15,6 @@ current_application_settings: Gitlab::CurrentSettings.current_application_settings) end - context 'status checks' do - context 'feature is not available' do - before do - stub_licensed_features(external_status_checks: false) - - render - end - - it 'hides the status checks area' do - expect(rendered).not_to have_content('Status check') - end - - context 'feature is available' do - before do - stub_licensed_features(external_status_checks: true) - - render - end - - it 'shows the status checks area' do - expect(rendered).to have_content('Status check') - end - end - end - end - describe 'prompt user about registration features' do context 'with no license and service ping disabled' do before do diff --git a/ee/spec/views/projects/merge_requests/_merge_request_approvals.html.haml_spec.rb b/ee/spec/views/projects/settings/merge_requests/_merge_request_approvals.html.haml_spec.rb similarity index 77% rename from ee/spec/views/projects/merge_requests/_merge_request_approvals.html.haml_spec.rb rename to ee/spec/views/projects/settings/merge_requests/_merge_request_approvals.html.haml_spec.rb index 54530f56968c26..9c0d54645dd1df 100644 --- a/ee/spec/views/projects/merge_requests/_merge_request_approvals.html.haml_spec.rb +++ b/ee/spec/views/projects/settings/merge_requests/_merge_request_approvals.html.haml_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'projects/_merge_request_approvals_settings' do +RSpec.describe 'projects/settings/merge_requests/_merge_request_approvals_settings' do let(:project) { build(:project) } before do @@ -11,7 +11,7 @@ allow(view).to receive(:expanded).and_return(true) allow(project).to receive(:feature_available?).and_return(true) - render partial: 'projects/merge_request_approvals_settings' + render partial: 'projects/settings/merge_requests/merge_request_approvals_settings' end it 'renders the settings title' do diff --git a/lib/sidebars/projects/menus/merge_requests_menu.rb b/lib/sidebars/projects/menus/merge_requests_menu.rb index fe501667d37ec2..3e543872d36885 100644 --- a/lib/sidebars/projects/menus/merge_requests_menu.rb +++ b/lib/sidebars/projects/menus/merge_requests_menu.rb @@ -59,9 +59,9 @@ def pill_html_options override :active_routes def active_routes if context.project.issues_enabled? - { controller: :merge_requests } + { controller: 'projects/merge_requests' } else - { controller: [:merge_requests, :milestones] } + { controller: ['projects/merge_requests', :milestones] } end end end diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb index 23be751ff1029d..f422d56ce4f3e6 100644 --- a/lib/sidebars/projects/menus/settings_menu.rb +++ b/lib/sidebars/projects/menus/settings_menu.rb @@ -13,6 +13,7 @@ def configure_menu_items add_item(webhooks_menu_item) add_item(access_tokens_menu_item) add_item(repository_menu_item) + add_item(merge_requests_menu_item) add_item(ci_cd_menu_item) add_item(packages_and_registries_menu_item) add_item(pages_menu_item) @@ -150,6 +151,17 @@ def usage_quotas_menu_item item_id: :usage_quotas ) end + + def merge_requests_menu_item + return unless context.project.merge_requests_enabled? + + ::Sidebars::MenuItem.new( + title: _('Merge requests'), + link: project_settings_merge_requests_path(context.project), + active_routes: { path: 'projects/settings/merge_requests#show' }, + item_id: :merge_requests + ) + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 856076adfb3c1c..ec403ee48f273c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -24759,6 +24759,9 @@ msgstr "" msgid "Merge requests" msgstr "" +msgid "Merge requests and approvals settings have moved." +msgstr "" + msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others" msgstr "" @@ -27029,6 +27032,9 @@ msgid_plural "On %{end_date}, your trial will end and %{namespace_name} will be msgstr[0] "" msgstr[1] "" +msgid "On the left sidebar, select %{merge_requests_link} to view them." +msgstr "" + msgid "On track" msgstr "" diff --git a/qa/qa/ee/page/project/settings/main.rb b/qa/qa/ee/page/project/settings/main.rb index 58ddf8a972b9af..453875e770dd81 100644 --- a/qa/qa/ee/page/project/settings/main.rb +++ b/qa/qa/ee/page/project/settings/main.rb @@ -16,7 +16,7 @@ def self.prepended(base) element :issue_template_settings_content end - view 'ee/app/views/projects/_merge_request_approvals_settings.html.haml' do + view 'ee/app/views/projects/settings/merge_requests/_merge_request_approvals_settings.html.haml' do element :merge_request_approvals_settings_content end end @@ -27,12 +27,6 @@ def expand_default_description_template_for_issues(&block) IssueTemplateDefault.perform(&block) end end - - def expand_merge_request_approvals_settings(&block) - expand_content(:merge_request_approvals_settings_content) do - MergeRequestApprovals.perform(&block) - end - end end end end diff --git a/qa/qa/ee/page/project/settings/merge_request.rb b/qa/qa/ee/page/project/settings/merge_request.rb index 7cf22f21ceda9c..d32cb2cba669b2 100644 --- a/qa/qa/ee/page/project/settings/merge_request.rb +++ b/qa/qa/ee/page/project/settings/merge_request.rb @@ -54,3 +54,7 @@ def set_default_merge_request_template(template) end end end + +QA::Page::Project::Settings::MergeRequest.prepend_mod_with( # rubocop:disable Cop/InjectEnterpriseEditionModule + "Page::Project::Settings::MergeRequestApprovals", + namespace: QA) diff --git a/qa/qa/ee/page/project/settings/merge_request_approvals.rb b/qa/qa/ee/page/project/settings/merge_request_approvals.rb index 9281f67e88c990..9be1c1726743cf 100644 --- a/qa/qa/ee/page/project/settings/merge_request_approvals.rb +++ b/qa/qa/ee/page/project/settings/merge_request_approvals.rb @@ -5,9 +5,17 @@ module EE module Page module Project module Settings - class MergeRequestApprovals < QA::Page::Base - view 'ee/app/assets/javascripts/approvals/components/mr_edit/rule_input.vue' do - element :approvals_number_field + module MergeRequestApprovals + extend QA::Page::PageConcern + + def self.prepended(base) + super + + base.class_eval do + view 'ee/app/assets/javascripts/approvals/components/mr_edit/rule_input.vue' do + element :approvals_number_field + end + end end def set_default_number_of_approvals_required(number) diff --git a/qa/qa/flow/merge_request.rb b/qa/qa/flow/merge_request.rb index cd8bac69fedaa4..24abfa9e356104 100644 --- a/qa/qa/flow/merge_request.rb +++ b/qa/qa/flow/merge_request.rb @@ -6,8 +6,7 @@ module MergeRequest extend self def enable_merge_trains - Page::Project::Menu.perform(&:go_to_general_settings) - Page::Project::Settings::Main.perform(&:expand_merge_requests_settings) + Page::Project::Menu.perform(&:go_to_merge_request_settings) Page::Project::Settings::MergeRequest.perform(&:enable_merge_train) end diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb index 52ed630ac669d0..ca5d13abdae1ae 100644 --- a/qa/qa/page/project/settings/main.rb +++ b/qa/qa/page/project/settings/main.rb @@ -13,11 +13,14 @@ class Main < Page::Base view 'app/views/projects/edit.html.haml' do element :advanced_settings_content - element :merge_request_settings_content element :visibility_features_permissions_content element :badges_settings_content end + view 'app/views/projects/settings/merge_requests/show.html.haml' do + element :merge_request_settings_content + end + view 'app/views/projects/settings/_general.html.haml' do element :project_name_field element :save_naming_topics_avatar_button @@ -42,12 +45,6 @@ def expand_advanced_settings(&block) end end - def expand_merge_requests_settings(&block) - expand_content(:merge_request_settings_content) do - MergeRequest.perform(&block) - end - end - def expand_visibility_project_features_permissions(&block) expand_content(:visibility_features_permissions_content) do VisibilityFeaturesPermissions.perform(&block) diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb index dd9c94ebbb769e..d862979aeecbe2 100644 --- a/qa/qa/page/project/settings/merge_request.rb +++ b/qa/qa/page/project/settings/merge_request.rb @@ -7,7 +7,7 @@ module Settings class MergeRequest < QA::Page::Base include QA::Page::Settings::Common - view 'app/views/projects/edit.html.haml' do + view 'app/views/projects/settings/merge_requests/show.html.haml' do element :save_merge_request_changes_button end diff --git a/qa/qa/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb index 53a5eaf60c5e7a..2ed4c28afb7686 100644 --- a/qa/qa/page/project/sub_menus/settings.rb +++ b/qa/qa/page/project/sub_menus/settings.rb @@ -77,6 +77,14 @@ def go_to_pages_settings end end + def go_to_merge_request_settings + hover_settings do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Merge requests') + end + end + end + private def hover_settings diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb index 2280cc971a787c..c7296b6eea2ca3 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb @@ -12,11 +12,9 @@ module QA it 'user rebases source branch of merge request', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347735' do merge_request.project.visit! - Page::Project::Menu.perform(&:go_to_general_settings) - Page::Project::Settings::Main.perform do |main| - main.expand_merge_requests_settings do |settings| - settings.enable_ff_only - end + Page::Project::Menu.perform(&:go_to_merge_request_settings) + Page::Project::Settings::MergeRequest.perform do |settings| + settings.enable_ff_only end Resource::Repository::ProjectPush.fabricate! do |push| diff --git a/qa/qa/specs/features/ee/browser_ui/3_create/merge_request/default_merge_request_template_spec.rb b/qa/qa/specs/features/ee/browser_ui/3_create/merge_request/default_merge_request_template_spec.rb index 359e564575b092..847dc4255b2ab7 100644 --- a/qa/qa/specs/features/ee/browser_ui/3_create/merge_request/default_merge_request_template_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/3_create/merge_request/default_merge_request_template_spec.rb @@ -18,10 +18,9 @@ module QA it 'uses default template when creating a merge request', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347721' do default_template_project.visit! - Page::Project::Menu.perform(&:go_to_general_settings) - Page::Project::Settings::Main.perform(&:expand_merge_requests_settings) - Page::Project::Settings::MergeRequest.perform do |mr_settings| - mr_settings.set_default_merge_request_template(template_content) + Page::Project::Menu.perform(&:go_to_merge_request_settings) + Page::Project::Settings::MergeRequest.perform do |settings| + settings.set_default_merge_request_template(template_content) end Resource::MergeRequest.fabricate_via_browser_ui! do |merge_request| diff --git a/qa/qa/specs/features/ee/browser_ui/4_verify/new_discussion_not_dropping_merge_trains_mr_spec.rb b/qa/qa/specs/features/ee/browser_ui/4_verify/new_discussion_not_dropping_merge_trains_mr_spec.rb index df9e4bb89b7ede..8a4dbbc77c17a7 100644 --- a/qa/qa/specs/features/ee/browser_ui/4_verify/new_discussion_not_dropping_merge_trains_mr_spec.rb +++ b/qa/qa/specs/features/ee/browser_ui/4_verify/new_discussion_not_dropping_merge_trains_mr_spec.rb @@ -34,7 +34,7 @@ module QA project.visit! Flow::MergeRequest.enable_merge_trains - Page::Project::Settings::Main.perform(&:expand_merge_requests_settings) + Page::Project::Menu.perform(&:go_to_merge_request_settings) Page::Project::Settings::MergeRequest.perform(&:enable_merge_if_all_disscussions_are_resolved) commit_ci_file diff --git a/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb b/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb index 4bbad9bf3e518a..01b229192cc512 100644 --- a/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb +++ b/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb @@ -8,11 +8,9 @@ module QA # Require one approval from any eligible user on any branch # This will confirm that this type of unrestricted approval is # also satisfied when a code owner grants approval - Page::Project::Menu.perform(&:go_to_general_settings) - Page::Project::Settings::Main.perform do |main| - main.expand_merge_request_approvals_settings do |settings| - settings.set_default_number_of_approvals_required(1) - end + Page::Project::Menu.perform(&:go_to_merge_request_settings) + Page::Project::Settings::MergeRequest.perform do |settings| + settings.set_default_number_of_approvals_required(1) end Resource::Repository::Commit.fabricate_via_api! do |commit| diff --git a/spec/controllers/projects/settings/merge_requests_controller_spec.rb b/spec/controllers/projects/settings/merge_requests_controller_spec.rb new file mode 100644 index 00000000000000..106ec62bea00fd --- /dev/null +++ b/spec/controllers/projects/settings/merge_requests_controller_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Settings::MergeRequestsController do + let(:project) { create(:project_empty_repo, :public) } + let(:user) { create(:user) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + describe 'GET show' do + it 'renders show with 200 status code' do + get :show, params: { namespace_id: project.namespace, project_id: project } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + end + end + + describe '#update', :enable_admin_mode do + render_views + + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + it 'updates Fast Forward Merge attributes' do + controller.instance_variable_set(:@project, project) + + params = { + merge_method: :ff + } + + put :update, + params: { + namespace_id: project.namespace, + project_id: project.id, + project: params + } + + expect(response).to redirect_to project_settings_merge_requests_path(project) + params.each do |param, value| + expect(project.public_send(param)).to eq(value) + end + end + end +end diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb new file mode 100644 index 00000000000000..ba84d8b6d1a8df --- /dev/null +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -0,0 +1,261 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects > Settings > Merge requests' do + include ProjectForksHelper + + let(:user) { create(:user) } + let(:project) { create(:project, :public, namespace: user.namespace, path: 'gitlab', name: 'sample') } + + before do + sign_in(user) + + visit(project_settings_merge_requests_path(project)) + end + + it 'shows "Merge commit" strategy' do + page.within '.merge-request-settings-form' do + expect(page).to have_content 'Merge commit' + end + end + + it 'shows "Merge commit with semi-linear history " strategy' do + page.within '.merge-request-settings-form' do + expect(page).to have_content 'Merge commit with semi-linear history' + end + end + + it 'shows "Fast-forward merge" strategy' do + page.within '.merge-request-settings-form' do + expect(page).to have_content 'Fast-forward merge' + end + end + + it 'shows Squash commit options', :aggregate_failures do + page.within '.merge-request-settings-form' do + expect(page).to have_content 'Do not allow' + expect(page).to have_content 'Squashing is never performed and the checkbox is hidden.' + + expect(page).to have_content 'Allow' + expect(page).to have_content 'Checkbox is visible and unselected by default.' + + expect(page).to have_content 'Encourage' + expect(page).to have_content 'Checkbox is visible and selected by default.' + + expect(page).to have_content 'Require' + end + end + + context 'when Merge Request and Pipelines are initially enabled', :js do + context 'when Pipelines are initially enabled' do + it 'shows the Merge Requests settings' do + expect(page).to have_content 'Pipelines must succeed' + expect(page).to have_content 'All threads must be resolved' + + visit edit_project_path(project) + + within('.sharing-permissions-form') do + within('[data-for="project[project_feature_attributes][merge_requests_access_level]"]') do + find('.gl-toggle').click + end + end + + find('[data-testid="project-features-save-button"]').send_keys(:return) + + visit project_settings_merge_requests_path(project) + + expect(page).to have_content('Not Found') + end + end + + context 'when Pipelines are initially disabled', :js do + before do + project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED) + + visit project_settings_merge_requests_path(project) + end + + it 'shows the Merge Requests settings that do not depend on Builds feature' do + expect(page).to have_content 'Pipelines must succeed' + expect(page).to have_content 'All threads must be resolved' + + visit edit_project_path(project) + + within('.sharing-permissions-form') do + within('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"]') do + find('.gl-toggle').click + end + end + + find('[data-testid="project-features-save-button"]').send_keys(:return) + + visit project_settings_merge_requests_path(project) + + expect(page).to have_content 'Pipelines must succeed' + expect(page).to have_content 'All threads must be resolved' + end + end + end + + context 'when Merge Request are initially disabled', :js do + before do + project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED) + + visit(project_settings_merge_requests_path(project)) + end + + it 'does not show the Merge Requests settings' do + expect(page).to have_content('Not Found') + + visit edit_project_path(project) + + within('.sharing-permissions-form') do + within('[data-for="project[project_feature_attributes][merge_requests_access_level]"]') do + find('.gl-toggle').click + end + end + + find('[data-testid="project-features-save-button"]').send_keys(:return) + + visit project_settings_merge_requests_path(project) + + expect(page).to have_content 'Pipelines must succeed' + expect(page).to have_content 'All threads must be resolved' + end + end + + describe 'Checkbox to enable merge request link', :js do + it 'is initially checked' do + checkbox = find_field('project_printing_merge_request_link_enabled') + expect(checkbox).to be_checked + end + + it 'when unchecked sets :printing_merge_request_link_enabled to false' do + uncheck('project_printing_merge_request_link_enabled') + within('.merge-request-settings-form') do + find('.rspec-save-merge-request-changes') + click_on('Save changes') + end + + find('.flash-notice') + checkbox = find_field('project_printing_merge_request_link_enabled') + + expect(checkbox).not_to be_checked + + project.reload + expect(project.printing_merge_request_link_enabled).to be(false) + end + end + + describe 'Checkbox to remove source branch after merge', :js do + it 'is initially checked' do + checkbox = find_field('project_remove_source_branch_after_merge') + expect(checkbox).to be_checked + end + + it 'when unchecked sets :remove_source_branch_after_merge to false' do + uncheck('project_remove_source_branch_after_merge') + within('.merge-request-settings-form') do + find('.rspec-save-merge-request-changes') + click_on('Save changes') + end + + find('.flash-notice') + checkbox = find_field('project_remove_source_branch_after_merge') + + expect(checkbox).not_to be_checked + + project.reload + expect(project.remove_source_branch_after_merge).to be(false) + end + end + + describe 'Squash commits when merging', :js do + it 'initially has :squash_option set to :default_off' do + radio = find_field('project_project_setting_attributes_squash_option_default_off') + expect(radio).to be_checked + end + + it 'allows :squash_option to be set to :default_on' do + choose('project_project_setting_attributes_squash_option_default_on') + + within('.merge-request-settings-form') do + find('.rspec-save-merge-request-changes') + click_on('Save changes') + end + + wait_for_requests + + radio = find_field('project_project_setting_attributes_squash_option_default_on') + + expect(radio).to be_checked + expect(project.reload.project_setting.squash_option).to eq('default_on') + end + + it 'allows :squash_option to be set to :always' do + choose('project_project_setting_attributes_squash_option_always') + + within('.merge-request-settings-form') do + find('.rspec-save-merge-request-changes') + click_on('Save changes') + end + + wait_for_requests + + radio = find_field('project_project_setting_attributes_squash_option_always') + + expect(radio).to be_checked + expect(project.reload.project_setting.squash_option).to eq('always') + end + + it 'allows :squash_option to be set to :never' do + choose('project_project_setting_attributes_squash_option_never') + + within('.merge-request-settings-form') do + find('.rspec-save-merge-request-changes') + click_on('Save changes') + end + + wait_for_requests + + radio = find_field('project_project_setting_attributes_squash_option_never') + + expect(radio).to be_checked + expect(project.reload.project_setting.squash_option).to eq('never') + end + end + + describe 'target project settings' do + context 'when project is a fork' do + let_it_be(:upstream) { create(:project, :public) } + + let(:project) { fork_project(upstream, user) } + + it 'allows to change merge request target project behavior' do + expect(page).to have_content 'The default target project for merge requests' + + radio = find_field('project_project_setting_attributes_mr_default_target_self_false') + expect(radio).to be_checked + + choose('project_project_setting_attributes_mr_default_target_self_true') + + within('.merge-request-settings-form') do + find('.rspec-save-merge-request-changes') + click_on('Save changes') + end + + wait_for_requests + + radio = find_field('project_project_setting_attributes_mr_default_target_self_true') + + expect(radio).to be_checked + expect(project.reload.project_setting.mr_default_target_self).to be_truthy + end + end + + it 'does not show target project section' do + expect(page).not_to have_content 'The default target project for merge requests' + end + end +end diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb index 6aa59f72d2a9d8..c76b4d0af88458 100644 --- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb @@ -9,29 +9,29 @@ before do sign_in(user) - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) end it 'shows "Merge commit" strategy' do - page.within '#js-merge-request-settings' do + page.within '.merge-request-settings-form' do expect(page).to have_content 'Merge commit' end end it 'shows "Merge commit with semi-linear history " strategy' do - page.within '#js-merge-request-settings' do + page.within '.merge-request-settings-form' do expect(page).to have_content 'Merge commit with semi-linear history' end end it 'shows "Fast-forward merge" strategy' do - page.within '#js-merge-request-settings' do + page.within '.merge-request-settings-form' do expect(page).to have_content 'Fast-forward merge' end end it 'shows Squash commit options', :aggregate_failures do - page.within '#js-merge-request-settings' do + page.within '.merge-request-settings-form' do expect(page).to have_content 'Do not allow' expect(page).to have_content 'Squashing is never performed and the checkbox is hidden.' @@ -52,30 +52,33 @@ expect(page).to have_content 'Pipelines must succeed' expect(page).to have_content 'All threads must be resolved' - within('.sharing-permissions-form') do - find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click - find('[data-testid="project-features-save-button"]').send_keys(:return) - end + visit edit_project_path(project) + + find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click + find('[data-testid="project-features-save-button"]').send_keys(:return) + + visit project_settings_merge_requests_path(project) - expect(page).not_to have_content 'Pipelines must succeed' - expect(page).not_to have_content 'All threads must be resolved' + expect(page).to have_content "Page Not Found" end end context 'when Pipelines are initially disabled', :js do before do project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED) - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) end it 'shows the Merge Requests settings that do not depend on Builds feature' do expect(page).to have_content 'Pipelines must succeed' expect(page).to have_content 'All threads must be resolved' - within('.sharing-permissions-form') do - find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click - find('[data-testid="project-features-save-button"]').send_keys(:return) - end + visit edit_project_path(project) + + find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click + find('[data-testid="project-features-save-button"]').send_keys(:return) + + visit project_settings_merge_requests_path(project) expect(page).to have_content 'Pipelines must succeed' expect(page).to have_content 'All threads must be resolved' @@ -86,18 +89,22 @@ context 'when Merge Request are initially disabled', :js do before do project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED) - visit edit_project_path(project) + visit project_settings_merge_requests_path(project) end it 'does not show the Merge Requests settings' do expect(page).not_to have_content 'Pipelines must succeed' expect(page).not_to have_content 'All threads must be resolved' + visit edit_project_path(project) + within('.sharing-permissions-form') do find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click find('[data-testid="project-features-save-button"]').send_keys(:return) end + visit project_settings_merge_requests_path(project) + expect(page).to have_content 'Pipelines must succeed' expect(page).to have_content 'All threads must be resolved' end diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb index fc78b5b576994c..5cb12544066a71 100644 --- a/spec/features/projects/settings/visibility_settings_spec.rb +++ b/spec/features/projects/settings/visibility_settings_spec.rb @@ -28,26 +28,12 @@ expect(visibility_select_container).to have_content 'Only accessible by project members. Membership must be explicitly granted to each user.' end - context 'merge requests select' do - it 'hides merge requests section' do - find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click - - expect(page).to have_selector('.merge-requests-feature', visible: false) - end - - context 'given project with merge_requests_disabled access level' do - let(:project) { create(:project, :merge_requests_disabled, namespace: user.namespace) } - - it 'hides merge requests section' do - expect(page).to have_selector('.merge-requests-feature', visible: false) - end - end - end - context 'builds select' do it 'hides builds select section' do find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click + visit project_settings_merge_requests_path(project) + expect(page).to have_selector('.builds-feature', visible: false) end @@ -55,6 +41,8 @@ let(:project) { create(:project, :builds_disabled, namespace: user.namespace) } it 'hides builds select section' do + visit project_settings_merge_requests_path(project) + expect(page).to have_selector('.builds-feature', visible: false) end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 9dcf3c5ab99bfe..cbd9340b73712f 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -418,8 +418,7 @@ visit path end - it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]' }, - { form: '.rspec-merge-request-settings', input: '#project_printing_merge_request_link_enabled' }] + it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]' }] end describe 'view for a user without an access to a repo' do diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb index 904b9f041b1d59..0733e0c6521c0c 100644 --- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb @@ -133,6 +133,12 @@ end end + describe 'Merge requests' do + let(:item_id) { :merge_requests } + + it_behaves_like 'access rights checks' + end + describe 'Packages and registries' do let(:item_id) { :packages_and_registries } let(:packages_enabled) { false } diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 6543fc327d2976..3b89ecf89958d3 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -109,6 +109,7 @@ _('Webhooks'), _('Access Tokens'), _('Repository'), + _('Merge requests'), _('CI/CD'), _('Packages and registries'), _('Monitor'), diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb index a85ddf7a005e6d..2935e4395ba804 100644 --- a/spec/views/projects/edit.html.haml_spec.rb +++ b/spec/views/projects/edit.html.haml_spec.rb @@ -28,62 +28,6 @@ end end - context 'merge suggestions settings' do - it 'displays a placeholder if none is set' do - render - - expect(rendered).to have_field('project[suggestion_commit_message]', placeholder: "Apply %{suggestions_count} suggestion(s) to %{files_count} file(s)") - end - - it 'displays the user entered value' do - project.update!(suggestion_commit_message: 'refactor: changed %{file_paths}') - - render - - expect(rendered).to have_field('project[suggestion_commit_message]', with: 'refactor: changed %{file_paths}') - end - end - - context 'merge commit template' do - it 'displays default template if none is set' do - render - - expect(rendered).to have_field('project[merge_commit_template_or_default]', with: <<~MSG.rstrip) - Merge branch '%{source_branch}' into '%{target_branch}' - - %{title} - - %{issues} - - See merge request %{reference} - MSG - end - - it 'displays the user entered value' do - project.update!(merge_commit_template: '%{title}') - - render - - expect(rendered).to have_field('project[merge_commit_template_or_default]', with: '%{title}') - end - end - - context 'squash template' do - it 'displays default template if none is set' do - render - - expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{title}') - end - - it 'displays the user entered value' do - project.update!(squash_commit_template: '%{first_multiline_commit}') - - render - - expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{first_multiline_commit}') - end - end - context 'forking' do before do assign(:project, project) diff --git a/spec/views/projects/settings/merge_requests/show.html.haml_spec.rb b/spec/views/projects/settings/merge_requests/show.html.haml_spec.rb new file mode 100644 index 00000000000000..821f430eb10ac0 --- /dev/null +++ b/spec/views/projects/settings/merge_requests/show.html.haml_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'projects/settings/merge_requests/show' do + include Devise::Test::ControllerHelpers + include ProjectForksHelper + + let(:project) { create(:project) } + let(:user) { create(:admin) } + + before do + assign(:project, project) + + allow(controller).to receive(:current_user).and_return(user) + allow(view).to receive_messages(current_user: user, + can?: true, + current_application_settings: Gitlab::CurrentSettings.current_application_settings) + end + + describe 'merge suggestions settings' do + it 'displays a placeholder if none is set' do + render + + placeholder = "Apply %{suggestions_count} suggestion(s) to %{files_count} file(s)" + + expect(rendered).to have_field('project[suggestion_commit_message]', placeholder: placeholder) + end + + it 'displays the user entered value' do + project.update!(suggestion_commit_message: 'refactor: changed %{file_paths}') + + render + + expect(rendered).to have_field('project[suggestion_commit_message]', with: 'refactor: changed %{file_paths}') + end + end + + describe 'merge commit template' do + it 'displays default template if none is set' do + render + + expect(rendered).to have_field('project[merge_commit_template_or_default]', with: <<~MSG.rstrip) + Merge branch '%{source_branch}' into '%{target_branch}' + + %{title} + + %{issues} + + See merge request %{reference} + MSG + end + + it 'displays the user entered value' do + project.update!(merge_commit_template: '%{title}') + + render + + expect(rendered).to have_field('project[merge_commit_template_or_default]', with: '%{title}') + end + end + + describe 'squash template' do + it 'displays default template if none is set' do + render + + expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{title}') + end + + it 'displays the user entered value' do + project.update!(squash_commit_template: '%{first_multiline_commit}') + + render + + expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{first_multiline_commit}') + end + end +end -- GitLab From 0376a74c924d0d814714f620599ba64442d9f978 Mon Sep 17 00:00:00 2001 From: Shinya Maeda <shinya@gitlab.com> Date: Tue, 6 Sep 2022 15:03:10 +0900 Subject: [PATCH 099/169] Graphql query for environment information This commit extends the GraphQL query for fetching environment's essential information, that is currently missing. Changelog: added --- app/graphql/types/environment_type.rb | 25 ++++++++++ doc/api/graphql/reference/index.md | 7 +++ spec/graphql/types/environment_type_spec.rb | 1 + .../api/graphql/project/environments_spec.rb | 48 +++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 spec/requests/api/graphql/project/environments_spec.rb diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb index 403c4015218cc8..f6af1608213131 100644 --- a/app/graphql/types/environment_type.rb +++ b/app/graphql/types/environment_type.rb @@ -21,9 +21,30 @@ class EnvironmentType < BaseObject field :path, GraphQL::Types::String, null: false, description: 'Path to the environment.' + field :slug, GraphQL::Types::String, + description: 'Slug of the environment.' + field :external_url, GraphQL::Types::String, null: true, description: 'External URL of the environment.' + field :created_at, Types::TimeType, + description: 'When the environment was created.' + + field :updated_at, Types::TimeType, + description: 'When the environment was updated.' + + field :auto_stop_at, Types::TimeType, + description: 'When the environment is going to be stopped automatically.' + + field :auto_delete_at, Types::TimeType, + description: 'When the environment is going to be deleted automatically.' + + field :tier, Types::DeploymentTierEnum, + description: 'Deployment tier of the environment.' + + field :environment_type, GraphQL::Types::String, + description: 'Folder name of the environment.' + field :metrics_dashboard, Types::Metrics::DashboardType, null: true, description: 'Metrics dashboard schema for the environment.', resolver: Resolvers::Metrics::DashboardResolver @@ -41,5 +62,9 @@ class EnvironmentType < BaseObject description: 'Deployments of the environment.', resolver: Resolvers::DeploymentsResolver, complexity: 150 + + def tier + object.tier.to_sym + end end end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f1ed7c5fc04964..b8a2a7b9c592f3 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -11539,12 +11539,19 @@ Describes where code is deployed for a project. | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="environmentautodeleteat"></a>`autoDeleteAt` | [`Time`](#time) | When the environment is going to be deleted automatically. | +| <a id="environmentautostopat"></a>`autoStopAt` | [`Time`](#time) | When the environment is going to be stopped automatically. | +| <a id="environmentcreatedat"></a>`createdAt` | [`Time`](#time) | When the environment was created. | +| <a id="environmentenvironmenttype"></a>`environmentType` | [`String`](#string) | Folder name of the environment. | | <a id="environmentexternalurl"></a>`externalUrl` | [`String`](#string) | External URL of the environment. | | <a id="environmentid"></a>`id` | [`ID!`](#id) | ID of the environment. | | <a id="environmentlatestopenedmostseverealert"></a>`latestOpenedMostSevereAlert` | [`AlertManagementAlert`](#alertmanagementalert) | Most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned. | | <a id="environmentname"></a>`name` | [`String!`](#string) | Human-readable name of the environment. | | <a id="environmentpath"></a>`path` | [`String!`](#string) | Path to the environment. | +| <a id="environmentslug"></a>`slug` | [`String`](#string) | Slug of the environment. | | <a id="environmentstate"></a>`state` | [`String!`](#string) | State of the environment, for example: available/stopped. | +| <a id="environmenttier"></a>`tier` | [`DeploymentTier`](#deploymenttier) | Deployment tier of the environment. | +| <a id="environmentupdatedat"></a>`updatedAt` | [`Time`](#time) | When the environment was updated. | #### Fields with arguments diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb index f652ba2a470614..f57e6ab3ce7ae9 100644 --- a/spec/graphql/types/environment_type_spec.rb +++ b/spec/graphql/types/environment_type_spec.rb @@ -8,6 +8,7 @@ it 'has the expected fields' do expected_fields = %w[ name id state metrics_dashboard latest_opened_most_severe_alert path external_url deployments + slug createdAt updatedAt autoStopAt autoDeleteAt tier environmentType ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb new file mode 100644 index 00000000000000..5bc3ae2563b98c --- /dev/null +++ b/spec/requests/api/graphql/project/environments_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project Environments query' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :private, :repository) } + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + + subject { post_graphql(query, current_user: user) } + + let(:user) { developer } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + slug + createdAt + updatedAt + autoStopAt + autoDeleteAt + tier + environmentType + } + } + } + ) + end + + it 'returns the specified fields of the environment', :aggregate_failures do + environment.update!(auto_stop_at: 1.day.ago, auto_delete_at: 2.days.ago, environment_type: 'review') + + subject + + environment_data = graphql_data.dig('project', 'environment') + expect(environment_data['slug']).to eq(environment.slug) + expect(environment_data['createdAt']).to eq(environment.created_at.iso8601) + expect(environment_data['updatedAt']).to eq(environment.updated_at.iso8601) + expect(environment_data['autoStopAt']).to eq(environment.auto_stop_at.iso8601) + expect(environment_data['autoDeleteAt']).to eq(environment.auto_delete_at.iso8601) + expect(environment_data['tier']).to eq(environment.tier.upcase) + expect(environment_data['environmentType']).to eq(environment.environment_type) + end +end -- GitLab From 3ea38fac153e2b509e3524fccbd34b364432dc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= <ayufan@ayufan.eu> Date: Tue, 6 Sep 2022 11:55:54 +0200 Subject: [PATCH 100/169] Make `GITALY_SERVER_VERSION` approvable by maintainers Currently `GITALY_SERVER_VERSION` can only be approved by @project_278964_bot6 which cases an issue if the Gitaly version update has an issue with Rails compatibility. This makes it impossible to fix, as only the release-bot user can approve, and non of maintainers or delivery members. This fixes that by allowing the version update by maintainers and delivery members which was the pattern used before. --- .gitlab/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index ec434585030144..72498138e34c83 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -9,7 +9,7 @@ docs/CODEOWNERS @clefelhocz1 @timzallmann @cdu1 @whaber @dsatcher @sgoldstein @j .gitlab/CODEOWNERS @clefelhocz1 @timzallmann @cdu1 @whaber @dsatcher @sgoldstein @jeromezng @stanhu ## Allows release tooling to update the Gitaly Version -GITALY_SERVER_VERSION @project_278964_bot6 +GITALY_SERVER_VERSION @project_278964_bot6 @gitlab-org/maintainers/rails-backend @gitlab-org/delivery ## Excludes documentation markdown file changes from required approval /doc/*.md -- GitLab From 7f8af7a6ff7fc6ab49feedd8235099c1ed39e8c1 Mon Sep 17 00:00:00 2001 From: Justin Ho <hduong@gitlab.com> Date: Tue, 6 Sep 2022 17:22:19 +0700 Subject: [PATCH 101/169] Refactor code and specs after review - Revert back to using computed prop for improved readability. - Refactor some code to be a bit cleaner. - Change specs to use actual string. --- .../jira_connect/subscriptions/components/app.vue | 3 +-- .../pages/sign_in/sign_in_gitlab_multiversion/index.vue | 7 +++++-- app/assets/javascripts/jira_connect/subscriptions/utils.js | 2 +- .../subscriptions/components/sign_in_oauth_button_spec.js | 6 +----- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue index fa2a5909cf75af..22a6c0751f486f 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue @@ -83,8 +83,7 @@ export default { * if the jiraConnectOauth flag is enabled. */ fetchSubscriptionsOauth() { - if (!this.isOauthEnabled) return; - if (!this.userSignedIn) return; + if (!this.isOauthEnabled || !this.userSignedIn) return; this.fetchSubscriptions(this.subscriptionsPath); }, diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue index dd8c6a8c6ce089..64cd7793925e1f 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue @@ -18,8 +18,11 @@ export default { }; }, computed: { + hasSelectedVersion() { + return this.gitlabBasePath !== null; + }, subtitle() { - return this.gitlabBasePath + return this.hasSelectedVersion ? this.$options.i18n.signInSubtitle : this.$options.i18n.versionSelectSubtitle; }, @@ -51,7 +54,7 @@ export default { <p data-testid="subtitle">{{ subtitle }}</p> </div> - <version-select-form v-if="!gitlabBasePath" class="gl-mt-7" @submit="onVersionSelect" /> + <version-select-form v-if="!hasSelectedVersion" class="gl-mt-7" @submit="onVersionSelect" /> <div v-else class="gl-text-center"> <sign-in-oauth-button diff --git a/app/assets/javascripts/jira_connect/subscriptions/utils.js b/app/assets/javascripts/jira_connect/subscriptions/utils.js index dd845d21d873ff..6db8b62d692ac8 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/utils.js +++ b/app/assets/javascripts/jira_connect/subscriptions/utils.js @@ -3,7 +3,7 @@ import { objectToQuery } from '~/lib/utils/url_utility'; import { ALERT_LOCALSTORAGE_KEY, BASE_URL_LOCALSTORAGE_KEY } from './constants'; const isFunction = (fn) => typeof fn === 'function'; -const canUseLocalStorage = () => AccessorUtilities.canUseLocalStorage(); +const { canUseLocalStorage } = AccessorUtilities; const persistToStorage = (key, payload) => { localStorage.setItem(key, payload); diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js index 94e5c5b6b9655e..951adb006f873e 100644 --- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js @@ -2,12 +2,10 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; -import { sprintf } from '~/locale'; import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue'; import { I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, - I18N_CUSTOM_SIGN_IN_BUTTON_TEXT, OAUTH_WINDOW_OPTIONS, } from '~/jira_connect/subscriptions/constants'; import axios from '~/lib/utils/axios_utils'; @@ -81,9 +79,7 @@ describe('SignInOauthButton', () => { }, }); - expect(findButton().text()).toBe( - sprintf(I18N_CUSTOM_SIGN_IN_BUTTON_TEXT, { url: mockBasePath }), - ); + expect(findButton().text()).toBe(`Sign in to ${mockBasePath}`); }); }); -- GitLab From 34f950ced268acc38a670e1963b51ed3b28c6d9f Mon Sep 17 00:00:00 2001 From: Omar Qunsul <oqunsul@gitlab.com> Date: Tue, 30 Aug 2022 18:49:13 +0200 Subject: [PATCH 102/169] Handling Database Timeouts in gitlab:db:truncate_legacy_tables:* Currently the rake tasks can fail due to the possibility of statement_timeout or lock_timeout This MR sets both timeouts to 0, to avoid any timeouts. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/372144 Changelog: fixed --- lib/gitlab/database/tables_truncate.rb | 29 ++++++++++++------- .../gitlab/database/tables_truncate_spec.rb | 14 +++++---- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/gitlab/database/tables_truncate.rb b/lib/gitlab/database/tables_truncate.rb index b629bff52ce78a..164520fbab3c55 100644 --- a/lib/gitlab/database/tables_truncate.rb +++ b/lib/gitlab/database/tables_truncate.rb @@ -61,12 +61,10 @@ def execute def truncate_tables_in_batches(connection, tables_sorted, min_batch_size) truncated_tables = [] - unless dry_run - tables_sorted.flatten.compact.each do |table| - sql_statement = "SELECT set_config('lock_writes.#{table}', 'false', false)" - logger&.info sql_statement - connection.execute(sql_statement) - end + tables_sorted.flatten.each do |table| + sql_statement = "SELECT set_config('lock_writes.#{table}', 'false', false)" + logger&.info(sql_statement) + connection.execute(sql_statement) unless dry_run end # We do the truncation in stages to avoid high IO @@ -74,14 +72,23 @@ def truncate_tables_in_batches(connection, tables_sorted, min_batch_size) # tables before. That's because PostgreSQL doesn't allow to truncate any table (A) # without truncating any other table (B) that has a Foreign Key pointing to the table (A). # even if table (B) is empty, because it has been already truncated in a previous stage. - tables_sorted.in_groups_of(min_batch_size).each do |tables_groups| - new_tables_to_truncate = tables_groups.flatten.compact + tables_sorted.in_groups_of(min_batch_size, false).each do |tables_groups| + new_tables_to_truncate = tables_groups.flatten logger&.info "= New tables to truncate: #{new_tables_to_truncate.join(', ')}" truncated_tables.push(*new_tables_to_truncate).tap(&:sort!) - sql_statement = "TRUNCATE TABLE #{truncated_tables.join(', ')} RESTRICT" + sql_statements = [ + "SET LOCAL statement_timeout = 0", + "SET LOCAL lock_timeout = 0", + "TRUNCATE TABLE #{truncated_tables.join(', ')} RESTRICT" + ] - logger&.info sql_statement - connection.execute(sql_statement) unless dry_run + sql_statements.each { |sql_statement| logger&.info(sql_statement) } + + next if dry_run + + connection.transaction do + sql_statements.each { |sql_statement| connection.execute(sql_statement) } + end end end end diff --git a/spec/lib/gitlab/database/tables_truncate_spec.rb b/spec/lib/gitlab/database/tables_truncate_spec.rb index 08f4ee50ef185d..01af9efd7826ca 100644 --- a/spec/lib/gitlab/database/tables_truncate_spec.rb +++ b/spec/lib/gitlab/database/tables_truncate_spec.rb @@ -53,8 +53,8 @@ ); SQL - ApplicationRecord.connection.execute(main_tables_sql) - Ci::ApplicationRecord.connection.execute(main_tables_sql) + main_connection.execute(main_tables_sql) + ci_connection.execute(main_tables_sql) ci_tables_sql = <<~SQL CREATE TABLE _test_gitlab_ci_items (id serial NOT NULL PRIMARY KEY); @@ -66,15 +66,15 @@ ); SQL - ApplicationRecord.connection.execute(ci_tables_sql) - Ci::ApplicationRecord.connection.execute(ci_tables_sql) + main_connection.execute(ci_tables_sql) + ci_connection.execute(ci_tables_sql) internal_tables_sql = <<~SQL CREATE TABLE _test_gitlab_shared_items (id serial NOT NULL PRIMARY KEY); SQL - ApplicationRecord.connection.execute(internal_tables_sql) - Ci::ApplicationRecord.connection.execute(internal_tables_sql) + main_connection.execute(internal_tables_sql) + ci_connection.execute(internal_tables_sql) # Filling the tables 5.times do |i| @@ -138,6 +138,8 @@ end it 'logs the sql statements to the logger' do + expect(logger).to receive(:info).with("SET LOCAL lock_timeout = 0") + expect(logger).to receive(:info).with("SET LOCAL statement_timeout = 0") expect(logger).to receive(:info) .with(/TRUNCATE TABLE #{legacy_tables_models.map(&:table_name).sort.join(', ')} RESTRICT/) truncate_legacy_tables -- GitLab From 7292eba75b6d43f18330803fa82af1851efa215a Mon Sep 17 00:00:00 2001 From: Jannik Lehmann <jlehmann@gitlab.com> Date: Tue, 6 Sep 2022 12:14:25 +0000 Subject: [PATCH 103/169] Revert "Remove can_generate_codequality_reports" This reverts commit 0a347aa7a5a2223652067a2f25a5f54c969fc79c. --- .../javascripts/pipelines/pipeline_tabs.js | 9 ++ app/helpers/projects/pipeline_helper.rb | 1 - .../codequality_report/codequality_report.vue | 14 +-- .../codequality_report_graphql.vue | 5 + .../pipelines/components/pipeline_tabs.vue | 37 +++++- ee/app/helpers/ee/projects/pipeline_helper.rb | 20 ++- .../pipelines/legacy_pipeline_spec.rb | 115 ++++++++++++++++++ .../projects/pipelines/pipeline_spec.rb | 5 - .../codequality_report_spec.js | 1 + .../components/pipeline_tabs_spec.js | 47 +++++-- .../ee/projects/pipeline_helper_spec.rb | 75 +++++++++++- spec/helpers/projects/pipeline_helper_spec.rb | 4 +- 12 files changed, 300 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/pipelines/pipeline_tabs.js b/app/assets/javascripts/pipelines/pipeline_tabs.js index 7051d356089c7d..508f188c229925 100644 --- a/app/assets/javascripts/pipelines/pipeline_tabs.js +++ b/app/assets/javascripts/pipelines/pipeline_tabs.js @@ -20,6 +20,8 @@ export const createAppOptions = (selector, apolloProvider) => { const { canGenerateCodequalityReports, codequalityReportDownloadPath, + codequalityBlobPath, + codequalityProjectPath, downloadablePathForReportType, exposeSecurityDashboard, exposeLicenseScanningData, @@ -40,9 +42,12 @@ export const createAppOptions = (selector, apolloProvider) => { hasTestReport, emptyStateImagePath, artifactsExpiredImagePath, + isFullCodequalityReportAvailable, testsCount, } = dataset; + // TODO remove projectPath variable once https://gitlab.com/gitlab-org/gitlab/-/issues/371641 is resolved + const projectPath = fullPath; const defaultTabValue = getPipelineDefaultTab(window.location.href); return { @@ -63,6 +68,10 @@ export const createAppOptions = (selector, apolloProvider) => { provide: { canGenerateCodequalityReports: parseBoolean(canGenerateCodequalityReports), codequalityReportDownloadPath, + codequalityBlobPath, + codequalityProjectPath, + isFullCodequalityReportAvailable: parseBoolean(isFullCodequalityReportAvailable), + projectPath, defaultTabValue, downloadablePathForReportType, exposeSecurityDashboard: parseBoolean(exposeSecurityDashboard), diff --git a/app/helpers/projects/pipeline_helper.rb b/app/helpers/projects/pipeline_helper.rb index 5f2a9f7bf21e87..c72beb4d722764 100644 --- a/app/helpers/projects/pipeline_helper.rb +++ b/app/helpers/projects/pipeline_helper.rb @@ -6,7 +6,6 @@ module PipelineHelper def js_pipeline_tabs_data(project, pipeline, _user) { - can_generate_codequality_reports: pipeline.can_generate_codequality_reports?.to_json, failed_jobs_count: pipeline.failed_builds.count, failed_jobs_summary: prepare_failed_jobs_summary_data(pipeline.failed_builds), full_path: project.full_path, diff --git a/ee/app/assets/javascripts/codequality_report/codequality_report.vue b/ee/app/assets/javascripts/codequality_report/codequality_report.vue index ab52617769ed42..b21e86b0742d1c 100644 --- a/ee/app/assets/javascripts/codequality_report/codequality_report.vue +++ b/ee/app/assets/javascripts/codequality_report/codequality_report.vue @@ -14,26 +14,21 @@ export default { }, mixins: [reportsMixin], props: { - // Todo make these props mandatory once the pipeline tabs component is implemented https://gitlab.com/gitlab-org/gitlab/-/issues/360797 endpoint: { type: String, - required: false, - default: '', + required: true, }, blobPath: { type: String, - required: false, - default: '', + required: true, }, projectPath: { type: String, - required: false, - default: '', + required: true, }, pipelineIid: { type: String, - required: false, - default: '', + required: true, }, }, componentNames, @@ -50,6 +45,7 @@ export default { codequalityText() { const text = []; const { codequalityIssueTotal } = this; + this.$emit('updateBadgeCount', codequalityIssueTotal); if (codequalityIssueTotal === 0) { return s__('ciReport|No code quality issues found'); diff --git a/ee/app/assets/javascripts/codequality_report/codequality_report_graphql.vue b/ee/app/assets/javascripts/codequality_report/codequality_report_graphql.vue index bea43ead682754..15d453b4f48abb 100644 --- a/ee/app/assets/javascripts/codequality_report/codequality_report_graphql.vue +++ b/ee/app/assets/javascripts/codequality_report/codequality_report_graphql.vue @@ -97,6 +97,11 @@ export default { return this.checkReportStatus(this.isLoading && !this.hasCodequalityViolations, this.errored); }, }, + watch: { + codequalityViolations() { + this.$emit('updateBadgeCount', this.codequalityViolations.count); + }, + }, i18n: { subHeading: s__('ciReport|This report contains all Code Quality issues in the source branch.'), loadingText: s__('ciReport|Loading Code Quality report'), diff --git a/ee/app/assets/javascripts/pipelines/components/pipeline_tabs.vue b/ee/app/assets/javascripts/pipelines/components/pipeline_tabs.vue index a27e274904b886..4b6599bd9e6f54 100644 --- a/ee/app/assets/javascripts/pipelines/components/pipeline_tabs.vue +++ b/ee/app/assets/javascripts/pipelines/components/pipeline_tabs.vue @@ -35,24 +35,31 @@ export default { mixins: [glFeatureFlagMixin()], inject: [ 'canGenerateCodequalityReports', + 'canManageLicenses', + 'codequalityBlobPath', 'codequalityReportDownloadPath', + 'codequalityProjectPath', 'defaultTabValue', 'exposeSecurityDashboard', 'exposeLicenseScanningData', - 'licenseManagementApiUrl', + 'isFullCodequalityReportAvailable', 'licensesApiPath', + 'licenseManagementApiUrl', 'licenseManagementSettingsPath', - 'canManageLicenses', + 'pipelineIid', ], data() { - return { licenseCount: 0 }; + return { licenseCount: 0, codeQualityCount: 0 }; }, computed: { isGraphqlCodeQuality() { return this.glFeatures.graphqlCodeQualityFullReport; }, showCodeQualityTab() { - return Boolean(this.codequalityReportDownloadPath || this.canGenerateCodequalityReports); + return Boolean( + this.isFullCodequalityReportAvailable && + (this.codequalityReportDownloadPath || this.canGenerateCodequalityReports), + ); }, showLicenseTab() { return Boolean(this.exposeLicenseScanningData); @@ -68,6 +75,9 @@ export default { updateLicenseCount(count) { this.licenseCount = count; }, + updateCodeQualityCount(count) { + this.codeQualityCount = count; + }, }, }; </script> @@ -111,8 +121,23 @@ export default { data-track-action="click_button" data-track-label="get_codequality_report" > - <codequality-report-app-graphql v-if="isGraphqlCodeQuality" /> - <codequality-report-app v-else /> + <template #title> + <span class="gl-mr-2">{{ $options.i18n.tabs.codeQualityTitle }}</span> + <gl-badge size="sm" data-testid="codequality-counter">{{ codeQualityCount }}</gl-badge> + </template> + + <codequality-report-app-graphql + v-if="isGraphqlCodeQuality" + @updateBadgeCount="updateCodeQualityCount" + /> + <codequality-report-app + v-else + :endpoint="codequalityReportDownloadPath" + :blob-path="codequalityBlobPath" + :project-path="codequalityProjectPath" + :pipeline-iid="pipelineIid" + @updateBadgeCount="updateCodeQualityCount" + /> </gl-tab> </base-pipeline-tabs> </template> diff --git a/ee/app/helpers/ee/projects/pipeline_helper.rb b/ee/app/helpers/ee/projects/pipeline_helper.rb index 0b4737c45a0afb..09b8a42b361f57 100644 --- a/ee/app/helpers/ee/projects/pipeline_helper.rb +++ b/ee/app/helpers/ee/projects/pipeline_helper.rb @@ -8,14 +8,18 @@ module PipelineHelper override :js_pipeline_tabs_data def js_pipeline_tabs_data(project, pipeline, user) super.merge( + can_generate_codequality_reports: pipeline.can_generate_codequality_reports?.to_json, + can_manage_licenses: user&.can?(:admin_software_license_policy, project).to_s, codequality_report_download_path: codequality_report_download_path(project, pipeline), + codequality_blob_path: codequality_blob_path(project, pipeline), + codequality_project_path: codequality_project_path(project, pipeline), expose_license_scanning_data: pipeline.expose_license_scanning_data?.to_json, expose_security_dashboard: pipeline.expose_security_dashboard?.to_json, - vulnerability_report_data: vulnerability_report_data(project, pipeline, user).to_json, + is_full_codequality_report_available: project.licensed_feature_available?(:full_codequality_report).to_json, license_management_api_url: license_management_api_url(project), license_management_settings_path: license_management_path(user, project), licenses_api_path: licenses_api_path(project, pipeline), - can_manage_licenses: user&.can?(:admin_software_license_policy, project).to_s + vulnerability_report_data: vulnerability_report_data(project, pipeline, user).to_json ) end @@ -31,6 +35,18 @@ def licenses_api_path(project, pipeline) end end + def codequality_blob_path(project, pipeline) + return unless project.licensed_feature_available?(:full_codequality_report) + + project_blob_path(project, pipeline) + end + + def codequality_project_path(project, pipeline) + return unless project.licensed_feature_available?(:full_codequality_report) + + project_path(project, pipeline) + end + def codequality_report_download_path(project, pipeline) return unless project.licensed_feature_available?(:full_codequality_report) diff --git a/ee/spec/features/projects/pipelines/legacy_pipeline_spec.rb b/ee/spec/features/projects/pipelines/legacy_pipeline_spec.rb index 46be7c786bd3a6..4a420ce47fea49 100644 --- a/ee/spec/features/projects/pipelines/legacy_pipeline_spec.rb +++ b/ee/spec/features/projects/pipelines/legacy_pipeline_spec.rb @@ -249,6 +249,121 @@ end end + describe 'GET /:project/-/pipelines/:id/codequality_report', :aggregate_failures do + shared_examples_for 'full codequality report' do + context 'when licensed' do + before do + stub_licensed_features(full_codequality_report: true) + end + + context 'with code quality artifact' do + before do + create(:ee_ci_build, :codequality, pipeline: pipeline) + end + + context 'when navigating directly to the code quality tab' do + before do + visit codequality_report_project_pipeline_path(project, pipeline) + end + + it_behaves_like 'an active code quality tab' + end + + context 'when starting from the pipeline tab' do + before do + visit project_pipeline_path(project, pipeline) + end + + it 'shows the code quality tab as inactive' do + expect(page).to have_content('Code Quality') + expect(page).not_to have_css('#js-tab-codequality') + end + + context 'when the code quality tab is clicked' do + before do + click_link 'Code Quality' + end + + it_behaves_like 'an active code quality tab' + end + end + end + + context 'with no code quality artifact' do + before do + create(:ee_ci_build, pipeline: pipeline) + visit project_pipeline_path(project, pipeline) + end + + it 'does not show code quality tab' do + expect(page).not_to have_content('Code Quality') + expect(page).not_to have_css('#js-tab-codequality') + end + end + end + + context 'when unlicensed' do + before do + stub_licensed_features(full_codequality_report: false) + + create(:ee_ci_build, :codequality, pipeline: pipeline) + visit project_pipeline_path(project, pipeline) + end + + it 'does not show code quality tab' do + expect(page).not_to have_content('Code Quality') + expect(page).not_to have_css('#js-tab-codequality') + end + end + end + + shared_examples_for 'an active code quality tab' do + it 'shows code quality tab pane as active, quality issue with link to file, and events for data tracking' do + expect(page).to have_content('Code Quality') + expect(page).to have_css('#js-tab-codequality') + + expect(page).to have_content('Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.') + expect(find_link('foo.rb:10')[:href]).to end_with("#{project_blob_path( + project, + File.join(pipeline.commit.id, 'foo.rb') + )}#L10") + + expect(page).to have_selector('[data-track-action="click_button"]') + expect(page).to have_selector('[data-track-label="get_codequality_report"]') + end + end + + context 'for a branch pipeline' do + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } + + it_behaves_like 'full codequality report' + end + + context 'for a merge request pipeline' do + let(:merge_request) do + create(:merge_request, + :with_merge_request_pipeline, + source_project: project, + target_project: project, + merge_sha: project.commit.id) + end + + let(:pipeline) do + merge_request.all_pipelines.last + end + + it_behaves_like 'full codequality report' + end + + context 'with graphql feature flag disabled' do + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } + + stub_feature_flags(graphql_code_quality_full_report: false) + + it_behaves_like 'full codequality report' + end + end + private def create_link(source_pipeline, pipeline) diff --git a/ee/spec/features/projects/pipelines/pipeline_spec.rb b/ee/spec/features/projects/pipelines/pipeline_spec.rb index 6c8a052025da17..8424b3443812c3 100644 --- a/ee/spec/features/projects/pipelines/pipeline_spec.rb +++ b/ee/spec/features/projects/pipelines/pipeline_spec.rb @@ -205,10 +205,6 @@ end describe 'GET /:project/-/pipelines/:id/codequality_report', :aggregate_failures do - before do - stub_feature_flags(pipeline_tabs_vue: false) - end - shared_examples_for 'full codequality report' do context 'when licensed' do before do @@ -279,7 +275,6 @@ shared_examples_for 'an active code quality tab' do it 'shows code quality tab pane as active, quality issue with link to file, and events for data tracking' do expect(page).to have_content('Code Quality') - expect(page).to have_css('#js-tab-codequality') expect(page).to have_content('Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.') expect(find_link('foo.rb:10')[:href]).to end_with(project_blob_path(project, File.join(pipeline.commit.id, 'foo.rb')) + '#L10') diff --git a/ee/spec/frontend/codequality_report/codequality_report_spec.js b/ee/spec/frontend/codequality_report/codequality_report_spec.js index ffb97a62a02a55..e6a9a9dbb13a32 100644 --- a/ee/spec/frontend/codequality_report/codequality_report_spec.js +++ b/ee/spec/frontend/codequality_report/codequality_report_spec.js @@ -87,6 +87,7 @@ describe('Codequality report app', () => { expect(findStatus().text()).toContain( `This report contains all Code Quality issues in the source branch.`, ); + expect(wrapper.emitted().updateBadgeCount).toBeDefined(); expect(wrapper.findAll('.report-block-list-issue')).toHaveLength(expectedIssueTotal); }); diff --git a/ee/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/ee/spec/frontend/pipelines/components/pipeline_tabs_spec.js index 9ad3f9711d1f64..ac3de96021ae31 100644 --- a/ee/spec/frontend/pipelines/components/pipeline_tabs_spec.js +++ b/ee/spec/frontend/pipelines/components/pipeline_tabs_spec.js @@ -27,13 +27,18 @@ describe('The Pipeline Tabs', () => { const findSecurityApp = () => wrapper.findComponent(PipelineSecurityDashboard); const getLicenseCount = () => wrapper.findByTestId('license-counter').text(); + const getCodequalityCount = () => wrapper.findByTestId('codequality-counter').text(); const defaultProvide = { canGenerateCodequalityReports: false, codequalityReportDownloadPath: '', + codequalityProjectPath: '', + codequalityBlobPath: '', + pipelineIid: '0', defaultTabValue: '', exposeSecurityDashboard: false, exposeLicenseScanningData: false, + isFullCodequalityReportAvailable: true, licenseManagementApiUrl: '/path/to/license_management_api_url', licensesApiPath: '/path/to/licenses_api', licenseManagementSettingsPath: '/path/to/license_management_settings', @@ -123,6 +128,7 @@ describe('The Pipeline Tabs', () => { graphqlCodeQualityFullReport: true, }, }, + stubs: { GlTab }, }); }); @@ -130,6 +136,16 @@ describe('The Pipeline Tabs', () => { expect(findCodeQualityAppGraphql().exists()).toBe(true); expect(findCodeQualityApp().exists()).toBe(false); }); + + it('updates the codequality badge after a new count has been emitted', async () => { + const newLicenseCount = 100; + expect(getCodequalityCount()).toBe('0'); + + findCodeQualityAppGraphql().vm.$emit('updateBadgeCount', newLicenseCount); + await nextTick(); + + expect(getCodequalityCount()).toBe(`${newLicenseCount}`); + }); }); describe('with `graphqlCodeQualityFullReport` disabled', () => { @@ -141,6 +157,7 @@ describe('The Pipeline Tabs', () => { graphqlCodeQualityFullReport: false, }, }, + stubs: { GlTab }, }); }); @@ -148,20 +165,36 @@ describe('The Pipeline Tabs', () => { expect(findCodeQualityAppGraphql().exists()).toBe(false); expect(findCodeQualityApp().exists()).toBe(true); }); + + it('updates the codequality badge after a new count has been emitted', async () => { + const newLicenseCount = 100; + + expect(getCodequalityCount()).toBe('0'); + + findCodeQualityApp().vm.$emit('updateBadgeCount', newLicenseCount); + await nextTick(); + + expect(getCodequalityCount()).toBe(`${newLicenseCount}`); + }); }); }); it.each` - provideValue | isVisible | codequalityReportDownloadPath | text - ${true} | ${true} | ${''} | ${'shows'} - ${false} | ${false} | ${''} | ${'hides'} - ${false} | ${true} | ${'/path'} | ${'shows'} - ${true} | ${true} | ${'/path'} | ${'shows'} + provideValue | isVisible | codequalityReportDownloadPath | isReportAvailable | text + ${true} | ${true} | ${''} | ${true} | ${'shows'} + ${false} | ${false} | ${''} | ${true} | ${'hides'} + ${false} | ${true} | ${'/path'} | ${true} | ${'shows'} + ${true} | ${true} | ${'/path'} | ${true} | ${'shows'} + ${true} | ${false} | ${'/path'} | ${false} | ${'hides'} `( '$text Code Quality and its associated component when canGenerateCodequalityReports is $provideValue and codequalityReportDownloadPath is $codequalityReportDownloadPath', - ({ provideValue, isVisible, codequalityReportDownloadPath }) => { + ({ provideValue, isReportAvailable, isVisible, codequalityReportDownloadPath }) => { createComponent({ - provide: { canGenerateCodequalityReports: provideValue, codequalityReportDownloadPath }, + provide: { + isFullCodequalityReportAvailable: isReportAvailable, + canGenerateCodequalityReports: provideValue, + codequalityReportDownloadPath, + }, }); expect(findCodeQualityTab().exists()).toBe(isVisible); expect(findCodeQualityApp().exists()).toBe(isVisible); diff --git a/ee/spec/helpers/ee/projects/pipeline_helper_spec.rb b/ee/spec/helpers/ee/projects/pipeline_helper_spec.rb index 161557d348618f..39eabacc3a6680 100644 --- a/ee/spec/helpers/ee/projects/pipeline_helper_spec.rb +++ b/ee/spec/helpers/ee/projects/pipeline_helper_spec.rb @@ -20,13 +20,16 @@ it 'returns pipeline tabs data' do expect(pipeline_tabs_data).to include({ can_generate_codequality_reports: pipeline.can_generate_codequality_reports?.to_json, + can_manage_licenses: 'false', codequality_report_download_path: helper.codequality_report_download_path(project, pipeline), + codequality_blob_path: codequality_blob_path(project, pipeline), + codequality_project_path: codequality_project_path(project, pipeline), expose_license_scanning_data: pipeline.expose_license_scanning_data?.to_json, expose_security_dashboard: pipeline.expose_security_dashboard?.to_json, + is_full_codequality_report_available: project.licensed_feature_available?(:full_codequality_report).to_json, license_management_api_url: license_management_api_url(project), license_management_settings_path: helper.license_management_path(user, project), licenses_api_path: helper.licenses_api_path(project, pipeline), - can_manage_licenses: 'false', failed_jobs_count: pipeline.failed_builds.count, failed_jobs_summary: prepare_failed_jobs_summary_data(pipeline.failed_builds), full_path: project.full_path, @@ -55,6 +58,76 @@ end end + describe 'codequality_project_path' do + before do + project.add_developer(user) + end + + subject(:codequality_report_path) { helper.codequality_project_path(project, pipeline) } + + describe 'when `full_codequality_report` feature is not available' do + before do + stub_licensed_features(full_codequality_report: false) + end + + it 'returns nil' do + is_expected.to be(nil) + end + end + + describe 'when `full_code_quality_report` feature is available' do + before do + stub_licensed_features(full_codequality_report: true) + end + + describe 'and there is an artefact for codequality' do + before do + create(:ci_build, :codequality_report, pipeline: raw_pipeline) + end + + it 'returns the downloadable path for `codequality`' do + is_expected.not_to be(nil) + is_expected.to eq(project_path(project, pipeline)) + end + end + end + end + + describe 'codequality_blob_path' do + before do + project.add_developer(user) + end + + subject(:codequality_report_path) { helper.codequality_blob_path(project, pipeline) } + + describe 'when `full_codequality_report` feature is not available' do + before do + stub_licensed_features(full_codequality_report: false) + end + + it 'returns nil' do + is_expected.to be(nil) + end + end + + describe 'when `full_code_quality_report` feature is available' do + before do + stub_licensed_features(full_codequality_report: true) + end + + describe 'and there is an artefact for codequality' do + before do + create(:ci_build, :codequality_report, pipeline: raw_pipeline) + end + + it 'returns the downloadable path for `codequality`' do + is_expected.not_to be(nil) + is_expected.to eq(project_blob_path(project, pipeline)) + end + end + end + end + describe 'codequality_report_download_path' do before do project.add_developer(user) diff --git a/spec/helpers/projects/pipeline_helper_spec.rb b/spec/helpers/projects/pipeline_helper_spec.rb index 59fc278543ff43..a7d756a67a1273 100644 --- a/spec/helpers/projects/pipeline_helper_spec.rb +++ b/spec/helpers/projects/pipeline_helper_spec.rb @@ -19,7 +19,6 @@ it 'returns pipeline tabs data' do expect(pipeline_tabs_data).to include({ - can_generate_codequality_reports: pipeline.can_generate_codequality_reports?.to_json, failed_jobs_count: pipeline.failed_builds.count, failed_jobs_summary: prepare_failed_jobs_summary_data(pipeline.failed_builds), full_path: project.full_path, @@ -33,7 +32,8 @@ blob_path: project_blob_path(project, pipeline.sha), has_test_report: pipeline.has_reports?(Ci::JobArtifact.of_report_type(:test)), empty_state_image_path: match_asset_path('illustrations/empty-state/empty-test-cases-lg.svg'), - artifacts_expired_image_path: match_asset_path('illustrations/pipeline.svg') + artifacts_expired_image_path: match_asset_path('illustrations/pipeline.svg'), + tests_count: pipeline.test_report_summary.total[:count] }) end end -- GitLab From 3af90fb938bcaa0a8caecf70c85f0fc446d8cfd2 Mon Sep 17 00:00:00 2001 From: Huzaifa Iftikhar <hiftikhar@gitlab.com> Date: Tue, 6 Sep 2022 12:18:10 +0000 Subject: [PATCH 104/169] Remove feature flag `inactive_projects_deletion` Changelog: other --- app/helpers/projects_helper.rb | 1 - .../inactive_projects_deletion_cron_worker.rb | 2 - .../inactive_projects_deletion.yml | 8 -- .../inactive_project_deletion.md | 10 +- doc/api/settings.md | 2 +- ...tive_projects_deletion_cron_worker_spec.rb | 1 - spec/helpers/projects_helper_spec.rb | 32 ++----- ...tive_projects_deletion_cron_worker_spec.rb | 92 +++++++------------ 8 files changed, 46 insertions(+), 102 deletions(-) delete mode 100644 config/feature_flags/development/inactive_projects_deletion.yml diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 65259122417a42..a64a1285bfb102 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -440,7 +440,6 @@ def import_from_gitlab_message def show_inactive_project_deletion_banner?(project) return false unless project.present? && project.saved? return false unless delete_inactive_projects? - return false unless Feature.enabled?(:inactive_projects_deletion, project.root_namespace) project.inactive? end diff --git a/app/workers/projects/inactive_projects_deletion_cron_worker.rb b/app/workers/projects/inactive_projects_deletion_cron_worker.rb index a280c9203d6e10..ba6d44ec4a5508 100644 --- a/app/workers/projects/inactive_projects_deletion_cron_worker.rb +++ b/app/workers/projects/inactive_projects_deletion_cron_worker.rb @@ -39,8 +39,6 @@ def perform raise TimeoutError end - next unless Feature.enabled?(:inactive_projects_deletion, project.root_namespace) - with_context(project: project, user: admin_user) do deletion_warning_email_sent_on = notified_inactive_projects["project:#{project.id}"] diff --git a/config/feature_flags/development/inactive_projects_deletion.yml b/config/feature_flags/development/inactive_projects_deletion.yml deleted file mode 100644 index e9bb91f62cc174..00000000000000 --- a/config/feature_flags/development/inactive_projects_deletion.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: inactive_projects_deletion -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85689 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/357968 -milestone: '15.0' -type: development -group: group::compliance -default_enabled: false diff --git a/doc/administration/inactive_project_deletion.md b/doc/administration/inactive_project_deletion.md index 224b52d420ed4c..ed46996143e225 100644 --- a/doc/administration/inactive_project_deletion.md +++ b/doc/administration/inactive_project_deletion.md @@ -6,18 +6,16 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Inactive project deletion **(FREE SELF)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85689) in GitLab 15.0 [with a flag](../administration/feature_flags.md) named `inactive_projects_deletion`. Disabled by default. - -FLAG: -On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to -[enable the feature flag](../administration/feature_flags.md) named `inactive_projects_deletion`. -On GitLab.com, this feature is not available. This feature is not ready for production use. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85689) in GitLab 15.0 [with a flag](../administration/feature_flags.md) named `inactive_projects_deletion`. Disabled by default. +> - [Feature flag `inactive_projects_deletion`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96803) removed in GitLab 15.4. Administrators of large GitLab instances can find that over time, projects become inactive and are no longer used. These projects take up unnecessary disk space. With inactive project deletion, you can identify these projects, warn the maintainers ahead of time, and then delete the projects if they remain inactive. When an inactive project is deleted, the action generates an audit event that it was performed by the first active administrator. +For the default setting on GitLab.com, see the [GitLab.com settings page](../user/gitlab_com/index.md#inactive-project-deletion). + ## Configure inactive project deletion You can configure inactive projects deletion or turn it off using either: diff --git a/doc/api/settings.md b/doc/api/settings.md index 5e1be5bce51283..c3578f53c32e9c 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -286,7 +286,7 @@ listed in the descriptions of the relevant settings. | `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `delayed_project_deletion` **(PREMIUM SELF)** | boolean | no | Enable delayed project deletion by default in new groups. Default is `false`. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), can only be enabled when `delayed_group_deletion` is true. | | `delayed_group_deletion` **(PREMIUM SELF)** | boolean | no | Enable delayed group deletion. Default is `true`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352959) in GitLab 15.0. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), disables and locks the group-level setting for delayed protect deletion when set to `false`. | -| `delete_inactive_projects` | boolean | no | Enable inactive project deletion feature. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84519) in GitLab 14.10. [Became operational](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85689) in GitLab 15.0 (with feature flag `inactive_projects_deletion`, disabled by default). | +| `delete_inactive_projects` | boolean | no | Enable inactive project deletion feature. Default is `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84519) in GitLab 14.10. [Became operational without feature flag](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96803) in GitLab 15.4. | | `deletion_adjourned_period` **(PREMIUM SELF)** | integer | no | The number of days to wait before deleting a project or group that is marked for deletion. Value must be between `1` and `90`. Defaults to `7`. [From GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/issues/352960), a hook on `deletion_adjourned_period` sets the period to `1` on every update, and sets both `delayed_project_deletion` and `delayed_group_deletion` to `false` if the period is `0`. | | `diff_max_patch_bytes` | integer | no | Maximum [diff patch size](../user/admin_area/diff_limits.md), in bytes. | | `diff_max_files` | integer | no | Maximum [files in a diff](../user/admin_area/diff_limits.md). | diff --git a/ee/spec/workers/ee/projects/inactive_projects_deletion_cron_worker_spec.rb b/ee/spec/workers/ee/projects/inactive_projects_deletion_cron_worker_spec.rb index e479ffd30d0cfc..4f639b7444b114 100644 --- a/ee/spec/workers/ee/projects/inactive_projects_deletion_cron_worker_spec.rb +++ b/ee/spec/workers/ee/projects/inactive_projects_deletion_cron_worker_spec.rb @@ -41,7 +41,6 @@ stub_application_setting(inactive_projects_delete_after_months: 14) stub_application_setting(deletion_adjourned_period: 7) stub_application_setting(delete_inactive_projects: true) - stub_feature_flags(inactive_projects_deletion: true) end it 'does not send deletion warning email for inactive projects that are already marked for deletion' do diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 04c066986b72a6..7365a3c3276077 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -1147,37 +1147,23 @@ def license_name context 'with the setting enabled' do before do stub_application_setting(delete_inactive_projects: true) + stub_application_setting(inactive_projects_min_size_mb: 0) + stub_application_setting(inactive_projects_send_warning_email_after_months: 1) end - context 'with the feature flag disabled' do - before do - stub_feature_flags(inactive_projects_deletion: false) - end - + context 'with an active project' do it_behaves_like 'does not show the banner' end - context 'with the feature flag enabled' do + context 'with an inactive project' do before do - stub_feature_flags(inactive_projects_deletion: true) - stub_application_setting(inactive_projects_min_size_mb: 0) - stub_application_setting(inactive_projects_send_warning_email_after_months: 1) + project.statistics.storage_size = 1.megabyte + project.last_activity_at = 1.year.ago + project.save! end - context 'with an active project' do - it_behaves_like 'does not show the banner' - end - - context 'with an inactive project' do - before do - project.statistics.storage_size = 1.megabyte - project.last_activity_at = 1.year.ago - project.save! - end - - it 'shows the banner' do - expect(helper.show_inactive_project_deletion_banner?(project)).to be(true) - end + it 'shows the banner' do + expect(helper.show_inactive_project_deletion_banner?(project)).to be(true) end end end diff --git a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb index ec10c66968d2b9..50b5b0a6e7bad4 100644 --- a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb +++ b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb @@ -85,86 +85,58 @@ end end - context 'when delete inactive projects feature is enabled' do + context 'when delete inactive projects feature is enabled', :clean_gitlab_redis_shared_state, :sidekiq_inline do before do stub_application_setting(delete_inactive_projects: true) end - context 'when feature flag is disabled' do - before do - stub_feature_flags(inactive_projects_deletion: false) - end - - it 'does not invoke Projects::InactiveProjectsDeletionNotificationWorker' do - expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async) - expect(::Projects::DestroyService).not_to receive(:new) - - worker.perform - end - - it 'does not delete the inactive projects' do - worker.perform - - expect(inactive_large_project.reload.pending_delete).to eq(false) + it 'invokes Projects::InactiveProjectsDeletionNotificationWorker for inactive projects' do + Gitlab::Redis::SharedState.with do |redis| + expect(redis).to receive(:hset).with('inactive_projects_deletion_warning_email_notified', + "project:#{inactive_large_project.id}", Date.current) end + expect(::Projects::InactiveProjectsDeletionNotificationWorker).to receive(:perform_async).with( + inactive_large_project.id, deletion_date).and_call_original + expect(::Projects::DestroyService).not_to receive(:new) - it_behaves_like 'worker is running for more than 4 minutes' - it_behaves_like 'worker finishes processing in less than 4 minutes' + worker.perform end - context 'when feature flag is enabled', :clean_gitlab_redis_shared_state, :sidekiq_inline do - before do - stub_feature_flags(inactive_projects_deletion: true) - end - - it 'invokes Projects::InactiveProjectsDeletionNotificationWorker for inactive projects' do - Gitlab::Redis::SharedState.with do |redis| - expect(redis).to receive(:hset).with('inactive_projects_deletion_warning_email_notified', - "project:#{inactive_large_project.id}", Date.current) - end - expect(::Projects::InactiveProjectsDeletionNotificationWorker).to receive(:perform_async).with( - inactive_large_project.id, deletion_date).and_call_original - expect(::Projects::DestroyService).not_to receive(:new) - - worker.perform + it 'does not invoke InactiveProjectsDeletionNotificationWorker for already notified inactive projects' do + Gitlab::Redis::SharedState.with do |redis| + redis.hset('inactive_projects_deletion_warning_email_notified', "project:#{inactive_large_project.id}", + Date.current.to_s) end - it 'does not invoke InactiveProjectsDeletionNotificationWorker for already notified inactive projects' do - Gitlab::Redis::SharedState.with do |redis| - redis.hset('inactive_projects_deletion_warning_email_notified', "project:#{inactive_large_project.id}", - Date.current.to_s) - end + expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async) + expect(::Projects::DestroyService).not_to receive(:new) - expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async) - expect(::Projects::DestroyService).not_to receive(:new) + worker.perform + end - worker.perform + it 'invokes Projects::DestroyService for projects that are inactive even after being notified' do + Gitlab::Redis::SharedState.with do |redis| + redis.hset('inactive_projects_deletion_warning_email_notified', "project:#{inactive_large_project.id}", + 15.months.ago.to_date.to_s) end - it 'invokes Projects::DestroyService for projects that are inactive even after being notified' do - Gitlab::Redis::SharedState.with do |redis| - redis.hset('inactive_projects_deletion_warning_email_notified', "project:#{inactive_large_project.id}", - 15.months.ago.to_date.to_s) - end - - expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async) - expect(::Projects::DestroyService).to receive(:new).with(inactive_large_project, admin_user, {}) - .at_least(:once).and_call_original + expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async) + expect(::Projects::DestroyService).to receive(:new).with(inactive_large_project, admin_user, {}) + .at_least(:once).and_call_original - worker.perform + worker.perform - expect(inactive_large_project.reload.pending_delete).to eq(true) + expect(inactive_large_project.reload.pending_delete).to eq(true) - Gitlab::Redis::SharedState.with do |redis| - expect(redis.hget('inactive_projects_deletion_warning_email_notified', - "project:#{inactive_large_project.id}")).to be_nil - end + Gitlab::Redis::SharedState.with do |redis| + expect(redis.hget('inactive_projects_deletion_warning_email_notified', + "project:#{inactive_large_project.id}")).to be_nil end - - it_behaves_like 'worker is running for more than 4 minutes' - it_behaves_like 'worker finishes processing in less than 4 minutes' end + it_behaves_like 'worker is running for more than 4 minutes' + it_behaves_like 'worker finishes processing in less than 4 minutes' + it_behaves_like 'an idempotent worker' end end -- GitLab From 6424e196b80b9ee08e99efd248c2b396db6bbc15 Mon Sep 17 00:00:00 2001 From: Daniel Schoemer <daniel.schoemer@gmx.net> Date: Mon, 5 Sep 2022 15:07:12 +0200 Subject: [PATCH 105/169] EE Group Settings General headers expand on click Four headers in Group Settings / General pages do not expand/collapse on mouse-click / tap, but only using the Expand/Collapse button. This may seem inconsistent to the user. * Insights * Compliance frameworks * Custom project templates, and * Templates (Only visible in GitLab EE) (https: //gitlab.com/gitlab-org/gitlab/-/issues/334655#note_1085315512) The headers get the expand/collapse behavior through CSS classes settings-title, js-settings-toggle and js-settings-toggle-trigger-only assigned to the HTML h4 elements. This MR adds the missing CSS classes settings-title, js-settings-toggle and js-settings-toggle-trigger-only to headers in Group Settings / General (GitLab EE/Enterprise Edition). This enables the expand/collapse behavior on on-click / on-tap for the headers. Changelog: changed EE: true --- ee/app/views/groups/_compliance_frameworks.html.haml | 2 +- ee/app/views/groups/_custom_project_templates_setting.html.haml | 2 +- ee/app/views/groups/_insights.html.haml | 2 +- ee/app/views/groups/_templates_setting.html.haml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ee/app/views/groups/_compliance_frameworks.html.haml b/ee/app/views/groups/_compliance_frameworks.html.haml index 7450970013a3e2..805938adc1c9d3 100644 --- a/ee/app/views/groups/_compliance_frameworks.html.haml +++ b/ee/app/views/groups/_compliance_frameworks.html.haml @@ -4,7 +4,7 @@ - if show_compliance_frameworks?(@group) %section.settings.no-animate#js-compliance-frameworks-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = s_('GroupSettings|Compliance frameworks') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/ee/app/views/groups/_custom_project_templates_setting.html.haml b/ee/app/views/groups/_custom_project_templates_setting.html.haml index ae6f3ffe274ab5..0a5b8ef55a173b 100644 --- a/ee/app/views/groups/_custom_project_templates_setting.html.haml +++ b/ee/app/views/groups/_custom_project_templates_setting.html.haml @@ -3,7 +3,7 @@ %section.settings.no-animate{ class: ('expanded' if expanded), data: { qa_selector: 'custom_project_templates_container' } } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = s_('GroupSettings|Custom project templates') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/ee/app/views/groups/_insights.html.haml b/ee/app/views/groups/_insights.html.haml index 3ab83bb2ab17ef..dde4befa0b76ee 100644 --- a/ee/app/views/groups/_insights.html.haml +++ b/ee/app/views/groups/_insights.html.haml @@ -2,7 +2,7 @@ %section.settings.insights-settings.no-animate{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Insights') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/ee/app/views/groups/_templates_setting.html.haml b/ee/app/views/groups/_templates_setting.html.haml index f456275dacda1c..add8b7f9997b23 100644 --- a/ee/app/views/groups/_templates_setting.html.haml +++ b/ee/app/views/groups/_templates_setting.html.haml @@ -2,7 +2,7 @@ %section.settings.no-animate#js-templates{ class: ('expanded' if expanded), data: { qa_selector: 'file_template_repositories_container' } } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Templates') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') -- GitLab From 0aa8beb1faaeadaebb50048f4c1f9df25937be22 Mon Sep 17 00:00:00 2001 From: Vasilii Iakliushin <viakliushin@gitlab.com> Date: Tue, 6 Sep 2022 14:27:28 +0200 Subject: [PATCH 106/169] Limit number of branches/tags loaded from Gitaly Contributes to https://gitlab.com/gitlab-org/gitlab/-/issues/372049 * Enable `use_gitaly_pagination_for_refs` FF by default Changelog: changed --- .../development/use_gitaly_pagination_for_refs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/feature_flags/development/use_gitaly_pagination_for_refs.yml b/config/feature_flags/development/use_gitaly_pagination_for_refs.yml index 40deacb1e20e4b..f44233e8d0b702 100644 --- a/config/feature_flags/development/use_gitaly_pagination_for_refs.yml +++ b/config/feature_flags/development/use_gitaly_pagination_for_refs.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372049 milestone: '15.4' type: development group: group::source code -default_enabled: false +default_enabled: true -- GitLab From 9081796ef6e712ab11f580188feaa0a740fbe363 Mon Sep 17 00:00:00 2001 From: karthik nayak <knayak@gitlab.com> Date: Tue, 6 Sep 2022 12:45:55 +0000 Subject: [PATCH 107/169] Gemfile: Update version of the Gitaly Gem to 15.4.0.pre.rc2 Update the version of the Gitaly Gem to 15.4.0.pre.rc2 in order to obtain the definitions for the newly type 'LocalBranches' in 'FindLocalBranches'. --- Gemfile | 2 +- Gemfile.lock | 4 +- lib/gitlab/gitaly_client/operation_service.rb | 16 +++- .../gitaly_client/operation_service_spec.rb | 79 +++++++++++++++++++ 4 files changed, 96 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 2fbe351ed23414..6017dcd909302f 100644 --- a/Gemfile +++ b/Gemfile @@ -483,7 +483,7 @@ gem 'ssh_data', '~> 1.3' gem 'spamcheck', '~> 1.0.0' # Gitaly GRPC protocol definitions -gem 'gitaly', '~> 15.3.0-rc4' +gem 'gitaly', '~> 15.4.0-rc2' # KAS GRPC protocol definitions gem 'kas-grpc', '~> 0.0.2' diff --git a/Gemfile.lock b/Gemfile.lock index f8e5f86ea0cd23..7b78495dea28f7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -531,7 +531,7 @@ GEM rails (>= 3.2.0) git (1.11.0) rchardet (~> 1.8) - gitaly (15.3.0.pre.rc4) + gitaly (15.4.0.pre.rc2) grpc (~> 1.0) github-markup (1.7.0) gitlab (4.16.1) @@ -1586,7 +1586,7 @@ DEPENDENCIES gettext (~> 3.3) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly (~> 15.3.0.pre.rc4) + gitaly (~> 15.4.0.pre.rc2) github-markup (~> 1.7.0) gitlab-chronic (~> 0.10.5) gitlab-dangerfiles (~> 3.5.1) diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index e9dbfe9ba1753f..7835fb32f59aac 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -85,8 +85,20 @@ def user_create_branch(branch_name, user, start_point) target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit) Gitlab::Git::Branch.new(@repository, branch.name, target_commit.id, target_commit) - rescue GRPC::FailedPrecondition => ex - raise Gitlab::Git::Repository::InvalidRef, ex + rescue GRPC::BadStatus => e + detailed_error = GitalyClient.decode_detailed_error(e) + + case detailed_error&.error + when :custom_hook + raise Gitlab::Git::PreReceiveError.new(custom_hook_error_message(detailed_error.custom_hook), + fallback_message: e.details) + else + if e.code == GRPC::Core::StatusCodes::FAILED_PRECONDITION + raise Gitlab::Git::Repository::InvalidRef, e + end + + raise + end end def user_update_branch(branch_name, user, newrev, oldrev) diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 5d854f0c9d1cf2..7e8aaa3cdf4426 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -56,6 +56,85 @@ Gitlab::Git::PreReceiveError, "something failed") end end + + context 'with structured errors' do + context 'with CustomHookError' do + let(:stdout) { nil } + let(:stderr) { nil } + let(:error_message) { "error_message" } + + let(:custom_hook_error) do + new_detailed_error( + GRPC::Core::StatusCodes::PERMISSION_DENIED, + error_message, + Gitaly::UserCreateBranchError.new( + custom_hook: Gitaly::CustomHookError.new( + stdout: stdout, + stderr: stderr, + hook_type: Gitaly::CustomHookError::HookType::HOOK_TYPE_PRERECEIVE + ))) + end + + shared_examples 'failed branch creation' do + it 'raised a PreRecieveError' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_create_branch) + .and_raise(custom_hook_error) + + expect { subject }.to raise_error do |error| + expect(error).to be_a(Gitlab::Git::PreReceiveError) + expect(error.message).to eq(expected_message) + expect(error.raw_message).to eq(expected_raw_message) + end + end + end + + context 'when details contain stderr without prefix' do + let(:stderr) { "something" } + let(:stdout) { "GL-HOOK-ERR: stdout is overridden by stderr" } + let(:expected_message) { error_message } + let(:expected_raw_message) { stderr } + + it_behaves_like 'failed branch creation' + end + + context 'when details contain stderr with prefix' do + let(:stderr) { "GL-HOOK-ERR: something" } + let(:stdout) { "GL-HOOK-ERR: stdout is overridden by stderr" } + let(:expected_message) { "something" } + let(:expected_raw_message) { stderr } + + it_behaves_like 'failed branch creation' + end + + context 'when details contain stdout without prefix' do + let(:stderr) { " \n" } + let(:stdout) { "something" } + let(:expected_message) { error_message } + let(:expected_raw_message) { stdout } + + it_behaves_like 'failed branch creation' + end + + context 'when details contain stdout with prefix' do + let(:stderr) { " \n" } + let(:stdout) { "GL-HOOK-ERR: something" } + let(:expected_message) { "something" } + let(:expected_raw_message) { stdout } + + it_behaves_like 'failed branch creation' + end + + context 'when details contain no stderr or stdout' do + let(:stderr) { " \n" } + let(:stdout) { "\n \n" } + let(:expected_message) { error_message } + let(:expected_raw_message) { "\n \n" } + + it_behaves_like 'failed branch creation' + end + end + end end describe '#user_update_branch' do -- GitLab From 9169d24e8d99f9e05e824c4eee4497daf6dd5efd Mon Sep 17 00:00:00 2001 From: Amy Qualls <aqualls@gitlab.com> Date: Tue, 6 Sep 2022 13:10:21 +0000 Subject: [PATCH 108/169] Update pages affected by UI change Adding a new page to the UI, and moving UI elements to it, means we must update the corresponding docs to match the changes. --- doc/ci/pipelines/merge_trains.md | 3 +- doc/ci/pipelines/merged_results_pipelines.md | 5 +-- doc/integration/jira/issues.md | 5 +-- doc/user/discussions/index.md | 12 +++--- doc/user/project/description_templates.md | 5 +-- .../project/merge_requests/approvals/rules.md | 31 +++++++------- .../merge_requests/approvals/settings.md | 41 +++++++++---------- .../merge_requests/commit_templates.md | 2 +- .../merge_when_pipeline_succeeds.md | 12 +++--- .../project/merge_requests/methods/index.md | 3 +- .../merge_requests/squash_and_merge.md | 3 +- .../project/merge_requests/status_checks.md | 5 +-- 12 files changed, 59 insertions(+), 68 deletions(-) diff --git a/doc/ci/pipelines/merge_trains.md b/doc/ci/pipelines/merge_trains.md index 6547ea3895b2c2..70a0d177ac6e6c 100644 --- a/doc/ci/pipelines/merge_trains.md +++ b/doc/ci/pipelines/merge_trains.md @@ -82,8 +82,7 @@ To enable merge trains for your project: 1. [Configure your CI/CD configuration file](merge_request_pipelines.md#prerequisites) so that the pipeline or individual jobs run for merge requests. 1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > General**. -1. Expand **Merge requests**. +1. On the left sidebar, select **Settings > Merge requests**. 1. In the **Merge method** section, verify that **Merge commit** is selected. 1. In the **Merge options** section, select **Enable merged results pipelines** (if not already selected) and **Enable merge trains**. 1. Select **Save changes**. diff --git a/doc/ci/pipelines/merged_results_pipelines.md b/doc/ci/pipelines/merged_results_pipelines.md index 777871a7c5f3e5..172f7e26d95a37 100644 --- a/doc/ci/pipelines/merged_results_pipelines.md +++ b/doc/ci/pipelines/merged_results_pipelines.md @@ -42,9 +42,8 @@ To enable merged results pipelines in a project, you must have at least the Maintainer role: 1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > General**. -1. Expand **Merge requests**. -1. Select **Enable merged results pipelines**. +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge options** section, select **Enable merged results pipelines**. 1. Select **Save changes**. WARNING: diff --git a/doc/integration/jira/issues.md b/doc/integration/jira/issues.md index 2d9e928e654665..a0b102eb21f554 100644 --- a/doc/integration/jira/issues.md +++ b/doc/integration/jira/issues.md @@ -55,9 +55,8 @@ You can prevent merge requests from being merged if they do not refer to a Jira To enforce this: 1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > General**. -1. Expand **Merge requests**. -1. Under **Merge checks**, select the **Require an associated issue from Jira** checkbox. +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge checks** section, select **Require an associated issue from Jira**. 1. Select **Save**. After you enable this feature, a merge request that doesn't reference an associated diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 3fb0be6480cfec..4714d19036589b 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -340,9 +340,8 @@ resolved. When this setting is enabled, the **Unresolved threads** counter in a is shown in orange when at least one thread remains unresolved. 1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > General**. -1. Expand **Merge requests**. -1. Under **Merge checks**, select the **All threads must be resolved** checkbox. +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge checks** section, select the **All threads must be resolved** checkbox. 1. Select **Save changes**. ### Automatically resolve threads in a merge request when they become outdated @@ -351,10 +350,9 @@ You can set merge requests to automatically resolve threads when lines are modif with a new push. 1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > General**. -1. Expand **Merge requests**. -1. Under **Merge options**, select the - **Automatically resolve merge request diff threads when they become outdated** checkbox. +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge options** section, select + **Automatically resolve merge request diff threads when they become outdated**. 1. Select **Save changes**. Threads are now resolved if a push makes a diff section outdated. diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md index 5df3a973ccae58..2a060430a7c415 100644 --- a/doc/user/project/description_templates.md +++ b/doc/user/project/description_templates.md @@ -135,9 +135,8 @@ To set a default description template for merge requests, either: - Users on GitLab Premium and higher: set the default template in project settings: 1. On the top bar, select **Menu > Projects** and find your project. - 1. On the left sidebar, select **Settings**. - 1. Expand **Merge requests**. - 1. Fill in the **Default description template for merge requests** text area. + 1. On the left sidebar, select **Settings > Merge requests**. + 1. In the **Merge commit message template** section, fill in **Default description template for merge requests**. 1. Select **Save changes**. To set a default description template for issues, either: diff --git a/doc/user/project/merge_requests/approvals/rules.md b/doc/user/project/merge_requests/approvals/rules.md index c9278c19322fee..32548215054613 100644 --- a/doc/user/project/merge_requests/approvals/rules.md +++ b/doc/user/project/merge_requests/approvals/rules.md @@ -32,8 +32,9 @@ use the default approval rules from the target (upstream) project, not the sourc To add a merge request approval rule: -1. Go to your project and select **Settings > General**. -1. Expand **Merge request (MR) approvals**, and then select **Add approval rule**. +1. Go to your project and select **Settings > Merge requests**. +1. In the **Merge request approvals** section, scroll to **Approval rules**. +1. Select **Add approval rule**. 1. Add a human-readable **Rule name**. 1. Set the number of required approvals in **Approvals required**. A value of `0` makes [the rule optional](#configure-optional-approval-rules), and any number greater than `0` @@ -65,8 +66,9 @@ to existing merge requests: To edit a merge request approval rule: -1. Go to your project and select **Settings > General**. -1. Expand **Merge request (MR) approvals**, and then select **Edit**. +1. Go to your project and select **Settings > Merge requests**. +1. In the **Merge request approvals** section, scroll to **Approval rules**. +1. Select **Edit** next to the rule you want to edit. 1. Optional. Change the **Rule name**. 1. Set the number of required approvals in **Approvals required**. The minimum value is `0`. 1. Add or remove eligible approvers, as needed: @@ -155,11 +157,11 @@ approve in these ways: If you add [code owners](../../code_owners.md) to your repository, the owners of files become eligible approvers in the project. To enable this merge request approval rule: -1. Go to your project and select **Settings > General**. -1. Expand **Merge request (MR) approvals**. -1. Locate **All eligible users** and select the number of approvals required: +1. Go to your project and select **Settings > Merge requests**. +1. In the **Merge request approvals** section, scroll to **Approval rules**. +1. Locate the **All eligible users** rule, and select the number of approvals required: - +  You can also [require code owner approval](../../protected_branches.md#require-code-owner-approval-on-a-protected-branch) @@ -182,9 +184,10 @@ granting them push access: and select the Reporter role for the user. 1. [Share the project with your group](../../members/share_project_with_groups.md#share-a-project-with-a-group-of-users), based on the Reporter role. -1. Go to your project and select **Settings > General**. -1. Expand **Merge request (MR) approvals**. -1. Select **Add approval rule** or **Update approval rule** and target the protected branch. +1. Go to your project and select **Settings > Merge requests**. +1. In the **Merge request approvals** section, scroll to **Approval rules**, and either: + - For a new rule, select **Add approval rule** and target the protected branch. + - For an existing rule, select **Edit** and target the protected branch. 1. [Add the group](../../../group/manage.md#create-a-group) to the permission list.  @@ -226,12 +229,12 @@ Approval rules are often relevant only to specific branches, like your approval rule for certain branches: 1. [Create an approval rule](#add-an-approval-rule). -1. Go to your project and select **Settings**. -1. Expand **Merge request (MR) approvals**. +1. Go to your project and select **Settings > Merge requests**. +1. In the **Merge request approvals** section, scroll to **Approval rules**. 1. Select a **Target branch**: - To apply the rule to all branches, select **All branches**. - To apply the rule to all protected branches, select **All protected branches** (GitLab 15.3 and later). - - To apply the rule to a specific branch, select it from the list: + - To apply the rule to a specific branch, select it from the list. 1. To enable this configuration, read [Code Owner's approvals for protected branches](../../protected_branches.md#require-code-owner-approval-on-a-protected-branch). diff --git a/doc/user/project/merge_requests/approvals/settings.md b/doc/user/project/merge_requests/approvals/settings.md index 3ca8ddb508a2c5..4fdf6d46b8b08f 100644 --- a/doc/user/project/merge_requests/approvals/settings.md +++ b/doc/user/project/merge_requests/approvals/settings.md @@ -16,8 +16,8 @@ those rules are applied as a merge request moves toward completion. To view or edit merge request approval settings: -1. Go to your project and select **Settings > General**. -1. Expand **Merge request (MR) approvals**. +1. Go to your project and select **Settings > Merge requests**. +1. Expand **Approvals**. ### Approval settings @@ -44,9 +44,9 @@ You can further define what happens to existing approvals when commits are added By default, the author of a merge request cannot approve it. To change this setting: -1. Go to your project and select **Settings > General**. -1. Expand **Merge request (MR) approvals**. -1. Clear the **Prevent approval by author** checkbox. +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge request approvals** section, scroll to **Approval settings** and + clear the **Prevent approval by author** checkbox. 1. Select **Save changes**. Authors can edit the approval rule in an individual merge request and override @@ -68,9 +68,9 @@ the project level or [instance level](../../../admin_area/merge_requests_approva you can prevent committers from approving merge requests that are partially their own. To do this: -1. Go to your project and select **Settings > General**. -1. Expand **Merge request (MR) approvals**. -1. Select the **Prevent approvals by users who add commits** checkbox. +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge request approvals** section, scroll to **Approval settings** and + select **Prevent approvals by users who add commits**. If this checkbox is cleared, an administrator has disabled it [at the instance level](../../../admin_area/merge_requests_approvals.md), and it can't be changed at the project level. @@ -94,9 +94,9 @@ By default, users can override the approval rules you [create for a project](rul on a per-merge-request basis. If you don't want users to change approval rules on merge requests, you can disable this setting: -1. Go to your project and select **Settings > General**. -1. Expand **Merge request (MR) approvals**. -1. Select the **Prevent editing approval rules in merge requests** checkbox. +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge request approvals** section, scroll to **Approval settings** and + select **Prevent editing approval rules in merge requests**. 1. Select **Save changes**. This change affects all open merge requests. @@ -112,9 +112,9 @@ permission enables an electronic signature for approvals, such as the one define 1. Enable password authentication for the web interface, as described in the [sign-in restrictions documentation](../../../admin_area/settings/sign_in_restrictions.md#password-authentication-enabled). -1. Go to your project and select **Settings > General**. -1. Expand **Merge request (MR) approvals**. -1. Select the **Require user password to approve** checkbox. +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge request approvals** section, scroll to **Approval settings** and + select **Require user password to approve**. 1. Select **Save changes**. ## Remove all approvals when commits are added to the source branch @@ -123,9 +123,9 @@ By default, an approval on a merge request remains in place, even if you add mor after the approval. If you want to remove all existing approvals on a merge request when more changes are added to it: -1. Go to your project and select **Settings > General**. -1. Expand **Merge request (MR) approvals**. -1. Select the **Remove all approvals when commits are added to the source branch** checkbox. +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge request approvals** section, scroll to **Approval settings** and + select **Remove all approvals when commits are added to the source branch**. 1. Select **Save changes**. Approvals aren't removed when a merge request is [rebased from the UI](../methods/index.md#rebasing-in-semi-linear-merge-methods) @@ -143,10 +143,9 @@ Prerequisite: To do this: -1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > General**. -1. Expand **Merge request approvals**. -1. Select **Remove approvals by Code Owners if their files changed**. +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge request approvals** section, scroll to **Approval settings** and + select **Remove approvals by Code Owners if their files changed**. 1. Select **Save changes**. ## Code coverage check approvals diff --git a/doc/user/project/merge_requests/commit_templates.md b/doc/user/project/merge_requests/commit_templates.md index 6f9bc452b9605b..6b27ea4471c922 100644 --- a/doc/user/project/merge_requests/commit_templates.md +++ b/doc/user/project/merge_requests/commit_templates.md @@ -30,7 +30,7 @@ Prerequisite: To do this: 1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > General** and expand **Merge requests**. +1. On the left sidebar, select **Settings > Merge requests**. 1. Depending on the type of template you want to create, scroll to either [**Merge commit message template**](#default-template-for-merge-commits) or [**Squash commit message template**](#default-template-for-squash-commits). diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md index 9182cf11566d14..c1b85bb4acc087 100644 --- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md +++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md @@ -57,9 +57,8 @@ does not disable this feature, as it is possible to use pipelines from external CI providers with this feature. To enable it, you must: 1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > General**. -1. Expand **Merge requests**. -1. Under **Merge checks**, select the **Pipelines must succeed** checkbox. +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge checks** section, select **Pipelines must succeed**. 1. Select **Save**. This setting also prevents merge requests from being merged if there is no pipeline. @@ -106,11 +105,10 @@ When the **Pipelines must succeed** checkbox is checked, [skipped pipelines](../ merge requests from being merged. To change this behavior: 1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > General**. -1. Expand **Merge requests**. -1. Under **Merge checks**: +1. On the left sidebar, select **Settings > Merge requests**. +1. In the **Merge checks** section: - Ensure **Pipelines must succeed** is selected. - - Select the **Skipped pipelines are considered successful** checkbox. + - Select **Skipped pipelines are considered successful**. 1. Select **Save**. ## From the command line diff --git a/doc/user/project/merge_requests/methods/index.md b/doc/user/project/merge_requests/methods/index.md index 7860221a9500ca..02a784a321cbbe 100644 --- a/doc/user/project/merge_requests/methods/index.md +++ b/doc/user/project/merge_requests/methods/index.md @@ -13,8 +13,7 @@ merge requests are merged into an existing branch. ## Configure a project's merge method 1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > General**. -1. Expand **Merge requests**. +1. On the left sidebar, select **Settings > Merge requests**. 1. In the **Merge method** section, select your desired merge method. 1. Select **Save changes**. diff --git a/doc/user/project/merge_requests/squash_and_merge.md b/doc/user/project/merge_requests/squash_and_merge.md index 7e37990b9bfb94..e113dcfdb58df2 100644 --- a/doc/user/project/merge_requests/squash_and_merge.md +++ b/doc/user/project/merge_requests/squash_and_merge.md @@ -61,8 +61,7 @@ squash the commits as part of the merge process: To configure the default squashing behavior for all merge requests in your project: 1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > General**. -1. Expand **Merge requests**. +1. On the left sidebar, select **Settings > Merge requests**. 1. In the **Squash commits when merging** section, select your desired behavior: - **Do not allow**: Squashing is never performed, and the option is not displayed. - **Allow**: Squashing is allowed, but cleared by default. diff --git a/doc/user/project/merge_requests/status_checks.md b/doc/user/project/merge_requests/status_checks.md index 0d7794a3ebde43..f6c552104199a5 100644 --- a/doc/user/project/merge_requests/status_checks.md +++ b/doc/user/project/merge_requests/status_checks.md @@ -61,9 +61,8 @@ using the API. You don't need to wait for a merge request webhook payload to be Within each project's settings, you can see a list of status checks added to the project: -1. In your project, go to **Settings > General**. -1. Expand the **Merge requests** section. -1. Scroll down to the **Status checks** sub-section. +1. In your project, go to **Settings > Merge requests** section. +1. Scroll down to **Status checks**.  -- GitLab From bc0d8a0172f3ccfb5027d1b45e72a1ca9a04bc6f Mon Sep 17 00:00:00 2001 From: Adam Hegyi <ahegyi@gitlab.com> Date: Mon, 5 Sep 2022 13:31:22 +0200 Subject: [PATCH 109/169] Add index to todos the improve query performance This change adds an extra async index to the `todos` table to improve the performance of the pending TODO query. Changelog: added --- ...sync_index_to_todos_to_cover_pending_query.rb | 16 ++++++++++++++++ db/schema_migrations/20220905112710 | 1 + 2 files changed, 17 insertions(+) create mode 100644 db/post_migrate/20220905112710_add_async_index_to_todos_to_cover_pending_query.rb create mode 100644 db/schema_migrations/20220905112710 diff --git a/db/post_migrate/20220905112710_add_async_index_to_todos_to_cover_pending_query.rb b/db/post_migrate/20220905112710_add_async_index_to_todos_to_cover_pending_query.rb new file mode 100644 index 00000000000000..e2bca2fae1a60b --- /dev/null +++ b/db/post_migrate/20220905112710_add_async_index_to_todos_to_cover_pending_query.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddAsyncIndexToTodosToCoverPendingQuery < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + INDEX_NAME = 'index_on_todos_user_project_target_and_state' + COLUMNS = %i[user_id project_id target_type target_id id].freeze + + def up + prepare_async_index :todos, COLUMNS, name: INDEX_NAME, where: "state = 'pending'" + end + + def down + unprepare_async_index :todos, COLUMNS, name: INDEX_NAME, where: "state='pending'" + end +end diff --git a/db/schema_migrations/20220905112710 b/db/schema_migrations/20220905112710 new file mode 100644 index 00000000000000..9f20a8cc9b628a --- /dev/null +++ b/db/schema_migrations/20220905112710 @@ -0,0 +1 @@ +85db0670a8557421a59678f19324411d61220eae12ea68f565d458a7393f6b2e \ No newline at end of file -- GitLab From 5eb6924a11e092863999e95609a70e96fb93c877 Mon Sep 17 00:00:00 2001 From: Nicolas Dular <ndular@gitlab.com> Date: Fri, 2 Sep 2022 11:41:17 +0200 Subject: [PATCH 110/169] Improve bulk issue creation on epics When creating issues on epics, it's common to create the issue in the same project. With this change we no longer re-mount the issue creation form and only reset the `title` of the issue. We also add event listeners on `enter` so it's easy to create issues with a keyboard. Changelog: changed EE: true --- .../components/create_issue_form.vue | 19 ++- .../components/related_items_tree_app.vue | 28 ++-- .../components/create_issue_form_spec.js | 131 +++++++++++------- 3 files changed, 111 insertions(+), 67 deletions(-) diff --git a/ee/app/assets/javascripts/related_items_tree/components/create_issue_form.vue b/ee/app/assets/javascripts/related_items_tree/components/create_issue_form.vue index 919897aee55e1d..c8b6c0abc228b2 100644 --- a/ee/app/assets/javascripts/related_items_tree/components/create_issue_form.vue +++ b/ee/app/assets/javascripts/related_items_tree/components/create_issue_form.vue @@ -56,6 +56,9 @@ export default { return __('Select a project'); }, + isIssueCreationDisabled() { + return !this.selectedProject || this.itemCreateInProgress || !this.title; + }, }, watch: { /** @@ -87,13 +90,21 @@ export default { this.$emit('cancel'); }, createIssue() { - if (!this.selectedProject) { + if (this.isIssueCreationDisabled) { return; } const { selectedProject, title } = this; const { issues: issuesEndpoint } = selectedProject._links; this.$emit('submit', { issuesEndpoint, title }); + this.resetForm(); + }, + resetForm() { + /** + * We do not reset the selected project as it's common to create multiple + * issues in one project at once. + */ + this.title = ''; }, handleDropdownShow() { this.searchKey = ''; @@ -165,10 +176,12 @@ export default { <gl-form-input ref="titleInput" v-model.trim="title" + data-testid="title-input" :placeholder=" parentItem.confidential ? __('New confidential issue title') : __('New issue title') " autofocus + @keyup.enter="createIssue" /> </div> <div class="col-sm-6"> @@ -219,6 +232,7 @@ export default { <gl-dropdown-item v-for="project in projects" :key="project.id" + :data-testid="`project-item-${project.id}`" class="gl-w-full select-project-dropdown" @click="selectedProject = project" > @@ -249,7 +263,8 @@ export default { class="w-100" variant="confirm" category="primary" - :disabled="!selectedProject || itemCreateInProgress" + data-testid="submit-button" + :disabled="isIssueCreationDisabled" :loading="itemCreateInProgress || recentItemFetchInProgress" @click="createIssue" >{{ __('Create issue') }}</gl-button diff --git a/ee/app/assets/javascripts/related_items_tree/components/related_items_tree_app.vue b/ee/app/assets/javascripts/related_items_tree/components/related_items_tree_app.vue index 16404f9fe79888..10837828baa58a 100644 --- a/ee/app/assets/javascripts/related_items_tree/components/related_items_tree_app.vue +++ b/ee/app/assets/javascripts/related_items_tree/components/related_items_tree_app.vue @@ -176,11 +176,7 @@ export default { <template> <div class="related-items-tree-container gl-mt-5"> - <div v-if="itemsFetchInProgress" class="mt-2"> - <gl-loading-icon size="lg" /> - </div> <div - v-else class="related-items-tree card card-slim" :class="{ 'disabled-content': disableContents, @@ -257,19 +253,19 @@ export default { /> </template> </slot-switch> + <div v-if="itemsFetchInProgress" class="gl-p-3"> + <gl-loading-icon size="lg" /> + </div> + <div v-else-if="!itemsFetchResultEmpty"> + <related-items-tree-actions :active-tab="activeTab" @tab-change="handleTabChange" /> - <related-items-tree-actions - v-if="!itemsFetchResultEmpty" - :active-tab="activeTab" - @tab-change="handleTabChange" - /> - - <related-items-tree-body - v-if="!itemsFetchResultEmpty && activeTab === $options.ITEM_TABS.TREE" - :parent-item="parentItem" - :children="directChildren" - /> - <related-items-roadmap-app v-if="activeTab === $options.ITEM_TABS.ROADMAP" /> + <related-items-tree-body + v-if="activeTab === $options.ITEM_TABS.TREE" + :parent-item="parentItem" + :children="directChildren" + /> + <related-items-roadmap-app v-if="activeTab === $options.ITEM_TABS.ROADMAP" /> + </div> <tree-item-remove-modal /> </div> </div> diff --git a/ee/spec/frontend/related_items_tree/components/create_issue_form_spec.js b/ee/spec/frontend/related_items_tree/components/create_issue_form_spec.js index 6fca7276c766a9..6ea6dfc54199b5 100644 --- a/ee/spec/frontend/related_items_tree/components/create_issue_form_spec.js +++ b/ee/spec/frontend/related_items_tree/components/create_issue_form_spec.js @@ -11,6 +11,7 @@ import { import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; +import { ENTER_KEY } from '~/lib/utils/keys'; import mockProjects from 'test_fixtures_static/projects.json'; import CreateIssueForm from 'ee/related_items_tree/components/create_issue_form.vue'; @@ -26,34 +27,46 @@ import { Vue.use(Vuex); -const createComponent = () => { - const store = createDefaultStore(); +describe('CreateIssueForm', () => { + const defaultProject = mockProjects[1]; + let wrapper; - store.dispatch('setInitialConfig', mockInitialConfig); - store.dispatch('setInitialParentItem', mockParentItem); + const createComponent = () => { + const store = createDefaultStore(); - return shallowMount(CreateIssueForm, { - store, - }); -}; + store.dispatch('setInitialConfig', mockInitialConfig); + store.dispatch('setInitialParentItem', mockParentItem); -const getLocalstorageKey = () => { - return 'root/frequent-projects'; -}; + wrapper = shallowMount(CreateIssueForm, { + store, + }); + }; -const setLocalstorageFrequentItems = (json = mockFrequentlyUsedProjects) => { - localStorage.setItem(getLocalstorageKey(), JSON.stringify(json)); -}; + const getLocalstorageKey = () => { + return 'root/frequent-projects'; + }; -const removeLocalstorageFrequentItems = () => { - localStorage.removeItem(getLocalstorageKey()); -}; + const setLocalstorageFrequentItems = (json = mockFrequentlyUsedProjects) => { + localStorage.setItem(getLocalstorageKey(), JSON.stringify(json)); + }; -describe('CreateIssueForm', () => { - let wrapper; + const removeLocalstorageFrequentItems = () => { + localStorage.removeItem(getLocalstorageKey()); + }; + + const selectProject = async (project = defaultProject) => { + wrapper.vm.$store.dispatch('receiveProjectsSuccess', mockProjects); + await nextTick(); + + const item = wrapper.find(`[data-testid="project-item-${project.id}"]`); + item.vm.$emit('click'); + }; + + const findSubmitButton = () => wrapper.find('[data-testid="submit-button"]'); + const findTitleInput = () => wrapper.find('[data-testid="title-input"]'); beforeEach(() => { - wrapper = createComponent(); + createComponent(); gon.current_username = 'root'; }); @@ -73,24 +86,18 @@ describe('CreateIssueForm', () => { describe('computed', () => { describe('dropdownToggleText', () => { it('returns project name with name_with_namespace when `selectedProject` is not empty', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - selectedProject: mockProjects[0], - }); + await selectProject(); - await nextTick(); - expect(wrapper.vm.dropdownToggleText).toBe(mockProjects[0].name_with_namespace); + expect(wrapper.vm.dropdownToggleText).toBe(defaultProject.name_with_namespace); }); it('returns project name with namespace when `selectedProject` is not empty and dont have name_with_namespace', async () => { - const project = { ...mockProjects[0], name_with_namespace: undefined, namespace: 'foo' }; - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - selectedProject: project, - }); + const project = { + ...defaultProject, + name_with_namespace: undefined, + namespace: 'H5bp / Html5 Boilerplate', + }; - await nextTick(); + await selectProject(project); expect(wrapper.vm.dropdownToggleText).toBe(project.namespace); }); @@ -109,24 +116,50 @@ describe('CreateIssueForm', () => { describe('createIssue', () => { it('emits event `submit` on component when `selectedProject` is not empty', async () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - selectedProject: { - ...mockProjects[0], - _links: { - issues: 'foo', - }, - }, - title: 'Some issue', - }); - - wrapper.vm.createIssue(); + const input = findTitleInput(); + + await selectProject(); + await input.vm.$emit('input', 'Some issue'); + await findSubmitButton().vm.$emit('click'); - await nextTick(); expect(wrapper.emitted('submit')[0]).toEqual( - expect.arrayContaining([{ issuesEndpoint: 'foo', title: 'Some issue' }]), + expect.arrayContaining([ + { issuesEndpoint: defaultProject._links.issues, title: 'Some issue' }, + ]), ); + expect(input.attributes('value')).toBe(''); + }); + + it('emits event `submit` on enter', async () => { + const input = findTitleInput(); + + await selectProject(); + await input.vm.$emit('input', 'Some issue'); + await input.vm.$emit('keyup', new KeyboardEvent({ key: ENTER_KEY })); + + expect(wrapper.emitted('submit')[0]).toEqual( + expect.arrayContaining([ + { issuesEndpoint: defaultProject._links.issues, title: 'Some issue' }, + ]), + ); + expect(input.attributes('value')).toBe(''); + }); + + it('does not emit event `submit` when `selectedProject` is empty', async () => { + const input = findTitleInput(); + + await input.vm.$emit('input', 'Some issue'); + await findSubmitButton().vm.$emit('click'); + + expect(wrapper.emitted('submit')).toBeUndefined(); + expect(input.attributes('value')).toBe('Some issue'); + }); + + it('does not emit event `submit` when `title` is empty', async () => { + await selectProject(); + await findSubmitButton().vm.$emit('click'); + + expect(wrapper.emitted('submit')).toBeUndefined(); }); }); -- GitLab From 78e3832b3777c0422428848a48c776bfe3dc58de Mon Sep 17 00:00:00 2001 From: Sampath Ranasinghe <sranasinghe@gitlab.com> Date: Tue, 6 Sep 2022 13:12:32 +0000 Subject: [PATCH 111/169] Note indicating future deprecation of proxying feature flag --- doc/administration/geo/secondary_proxy/index.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/administration/geo/secondary_proxy/index.md b/doc/administration/geo/secondary_proxy/index.md index d873ea14372be2..731b5012663fcf 100644 --- a/doc/administration/geo/secondary_proxy/index.md +++ b/doc/administration/geo/secondary_proxy/index.md @@ -105,7 +105,10 @@ gitlab: ## Geo proxying with Separate URLs -Since GitLab 15.1, Geo secondary proxying is enabled by default for separate URLs also. +> Geo secondary proxying for separate URLs is [enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/346112) in GitLab 15.1. + +NOTE: +The feature flag described in this section is planned to be deprecated and removed in a future release. Support for read-only Geo secondary sites is proposed in [issue 366810](https://gitlab.com/gitlab-org/gitlab/-/issues/366810), you can upvote and share your use cases in that issue. There are minor known issues linked in the ["Geo secondary proxying with separate URLs" epic](https://gitlab.com/groups/gitlab-org/-/epics/6865). -- GitLab From 6c51fcc55943cadd04798f0ddef427fbd6e530b0 Mon Sep 17 00:00:00 2001 From: Pedro Pombeiro <noreply@pedro.pombei.ro> Date: Tue, 6 Sep 2022 13:17:07 +0000 Subject: [PATCH 112/169] Create ProjectPolicyPreloader --- .../preloaders/group_policy_preloader.rb | 2 +- .../preloaders/project_policy_preloader.rb | 23 +++++ .../project_root_ancestor_preloader.rb | 37 +++++++ ..._max_access_level_in_projects_preloader.rb | 2 +- .../ee/preloaders/project_policy_preloader.rb | 32 ++++++ .../project_policy_preloader_spec.rb | 55 +++++++++++ .../project_root_ancestor_preloader_spec.rb | 99 +++++++++++++++++++ 7 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 app/models/preloaders/project_policy_preloader.rb create mode 100644 app/models/preloaders/project_root_ancestor_preloader.rb create mode 100644 ee/app/models/ee/preloaders/project_policy_preloader.rb create mode 100644 spec/models/preloaders/project_policy_preloader_spec.rb create mode 100644 spec/models/preloaders/project_root_ancestor_preloader_spec.rb diff --git a/app/models/preloaders/group_policy_preloader.rb b/app/models/preloaders/group_policy_preloader.rb index 44030140ce3d4d..23632a9b6c2718 100644 --- a/app/models/preloaders/group_policy_preloader.rb +++ b/app/models/preloaders/group_policy_preloader.rb @@ -17,4 +17,4 @@ def execute end end -Preloaders::GroupPolicyPreloader.prepend_mod_with('Preloaders::GroupPolicyPreloader') +Preloaders::GroupPolicyPreloader.prepend_mod diff --git a/app/models/preloaders/project_policy_preloader.rb b/app/models/preloaders/project_policy_preloader.rb new file mode 100644 index 00000000000000..fe9db3464c7e07 --- /dev/null +++ b/app/models/preloaders/project_policy_preloader.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Preloaders + class ProjectPolicyPreloader + def initialize(projects, current_user) + @projects = projects + @current_user = current_user + end + + def execute + return if projects.is_a?(ActiveRecord::NullRelation) + + ActiveRecord::Associations::Preloader.new.preload(projects, { group: :route, namespace: :owner }) + ::Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute + end + + private + + attr_reader :projects, :current_user + end +end + +Preloaders::ProjectPolicyPreloader.prepend_mod diff --git a/app/models/preloaders/project_root_ancestor_preloader.rb b/app/models/preloaders/project_root_ancestor_preloader.rb new file mode 100644 index 00000000000000..8d04e71774cd32 --- /dev/null +++ b/app/models/preloaders/project_root_ancestor_preloader.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Preloaders + class ProjectRootAncestorPreloader + def initialize(projects, namespace_sti_name = :namespace, root_ancestor_preloads = []) + @projects = projects + @namespace_sti_name = namespace_sti_name + @root_ancestor_preloads = root_ancestor_preloads + end + + def execute + return if @projects.is_a?(ActiveRecord::NullRelation) + return unless ::Feature.enabled?(:use_traversal_ids) + + root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id") + .select('namespaces.*, root_query.id as source_id') + + root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any? + + root_ancestors_by_id = root_query.group_by(&:source_id) + + ActiveRecord::Associations::Preloader.new.preload(@projects, :namespace) + @projects.each do |project| + project.namespace.root_ancestor = root_ancestors_by_id[project.id]&.first + end + end + + private + + def join_sql + @projects + .joins(@namespace_sti_name) + .select('projects.id, namespaces.traversal_ids[1] as root_id') + .to_sql + end + end +end diff --git a/app/models/preloaders/users_max_access_level_in_projects_preloader.rb b/app/models/preloaders/users_max_access_level_in_projects_preloader.rb index 99a31a620c5236..f32184f168db96 100644 --- a/app/models/preloaders/users_max_access_level_in_projects_preloader.rb +++ b/app/models/preloaders/users_max_access_level_in_projects_preloader.rb @@ -51,4 +51,4 @@ def preload_users_namespace_bans(_users) end end -# Preloaders::UsersMaxAccessLevelInProjectsPreloader.prepend_mod +Preloaders::UsersMaxAccessLevelInProjectsPreloader.prepend_mod diff --git a/ee/app/models/ee/preloaders/project_policy_preloader.rb b/ee/app/models/ee/preloaders/project_policy_preloader.rb new file mode 100644 index 00000000000000..c91985f09921a7 --- /dev/null +++ b/ee/app/models/ee/preloaders/project_policy_preloader.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module EE + module Preloaders + module ProjectPolicyPreloader + extend ::Gitlab::Utils::Override + + override :execute + def execute + return if projects.is_a?(ActiveRecord::NullRelation) + + super + + ActiveRecord::Associations::Preloader.new.preload(projects, :group) + ::Preloaders::ProjectRootAncestorPreloader.new(projects, :group, root_ancestor_preloads).execute + + # Manually preloads saml_providers, which cannot be done in AR, since the + # relationship is on the root ancestor. + # This is required since the `:read_group` ability depends on `Group.saml_provider` + projects.select(&:group).each do |project| + project.group.root_saml_provider = project.root_ancestor.saml_provider + end + end + + private + + def root_ancestor_preloads + [:ip_restrictions, :saml_provider] + end + end + end +end diff --git a/spec/models/preloaders/project_policy_preloader_spec.rb b/spec/models/preloaders/project_policy_preloader_spec.rb new file mode 100644 index 00000000000000..79f232f5ce2ecc --- /dev/null +++ b/spec/models/preloaders/project_policy_preloader_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Preloaders::ProjectPolicyPreloader do + let_it_be(:user) { create(:user) } + let_it_be(:root_parent) { create(:group, :private, name: 'root-1', path: 'root-1') } + let_it_be(:guest_project) { create(:project, name: 'public guest', path: 'public-guest') } + let_it_be(:private_maintainer_project) do + create(:project, :private, name: 'b private maintainer', path: 'b-private-maintainer', namespace: root_parent) + end + + let_it_be(:private_developer_project) do + create(:project, :private, name: 'c public developer', path: 'c-public-developer') + end + + let_it_be(:public_maintainer_project) do + create(:project, :private, name: 'a public maintainer', path: 'a-public-maintainer') + end + + let(:base_projects) do + Project.where(id: [guest_project, private_maintainer_project, private_developer_project, public_maintainer_project]) + end + + before_all do + guest_project.add_guest(user) + private_maintainer_project.add_maintainer(user) + private_developer_project.add_developer(user) + public_maintainer_project.add_maintainer(user) + end + + it 'avoids N+1 queries when authorizing a list of projects', :request_store do + preload_projects_for_policy(user) + control = ActiveRecord::QueryRecorder.new { authorize_all_projects(user) } + + new_project1 = create(:project, :private).tap { |project| project.add_maintainer(user) } + new_project2 = create(:project, :private, namespace: root_parent) + + another_root = create(:group, :private, name: 'root-3', path: 'root-3') + new_project3 = create(:project, :private, namespace: another_root).tap { |project| project.add_maintainer(user) } + + pristine_projects = Project.where(id: base_projects + [new_project1, new_project2, new_project3]) + + preload_projects_for_policy(user, pristine_projects) + expect { authorize_all_projects(user, pristine_projects) }.not_to exceed_query_limit(control) + end + + def authorize_all_projects(current_user, project_list = base_projects) + project_list.each { |project| current_user.can?(:read_project, project) } + end + + def preload_projects_for_policy(current_user, project_list = base_projects) + described_class.new(project_list, current_user).execute + end +end diff --git a/spec/models/preloaders/project_root_ancestor_preloader_spec.rb b/spec/models/preloaders/project_root_ancestor_preloader_spec.rb new file mode 100644 index 00000000000000..30036a6a03379c --- /dev/null +++ b/spec/models/preloaders/project_root_ancestor_preloader_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Preloaders::ProjectRootAncestorPreloader do + let_it_be(:root_parent1) { create(:group, :private, name: 'root-1', path: 'root-1') } + let_it_be(:root_parent2) { create(:group, :private, name: 'root-2', path: 'root-2') } + let_it_be(:guest_project) { create(:project, name: 'public guest', path: 'public-guest') } + let_it_be(:private_maintainer_project) do + create(:project, :private, name: 'b private maintainer', path: 'b-private-maintainer', namespace: root_parent1) + end + + let_it_be(:private_developer_project) do + create(:project, :private, name: 'c public developer', path: 'c-public-developer') + end + + let_it_be(:public_maintainer_project) do + create(:project, :private, name: 'a public maintainer', path: 'a-public-maintainer', namespace: root_parent2) + end + + let(:root_query_regex) { /\ASELECT.+FROM "namespaces" WHERE "namespaces"."id" = \d+/ } + let(:additional_preloads) { [] } + let(:projects) { [guest_project, private_maintainer_project, private_developer_project, public_maintainer_project] } + let(:pristine_projects) { Project.where(id: projects) } + + shared_examples 'executes N matching DB queries' do |expected_query_count, query_method = nil| + it 'executes the specified root_ancestor queries' do + expect do + pristine_projects.each do |project| + root_ancestor = project.root_ancestor + + root_ancestor.public_send(query_method) if query_method.present? + end + end.to make_queries_matching(root_query_regex, expected_query_count) + end + + it 'strong_memoizes the correct root_ancestor' do + pristine_projects.each do |project| + expected_parent_id = project.root_ancestor&.id + + expect(project.parent_id).to eq(expected_parent_id) + end + end + end + + context 'when use_traversal_ids FF is enabled' do + context 'when the preloader is used' do + before do + preload_ancestors + end + + context 'when no additional preloads are provided' do + it_behaves_like 'executes N matching DB queries', 0 + end + + context 'when additional preloads are provided' do + let(:additional_preloads) { [:route] } + let(:root_query_regex) { /\ASELECT.+FROM "routes" WHERE "routes"."source_id" = \d+/ } + + it_behaves_like 'executes N matching DB queries', 0, :full_path + end + end + + context 'when the preloader is not used' do + it_behaves_like 'executes N matching DB queries', 4 + end + end + + context 'when use_traversal_ids FF is disabled' do + before do + stub_feature_flags(use_traversal_ids: false) + end + + context 'when the preloader is used' do + before do + preload_ancestors + end + + context 'when no additional preloads are provided' do + it_behaves_like 'executes N matching DB queries', 4 + end + + context 'when additional preloads are provided' do + let(:additional_preloads) { [:route] } + let(:root_query_regex) { /\ASELECT.+FROM "routes" WHERE "routes"."source_id" = \d+/ } + + it_behaves_like 'executes N matching DB queries', 4, :full_path + end + end + + context 'when the preloader is not used' do + it_behaves_like 'executes N matching DB queries', 4 + end + end + + def preload_ancestors + described_class.new(pristine_projects, :namespace, additional_preloads).execute + end +end -- GitLab From 57e68188858adbd1ea256a036f5904ac08c20ee7 Mon Sep 17 00:00:00 2001 From: Stanislav Lashmanov <slashmanov@gitlab.com> Date: Wed, 31 Aug 2022 04:10:23 +0400 Subject: [PATCH 113/169] Fix MR widget icons alignment --- .../components/extensions/base.vue | 16 ++- .../components/extensions/child_content.vue | 4 +- .../components/state_container.vue | 9 +- .../__snapshots__/index_spec.js.snap | 108 ++++++++++-------- 4 files changed, 78 insertions(+), 59 deletions(-) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index aa5ab87597fcfa..5325aee8a8b4fd 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -13,6 +13,7 @@ import Poll from '~/lib/utils/poll'; import { normalizeHeaders } from '~/lib/utils/common_utils'; import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants'; import Actions from '../action_buttons.vue'; +import StateContainer from '../state_container.vue'; import StatusIcon from './status_icon.vue'; import ChildContent from './child_content.vue'; import { createTelemetryHub } from './telemetry'; @@ -36,6 +37,7 @@ export default { ChildContent, DynamicScroller, DynamicScrollerItem, + StateContainer, }, directives: { SafeHtml: GlSafeHtmlDirective, @@ -312,18 +314,14 @@ export default { data-testid="widget-extension" data-qa-selector="mr_widget_extension" > - <div + <state-container + :status="statusIconName" + :is-loading="isLoadingSummary" :class="{ 'gl-cursor-pointer': isCollapsible }" - class="media gl-p-5" + class="gl-p-5" @mousedown="onRowMouseDown" @mouseup="onRowMouseUp" > - <status-icon - :level="1" - :name="$options.label || $options.name" - :is-loading="isLoadingSummary" - :icon-name="statusIconName" - /> <div class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center" data-testid="widget-extension-top-level" @@ -362,7 +360,7 @@ export default { /> </div> </div> - </div> + </state-container> <div v-if="!isCollapsed" class="mr-widget-grouped-section gl-relative" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue index 7f2049904fd8aa..38a391eadab888 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue @@ -62,7 +62,9 @@ export default { <strong v-else v-safe-html="generateText(data.header)"></strong> </div> <div class="gl-display-flex"> - <status-icon v-if="data.icon" :icon-name="data.icon.name" :size="12" class="gl-pl-0" /> + <div v-if="data.icon" class="gl-h-5 gl-display-flex"> + <status-icon :icon-name="data.icon.name" :size="12" class="gl-m-auto" /> + </div> <div class="gl-w-full"> <div class="gl-display-flex gl-flex-nowrap"> <div class="gl-flex-wrap gl-display-flex gl-w-full"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue index 2bba8d2dc82c98..03728e2831b988 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue @@ -44,7 +44,14 @@ export default { <template> <div class="mr-widget-body media"> <div v-if="isLoading" class="gl-w-full mr-conflict-loader"> - <slot name="loading"></slot> + <slot name="loading"> + <div class="gl-display-flex"> + <status-icon status="loading" /> + <div class="media-body"> + <slot></slot> + </div> + </div> + </slot> </div> <template v-else> <slot name="icon"> diff --git a/ee/spec/frontend/vue_merge_request_widget/extensions/license_compliance/__snapshots__/index_spec.js.snap b/ee/spec/frontend/vue_merge_request_widget/extensions/license_compliance/__snapshots__/index_spec.js.snap index f88bb4eb86bf39..00aedf1786c1ff 100644 --- a/ee/spec/frontend/vue_merge_request_widget/extensions/license_compliance/__snapshots__/index_spec.js.snap +++ b/ee/spec/frontend/vue_merge_request_widget/extensions/license_compliance/__snapshots__/index_spec.js.snap @@ -80,28 +80,32 @@ exports[`License Compliance extension expanded data with new licenses displays a class="gl-display-flex" > <div - class="gl-mr-3 gl-p-2 gl-pl-0 gl-text-green-500" + class="gl-h-5 gl-display-flex" > <div - class="gl-rounded-full gl-relative gl-display-flex" + class="gl-mr-3 gl-p-2 gl-m-auto gl-text-green-500" > <div - class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto" + class="gl-rounded-full gl-relative gl-display-flex" > <div - class="gl-display-flex gl-m-auto gl-translate-y-n50" + class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto" > - <svg - aria-label="Success " - class="gl-display-block gl-icon s12" - data-qa-selector="status_success_icon" - data-testid="status-success-icon" - role="img" + <div + class="gl-display-flex gl-m-auto gl-translate-y-n50" > - <use - href="#status-success" - /> - </svg> + <svg + aria-label="Success " + class="gl-display-block gl-icon s12" + data-qa-selector="status_success_icon" + data-testid="status-success-icon" + role="img" + > + <use + href="#status-success" + /> + </svg> + </div> </div> </div> </div> @@ -147,7 +151,7 @@ exports[`License Compliance extension expanded data with new licenses displays a > <div class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" - id="__BVID__411" + id="__BVID__619" lazy="" no-caret="" > @@ -156,7 +160,7 @@ exports[`License Compliance extension expanded data with new licenses displays a aria-expanded="false" aria-haspopup="true" class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" - id="__BVID__411__BV_toggle_" + id="__BVID__619__BV_toggle_" type="button" > <!----> @@ -190,7 +194,7 @@ exports[`License Compliance extension expanded data with new licenses displays a </svg> </button> <ul - aria-labelledby="__BVID__411__BV_toggle_" + aria-labelledby="__BVID__619__BV_toggle_" class="dropdown-menu dropdown-menu-right" role="menu" tabindex="-1" @@ -313,28 +317,32 @@ exports[`License Compliance extension expanded data with new licenses displays d class="gl-display-flex" > <div - class="gl-mr-3 gl-p-2 gl-pl-0 gl-text-red-500" + class="gl-h-5 gl-display-flex" > <div - class="gl-rounded-full gl-relative gl-display-flex" + class="gl-mr-3 gl-p-2 gl-m-auto gl-text-red-500" > <div - class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto" + class="gl-rounded-full gl-relative gl-display-flex" > <div - class="gl-display-flex gl-m-auto gl-translate-y-n50" + class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto" > - <svg - aria-label="Failed " - class="gl-display-block gl-icon s12" - data-qa-selector="status_failed_icon" - data-testid="status-failed-icon" - role="img" + <div + class="gl-display-flex gl-m-auto gl-translate-y-n50" > - <use - href="#status-failed" - /> - </svg> + <svg + aria-label="Failed " + class="gl-display-block gl-icon s12" + data-qa-selector="status_failed_icon" + data-testid="status-failed-icon" + role="img" + > + <use + href="#status-failed" + /> + </svg> + </div> </div> </div> </div> @@ -484,28 +492,32 @@ exports[`License Compliance extension expanded data with new licenses displays u class="gl-display-flex" > <div - class="gl-mr-3 gl-p-2 gl-pl-0 gl-text-gray-500" + class="gl-h-5 gl-display-flex" > <div - class="gl-rounded-full gl-relative gl-display-flex" + class="gl-mr-3 gl-p-2 gl-m-auto gl-text-gray-500" > <div - class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto" + class="gl-rounded-full gl-relative gl-display-flex" > <div - class="gl-display-flex gl-m-auto gl-translate-y-n50" + class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto" > - <svg - aria-label="Notice " - class="gl-display-block gl-icon s12" - data-qa-selector="status_notice_icon" - data-testid="status-alert-icon" - role="img" + <div + class="gl-display-flex gl-m-auto gl-translate-y-n50" > - <use - href="#status-alert" - /> - </svg> + <svg + aria-label="Notice " + class="gl-display-block gl-icon s12" + data-qa-selector="status_notice_icon" + data-testid="status-alert-icon" + role="img" + > + <use + href="#status-alert" + /> + </svg> + </div> </div> </div> </div> @@ -551,7 +563,7 @@ exports[`License Compliance extension expanded data with new licenses displays u > <div class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" - id="__BVID__332" + id="__BVID__527" lazy="" no-caret="" > @@ -560,7 +572,7 @@ exports[`License Compliance extension expanded data with new licenses displays u aria-expanded="false" aria-haspopup="true" class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" - id="__BVID__332__BV_toggle_" + id="__BVID__527__BV_toggle_" type="button" > <!----> @@ -594,7 +606,7 @@ exports[`License Compliance extension expanded data with new licenses displays u </svg> </button> <ul - aria-labelledby="__BVID__332__BV_toggle_" + aria-labelledby="__BVID__527__BV_toggle_" class="dropdown-menu dropdown-menu-right" role="menu" tabindex="-1" -- GitLab From 1e0ec0e635a65cf93d016d85b5f157af0142fde5 Mon Sep 17 00:00:00 2001 From: Stanislav Lashmanov <slashmanov@gitlab.com> Date: Fri, 2 Sep 2022 00:22:37 +0400 Subject: [PATCH 114/169] Align child icon Fix state container --- .../components/extensions/base.vue | 1 + .../components/extensions/child_content.vue | 2 +- .../stylesheets/page_bundles/reports.scss | 4 ++++ .../__snapshots__/index_spec.js.snap | 18 +++++++++--------- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index 5325aee8a8b4fd..300e2a672cbc2f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -315,6 +315,7 @@ export default { data-qa-selector="mr_widget_extension" > <state-container + :mr="mr" :status="statusIconName" :is-loading="isLoadingSummary" :class="{ 'gl-cursor-pointer': isCollapsible }" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue index 38a391eadab888..52c9f047b76b24 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue @@ -62,7 +62,7 @@ export default { <strong v-else v-safe-html="generateText(data.header)"></strong> </div> <div class="gl-display-flex"> - <div v-if="data.icon" class="gl-h-5 gl-display-flex"> + <div v-if="data.icon" class="report-block-child-icon gl-display-flex"> <status-icon :icon-name="data.icon.name" :size="12" class="gl-m-auto" /> </div> <div class="gl-w-full"> diff --git a/app/assets/stylesheets/page_bundles/reports.scss b/app/assets/stylesheets/page_bundles/reports.scss index d0748779f478cc..03c9fc7508ddb7 100644 --- a/app/assets/stylesheets/page_bundles/reports.scss +++ b/app/assets/stylesheets/page_bundles/reports.scss @@ -16,6 +16,10 @@ line-height: 20px; } +.report-block-child-icon { + height: 20px; +} + .report-block-list { list-style: none; padding: 0 1px; diff --git a/ee/spec/frontend/vue_merge_request_widget/extensions/license_compliance/__snapshots__/index_spec.js.snap b/ee/spec/frontend/vue_merge_request_widget/extensions/license_compliance/__snapshots__/index_spec.js.snap index 00aedf1786c1ff..1e0875110a59d5 100644 --- a/ee/spec/frontend/vue_merge_request_widget/extensions/license_compliance/__snapshots__/index_spec.js.snap +++ b/ee/spec/frontend/vue_merge_request_widget/extensions/license_compliance/__snapshots__/index_spec.js.snap @@ -80,7 +80,7 @@ exports[`License Compliance extension expanded data with new licenses displays a class="gl-display-flex" > <div - class="gl-h-5 gl-display-flex" + class="report-block-child-icon gl-display-flex" > <div class="gl-mr-3 gl-p-2 gl-m-auto gl-text-green-500" @@ -151,7 +151,7 @@ exports[`License Compliance extension expanded data with new licenses displays a > <div class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" - id="__BVID__619" + id="__BVID__651" lazy="" no-caret="" > @@ -160,7 +160,7 @@ exports[`License Compliance extension expanded data with new licenses displays a aria-expanded="false" aria-haspopup="true" class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" - id="__BVID__619__BV_toggle_" + id="__BVID__651__BV_toggle_" type="button" > <!----> @@ -194,7 +194,7 @@ exports[`License Compliance extension expanded data with new licenses displays a </svg> </button> <ul - aria-labelledby="__BVID__619__BV_toggle_" + aria-labelledby="__BVID__651__BV_toggle_" class="dropdown-menu dropdown-menu-right" role="menu" tabindex="-1" @@ -317,7 +317,7 @@ exports[`License Compliance extension expanded data with new licenses displays d class="gl-display-flex" > <div - class="gl-h-5 gl-display-flex" + class="report-block-child-icon gl-display-flex" > <div class="gl-mr-3 gl-p-2 gl-m-auto gl-text-red-500" @@ -492,7 +492,7 @@ exports[`License Compliance extension expanded data with new licenses displays u class="gl-display-flex" > <div - class="gl-h-5 gl-display-flex" + class="report-block-child-icon gl-display-flex" > <div class="gl-mr-3 gl-p-2 gl-m-auto gl-text-gray-500" @@ -563,7 +563,7 @@ exports[`License Compliance extension expanded data with new licenses displays u > <div class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" - id="__BVID__527" + id="__BVID__557" lazy="" no-caret="" > @@ -572,7 +572,7 @@ exports[`License Compliance extension expanded data with new licenses displays u aria-expanded="false" aria-haspopup="true" class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" - id="__BVID__527__BV_toggle_" + id="__BVID__557__BV_toggle_" type="button" > <!----> @@ -606,7 +606,7 @@ exports[`License Compliance extension expanded data with new licenses displays u </svg> </button> <ul - aria-labelledby="__BVID__527__BV_toggle_" + aria-labelledby="__BVID__557__BV_toggle_" class="dropdown-menu dropdown-menu-right" role="menu" tabindex="-1" -- GitLab From bcca7cbb3d7fa6a0340e654c9d21e62a13677d85 Mon Sep 17 00:00:00 2001 From: Eduardo Bonet <ebonet@gitlab.com> Date: Wed, 24 Aug 2022 11:02:52 +0200 Subject: [PATCH 115/169] Adds first endpoints for MLFlow Integration Implements experiment/create, experiment/get and experiment/get-by-name fromt he MLFlow "REST" API https://www.mlflow.org/docs/1.28.0/rest-api.html# FeatureFlag: ml_experiment_tracking Changelog: added --- app/models/concerns/enums/internal_id.rb | 3 +- app/models/ml/experiment.rb | 24 ++- .../development/ml_experiment_tracking.yml | 8 + ...132108_add_deleted_on_to_ml_experiments.rb | 10 + db/schema_migrations/20220818132108 | 1 + db/structure.sql | 1 + lib/api/api.rb | 1 + lib/api/entities/ml/mlflow/get_experiment.rb | 28 +++ lib/api/entities/ml/mlflow/new_experiment.rb | 19 ++ lib/api/ml/mlflow.rb | 94 +++++++++ spec/factories/ml/experiments.rb | 8 + .../api/schemas/ml/get_experiment.json | 23 ++ spec/models/ml/experiment_spec.rb | 51 +++++ spec/requests/api/ml/mlflow_spec.rb | 196 ++++++++++++++++++ 14 files changed, 464 insertions(+), 3 deletions(-) create mode 100644 config/feature_flags/development/ml_experiment_tracking.yml create mode 100644 db/migrate/20220818132108_add_deleted_on_to_ml_experiments.rb create mode 100644 db/schema_migrations/20220818132108 create mode 100644 lib/api/entities/ml/mlflow/get_experiment.rb create mode 100644 lib/api/entities/ml/mlflow/new_experiment.rb create mode 100644 lib/api/ml/mlflow.rb create mode 100644 spec/factories/ml/experiments.rb create mode 100644 spec/fixtures/api/schemas/ml/get_experiment.json create mode 100644 spec/requests/api/ml/mlflow_spec.rb diff --git a/app/models/concerns/enums/internal_id.rb b/app/models/concerns/enums/internal_id.rb index 71c86bab1361d9..a8227363a22a9d 100644 --- a/app/models/concerns/enums/internal_id.rb +++ b/app/models/concerns/enums/internal_id.rb @@ -16,7 +16,8 @@ def self.usage_resources alert_management_alerts: 8, sprints: 9, # iterations design_management_designs: 10, - incident_management_oncall_schedules: 11 + incident_management_oncall_schedules: 11, + ml_experiments: 12 } end end diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb index 7ef9c70ba7ee69..218130fe417b92 100644 --- a/app/models/ml/experiment.rb +++ b/app/models/ml/experiment.rb @@ -2,11 +2,31 @@ module Ml class Experiment < ApplicationRecord - validates :name, :iid, :project, presence: true - validates :iid, :name, uniqueness: { scope: :project, message: "should be unique in the project" } + include AtomicInternalId + + validates :name, :project, presence: true + validates :name, uniqueness: { scope: :project, message: "should be unique in the project" } belongs_to :project belongs_to :user has_many :candidates, class_name: 'Ml::Candidate' + + has_internal_id :iid, scope: :project + + def artifact_location + 'not_implemented' + end + + def self.by_project_id_and_iid(project_id, iid) + find_by(project_id: project_id, iid: iid) + end + + def self.by_project_id_and_name(project_id, name) + find_by(project_id: project_id, name: name) + end + + def self.has_record?(project_id, name) + where(project_id: project_id, name: name).exists? + end end end diff --git a/config/feature_flags/development/ml_experiment_tracking.yml b/config/feature_flags/development/ml_experiment_tracking.yml new file mode 100644 index 00000000000000..2749cbc3fc1e4f --- /dev/null +++ b/config/feature_flags/development/ml_experiment_tracking.yml @@ -0,0 +1,8 @@ +--- +name: ml_experiment_tracking +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95689 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371669 +milestone: '15.4' +type: development +group: group::incubation +default_enabled: false diff --git a/db/migrate/20220818132108_add_deleted_on_to_ml_experiments.rb b/db/migrate/20220818132108_add_deleted_on_to_ml_experiments.rb new file mode 100644 index 00000000000000..720415f17cd6b0 --- /dev/null +++ b/db/migrate/20220818132108_add_deleted_on_to_ml_experiments.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# See https://docs.gitlab.com/ee/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddDeletedOnToMlExperiments < Gitlab::Database::Migration[2.0] + def change + add_column :ml_experiments, :deleted_on, :datetime_with_timezone, index: true + end +end diff --git a/db/schema_migrations/20220818132108 b/db/schema_migrations/20220818132108 new file mode 100644 index 00000000000000..77683e61f2e4bc --- /dev/null +++ b/db/schema_migrations/20220818132108 @@ -0,0 +1 @@ +7abea29f31054d1e0337d3fa434f55cc1c354701da89e257c764b85cd2cc2768 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index ae1b0ca99902c0..e9ec938289d4c1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17718,6 +17718,7 @@ CREATE TABLE ml_experiments ( project_id bigint NOT NULL, user_id bigint, name text NOT NULL, + deleted_on timestamp with time zone, CONSTRAINT check_ee07a0be2c CHECK ((char_length(name) <= 255)) ); diff --git a/lib/api/api.rb b/lib/api/api.rb index 5a8772d6c568b9..443bf1d649aace 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -318,6 +318,7 @@ class API < ::API::Base mount ::API::Users mount ::API::Version mount ::API::Wikis + mount ::API::Ml::Mlflow end mount ::API::Internal::Base diff --git a/lib/api/entities/ml/mlflow/get_experiment.rb b/lib/api/entities/ml/mlflow/get_experiment.rb new file mode 100644 index 00000000000000..283231dd628968 --- /dev/null +++ b/lib/api/entities/ml/mlflow/get_experiment.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class GetExperiment < Grape::Entity + expose :experiment do + expose :experiment_id + expose :name + expose :lifecycle_stage + expose :artifact_location + end + + private + + def lifecycle_stage + object.deleted_on.present? ? 'deleted' : 'active' + end + + def experiment_id + object.iid.to_s + end + end + end + end + end +end diff --git a/lib/api/entities/ml/mlflow/new_experiment.rb b/lib/api/entities/ml/mlflow/new_experiment.rb new file mode 100644 index 00000000000000..0979183985067f --- /dev/null +++ b/lib/api/entities/ml/mlflow/new_experiment.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class NewExperiment < Grape::Entity + expose :experiment_id + + private + + def experiment_id + object.iid.to_s + end + end + end + end + end +end diff --git a/lib/api/ml/mlflow.rb b/lib/api/ml/mlflow.rb new file mode 100644 index 00000000000000..9e00e86e0cca70 --- /dev/null +++ b/lib/api/ml/mlflow.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'mime/types' + +module API + # MLFlow integration API, replicating the Rest API https://www.mlflow.org/docs/latest/rest-api.html#rest-api + module Ml + class Mlflow < ::API::Base + # The first part of the url is the namespace, the second part of the URL is what the MLFlow client calls + MLFLOW_API_PREFIX = ':id/ml/mflow/api/2.0/mlflow/' + + before do + authenticate! + not_found! unless Feature.enabled?(:ml_experiment_tracking, user_project) + end + + feature_category :mlops + + content_type :json, 'application/json' + default_format :json + + helpers do + def resource_not_found! + render_structured_api_error!({ error_code: 'RESOURCE_DOES_NOT_EXIST' }, 404) + end + + def resource_already_exists! + render_structured_api_error!({ error_code: 'RESOURCE_ALREADY_EXISTS' }, 400) + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'API to interface with MLFlow Client, REST API version 1.28.0' do + detail 'This feature is gated by :ml_experiment_tracking.' + end + namespace MLFLOW_API_PREFIX do + resource :experiments do + desc 'Fetch experiment by experiment_id' do + success Entities::Ml::Mlflow::GetExperiment + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment' + end + params do + optional :experiment_id, type: String, default: '', desc: 'Experiment ID (<project_id>:<experiment_name>)' + end + get 'get', urgency: :low do + experiment = ::Ml::Experiment.by_project_id_and_iid(user_project.id, params[:experiment_id]) + + resource_not_found! unless experiment + + present experiment, with: Entities::Ml::Mlflow::GetExperiment + end + + desc 'Fetch experiment by experiment_name' do + success Entities::Ml::Mlflow::GetExperiment + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment-by-name' + end + params do + optional :experiment_name, type: String, default: '', desc: 'Experiment name' + end + get 'get-by-name', urgency: :low do + experiment = ::Ml::Experiment.by_project_id_and_name(user_project, params[:experiment_name]) + + resource_not_found! unless experiment + + present experiment, with: Entities::Ml::Mlflow::GetExperiment + end + + desc 'Create experiment' do + success Entities::Ml::Mlflow::NewExperiment + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#create-experiment' + end + params do + requires :name, type: String, desc: 'Experiment name' + optional :artifact_location, type: String, desc: 'This will be ignored' + optional :tags, type: Array, desc: 'This will be ignored' + end + post 'create', urgency: :low do + resource_already_exists! if ::Ml::Experiment.has_record?(user_project.id, params[:name]) + + experiment = ::Ml::Experiment.create!(name: params[:name], + user: current_user, + project: user_project) + + present experiment, with: Entities::Ml::Mlflow::NewExperiment + end + end + end + end + end + end +end diff --git a/spec/factories/ml/experiments.rb b/spec/factories/ml/experiments.rb new file mode 100644 index 00000000000000..043ca712e60886 --- /dev/null +++ b/spec/factories/ml/experiments.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +FactoryBot.define do + factory :ml_experiments, class: '::Ml::Experiment' do + sequence(:name) { |n| "experiment#{n}" } + association :project + association :user + end +end diff --git a/spec/fixtures/api/schemas/ml/get_experiment.json b/spec/fixtures/api/schemas/ml/get_experiment.json new file mode 100644 index 00000000000000..cf8da7f999f4df --- /dev/null +++ b/spec/fixtures/api/schemas/ml/get_experiment.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "required": [ + "experiment" + ], + "properties": { + "experiment": { + "type": "object", + "required" : [ + "experiment_id", + "name", + "artifact_location", + "lifecycle_stage" + ], + "properties" : { + "experiment_id": { "type": "string" }, + "name": { "type": "string" }, + "artifact_location": { "type": "string" }, + "lifecycle_stage": { "type": { "enum" : ["active", "deleted"] } } + } + } + } +} diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb index dca5280a8fe794..9d7b510b4c7aac 100644 --- a/spec/models/ml/experiment_spec.rb +++ b/spec/models/ml/experiment_spec.rb @@ -8,4 +8,55 @@ it { is_expected.to belong_to(:user) } it { is_expected.to have_many(:candidates) } end + + describe '#by_project_id_and_iid?' do + let(:exp) { create(:ml_experiments) } + let(:iid) { exp.iid } + + subject { described_class.by_project_id_and_iid(exp.project_id, iid) } + + context 'if exists' do + it { is_expected.to eq(exp) } + end + + context 'if does not exist' do + let(:iid) { 3 } + + it { is_expected.to be(nil) } + end + end + + describe '#by_project_id_and_name?' do + let(:exp) { create(:ml_experiments) } + let(:exp_name) { exp.name } + + subject { described_class.by_project_id_and_name(exp.project_id, exp_name) } + + context 'if exists' do + it { is_expected.to eq(exp) } + end + + context 'if does not exist' do + let(:exp_name) { 'hello' } + + it { is_expected.to be_nil } + end + end + + describe '#has_record?' do + let(:exp) { create(:ml_experiments) } + let(:exp_name) { exp.name } + + subject { described_class.has_record?(exp.project_id, exp_name) } + + context 'if exists' do + it { is_expected.to be_truthy } + end + + context 'if does not exist' do + let(:exp_name) { 'hello' } + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/requests/api/ml/mlflow_spec.rb b/spec/requests/api/ml/mlflow_spec.rb new file mode 100644 index 00000000000000..0029ba1f2f9be2 --- /dev/null +++ b/spec/requests/api/ml/mlflow_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'mime/types' + +RSpec.describe API::Ml::Mlflow do + include SessionHelpers + include ApiHelpers + include HttpBasicAuthHelpers + + let(:user) { create(:user) } + let(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let(:current_user) { developer } + + let(:project) do + create(:project, + :private, + creator_id: user.id, + namespace: user.namespace, + path: 'my.project', + id: 123454) + end + + let(:ff_value) { true } + + let(:experiment) { create(:ml_experiments, name: 'the_experiment', user: user, project: project) } + + let(:scopes) { %w[api] } + let(:headers) do + { 'Authorization' => "Bearer #{create(:personal_access_token, scopes: scopes, user: current_user).token}" } + end + + let(:params) { {} } + let(:request) { get api(route), params: params, headers: headers } + + before do + stub_feature_flags(ml_experiment_tracking: ff_value) + + create(:ml_experiments, name: 'existing_experiment', user: user, project: project) + + request + end + + shared_examples 'Not Found' do |message| + it "is Not Found" do + expect(response).to have_gitlab_http_status(:not_found) + + expect(json_response['message']).to eq(message) if message.present? + end + end + + shared_examples 'Not Found - Resource Does Not Exist' do + it "is Resource Does Not Exist" do + expect(response).to have_gitlab_http_status(:not_found) + + expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' }) + end + end + + shared_examples 'Bad Request' do |error_code = nil| + it "is Bad Request" do + expect(response).to have_gitlab_http_status(:bad_request) + + expect(json_response).to include({ 'error_code' => error_code }) if error_code.present? + end + end + + shared_examples 'shared error cases' do + context 'when not authenticated' do + let(:headers) { {} } + + it "is Unauthorized" do + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when user does not have access' do + let(:current_user) { create(:user) } + + it_behaves_like 'Not Found' + end + + context 'when ff is disabled' do + let(:ff_value) { false } + + it_behaves_like 'Not Found' + end + end + + describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/get' do + let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_id}" } + let(:experiment_id) { experiment.iid.to_s } + + it 'returns the experiment' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('ml/get_experiment') + expect(json_response).to include({ + 'experiment' => { + 'experiment_id' => '2', + 'name' => 'the_experiment', + 'lifecycle_stage' => 'active', + 'artifact_location' => 'not_implemented' + } + }) + end + + describe 'Error States' do + context 'when has access' do + context 'and experiment does not exist' do + let(:experiment_id) { '2' } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + context 'and experiment_id is not passed' do + let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get" } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + end + + it_behaves_like 'shared error cases' + end + end + + describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/experiments/get-by-name' do + let(:route) do + "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get-by-name?experiment_name=#{experiment_name}" + end + + let(:experiment_name) { experiment.name } + + it 'returns the experiment' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('ml/get_experiment') + expect(json_response).to include({ + 'experiment' => { + 'experiment_id' => '2', + 'name' => 'the_experiment', + 'lifecycle_stage' => 'active', + 'artifact_location' => 'not_implemented' + } + }) + end + + describe 'Error States' do + context 'when has access but experiment does not exist' do + let(:experiment_name) { "random_experiment" } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + context 'when has access but experiment_name is not passed' do + let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get-by-name" } + + it_behaves_like 'Not Found - Resource Does Not Exist' + end + + it_behaves_like 'shared error cases' + end + end + + describe 'POST /projects/:id/ml/mflow/api/2.0/mlflow/experiments/create' do + let(:route) do + "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/create" + end + + let(:params) { { name: 'new_experiment' } } + let(:request) { post api(route), params: params, headers: headers } + + it 'creates the experiment' do + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include({ 'experiment_id' => '2' }) + end + + describe 'Error States' do + context 'when experiment name is not passed' do + let(:params) { {} } + + it_behaves_like 'Bad Request' + end + + context 'when experiment name already exists' do + let(:params) { { name: 'existing_experiment' } } + + it_behaves_like 'Bad Request', 'RESOURCE_ALREADY_EXISTS' + end + + context 'when project does not exist' do + let(:route) { "/projects/9999999/ml/mflow/api/2.0/mlflow/experiments/create" } + + it_behaves_like 'Not Found', '404 Project Not Found' + end + end + end +end -- GitLab From 7661e68e365a3eb4137fa015edbd5d4e168e7199 Mon Sep 17 00:00:00 2001 From: Eduardo Bonet <ebonet@gitlab.com> Date: Thu, 1 Sep 2022 16:28:34 +0200 Subject: [PATCH 116/169] Adds small comestic improvements --- app/models/ml/experiment.rb | 22 ++++++++++--------- ...132108_add_deleted_on_to_ml_experiments.rb | 3 --- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb index 218130fe417b92..e4e9baac4c8cd3 100644 --- a/app/models/ml/experiment.rb +++ b/app/models/ml/experiment.rb @@ -17,16 +17,18 @@ def artifact_location 'not_implemented' end - def self.by_project_id_and_iid(project_id, iid) - find_by(project_id: project_id, iid: iid) - end - - def self.by_project_id_and_name(project_id, name) - find_by(project_id: project_id, name: name) - end - - def self.has_record?(project_id, name) - where(project_id: project_id, name: name).exists? + class << self + def by_project_id_and_iid(project_id, iid) + find_by(project_id: project_id, iid: iid) + end + + def by_project_id_and_name(project_id, name) + find_by(project_id: project_id, name: name) + end + + def has_record?(project_id, name) + where(project_id: project_id, name: name).exists? + end end end end diff --git a/db/migrate/20220818132108_add_deleted_on_to_ml_experiments.rb b/db/migrate/20220818132108_add_deleted_on_to_ml_experiments.rb index 720415f17cd6b0..e6ba9f78553b3a 100644 --- a/db/migrate/20220818132108_add_deleted_on_to_ml_experiments.rb +++ b/db/migrate/20220818132108_add_deleted_on_to_ml_experiments.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -# See https://docs.gitlab.com/ee/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - class AddDeletedOnToMlExperiments < Gitlab::Database::Migration[2.0] def change add_column :ml_experiments, :deleted_on, :datetime_with_timezone, index: true -- GitLab From 2d88c3639710aaca06a1b3b5a797315338ff00dc Mon Sep 17 00:00:00 2001 From: Eduardo Bonet <ebonet@gitlab.com> Date: Fri, 2 Sep 2022 15:45:11 +0200 Subject: [PATCH 117/169] Improves test cases --- lib/api/entities/ml/mlflow/get_experiment.rb | 2 +- lib/api/ml/mlflow.rb | 2 +- spec/models/ml/experiment_spec.rb | 2 +- spec/requests/api/ml/mlflow_spec.rb | 48 ++++++++------------ 4 files changed, 23 insertions(+), 31 deletions(-) diff --git a/lib/api/entities/ml/mlflow/get_experiment.rb b/lib/api/entities/ml/mlflow/get_experiment.rb index 283231dd628968..cd42c45c06b71e 100644 --- a/lib/api/entities/ml/mlflow/get_experiment.rb +++ b/lib/api/entities/ml/mlflow/get_experiment.rb @@ -15,7 +15,7 @@ class GetExperiment < Grape::Entity private def lifecycle_stage - object.deleted_on.present? ? 'deleted' : 'active' + object.deleted_on? ? 'deleted' : 'active' end def experiment_id diff --git a/lib/api/ml/mlflow.rb b/lib/api/ml/mlflow.rb index 9e00e86e0cca70..dbdfa03411453d 100644 --- a/lib/api/ml/mlflow.rb +++ b/lib/api/ml/mlflow.rb @@ -43,7 +43,7 @@ def resource_already_exists! detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment' end params do - optional :experiment_id, type: String, default: '', desc: 'Experiment ID (<project_id>:<experiment_name>)' + optional :experiment_id, type: String, default: '', desc: 'Experiment ID, in reference to the project' end get 'get', urgency: :low do experiment = ::Ml::Experiment.by_project_id_and_iid(user_project.id, params[:experiment_id]) diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb index 9d7b510b4c7aac..e300f82d2904ff 100644 --- a/spec/models/ml/experiment_spec.rb +++ b/spec/models/ml/experiment_spec.rb @@ -20,7 +20,7 @@ end context 'if does not exist' do - let(:iid) { 3 } + let(:iid) { non_existing_record_id } it { is_expected.to be(nil) } end diff --git a/spec/requests/api/ml/mlflow_spec.rb b/spec/requests/api/ml/mlflow_spec.rb index 0029ba1f2f9be2..534947fcac7c81 100644 --- a/spec/requests/api/ml/mlflow_spec.rb +++ b/spec/requests/api/ml/mlflow_spec.rb @@ -8,23 +8,14 @@ include ApiHelpers include HttpBasicAuthHelpers - let(:user) { create(:user) } - let(:developer) { create(:user).tap { |u| project.add_developer(u) } } - let(:current_user) { developer } - - let(:project) do - create(:project, - :private, - creator_id: user.id, - namespace: user.namespace, - path: 'my.project', - id: 123454) + let_it_be(:project) { create(:project, :private) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:experiment) do + create(:ml_experiments, user: project.creator, project: project) end + let(:current_user) { developer } let(:ff_value) { true } - - let(:experiment) { create(:ml_experiments, name: 'the_experiment', user: user, project: project) } - let(:scopes) { %w[api] } let(:headers) do { 'Authorization' => "Bearer #{create(:personal_access_token, scopes: scopes, user: current_user).token}" } @@ -36,8 +27,6 @@ before do stub_feature_flags(ml_experiment_tracking: ff_value) - create(:ml_experiments, name: 'existing_experiment', user: user, project: project) - request end @@ -88,16 +77,16 @@ end describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/get' do - let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_id}" } - let(:experiment_id) { experiment.iid.to_s } + let(:experiment_iid) { experiment.iid.to_s } + let(:route) { "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_iid}" } it 'returns the experiment' do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('ml/get_experiment') expect(json_response).to include({ 'experiment' => { - 'experiment_id' => '2', - 'name' => 'the_experiment', + 'experiment_id' => experiment_iid, + 'name' => experiment.name, 'lifecycle_stage' => 'active', 'artifact_location' => 'not_implemented' } @@ -107,7 +96,7 @@ describe 'Error States' do context 'when has access' do context 'and experiment does not exist' do - let(:experiment_id) { '2' } + let(:experiment_iid) { non_existing_record_iid.to_s } it_behaves_like 'Not Found - Resource Does Not Exist' end @@ -124,19 +113,18 @@ end describe 'GET /projects/:id/ml/mflow/api/2.0/mlflow/experiments/get-by-name' do + let(:experiment_name) { experiment.name } let(:route) do "/projects/#{project.id}/ml/mflow/api/2.0/mlflow/experiments/get-by-name?experiment_name=#{experiment_name}" end - let(:experiment_name) { experiment.name } - it 'returns the experiment' do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('ml/get_experiment') expect(json_response).to include({ 'experiment' => { - 'experiment_id' => '2', - 'name' => 'the_experiment', + 'experiment_id' => experiment.iid.to_s, + 'name' => experiment_name, 'lifecycle_stage' => 'active', 'artifact_location' => 'not_implemented' } @@ -170,7 +158,7 @@ it 'creates the experiment' do expect(response).to have_gitlab_http_status(:created) - expect(json_response).to include({ 'experiment_id' => '2' }) + expect(json_response).to include('experiment_id' ) end describe 'Error States' do @@ -181,13 +169,17 @@ end context 'when experiment name already exists' do - let(:params) { { name: 'existing_experiment' } } + let(:existing_experiment) do + create(:ml_experiments, user: current_user, project: project) + end + + let(:params) { { name: existing_experiment.name } } it_behaves_like 'Bad Request', 'RESOURCE_ALREADY_EXISTS' end context 'when project does not exist' do - let(:route) { "/projects/9999999/ml/mflow/api/2.0/mlflow/experiments/create" } + let(:route) { "/projects/#{non_existing_record_id}/ml/mflow/api/2.0/mlflow/experiments/create" } it_behaves_like 'Not Found', '404 Project Not Found' end -- GitLab From c0da20d1a2bd382be8aff907cfbc538d31bec54a Mon Sep 17 00:00:00 2001 From: Savas Vedova <svedova@gitlab.com> Date: Tue, 6 Sep 2022 16:44:03 +0300 Subject: [PATCH 118/169] Use includes instead of indexOf --- .../components/widget/widget_content_section.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue index 42fd02f978bdb1..61e3744b5dc1a0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue @@ -11,7 +11,7 @@ export default { type: String, default: '', required: false, - validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).indexOf(value) > -1, + validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value), }, widgetName: { type: String, -- GitLab From d7d6efe12d962b68085acff413abbca2299caf28 Mon Sep 17 00:00:00 2001 From: Marius Bobin <mbobin@gitlab.com> Date: Fri, 2 Sep 2022 10:13:23 +0300 Subject: [PATCH 119/169] Add partition_id column to selected CI database tables Changelog: added --- app/models/ci/bridge.rb | 2 +- app/models/ci/build.rb | 3 +- app/models/ci/partition.rb | 6 ++++ db/docs/ci_partitions.yml | 9 ++++++ .../20220902065314_create_ci_partitions.rb | 9 ++++++ ...2065316_create_default_partition_record.rb | 21 +++++++++++++ ...902065317_add_partition_id_to_ci_builds.rb | 11 +++++++ ..._add_partition_id_to_ci_builds_metadata.rb | 9 ++++++ ...11_add_partition_id_to_ci_job_artifacts.rb | 9 ++++++ ...065623_add_partition_id_to_ci_pipelines.rb | 9 ++++++ ...902065635_add_partition_id_to_ci_stages.rb | 9 ++++++ ...d_partition_id_to_ci_pipeline_variables.rb | 9 ++++++ db/schema_migrations/20220902065314 | 1 + db/schema_migrations/20220902065316 | 1 + db/schema_migrations/20220902065317 | 1 + db/schema_migrations/20220902065558 | 1 + db/schema_migrations/20220902065611 | 1 + db/schema_migrations/20220902065623 | 1 + db/schema_migrations/20220902065635 | 1 + db/schema_migrations/20220902065647 | 1 + db/structure.sql | 30 +++++++++++++++++-- lib/gitlab/database/gitlab_schemas.yml | 1 + spec/db/schema_spec.rb | 7 ++++- 23 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 app/models/ci/partition.rb create mode 100644 db/docs/ci_partitions.yml create mode 100644 db/migrate/20220902065314_create_ci_partitions.rb create mode 100644 db/migrate/20220902065316_create_default_partition_record.rb create mode 100644 db/migrate/20220902065317_add_partition_id_to_ci_builds.rb create mode 100644 db/migrate/20220902065558_add_partition_id_to_ci_builds_metadata.rb create mode 100644 db/migrate/20220902065611_add_partition_id_to_ci_job_artifacts.rb create mode 100644 db/migrate/20220902065623_add_partition_id_to_ci_pipelines.rb create mode 100644 db/migrate/20220902065635_add_partition_id_to_ci_stages.rb create mode 100644 db/migrate/20220902065647_add_partition_id_to_ci_pipeline_variables.rb create mode 100644 db/schema_migrations/20220902065314 create mode 100644 db/schema_migrations/20220902065316 create mode 100644 db/schema_migrations/20220902065317 create mode 100644 db/schema_migrations/20220902065558 create mode 100644 db/schema_migrations/20220902065611 create mode 100644 db/schema_migrations/20220902065623 create mode 100644 db/schema_migrations/20220902065635 create mode 100644 db/schema_migrations/20220902065647 diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 0374d076da8b5f..0223ad3818c4ee 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -77,7 +77,7 @@ def self.clone_accessors %i[pipeline project ref tag options name allow_failure stage stage_idx yaml_variables when description needs_attributes - scheduling_type ci_stage].freeze + scheduling_type ci_stage partition_id].freeze end def inherit_status_from_downstream!(pipeline) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index a08fdf3652abfd..60a1d8b4b534e8 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -217,7 +217,8 @@ def clone_accessors allow_failure stage stage_idx trigger_request yaml_variables when environment coverage_regex description tag_list protected needs_attributes - job_variables_attributes resource_group scheduling_type ci_stage].freeze + job_variables_attributes resource_group scheduling_type + ci_stage partition_id].freeze end end diff --git a/app/models/ci/partition.rb b/app/models/ci/partition.rb new file mode 100644 index 00000000000000..d773038df01983 --- /dev/null +++ b/app/models/ci/partition.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Ci + class Partition < Ci::ApplicationRecord + end +end diff --git a/db/docs/ci_partitions.yml b/db/docs/ci_partitions.yml new file mode 100644 index 00000000000000..8dfa31f05f90cb --- /dev/null +++ b/db/docs/ci_partitions.yml @@ -0,0 +1,9 @@ +--- +table_name: ci_partitions +classes: +- Ci::Partition +feature_categories: +- continuous_integration +description: Database partitioning metadata for CI tables +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96856 +milestone: '15.4' diff --git a/db/migrate/20220902065314_create_ci_partitions.rb b/db/migrate/20220902065314_create_ci_partitions.rb new file mode 100644 index 00000000000000..1a8a4f172f86c8 --- /dev/null +++ b/db/migrate/20220902065314_create_ci_partitions.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class CreateCiPartitions < Gitlab::Database::Migration[2.0] + def change + create_table :ci_partitions do |t| + t.timestamps_with_timezone null: false + end + end +end diff --git a/db/migrate/20220902065316_create_default_partition_record.rb b/db/migrate/20220902065316_create_default_partition_record.rb new file mode 100644 index 00000000000000..6493fb23d4c59f --- /dev/null +++ b/db/migrate/20220902065316_create_default_partition_record.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class CreateDefaultPartitionRecord < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + restrict_gitlab_migration gitlab_schema: :gitlab_ci + + def up + execute(<<~SQL) + INSERT INTO "ci_partitions" ("id", "created_at", "updated_at") + VALUES (100, now(), now()); + SQL + + reset_pk_sequence!('ci_partitions') + end + + def down + execute(<<~SQL) + DELETE FROM "ci_partitions" WHERE "ci_partitions"."id" = 100; + SQL + end +end diff --git a/db/migrate/20220902065317_add_partition_id_to_ci_builds.rb b/db/migrate/20220902065317_add_partition_id_to_ci_builds.rb new file mode 100644 index 00000000000000..6257164b44e5bc --- /dev/null +++ b/db/migrate/20220902065317_add_partition_id_to_ci_builds.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddPartitionIdToCiBuilds < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + # rubocop:disable Migration/AddColumnsToWideTables + def change + add_column :ci_builds, :partition_id, :bigint, default: 100, null: false + end + # rubocop:enable Migration/AddColumnsToWideTables +end diff --git a/db/migrate/20220902065558_add_partition_id_to_ci_builds_metadata.rb b/db/migrate/20220902065558_add_partition_id_to_ci_builds_metadata.rb new file mode 100644 index 00000000000000..e04ea99539f61f --- /dev/null +++ b/db/migrate/20220902065558_add_partition_id_to_ci_builds_metadata.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddPartitionIdToCiBuildsMetadata < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + def change + add_column :ci_builds_metadata, :partition_id, :bigint, default: 100, null: false + end +end diff --git a/db/migrate/20220902065611_add_partition_id_to_ci_job_artifacts.rb b/db/migrate/20220902065611_add_partition_id_to_ci_job_artifacts.rb new file mode 100644 index 00000000000000..1d9eeb0330efdd --- /dev/null +++ b/db/migrate/20220902065611_add_partition_id_to_ci_job_artifacts.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddPartitionIdToCiJobArtifacts < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + def change + add_column :ci_job_artifacts, :partition_id, :bigint, default: 100, null: false + end +end diff --git a/db/migrate/20220902065623_add_partition_id_to_ci_pipelines.rb b/db/migrate/20220902065623_add_partition_id_to_ci_pipelines.rb new file mode 100644 index 00000000000000..bb3e7c27ee8856 --- /dev/null +++ b/db/migrate/20220902065623_add_partition_id_to_ci_pipelines.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddPartitionIdToCiPipelines < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + def change + add_column :ci_pipelines, :partition_id, :bigint, default: 100, null: false + end +end diff --git a/db/migrate/20220902065635_add_partition_id_to_ci_stages.rb b/db/migrate/20220902065635_add_partition_id_to_ci_stages.rb new file mode 100644 index 00000000000000..0ddbf491ee9416 --- /dev/null +++ b/db/migrate/20220902065635_add_partition_id_to_ci_stages.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddPartitionIdToCiStages < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + def change + add_column :ci_stages, :partition_id, :bigint, default: 100, null: false + end +end diff --git a/db/migrate/20220902065647_add_partition_id_to_ci_pipeline_variables.rb b/db/migrate/20220902065647_add_partition_id_to_ci_pipeline_variables.rb new file mode 100644 index 00000000000000..14f17b371b46e8 --- /dev/null +++ b/db/migrate/20220902065647_add_partition_id_to_ci_pipeline_variables.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddPartitionIdToCiPipelineVariables < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + def change + add_column :ci_pipeline_variables, :partition_id, :bigint, default: 100, null: false + end +end diff --git a/db/schema_migrations/20220902065314 b/db/schema_migrations/20220902065314 new file mode 100644 index 00000000000000..8197a41403d6b7 --- /dev/null +++ b/db/schema_migrations/20220902065314 @@ -0,0 +1 @@ +d1ca445a17c742d435cba3d898e61242a3df9c92caeadecba147fce858d8cb80 \ No newline at end of file diff --git a/db/schema_migrations/20220902065316 b/db/schema_migrations/20220902065316 new file mode 100644 index 00000000000000..e9c3598206eda3 --- /dev/null +++ b/db/schema_migrations/20220902065316 @@ -0,0 +1 @@ +910d87fbab226671b8e12b236be43970f6b2a3083f30df9586b3f8edf779f4af \ No newline at end of file diff --git a/db/schema_migrations/20220902065317 b/db/schema_migrations/20220902065317 new file mode 100644 index 00000000000000..fa60ee97fef7b6 --- /dev/null +++ b/db/schema_migrations/20220902065317 @@ -0,0 +1 @@ +11c65391a6744d7d7c303c6593dafa8e6dca392675974a2a1df2c164afbd4fe1 \ No newline at end of file diff --git a/db/schema_migrations/20220902065558 b/db/schema_migrations/20220902065558 new file mode 100644 index 00000000000000..2886e656d41ea2 --- /dev/null +++ b/db/schema_migrations/20220902065558 @@ -0,0 +1 @@ +cce779cc52b2bb175ccd3d07ac6a7df3711ae362fa0a5004bfc58fa1eb440e1f \ No newline at end of file diff --git a/db/schema_migrations/20220902065611 b/db/schema_migrations/20220902065611 new file mode 100644 index 00000000000000..365cb0f6194e32 --- /dev/null +++ b/db/schema_migrations/20220902065611 @@ -0,0 +1 @@ +8ec0cc23559ba1b83042bed4abf8c47487ecb999fa66e602fbf4a9edac0569ec \ No newline at end of file diff --git a/db/schema_migrations/20220902065623 b/db/schema_migrations/20220902065623 new file mode 100644 index 00000000000000..cf75e086f31766 --- /dev/null +++ b/db/schema_migrations/20220902065623 @@ -0,0 +1 @@ +4f2076138e65849d60cf093f140afa1abaa7beea4d6c95048e6743168a7f17a9 \ No newline at end of file diff --git a/db/schema_migrations/20220902065635 b/db/schema_migrations/20220902065635 new file mode 100644 index 00000000000000..bd131598d7883b --- /dev/null +++ b/db/schema_migrations/20220902065635 @@ -0,0 +1 @@ +49a86fa87974f2c0cdc5a38726ab792f70c43e7f215495323d0999fd9f6e45f6 \ No newline at end of file diff --git a/db/schema_migrations/20220902065647 b/db/schema_migrations/20220902065647 new file mode 100644 index 00000000000000..31ee9352fe65d3 --- /dev/null +++ b/db/schema_migrations/20220902065647 @@ -0,0 +1 @@ +812f25371d731d03bd4727328ad0daaf954595e24a314dd5f1adccdc3a4532c4 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index ae1b0ca99902c0..7a292144149865 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12616,6 +12616,7 @@ CREATE TABLE ci_builds ( scheduling_type smallint, id bigint NOT NULL, stage_id bigint, + partition_id bigint DEFAULT 100 NOT NULL, CONSTRAINT check_1e2fbd1b39 CHECK ((lock_version IS NOT NULL)) ); @@ -12642,7 +12643,8 @@ CREATE TABLE ci_builds_metadata ( build_id bigint NOT NULL, id bigint NOT NULL, runtime_runner_features jsonb DEFAULT '{}'::jsonb NOT NULL, - id_tokens jsonb DEFAULT '{}'::jsonb NOT NULL + id_tokens jsonb DEFAULT '{}'::jsonb NOT NULL, + partition_id bigint DEFAULT 100 NOT NULL ); CREATE SEQUENCE ci_builds_metadata_id_seq @@ -12807,6 +12809,7 @@ CREATE TABLE ci_job_artifacts ( job_id bigint NOT NULL, locked smallint DEFAULT 2, original_filename text, + partition_id bigint DEFAULT 100 NOT NULL, CONSTRAINT check_27f0f6dbab CHECK ((file_store IS NOT NULL)), CONSTRAINT check_85573000db CHECK ((char_length(original_filename) <= 512)) ); @@ -12912,6 +12915,21 @@ CREATE SEQUENCE ci_namespace_monthly_usages_id_seq ALTER SEQUENCE ci_namespace_monthly_usages_id_seq OWNED BY ci_namespace_monthly_usages.id; +CREATE TABLE ci_partitions ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE ci_partitions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE ci_partitions_id_seq OWNED BY ci_partitions.id; + CREATE TABLE ci_pending_builds ( id bigint NOT NULL, build_id bigint NOT NULL, @@ -13055,7 +13073,8 @@ CREATE TABLE ci_pipeline_variables ( encrypted_value_iv character varying, pipeline_id integer NOT NULL, variable_type smallint DEFAULT 1 NOT NULL, - raw boolean DEFAULT true NOT NULL + raw boolean DEFAULT true NOT NULL, + partition_id bigint DEFAULT 100 NOT NULL ); CREATE SEQUENCE ci_pipeline_variables_id_seq @@ -13097,6 +13116,7 @@ CREATE TABLE ci_pipelines ( external_pull_request_id bigint, ci_ref_id bigint, locked smallint DEFAULT 1 NOT NULL, + partition_id bigint DEFAULT 100 NOT NULL, CONSTRAINT check_d7e99a025e CHECK ((lock_version IS NOT NULL)) ); @@ -13401,6 +13421,7 @@ CREATE TABLE ci_stages ( lock_version integer DEFAULT 0, "position" integer, id bigint NOT NULL, + partition_id bigint DEFAULT 100 NOT NULL, CONSTRAINT check_81b431e49b CHECK ((lock_version IS NOT NULL)) ); @@ -23238,6 +23259,8 @@ ALTER TABLE ONLY ci_namespace_mirrors ALTER COLUMN id SET DEFAULT nextval('ci_na ALTER TABLE ONLY ci_namespace_monthly_usages ALTER COLUMN id SET DEFAULT nextval('ci_namespace_monthly_usages_id_seq'::regclass); +ALTER TABLE ONLY ci_partitions ALTER COLUMN id SET DEFAULT nextval('ci_partitions_id_seq'::regclass); + ALTER TABLE ONLY ci_pending_builds ALTER COLUMN id SET DEFAULT nextval('ci_pending_builds_id_seq'::regclass); ALTER TABLE ONLY ci_pipeline_artifacts ALTER COLUMN id SET DEFAULT nextval('ci_pipeline_artifacts_id_seq'::regclass); @@ -24973,6 +24996,9 @@ ALTER TABLE ONLY ci_namespace_mirrors ALTER TABLE ONLY ci_namespace_monthly_usages ADD CONSTRAINT ci_namespace_monthly_usages_pkey PRIMARY KEY (id); +ALTER TABLE ONLY ci_partitions + ADD CONSTRAINT ci_partitions_pkey PRIMARY KEY (id); + ALTER TABLE ONLY ci_pending_builds ADD CONSTRAINT ci_pending_builds_pkey PRIMARY KEY (id); diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml index 93fe5871a9b09f..1c2d04561b4e5c 100644 --- a/lib/gitlab/database/gitlab_schemas.yml +++ b/lib/gitlab/database/gitlab_schemas.yml @@ -91,6 +91,7 @@ ci_job_artifact_states: :gitlab_ci ci_minutes_additional_packs: :gitlab_ci ci_namespace_monthly_usages: :gitlab_ci ci_namespace_mirrors: :gitlab_ci +ci_partitions: :gitlab_ci ci_pending_builds: :gitlab_ci ci_pipeline_artifacts: :gitlab_ci ci_pipeline_chat_data: :gitlab_ci diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 4092f639eaeb08..4aeafed5712649 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -31,9 +31,14 @@ boards: %w[milestone_id iteration_id], chat_names: %w[chat_id team_id user_id], chat_teams: %w[team_id], - ci_builds: %w[erased_by_id trigger_request_id], + ci_builds: %w[erased_by_id trigger_request_id partition_id], + ci_builds_metadata: %w[partition_id], + ci_job_artifacts: %w[partition_id], ci_namespace_monthly_usages: %w[namespace_id], + ci_pipeline_variables: %w[partition_id], + ci_pipelines: %w[partition_id], ci_runner_projects: %w[runner_id], + ci_stages: %w[partition_id], ci_trigger_requests: %w[commit_id], cluster_providers_aws: %w[security_group_id vpc_id access_key_id], cluster_providers_gcp: %w[gcp_project_id operation_id], -- GitLab From 9d607b26acc6bfac12a377ba5232cbe06fc22c8d Mon Sep 17 00:00:00 2001 From: Vasilii Iakliushin <viakliushin@gitlab.com> Date: Wed, 31 Aug 2022 11:17:55 +0200 Subject: [PATCH 120/169] Fix subgroup support for approval rules Contributes to https://gitlab.com/gitlab-org/gitlab/-/issues/371106 **Problem** We had a problem with inherited permissions for subgroups. Users couldn't see appovers for groups if they don't have a direct membership in them. It was fixed in scope of https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91598. However, there is a similar invalid check that filters subgroups with inherited permissions out. **Solution** * `public_or_visible_to_user` method doesn't correctly support subgroup permissions. Instead we can verify if user has an access to the group. * Add `accessible_to_user` scope that correctly supports subgroups Changelog: fixed EE: true --- app/models/group.rb | 16 ++++++++ ee/app/finders/approval_rules/group_finder.rb | 7 +--- .../params_filtering_service.rb | 6 ++- .../params_filtering_service_spec.rb | 22 ++++++++++- spec/models/group_spec.rb | 37 ++++++++++++++++++- 5 files changed, 78 insertions(+), 10 deletions(-) diff --git a/app/models/group.rb b/app/models/group.rb index 0ce3a4398689b1..9d92e01df3a532 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -191,6 +191,22 @@ def of_ancestors_and_self .where(group_group_links: { shared_group_id: group.self_and_ancestors }) end + # WARNING: This method should never be used on its own + # please do make sure the number of rows you are filtering is small + # enough for this query + # + # It's a replacement for `public_or_visible_to_user` that correctly + # supports subgroup permissions + scope :accessible_to_user, -> (user) do + if user + Preloaders::GroupPolicyPreloader.new(self, user).execute + + select { |group| user.can?(:read_group, group) } + else + public_to_user + end + end + class << self def sort_by_attribute(method) if method == 'storage_size_desc' diff --git a/ee/app/finders/approval_rules/group_finder.rb b/ee/app/finders/approval_rules/group_finder.rb index 55272ba5f1a286..b339561da12004 100644 --- a/ee/app/finders/approval_rules/group_finder.rb +++ b/ee/app/finders/approval_rules/group_finder.rb @@ -15,12 +15,7 @@ def initialize(rule, user) def visible_groups if Feature.enabled?(:subgroups_approval_rules, rule.project) strong_memoize(:visible_groups) do - if current_user - Preloaders::GroupPolicyPreloader.new(groups, current_user).execute - groups.select { |group| current_user.can?(:read_group, group) } - else - groups.public_to_user - end + groups.accessible_to_user(current_user) end else @visible_groups ||= groups.public_or_visible_to_user(current_user) diff --git a/ee/app/services/approval_rules/params_filtering_service.rb b/ee/app/services/approval_rules/params_filtering_service.rb index fe724c65164166..c50db1d273483f 100644 --- a/ee/app/services/approval_rules/params_filtering_service.rb +++ b/ee/app/services/approval_rules/params_filtering_service.rb @@ -83,7 +83,11 @@ def batch_load_visible_user_and_group_ids # rubocop: disable CodeReuse/ActiveRecord @visible_group_ids = params[:approval_rules_attributes].flat_map { |hash| hash[:group_ids] } if @visible_group_ids.present? - @visible_group_ids = ::Group.id_in(@visible_group_ids).public_or_visible_to_user(current_user).pluck(:id) + @visible_group_ids = if Feature.enabled?(:subgroups_approval_rules, project) + ::Group.id_in(@visible_group_ids).accessible_to_user(current_user).pluck(:id) + else + ::Group.id_in(@visible_group_ids).public_or_visible_to_user(current_user).pluck(:id) + end end @visible_user_ids = params[:approval_rules_attributes].flat_map { |hash| hash[:user_ids] } diff --git a/ee/spec/services/approval_rules/params_filtering_service_spec.rb b/ee/spec/services/approval_rules/params_filtering_service_spec.rb index ac9dab11b28ced..2400a1629e43f8 100644 --- a/ee/spec/services/approval_rules/params_filtering_service_spec.rb +++ b/ee/spec/services/approval_rules/params_filtering_service_spec.rb @@ -7,6 +7,7 @@ let(:project_member) { create(:user) } let(:outsider) { create(:user) } let(:accessible_group) { create(:group, :private) } + let(:accessible_subgroup) { create(:group, :private, parent: accessible_group) } let(:inaccessible_group) { create(:group, :private) } let(:project) { create(:project, :repository) } let(:user) { create(:user) } @@ -73,11 +74,28 @@ let(:approval_rules_attributes) do [ { name: 'foo', user_ids: [project_member.id, outsider.id] }, - { name: 'bar', user_ids: [outsider.id], group_ids: [accessible_group.id, inaccessible_group.id] } + { name: 'bar', user_ids: [outsider.id], group_ids: [accessible_group.id, accessible_subgroup.id, inaccessible_group.id] } ] end - let(:expected_groups) { [accessible_group] } + let(:expected_groups) { [accessible_group, accessible_subgroup] } + end + + context 'when subgroups_approval_rules is disabled' do + before do + stub_feature_flags(subgroups_approval_rules: false) + end + + it_behaves_like :assigning_users_and_groups do + let(:approval_rules_attributes) do + [ + { name: 'foo', user_ids: [project_member.id, outsider.id] }, + { name: 'bar', user_ids: [outsider.id], group_ids: [accessible_group.id, accessible_subgroup.id, inaccessible_group.id] } + ] + end + + let(:expected_groups) { [accessible_group] } + end end # When a project approval rule is genuinely empty, it should not be converted diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 170ac657b8403b..2ce75fb1290f4a 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -707,7 +707,8 @@ end describe '.public_or_visible_to_user' do - let!(:private_group) { create(:group, :private) } + let!(:private_group) { create(:group, :private) } + let!(:private_subgroup) { create(:group, :private, parent: private_group) } let!(:internal_group) { create(:group, :internal) } subject { described_class.public_or_visible_to_user(user) } @@ -731,6 +732,10 @@ end it { is_expected.to match_array([private_group, internal_group, group]) } + + it 'does not have access to subgroups (see accessible_to_user scope)' do + is_expected.not_to include(private_subgroup) + end end context 'when user is a member of private subgroup' do @@ -839,6 +844,36 @@ expect(described_class.by_ids_or_paths([new_group.id], [group_path])).to match_array([group, new_group]) end end + + describe 'accessible_to_user' do + subject { described_class.accessible_to_user(user) } + + let_it_be(:public_group) { create(:group, :public) } + let_it_be(:unaccessible_group) { create(:group, :private) } + let_it_be(:unaccessible_subgroup) { create(:group, :private, parent: unaccessible_group) } + let_it_be(:accessible_group) { create(:group, :private) } + let_it_be(:accessible_subgroup) { create(:group, :private, parent: accessible_group) } + + context 'when user is nil' do + let(:user) { nil } + + it { is_expected.to match_array([group, public_group]) } + end + + context 'when user is present' do + let(:user) { create(:user) } + + it { is_expected.to match_array([group, internal_group, public_group]) } + + context 'when user has access to accessible group' do + before do + accessible_group.add_developer(user) + end + + it { is_expected.to match_array([group, internal_group, public_group, accessible_group, accessible_subgroup]) } + end + end + end end describe '#to_reference' do -- GitLab From cf058460806f0bd10a30fe7c4eb48bb869b46ebf Mon Sep 17 00:00:00 2001 From: Marcel Amirault <mamirault@gitlab.com> Date: Tue, 6 Sep 2022 14:12:08 +0000 Subject: [PATCH 121/169] Add missing trigger subkeys --- doc/ci/yaml/index.md | 97 ++++++++++++++++++++++++++++++++------------ 1 file changed, 71 insertions(+), 26 deletions(-) diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index b1f4331030aff5..e8ab13e16080b2 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3882,16 +3882,13 @@ test: ### `trigger` -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8997) in GitLab Premium 11.8. -> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8. - Use `trigger` to declare that a job is a "trigger job" which starts a [downstream pipeline](../pipelines/downstream_pipelines.md) that is either: - [A multi-project pipeline](../pipelines/downstream_pipelines.md#multi-project-pipelines). - [A child pipeline](../pipelines/downstream_pipelines.md#parent-child-pipelines). -Trigger jobs can use only a limited set of the GitLab CI/CD configuration keywords. +Trigger jobs can use only a limited set of GitLab CI/CD configuration keywords. The keywords available for use in trigger jobs are: - [`trigger`](#trigger). @@ -3907,29 +3904,16 @@ The keywords available for use in trigger jobs are: **Possible inputs**: -- For multi-project pipelines, path to the downstream project. CI/CD variables - [are supported](../variables/where_variables_can_be_used.md#gitlab-ciyml-file) +- For multi-project pipelines, the path to the downstream project. CI/CD variables [are supported](../variables/where_variables_can_be_used.md#gitlab-ciyml-file) in GitLab 15.3 and later, but not [job-level persisted variables](../variables/where_variables_can_be_used.md#persisted-variables). -- For child pipelines, path to the child pipeline CI/CD configuration file. - -**Example of `trigger` for multi-project pipeline**: - -```yaml -rspec: - stage: test - script: bundle exec rspec + Alternatively, use [`trigger:project](#triggerproject). +- For child pipelines, use [`trigger:include`](#triggerinclude). -staging: - stage: deploy - trigger: my/deployment -``` - -**Example of `trigger` for child pipelines**: +**Example of `trigger`**: ```yaml -trigger_job: - trigger: - include: path/to/child-pipeline.yml +trigger-multi-project-pipeline: + trigger: my-group/my-project ``` **Additional details**: @@ -3938,8 +3922,6 @@ trigger_job: - In [GitLab 13.5 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/201938), you can use [`when:manual`](#when) in the same job as `trigger`. In GitLab 13.4 and earlier, using them together causes the error `jobs:#{job-name} when should be on_success, on_failure or always`. -- In [GitLab 13.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/197140/), you can - view which job triggered a downstream pipeline in the [pipeline graph](../pipelines/index.md#visualize-pipelines). - [Manual pipeline variables](../variables/index.md#override-a-defined-cicd-variable) and [scheduled pipeline variables](../pipelines/schedules.md#add-a-pipeline-schedule) are not passed to downstream pipelines by default. Use [trigger:forward](#triggerforward) @@ -3950,11 +3932,74 @@ trigger_job: **Related topics**: - [Multi-project pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-from-a-job-in-your-gitlab-ciyml-file). -- [Child pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-parent-child-pipeline). - To run a pipeline for a specific branch, tag, or commit, you can use a [trigger token](../triggers/index.md) to authenticate with the [pipeline triggers API](../../api/pipeline_triggers.md). The trigger token is different than the `trigger` keyword. +#### `trigger:include` + +Use `trigger:include` to declare that a job is a "trigger job" which starts a +[child pipeline](../pipelines/downstream_pipelines.md#parent-child-pipelines). + +Use `trigger:include:artifact` to trigger a [dynamic child pipeline](../pipelines/downstream_pipelines.md#dynamic-child-pipelines). + +**Keyword type**: Job keyword. You can use it only as part of a job. + +**Possible inputs**: + +- The path to the child pipeline's configuration file. + +**Example of `trigger:include`**: + +```yaml +trigger-child-pipeline: + trigger: + include: path/to/child-pipeline.gitlab-ci.yml +``` + +**Related topics**: + +- [Child pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-parent-child-pipeline). + +#### `trigger:project` + +Use `trigger:project` to declare that a job is a "trigger job" which starts a +[multi-project pipeline](../pipelines/downstream_pipelines.md#multi-project-pipelines). + +By default, the multi-project pipeline triggers for the default branch. Use `trigger:branch` +to specify a different branch. + +**Keyword type**: Job keyword. You can use it only as part of a job. + +**Possible inputs**: + +- The path to the downstream project. CI/CD variables [are supported](../variables/where_variables_can_be_used.md#gitlab-ciyml-file) + in GitLab 15.3 and later, but not [job-level persisted variables](../variables/where_variables_can_be_used.md#persisted-variables). + +**Example of `trigger:project`**: + +```yaml +trigger-multi-project-pipeline: + trigger: + project: my-group/my-project +``` + +**Example of `trigger:project` for a different branch**: + +```yaml +trigger-multi-project-pipeline: + trigger: + project: my-group/my-project + branch: development +``` + +**Related topics**: + +- [Multi-project pipeline configuration examples](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-from-a-job-in-your-gitlab-ciyml-file). +- To run a pipeline for a specific branch, tag, or commit, you can also use a [trigger token](../triggers/index.md) + to authenticate with the [pipeline triggers API](../../api/pipeline_triggers.md). + The trigger token is different than the `trigger` keyword. + #### `trigger:strategy` Use `trigger:strategy` to force the `trigger` job to wait for the downstream pipeline to complete -- GitLab From 5bcbb1cf1ca8554ff3843c839fe38f1afb779ca0 Mon Sep 17 00:00:00 2001 From: Marcin Sedlak-Jakubowski <msedlakjakubowski@gitlab.com> Date: Tue, 6 Sep 2022 14:12:16 +0000 Subject: [PATCH 122/169] Use shorter sentences --- doc/development/database/not_null_constraints.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/development/database/not_null_constraints.md b/doc/development/database/not_null_constraints.md index 72921d4b567b2d..cd2adc3ca289c8 100644 --- a/doc/development/database/not_null_constraints.md +++ b/doc/development/database/not_null_constraints.md @@ -54,10 +54,12 @@ end ## Add a `NOT NULL` constraint to an existing column Adding `NOT NULL` to existing database columns usually requires multiple steps split into at least two -different releases. If your table is sufficiently small, that you don't need to -use a background migration, then you can include all these in the same merge -request, but it's still recommended to use separate migrations to reduce -transaction durations. The steps required are: +different releases. If your table is small enough that you don't need to +use a background migration, you can include all these in the same merge +request. We recommend to use separate migrations to reduce +transaction durations. + +The steps required are: 1. Release `N.M` (current release) -- GitLab From dfee08b6688e79e4c498387f1cc877df5180ae7e Mon Sep 17 00:00:00 2001 From: Sri <srirangan@gmail.com> Date: Tue, 6 Sep 2022 15:57:39 +0200 Subject: [PATCH 123/169] Add Cloud SQL related documentation --- doc/cloud_seed/index.md | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/doc/cloud_seed/index.md b/doc/cloud_seed/index.md index 77b22609b12fef..0a54c5b2d919e5 100644 --- a/doc/cloud_seed/index.md +++ b/doc/cloud_seed/index.md @@ -106,6 +106,53 @@ This creates a new branch with the Cloud Run deployment pipeline (or injected in and creates an associated merge request where the changes and deployment pipeline execution can be reviewed and merged into the main branch. +## Provision Cloud SQL Databases + +Relational database instances can be provisioned from the `Project :: Infrastructure :: Google Cloud` page. Cloud SQL is +the underlying Google Cloud service that is used to provision the database instances. + +The following databases and versions are supported: + +- PostgreSQL: 14, 13, 12, 11, 10 and 9.6 +- MySQL: 8.0, 5.7 and 5.6 +- SQL Server + - 2019: Standard, Enterprise, Express and Web + - 2017: Standard, Enterprise, Express and Web + +Google Cloud pricing applies. Please refer to the [Cloud SQL pricing page](https://cloud.google.com/sql/pricing). + +1. [Create a database instance](#create-a-database-instance) +1. [Database setup through a background worker](#database-setup-through-a-background-worker) +1. [Connect to the database](#connect-to-the-database) +1. [Managing the database instance](#managing-the-database-instance) + +### Create a database instance + +From the `Project :: Infrastructure :: Google Cloud` page, select the **Database** tab. Here you will find three +buttons to create Postgres, MySQL, and SQL Server database instances. + +The database instance creation form has fields for GCP project, Git ref (branch or tag), database version and +machine type. Upon submission, the database instance is created and the database setup is queued as a background job. + +### Database setup through a background worker + +Successful creation of the database instance triggers a background worker to perform the following tasks: + +- Create a database user +- Create a database schema +- Store the database details in the project's CI/CD variables + +### Connect to the database + +Once the database instance setup is complete, the database connection details are available as project variables. These +can be managed through the `Project :: Settings :: CI` page and are made available to pipeline executing in the +appropriate environment. + +### Managing the database instance + +The list of instances in the `Project :: Infrastructure :: Google Cloud :: Databases` links back to the Google Cloud +Console. Select an instance to view the details and manage the instance. + ## Contribute to Cloud Seed There are several ways you can contribute to Cloud Seed: -- GitLab From f5e4b1418646be79409074da268d2ed37eb8e494 Mon Sep 17 00:00:00 2001 From: qt <qtchen@jihulab.com> Date: Tue, 6 Sep 2022 14:29:02 +0000 Subject: [PATCH 124/169] Fix: notify locale on resolved all discussions email Changelog: changed --- app/views/notify/resolved_all_discussions_email.html.haml | 3 +-- locale/gitlab.pot | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/notify/resolved_all_discussions_email.html.haml b/app/views/notify/resolved_all_discussions_email.html.haml index 209415e0aee521..bd9778ae142edd 100644 --- a/app/views/notify/resolved_all_discussions_email.html.haml +++ b/app/views/notify/resolved_all_discussions_email.html.haml @@ -1,3 +1,2 @@ %p - All discussions on merge request #{merge_request_reference_link(@merge_request)} - were resolved by #{sanitize_name(@resolved_by.name)} + = s_('Notify|All discussions on merge request %{mr_link} were resolved by %{name}') %{mr_link: sanitize(merge_request_reference_link(@merge_request)), name: sanitize_name(@resolved_by.name)} diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bcfc87fa07845f..7fb9ea2eb18321 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -26835,6 +26835,9 @@ msgstr "" msgid "Notify|A new GPG key was added to your account:" msgstr "" +msgid "Notify|All discussions on merge request %{mr_link} were resolved by %{name}" +msgstr "" + msgid "Notify|Assignee changed from %{fromNames} to %{toNames}" msgstr "" -- GitLab From 03499bec747126b00cb026d1ec2d12f1cecf1352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thiago=20Figueir=C3=B3?= <tfigueiro@gitlab.com> Date: Tue, 6 Sep 2022 14:31:11 +0000 Subject: [PATCH 125/169] Fix Layout/HashAlignment offenses 44 --- .rubocop_todo/layout/hash_alignment.yml | 6 -- .../merge_requests_controller_spec.rb | 91 ++++++++++--------- spec/routing/project_routing_spec.rb | 20 ++-- spec/serializers/ci/lint/job_entity_spec.rb | 2 +- .../container_repository_entity_spec.rb | 3 +- spec/serializers/deployment_entity_spec.rb | 3 +- .../merge_request_metrics_helper_spec.rb | 6 +- 7 files changed, 65 insertions(+), 66 deletions(-) diff --git a/.rubocop_todo/layout/hash_alignment.yml b/.rubocop_todo/layout/hash_alignment.yml index a6857e2eb2711b..62d877624a750e 100644 --- a/.rubocop_todo/layout/hash_alignment.yml +++ b/.rubocop_todo/layout/hash_alignment.yml @@ -3,10 +3,4 @@ Layout/HashAlignment: Exclude: - 'ee/spec/lib/ee/gitlab/usage_data_spec.rb' - - 'spec/controllers/projects/merge_requests_controller_spec.rb' - - 'spec/routing/project_routing_spec.rb' - - 'spec/serializers/ci/lint/job_entity_spec.rb' - - 'spec/serializers/container_repository_entity_spec.rb' - - 'spec/serializers/deployment_entity_spec.rb' - 'spec/serializers/environment_serializer_spec.rb' - - 'spec/serializers/merge_request_metrics_helper_spec.rb' diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index ed5e32df8ea5cd..9c4baeae8364da 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -896,12 +896,13 @@ def go(format: 'html') end subject do - get :exposed_artifacts, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid - }, - format: :json + get :exposed_artifacts, + params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid + }, + format: :json end describe 'permissions on a public project with private CI/CD' do @@ -1031,12 +1032,13 @@ def go(format: 'html') end subject do - get :coverage_reports, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid - }, - format: :json + get :coverage_reports, + params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid + }, + format: :json end describe 'permissions on a public project with private CI/CD' do @@ -1161,12 +1163,13 @@ def go(format: 'html') end subject(:get_codequality_mr_diff_reports) do - get :codequality_mr_diff_reports, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid - }, - format: :json + get :codequality_mr_diff_reports, + params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid + }, + format: :json end context 'permissions on a public project with private CI/CD' do @@ -1264,12 +1267,13 @@ def go(format: 'html') end subject do - get :terraform_reports, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid - }, - format: :json + get :terraform_reports, + params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid + }, + format: :json end describe 'permissions on a public project with private CI/CD' do @@ -1394,12 +1398,13 @@ def go(format: 'html') end subject do - get :test_reports, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid - }, - format: :json + get :test_reports, + params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid + }, + format: :json end before do @@ -1522,12 +1527,13 @@ def go(format: 'html') end subject do - get :accessibility_reports, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid - }, - format: :json + get :accessibility_reports, + params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid + }, + format: :json end context 'permissions on a public project with private CI/CD' do @@ -1642,12 +1648,13 @@ def go(format: 'html') end subject do - get :codequality_reports, params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid - }, - format: :json + get :codequality_reports, + params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid + }, + format: :json end context 'permissions on a public project with private CI/CD' do diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index f701dd9c4880c7..9317a661188490 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -480,7 +480,7 @@ newline_file = "new\n\nline.txt" url_encoded_newline_file = ERB::Util.url_encode(newline_file) assert_routing({ path: "/gitlab/gitlabhq/-/blame/master/#{url_encoded_newline_file}", - method: :get }, + method: :get }, { controller: 'projects/blame', action: 'show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "master/#{newline_file}" }) @@ -499,7 +499,7 @@ newline_file = "new\n\nline.txt" url_encoded_newline_file = ERB::Util.url_encode(newline_file) assert_routing({ path: "/gitlab/gitlabhq/-/blob/blob/master/blob/#{url_encoded_newline_file}", - method: :get }, + method: :get }, { controller: 'projects/blob', action: 'show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "blob/master/blob/#{newline_file}" }) @@ -520,7 +520,7 @@ newline_file = "new\n\nline.txt" url_encoded_newline_file = ERB::Util.url_encode(newline_file) assert_routing({ path: "/gitlab/gitlabhq/-/tree/master/#{url_encoded_newline_file}", - method: :get }, + method: :get }, { controller: 'projects/tree', action: 'show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "master/#{newline_file}" }) @@ -540,7 +540,7 @@ newline_file = "new\n\nline.txt" url_encoded_newline_file = ERB::Util.url_encode(newline_file) assert_routing({ path: "/gitlab/gitlabhq/-/find_file/#{url_encoded_newline_file}", - method: :get }, + method: :get }, { controller: 'projects/find_file', action: 'show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "#{newline_file}" }) @@ -551,7 +551,7 @@ newline_file = "new\n\nline.txt" url_encoded_newline_file = ERB::Util.url_encode(newline_file) assert_routing({ path: "/gitlab/gitlabhq/-/files/#{url_encoded_newline_file}", - method: :get }, + method: :get }, { controller: 'projects/find_file', action: 'list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "#{newline_file}" }) @@ -570,7 +570,7 @@ newline_file = "new\n\nline.txt" url_encoded_newline_file = ERB::Util.url_encode(newline_file) assert_routing({ path: "/gitlab/gitlabhq/-/edit/master/docs/#{url_encoded_newline_file}", - method: :get }, + method: :get }, { controller: 'projects/blob', action: 'edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "master/docs/#{newline_file}" }) @@ -584,7 +584,7 @@ newline_file = "new\n\nline.txt" url_encoded_newline_file = ERB::Util.url_encode(newline_file) assert_routing({ path: "/gitlab/gitlabhq/-/edit/master/docs/#{url_encoded_newline_file}", - method: :get }, + method: :get }, { controller: 'projects/blob', action: 'edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "master/docs/#{newline_file}" }) @@ -600,7 +600,7 @@ newline_file = "new\n\nline.txt" url_encoded_newline_file = ERB::Util.url_encode(newline_file) assert_routing({ path: "/gitlab/gitlabhq/-/raw/master/#{url_encoded_newline_file}", - method: :get }, + method: :get }, { controller: 'projects/raw', action: 'show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "master/#{newline_file}" }) @@ -889,8 +889,8 @@ def show_with_template_type(template_type) describe Projects::Snippets::BlobsController, "routing" do it "to #raw" do expect(get('/gitlab/gitlabhq/-/snippets/1/raw/master/lib/version.rb')) - .to route_to('projects/snippets/blobs#raw', namespace_id: 'gitlab', - project_id: 'gitlabhq', snippet_id: '1', ref: 'master', path: 'lib/version.rb') + .to route_to('projects/snippets/blobs#raw', + namespace_id: 'gitlab', project_id: 'gitlabhq', snippet_id: '1', ref: 'master', path: 'lib/version.rb') end end diff --git a/spec/serializers/ci/lint/job_entity_spec.rb b/spec/serializers/ci/lint/job_entity_spec.rb index 2ef86cfd004309..e1477612ad5775 100644 --- a/spec/serializers/ci/lint/job_entity_spec.rb +++ b/spec/serializers/ci/lint/job_entity_spec.rb @@ -10,7 +10,7 @@ stage: 'test', before_script: ['bundle install', 'bundle exec rake db:create'], script: ["rake spec"], - after_script: ["rake spec"], + after_script: ["rake spec"], tag_list: %w[ruby postgres], environment: { name: 'hello', url: 'world' }, when: 'on_success', diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb index 9ea00bc79e101a..00e6a26d0be8c7 100644 --- a/spec/serializers/container_repository_entity_spec.rb +++ b/spec/serializers/container_repository_entity_spec.rb @@ -14,8 +14,7 @@ before do stub_container_registry_config(enabled: true) - stub_container_registry_tags(repository: :any, - tags: %w[stable latest]) + stub_container_registry_tags(repository: :any, tags: %w[stable latest]) allow(request).to receive(:project).and_return(project) allow(request).to receive(:current_user).and_return(user) end diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index a017f7523e9770..433ce3446808d2 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -61,8 +61,7 @@ context 'when the pipeline has another manual action' do let!(:other_build) do - create(:ci_build, :manual, name: 'another deploy', - pipeline: pipeline, environment: build.environment) + create(:ci_build, :manual, name: 'another deploy', pipeline: pipeline, environment: build.environment) end let!(:other_deployment) { create(:deployment, deployable: build) } diff --git a/spec/serializers/merge_request_metrics_helper_spec.rb b/spec/serializers/merge_request_metrics_helper_spec.rb index 8f683df1faacb8..ec764bf7853322 100644 --- a/spec/serializers/merge_request_metrics_helper_spec.rb +++ b/spec/serializers/merge_request_metrics_helper_spec.rb @@ -57,9 +57,9 @@ expect(MergeRequest::Metrics).to receive(:new) .with(latest_closed_at: closed_event&.updated_at, - latest_closed_by: closed_event&.author, - merged_at: merge_event&.updated_at, - merged_by: merge_event&.author) + latest_closed_by: closed_event&.author, + merged_at: merge_event&.updated_at, + merged_by: merge_event&.author) .and_call_original subject -- GitLab From 446f58a773735c593c07a34cd6e92d744529bbf2 Mon Sep 17 00:00:00 2001 From: Suzanne Selhorn <sselhorn@gitlab.com> Date: Tue, 6 Sep 2022 15:05:49 +0000 Subject: [PATCH 126/169] SaaS to self-managed comparison This MR attempts to reorganize information about migrating And compares SM with SaaS Related to: https://gitlab.com/gitlab-com/sales-team/field-operations/customer-success-operations/-/issues/1222 I squashed 15 commits into this first commit --- doc/install/migrate/compare_sm_to_saas.md | 124 ++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 doc/install/migrate/compare_sm_to_saas.md diff --git a/doc/install/migrate/compare_sm_to_saas.md b/doc/install/migrate/compare_sm_to_saas.md new file mode 100644 index 00000000000000..df79987a2fa250 --- /dev/null +++ b/doc/install/migrate/compare_sm_to_saas.md @@ -0,0 +1,124 @@ +--- +stage: Systems +group: Distribution +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Comparison of GitLab self-managed with GitLab SaaS + +GitLab SaaS is the largest hosted instance of GitLab in the world, managed by an +[all-remote team](https://about.gitlab.com/company/culture/all-remote/) that knows GitLab best. With GitLab SaaS, updates, maintenance, and patches are all performed by this team. + +Self-managed GitLab gives you a deeper breadth of control over many of the functions and systems of the application. + +## Administration + +In GitLab SaaS, administration tasks are limited compared to a self-managed application. + +In a self-managed instance: + +- You have complete access and administrative control over the application, including the [Admin Area](../../user/admin_area/settings/index.md). +- You can impersonate, create, add, and remove users. +- You can assign the [`Auditor`](../../administration/auditor_users.md) user type and `External` role. + +On GitLab SaaS: + +- You have limited administrative control. For example, you cannot impersonate, create, add, or remove users. +- You cannot access the [Admin Area](../../user/admin_area/settings/index.md). +- You cannot assign the `Auditor` user type and `External` role. + +## Logs + +Logs give insight into your processes and can help GitLab Support maintain your application and resolve problems. + +In a self-managed instance: + +- You have full access to system logs. + +On GitLab SaaS: + +- You do not have access to system logs because they are at the instance level, and managed by the GitLab [infrastructure team](https://about.gitlab.com/handbook/engineering/infrastructure/). +- You can view [Audit Events](../../administration/audit_events.md) and the [GitLab API](../../api/audit_events.md). +- You must [request audit information](https://about.gitlab.com/handbook/support/workflows/log_requests.html) from the Support team. + +## Runners + +Runners are available for both SaaS and self-managed applications. + +In a self-managed instance, your runner availability and options are broader, but there are more [security concerns](https://docs.gitlab.com/runner/security/#security-for-self-managed-runners) to consider. + +On GitLab SaaS: + +- Private [runners](../../ci/runners/index.md) are available for GitLab SaaS [groups](../../user/group/index.md) and [projects](../../user/project/index.md). +- Shared runners provided by GitLab SaaS are not configurable. Each runner instance is used once for only one job, ensuring any sensitive data left on the system is destroyed after the job is complete. +- Shared runners are subject to usage limits and are [plan specific](https://about.gitlab.com/pricing/). + +## Custom Git hooks + +In a self-managed instance you can use any custom Git hooks. + +On GitLab SaaS: + +- SaaS users do not have access to the file system, and cannot use custom Git hooks. +- You can use [webhooks](../../user/project/integrations/webhooks.md) as an alternative. + +## API and GraphQL + +In a self-managed instance, users can access all API endpoints, including those that require instance `admin` permissions. + +On GitLab SaaS: + +- SaaS users have access to all of the [API endpoints](../../api/index.md) except those that require instance `admin` permissions. +- Only authorized GitLab engineers have administrative access. + +## Authentication + +In a self-managed instance: + +- You can use an internal encryption key for your data store. +- You can view console logs. +- You can enforce jobs on every pipeline across the group or organization. +- You have control over your data backup. +- You can use the [Interactive Web Terminal](../../ci/interactive_web_terminal/index.md#interactive-web-terminals) for shared runners. + +On GitLab SaaS: + +- You cannot use internal encryption key for the data store ([bring-your-own-key](https://about.gitlab.com/handbook/engineering/security/vulnerability_management/encryption-policy.html#rolling-your-own-crypto)). +- You cannot view console logs. +- You cannot enforce jobs on every pipeline across the group or organization. +- You cannot configure or control data backups. You must use [group](../../api/group_import_export.md) and [project](../../api/project_import_export.md) export. +- The [Interactive Web Terminal](../../ci/interactive_web_terminal/index.md#interactive-web-terminals) is not available for shared runners. + +## Public or private projects + +Project privacy is different when using a self-managed application or GitLab SaaS. + +In a self-managed instance, you control who can view your projects. + +On GitLab SaaS: + +- The GitLab SaaS instance is open to the public. +- When your projects are set as `Public`, they are open to everyone on the public internet. + +## Encryption + +In a self-managed instance, you control the encryption type and configuration. + +On GitLab SaaS: + +- An [Access Management Process](https://about.gitlab.com/handbook/engineering/security/#access-management-process) is in place. +- All data on GitLab.com is encrypted at rest by default. Access to encryption keys is strictly managed by GitLab. +- GitLab does not access your tenant data except as part of a verified service request from you. + +## Support + +In a self-managed instance: + +- You can access any of your back-end systems. +- Our Support team can request logs to assist you. + +On GitLab SaaS: + +- For your privacy and security, there is no public access to GitLab back-end systems. +- Support staff work with [Site Reliability Engineers](https://about.gitlab.com/job-families/engineering/infrastructure/site-reliability-engineer/) to support the [infrastructure](https://about.gitlab.com/handbook/engineering/infrastructure/). +- GitLab Support can access instance logs and view projects, as well as impersonate users. The Support Team can access your logs. -- GitLab From c85fb4f9878ab01262086b627da2c2b7aefc2def Mon Sep 17 00:00:00 2001 From: Marcel Amirault <mamirault@gitlab.com> Date: Tue, 6 Sep 2022 15:08:18 +0000 Subject: [PATCH 127/169] Roll forward docs linting versions --- .gitlab/ci/docs.gitlab-ci.yml | 2 +- .markdownlint.yml | 3 +- .../ci_data_decay/pipeline_partitioning.md | 18 +-- doc/ci/examples/php.md | 2 +- doc/development/secure_coding_guidelines.md | 2 +- doc/user/markdown.md | 4 + package.json | 2 +- scripts/lint-doc.sh | 2 +- yarn.lock | 127 +++++++++--------- 9 files changed, 81 insertions(+), 81 deletions(-) diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml index 3af156e9bd0ad0..7e1571711831df 100644 --- a/.gitlab/ci/docs.gitlab-ci.yml +++ b/.gitlab/ci/docs.gitlab-ci.yml @@ -44,7 +44,7 @@ docs-lint markdown: - .default-retry - .docs:rules:docs-lint # When updating the image version here, update it in /scripts/lint-doc.sh too. - image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-docs/lint-markdown:alpine-3.16-vale-2.17.0-markdownlint-0.31.1 + image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-docs/lint-markdown:alpine-3.16-vale-2.20.1-markdownlint-0.32.2 stage: lint needs: [] script: diff --git a/.markdownlint.yml b/.markdownlint.yml index a5f748908154f4..2ad24e5f754548 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -23,7 +23,8 @@ first-line-h1: false code-block-style: style: "fenced" emphasis-style: false -strong-style: false +link-fragments: false +reference-links-images: false proper-names: names: [ "Akismet", diff --git a/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md b/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md index 4e3abcdae09f37..a70a01031a7ae4 100644 --- a/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md +++ b/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md @@ -79,7 +79,7 @@ cannot be cleaned by `autovacuum`. This highlight the need for small tables. We will measure how much bloat we accumulate when [re]indexing huge tables. Base on this analysis, we will be able to set up SLO (dead tuples / bloat), associated with [re]indexing. -We’ve seen numerous S1 and S2 database-related production environment +We've seen numerous S1 and S2 database-related production environment incidents, over the last couple of months, for example: - S1: 2022-03-17 [Increase in writes in `ci_builds` table](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6625) @@ -135,7 +135,7 @@ remaining database tables when it becomes necessary. It is also important to avoid large data migrations. We store almost 6 terabytes of data in the biggest CI/CD tables, in many different columns and indexes. Migrating this amount of data might be challenging and could cause -instability in the production environment. Due to this concern, we’ve developed +instability in the production environment. Due to this concern, we've developed a way to attach an existing database table as a partition zero without downtime and excessive database locking, what has been demonstrated in one of the [first proofs of concept](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80186). @@ -150,7 +150,7 @@ Our plan is to use logical partition IDs. We want to start with the `ci_pipelines` table and create a `partition_id` column with a `DEFAULT` value of `100` or `1000`. Using a `DEFAULT` value avoids the challenge of backfilling this value for every row. Adding a `CHECK` constraint prior to attaching the -first partition tells PostgreSQL that we’ve already ensured consistency and +first partition tells PostgreSQL that we've already ensured consistency and there is no need to check it while holding an exclusive table lock when attaching this table as a partition to the routing table (partitioned schema definition). We will increment this value every time we create a new partition @@ -256,12 +256,12 @@ smart enough to move rows between partitions on its own. ### Naming conventions -A partitioned table is called a __routing__ table and it will use the `p_` +A partitioned table is called a **routing** table and it will use the `p_` prefix which should help us with building automated tooling for query analysis. -A table partition will be simply called __partition__ and it can use the a +A table partition will be simply called **partition** and it can use the a physical partition ID as suffix, leaded by a `p` letter, for example -`ci_builds_p101`. Existing CI tables will become __zero partitions__ of the +`ci_builds_p101`. Existing CI tables will become **zero partitions** of the new routing tables. Depending on the chosen [partitioning strategy](#how-do-we-want-to-partition-cicd-data) for a given table, it is possible to have many logical partitions per one physical partition. @@ -274,8 +274,8 @@ metadata table, called `ci_partitions`. In that table we would store metadata about all the logical partitions, with many pipelines per partition. We may need to store a range of pipeline ids per logical partition. Using it we will be able to find the `partition_id` number for a given pipeline ID and we will -also find information about which logical partitions are “active†or -“archivedâ€, which will help us to implement a time-decay pattern using database +also find information about which logical partitions are "active" or +"archived", which will help us to implement a time-decay pattern using database declarative partitioning. `ci_partitions` table will store information about a partition identifier, @@ -621,7 +621,7 @@ strategy. The strategy, described in this document, is subject to iteration as well. Whenever we find a better way to reduce the risk and improve our plan, we should update this document as well. -We’ve managed to find a way to avoid large-scale data migrations, and we are +We've managed to find a way to avoid large-scale data migrations, and we are building an iterative strategy for partitioning CI/CD data. We documented our strategy here to share knowledge and solicit feedback from other team members. diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index 666c4d444d8f6c..9b9f87fffbb115 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -176,7 +176,7 @@ Using phpenv also allows to easily configure the PHP environment with: phpenv config-add my_config.ini ``` -*__Important note:__ It seems `phpenv/phpenv` +**Important note:** It seems `phpenv/phpenv` [is abandoned](https://github.com/phpenv/phpenv/issues/57). There is a fork at [`madumlao/phpenv`](https://github.com/madumlao/phpenv) that tries to bring the project back to life. [`CHH/phpenv`](https://github.com/CHH/phpenv) also diff --git a/doc/development/secure_coding_guidelines.md b/doc/development/secure_coding_guidelines.md index 8053b4285e6453..4c2f31183665a1 100644 --- a/doc/development/secure_coding_guidelines.md +++ b/doc/development/secure_coding_guidelines.md @@ -81,7 +81,7 @@ text = "foo\nbar" p text.match /^bar$/ ``` -The output of this example is `#<MatchData "bar">`, as Ruby treats the input `text` line by line. In order to match the whole __string__ the Regex anchors `\A` and `\z` should be used. +The output of this example is `#<MatchData "bar">`, as Ruby treats the input `text` line by line. To match the whole **string**, the Regex anchors `\A` and `\z` should be used. #### Impact diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 98f19750d49e47..6f90a9f0b1fa53 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -774,6 +774,8 @@ Combined emphasis with **asterisks and _underscores_**. Strikethrough uses two tildes. ~~Scratch this.~~ ``` +<!-- markdownlint-disable MD050 --> + Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with double **asterisks** or __underscores__. @@ -782,6 +784,8 @@ Combined emphasis with **asterisks and _underscores_**. Strikethrough uses two tildes. ~~Scratch this.~~ +<!-- markdownlint-enable MD050 --> + #### Multiple underscores in words and mid-word emphasis If this section isn't rendered correctly, diff --git a/package.json b/package.json index d8c5eb4749e2c6..f48844ac1433de 100644 --- a/package.json +++ b/package.json @@ -231,7 +231,7 @@ "jest-raw-loader": "^1.0.1", "jest-transform-graphql": "^2.1.0", "jest-util": "^27.5.1", - "markdownlint-cli": "0.31.0", + "markdownlint-cli": "0.32.2", "miragejs": "^0.1.40", "mock-apollo-client": "1.2.0", "nodemon": "^2.0.19", diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh index afc04da19a7016..f954b2d8106d12 100755 --- a/scripts/lint-doc.sh +++ b/scripts/lint-doc.sh @@ -128,7 +128,7 @@ function run_locally_or_in_docker() { $cmd $args elif hash docker 2>/dev/null then - docker run -t -v ${PWD}:/gitlab -w /gitlab --rm registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.15-vale-2.15.5-markdownlint-0.31.1 ${cmd} ${args} + docker run -t -v ${PWD}:/gitlab -w /gitlab --rm registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.16-vale-2.20.1-markdownlint-0.32.2 ${cmd} ${args} else echo echo " ✖ ERROR: '${cmd}' not found. Install '${cmd}' or Docker to proceed." >&2 diff --git a/yarn.lock b/yarn.lock index 7b3291b9c79096..7ecbe845056fbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3021,6 +3021,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -3565,16 +3572,11 @@ commander@^6.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== -commander@^9.4.0: +commander@^9.4.0, commander@~9.4.0: version "9.4.0" resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.0.tgz#bc4a40918fefe52e22450c111ecd6b7acce6f11c" integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw== -commander@~9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-9.0.0.tgz#86d58f24ee98126568936bd1d3574e0308a99a40" - integrity sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw== - commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -4974,7 +4976,7 @@ enhanced-resolve@^4.5.0: memory-fs "^0.5.0" tapable "^1.0.0" -entities@^2.0.0, entities@~2.1.0: +entities@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== @@ -5967,7 +5969,7 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" -"glob@5 - 7", glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.2.0: +"glob@5 - 7", glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -5979,6 +5981,17 @@ glob-parent@^6.0.1: once "^1.3.0" path-is-absolute "^1.0.0" +glob@~8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" + integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + global-modules@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -6596,10 +6609,10 @@ ini@^1.3.4, ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -ini@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== +ini@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ini/-/ini-3.0.1.tgz#c76ec81007875bc44d544ff7a11a55d12294102d" + integrity sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ== inline-style-parser@0.1.1: version "0.1.1" @@ -7569,10 +7582,10 @@ json5@^2.1.2, json5@^2.2.1: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== -jsonc-parser@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" - integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== +jsonc-parser@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.1.0.tgz#73b8f0e5c940b83d03476bc2e51a20ef0932615d" + integrity sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg== jsonfile@^4.0.0: version "4.0.0" @@ -7682,13 +7695,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -linkify-it@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8" - integrity sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ== - dependencies: - uc.micro "^1.0.1" - linkify-it@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec" @@ -7991,18 +7997,7 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -markdown-it@12.3.2: - version "12.3.2" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" - integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== - dependencies: - argparse "^2.0.1" - entities "~2.1.0" - linkify-it "^3.0.1" - mdurl "^1.0.1" - uc.micro "^1.0.5" - -markdown-it@^13.0.1: +markdown-it@13.0.1, markdown-it@^13.0.1: version "13.0.1" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430" integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q== @@ -8018,33 +8013,33 @@ markdown-table@^3.0.0: resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.2.tgz#9b59eb2c1b22fe71954a65ff512887065a7bb57c" integrity sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA== -markdownlint-cli@0.31.0: - version "0.31.0" - resolved "https://registry.yarnpkg.com/markdownlint-cli/-/markdownlint-cli-0.31.0.tgz#a44264a71066475228292b7af19d3d18b827676d" - integrity sha512-UCNA10I2evrEqGWUGM4I6ae6LubLeySkKegP1GQaZSES516BYBgOn8Ai8MXU+5rSIeCvMyKi91alqHyRDuUnYA== +markdownlint-cli@0.32.2: + version "0.32.2" + resolved "https://registry.yarnpkg.com/markdownlint-cli/-/markdownlint-cli-0.32.2.tgz#b7b5c5808039aef4022aef603efaa607caf8e0de" + integrity sha512-xmJT1rGueUgT4yGNwk6D0oqQr90UJ7nMyakXtqjgswAkEhYYqjHew9RY8wDbOmh2R270IWjuKSeZzHDEGPAUkQ== dependencies: - commander "~9.0.0" + commander "~9.4.0" get-stdin "~9.0.0" - glob "~7.2.0" + glob "~8.0.3" ignore "~5.2.0" js-yaml "^4.1.0" - jsonc-parser "~3.0.0" - markdownlint "~0.25.1" - markdownlint-rule-helpers "~0.16.0" - minimatch "~3.0.4" - run-con "~1.2.10" + jsonc-parser "~3.1.0" + markdownlint "~0.26.2" + markdownlint-rule-helpers "~0.17.2" + minimatch "~5.1.0" + run-con "~1.2.11" -markdownlint-rule-helpers@~0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.16.0.tgz#c327f72782bd2b9475127a240508231f0413a25e" - integrity sha512-oEacRUVeTJ5D5hW1UYd2qExYI0oELdYK72k1TKGvIeYJIbqQWAz476NAc7LNixSySUhcNl++d02DvX0ccDk9/w== +markdownlint-rule-helpers@~0.17.2: + version "0.17.2" + resolved "https://registry.yarnpkg.com/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.17.2.tgz#64d6e8c66e497e631b0e40cf1cef7ca622a0b654" + integrity sha512-XaeoW2NYSlWxMCZM2B3H7YTG6nlaLfkEZWMBhr4hSPlq9MuY2sy83+Xr89jXOqZMZYjvi5nBCGoFh7hHoPKZmA== -markdownlint@~0.25.1: - version "0.25.1" - resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.25.1.tgz#df04536607ebeeda5ccd5e4f38138823ed623788" - integrity sha512-AG7UkLzNa1fxiOv5B+owPsPhtM4D6DoODhsJgiaNg1xowXovrYgOnLqAgOOFQpWOlHFVQUzjMY5ypNNTeov92g== +markdownlint@~0.26.2: + version "0.26.2" + resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.26.2.tgz#11d3d03e7f0dd3c2e239753ee8fd064a861d9237" + integrity sha512-2Am42YX2Ex5SQhRq35HxYWDfz1NLEOZWWN25nqd2h3AHRKsGRE+Qg1gt1++exW792eXTrR4jCNHfShfWk9Nz8w== dependencies: - markdown-it "12.3.2" + markdown-it "13.0.1" marked@^4.0.18: version "4.0.18" @@ -8693,12 +8688,12 @@ minimatch@^3.0.4, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@~3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +minimatch@^5.0.1, minimatch@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" + integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== dependencies: - brace-expansion "^1.1.7" + brace-expansion "^2.0.1" minimist-options@4.1.0: version "4.1.0" @@ -10335,14 +10330,14 @@ route-recognizer@^0.3.3: resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3" integrity sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g== -run-con@~1.2.10: - version "1.2.10" - resolved "https://registry.yarnpkg.com/run-con/-/run-con-1.2.10.tgz#90de9d43d20274d00478f4c000495bd72f417d22" - integrity sha512-n7PZpYmMM26ZO21dd8y3Yw1TRtGABjRtgPSgFS/nhzfvbJMXFtJhJVyEgayMiP+w/23craJjsnfDvx4W4ue/HQ== +run-con@~1.2.11: + version "1.2.11" + resolved "https://registry.yarnpkg.com/run-con/-/run-con-1.2.11.tgz#0014ed430bad034a60568dfe7de2235f32e3f3c4" + integrity sha512-NEMGsUT+cglWkzEr4IFK21P4Jca45HqiAbIIZIBdX5+UZTB24Mb/21iNGgz9xZa8tL6vbW7CXmq7MFN42+VjNQ== dependencies: deep-extend "^0.6.0" - ini "~2.0.0" - minimist "^1.2.5" + ini "~3.0.0" + minimist "^1.2.6" strip-json-comments "~3.1.1" run-parallel@^1.1.9: -- GitLab From 596a04bdd3c0275662099033c540966b58bd3c97 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <jen-shin@gitlab.com> Date: Tue, 6 Sep 2022 15:12:15 +0000 Subject: [PATCH 128/169] Use CI_REGISTRY_GROUP in RELEASE image --- .gitlab/ci/package-and-test/variables.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/ci/package-and-test/variables.gitlab-ci.yml b/.gitlab/ci/package-and-test/variables.gitlab-ci.yml index 7475df669479c3..36bb2f8d5f676c 100644 --- a/.gitlab/ci/package-and-test/variables.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/variables.gitlab-ci.yml @@ -1,7 +1,7 @@ # Default variables for package-and-test variables: - RELEASE: "${CI_REGISTRY}/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:${CI_COMMIT_SHA}" + RELEASE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/build/omnibus-gitlab-mirror/gitlab-ee:${CI_COMMIT_SHA}" SKIP_REPORT_IN_ISSUES: "true" OMNIBUS_GITLAB_CACHE_UPDATE: "false" COLORIZED_LOGS: "true" -- GitLab From ed7c7658529b053a914016091a5171d00d9d5498 Mon Sep 17 00:00:00 2001 From: GitLab Renovate Bot <gitlab-bot@gitlab.com> Date: Tue, 6 Sep 2022 15:21:17 +0000 Subject: [PATCH 129/169] Update dependency @gitlab/ui to v43.13.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f5546a8f775924..28e655d7999d7b 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@gitlab/at.js": "1.5.7", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/svgs": "3.3.0", - "@gitlab/ui": "43.9.3", + "@gitlab/ui": "43.13.0", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20220815034418", "@rails/actioncable": "6.1.4-7", diff --git a/yarn.lock b/yarn.lock index 7b3291b9c79096..d753d749b46cac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1064,10 +1064,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.3.0.tgz#99b044484fcf3d5a6431281e320e2405540ff5a9" integrity sha512-S8Hqf+ms8aNrSgmci9SVoIyj/0qQnizU5uV5vUPAOwiufMDFDyI5qfcgn4EYZ6mnju3LiO+ReSL/PPTD4qNgHA== -"@gitlab/ui@43.9.3": - version "43.9.3" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-43.9.3.tgz#2dd91b14da769a873e45ffe07b5863f6c47211ba" - integrity sha512-TONSf+6UJYWTVs5qnItR1uLZ/0kBE8jGN8aLOVv4CDAsORvln0ZxtcZvMTCFp76YEtzXLkMUfvm7ZngQ26tIiA== +"@gitlab/ui@43.13.0": + version "43.13.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-43.13.0.tgz#7e4e7d41287cfba8a46dbdd3c8ba998a853a9ad2" + integrity sha512-y0BrVKsqRBEQMrsJseakBeMrFHVMTg7DVMa3tbdkKkrruV8SYOsX8wLrv20taDhiMlceKRB8lF5gLTPPHLwCGA== dependencies: "@popperjs/core" "^2.11.2" bootstrap-vue "2.20.1" -- GitLab From e71dfc4ed0bdfc8e8140d8d84a42170e7c16aac8 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu <heinrich@gitlab.com> Date: Tue, 6 Sep 2022 23:21:29 +0800 Subject: [PATCH 130/169] Fix deprecated usage of redis pipeline Since we don't actually need a pipeline here, I just removed the pipelined call --- lib/gitlab/sidekiq_versioning.rb | 6 +----- spec/lib/gitlab/sidekiq_versioning_spec.rb | 25 ++-------------------- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/lib/gitlab/sidekiq_versioning.rb b/lib/gitlab/sidekiq_versioning.rb index 80c0b7650f3614..28c9714f82f313 100644 --- a/lib/gitlab/sidekiq_versioning.rb +++ b/lib/gitlab/sidekiq_versioning.rb @@ -10,11 +10,7 @@ def self.install! if queues.any? Sidekiq.redis do |conn| - conn.pipelined do - queues.each do |queue| - conn.sadd('queues', queue) - end - end + conn.sadd('queues', queues) end end rescue ::Redis::BaseError, SocketError, Errno::ENOENT, Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED diff --git a/spec/lib/gitlab/sidekiq_versioning_spec.rb b/spec/lib/gitlab/sidekiq_versioning_spec.rb index afafd04d87d0e6..bdbba04e0c0fb8 100644 --- a/spec/lib/gitlab/sidekiq_versioning_spec.rb +++ b/spec/lib/gitlab/sidekiq_versioning_spec.rb @@ -2,30 +2,9 @@ require 'spec_helper' -RSpec.describe Gitlab::SidekiqVersioning, :redis do - let(:foo_worker) do - Class.new do - def self.name - 'FooWorker' - end - - include ApplicationWorker - end - end - - let(:bar_worker) do - Class.new do - def self.name - 'BarWorker' - end - - include ApplicationWorker - end - end - +RSpec.describe Gitlab::SidekiqVersioning, :clean_gitlab_redis_queues do before do - allow(Gitlab::SidekiqConfig).to receive(:workers).and_return([foo_worker, bar_worker]) - allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return([foo_worker.queue, bar_worker.queue]) + allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return(%w[foo bar]) end describe '.install!' do -- GitLab From a0f73de8cc5caa559e6c9909bebac78460e0f417 Mon Sep 17 00:00:00 2001 From: Pedro Pombeiro <noreply@pedro.pombei.ro> Date: Tue, 6 Sep 2022 15:41:19 +0000 Subject: [PATCH 131/169] GraphQL: Add resolver to runner projects Changelog: added --- app/finders/projects_finder.rb | 4 +- .../ci/runner_owner_project_resolver.rb | 15 ++- .../resolvers/ci/runner_projects_resolver.rb | 63 +++++++++++ .../concerns/project_search_arguments.rb | 36 ++++++ app/graphql/resolvers/projects_resolver.rb | 32 +----- app/graphql/types/ci/runner_type.rb | 20 +--- doc/api/graphql/reference/index.md | 31 +++++- ee/app/graphql/ee/types/ci/runner_type.rb | 4 - .../projects_resolver.rb | 2 +- .../ci/runner_projects_resolver_spec.rb | 69 ++++++++++++ spec/requests/api/graphql/ci/runner_spec.rb | 103 ++++++++++++++++-- 11 files changed, 307 insertions(+), 72 deletions(-) create mode 100644 app/graphql/resolvers/ci/runner_projects_resolver.rb create mode 100644 app/graphql/resolvers/concerns/project_search_arguments.rb create mode 100644 spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 6b8dcd61d29408..6bfe730ebc9697 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -119,9 +119,9 @@ def collection_without_user # This is an optimization - surprisingly PostgreSQL does not optimize # for this. # - # If the default visiblity level and desired visiblity level filter cancels + # If the default visibility level and desired visibility level filter cancels # each other out, don't use the SQL clause for visibility level in - # `Project.public_or_visible_to_user`. In fact, this then becames equivalent + # `Project.public_or_visible_to_user`. In fact, this then becomes equivalent # to just authorized projects for the user. # # E.g. diff --git a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb index 14b5f8f90eb194..da8fab9361938e 100644 --- a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb +++ b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb @@ -9,7 +9,7 @@ class RunnerOwnerProjectResolver < BaseResolver alias_method :runner, :object - def resolve_with_lookahead(**args) + def resolve_with_lookahead(**_args) resolve_owner end @@ -19,6 +19,8 @@ def preloads } end + private + def filtered_preloads selection = lookahead @@ -27,8 +29,6 @@ def filtered_preloads end end - private - def resolve_owner return unless runner.project_type? @@ -48,14 +48,13 @@ def resolve_owner .transform_values { |runner_projects| runner_projects.first.project_id } project_ids = owner_project_id_by_runner_id.values.uniq - all_preloads = unconditional_includes + filtered_preloads - owner_relation = Project.all - owner_relation = owner_relation.preload(*all_preloads) if all_preloads.any? - projects = owner_relation.where(id: project_ids).index_by(&:id) + projects = Project.where(id: project_ids) + Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute + projects_by_id = projects.index_by(&:id) runner_ids.each do |runner_id| owner_project_id = owner_project_id_by_runner_id[runner_id] - loader.call(runner_id, projects[owner_project_id]) + loader.call(runner_id, projects_by_id[owner_project_id]) end # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/graphql/resolvers/ci/runner_projects_resolver.rb b/app/graphql/resolvers/ci/runner_projects_resolver.rb new file mode 100644 index 00000000000000..ca3b4ebb797558 --- /dev/null +++ b/app/graphql/resolvers/ci/runner_projects_resolver.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class RunnerProjectsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include LooksAhead + include ProjectSearchArguments + + type Types::ProjectType.connection_type, null: true + authorize :read_runner + authorizes_object! + + alias_method :runner, :object + + argument :sort, GraphQL::Types::String, + required: false, + default_value: 'id_asc', # TODO: Remove in %16.0 and move :sort to ProjectSearchArguments, see https://gitlab.com/gitlab-org/gitlab/-/issues/372117 + deprecated: { + reason: 'Default sort order will change in 16.0. ' \ + 'Specify `"id_asc"` if query results\' order is important', + milestone: '15.4' + }, + description: "Sort order of results. Format: '<field_name>_<sort_direction>', " \ + "for example: 'id_desc' or 'name_asc'" + + def resolve_with_lookahead(**args) + return unless runner.project_type? + + # rubocop:disable CodeReuse/ActiveRecord + BatchLoader::GraphQL.for(runner.id).batch(key: :runner_projects) do |runner_ids, loader| + plucked_runner_and_project_ids = ::Ci::RunnerProject + .select(:runner_id, :project_id) + .where(runner_id: runner_ids) + .pluck(:runner_id, :project_id) + + project_ids = plucked_runner_and_project_ids.collect { |_runner_id, project_id| project_id }.uniq + projects = ProjectsFinder + .new(current_user: current_user, + params: project_finder_params(args), + project_ids_relation: project_ids) + .execute + Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute + projects_by_id = projects.index_by(&:id) + + # In plucked_runner_and_project_ids, first() represents the runner ID, and second() the project ID, + # so let's group the project IDs by runner ID + runner_project_ids_by_runner_id = + plucked_runner_and_project_ids + .group_by(&:first) + .transform_values { |values| values.map(&:second).filter_map { |project_id| projects_by_id[project_id] } } + + runner_ids.each do |runner_id| + runner_projects = runner_project_ids_by_runner_id[runner_id] || [] + + loader.call(runner_id, runner_projects) + end + end + # rubocop:enable CodeReuse/ActiveRecord + end + end + end +end diff --git a/app/graphql/resolvers/concerns/project_search_arguments.rb b/app/graphql/resolvers/concerns/project_search_arguments.rb new file mode 100644 index 00000000000000..7e03963f412c06 --- /dev/null +++ b/app/graphql/resolvers/concerns/project_search_arguments.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module ProjectSearchArguments + extend ActiveSupport::Concern + + included do + argument :membership, GraphQL::Types::Boolean, + required: false, + description: 'Return only projects that the current user is a member of.' + + argument :search, GraphQL::Types::String, + required: false, + description: 'Search query, which can be for the project name, a path, or a description.' + + argument :search_namespaces, GraphQL::Types::Boolean, + required: false, + description: 'Include namespace in project search.' + + argument :topics, type: [GraphQL::Types::String], + required: false, + description: 'Filter projects by topics.' + end + + private + + def project_finder_params(params) + { + without_deleted: true, + non_public: params[:membership], + search: params[:search], + search_namespaces: params[:search_namespaces], + sort: params[:sort], + topic: params[:topics] + }.compact + end +end diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb index facf8ffe36f206..4d1e1b867da883 100644 --- a/app/graphql/resolvers/projects_resolver.rb +++ b/app/graphql/resolvers/projects_resolver.rb @@ -2,31 +2,18 @@ module Resolvers class ProjectsResolver < BaseResolver - type Types::ProjectType, null: true - - argument :membership, GraphQL::Types::Boolean, - required: false, - description: 'Limit projects that the current user is a member of.' + include ProjectSearchArguments - argument :search, GraphQL::Types::String, - required: false, - description: 'Search query for project name, path, or description.' + type Types::ProjectType, null: true argument :ids, [GraphQL::Types::ID], required: false, description: 'Filter projects by IDs.' - argument :search_namespaces, GraphQL::Types::Boolean, - required: false, - description: 'Include namespace in project search.' - argument :sort, GraphQL::Types::String, required: false, - description: 'Sort order of results.' - - argument :topics, type: [GraphQL::Types::String], - required: false, - description: 'Filters projects by topics.' + description: "Sort order of results. Format: '<field_name>_<sort_direction>', " \ + "for example: 'id_desc' or 'name_asc'" def resolve(**args) ProjectsFinder @@ -36,17 +23,6 @@ def resolve(**args) private - def project_finder_params(params) - { - without_deleted: true, - non_public: params[:membership], - search: params[:search], - search_namespaces: params[:search_namespaces], - sort: params[:sort], - topic: params[:topics] - }.compact - end - def parse_gids(gids) gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Project).model_id } end diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index 0afb61d2b64730..ee0ded60d82f77 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -63,8 +63,11 @@ class RunnerType < BaseObject description: 'Indicates the runner is paused and not available to run jobs.' field :project_count, GraphQL::Types::Int, null: true, description: 'Number of projects that the runner is associated with.' - field :projects, ::Types::ProjectType.connection_type, null: true, - description: 'Projects the runner is associated with. For project runners only.' + field :projects, + ::Types::ProjectType.connection_type, + null: true, + resolver: ::Resolvers::Ci::RunnerProjectsResolver, + description: 'Find projects the runner is associated with. For project runners only.' field :revision, GraphQL::Types::String, null: true, description: 'Revision of the runner.' field :run_untagged, GraphQL::Types::Boolean, null: false, @@ -131,12 +134,6 @@ def groups batched_owners(::Ci::RunnerNamespace, Group, :runner_groups, :namespace_id) end - def projects - return unless runner.project_type? - - batched_owners(::Ci::RunnerProject, Project, :runner_projects, :project_id) - end - private def can_admin_runners? @@ -159,19 +156,12 @@ def batched_owners(runner_assoc_type, assoc_type, key, column_name) owner_ids = runner_owner_ids_by_runner_id.values.flatten.uniq owners = assoc_type.where(id: owner_ids).index_by(&:id) - # Preload projects namespaces to avoid N+1 queries when checking the `read_project` policy for each - preload_projects_namespaces(owners.values) if assoc_type == Project - runner_ids.each do |runner_id| loader.call(runner_id, runner_owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || []) end end end # rubocop: enable CodeReuse/ActiveRecord - - def preload_projects_namespaces(_projects) - # overridden in EE - end end end end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index d7e3ae81ad0520..0b6591136ec935 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -317,11 +317,11 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | <a id="queryprojectsids"></a>`ids` | [`[ID!]`](#id) | Filter projects by IDs. | -| <a id="queryprojectsmembership"></a>`membership` | [`Boolean`](#boolean) | Limit projects that the current user is a member of. | -| <a id="queryprojectssearch"></a>`search` | [`String`](#string) | Search query for project name, path, or description. | +| <a id="queryprojectsmembership"></a>`membership` | [`Boolean`](#boolean) | Return only projects that the current user is a member of. | +| <a id="queryprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. | | <a id="queryprojectssearchnamespaces"></a>`searchNamespaces` | [`Boolean`](#boolean) | Include namespace in project search. | -| <a id="queryprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. | -| <a id="queryprojectstopics"></a>`topics` | [`[String!]`](#string) | Filters projects by topics. | +| <a id="queryprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: '<field_name>_<sort_direction>', for example: 'id_desc' or 'name_asc'. | +| <a id="queryprojectstopics"></a>`topics` | [`[String!]`](#string) | Filter projects by topics. | ### `Query.queryComplexity` @@ -10360,7 +10360,6 @@ CI/CD variables for a project. | <a id="cirunnerplatformname"></a>`platformName` | [`String`](#string) | Platform provided by the runner. | | <a id="cirunnerprivateprojectsminutescostfactor"></a>`privateProjectsMinutesCostFactor` | [`Float`](#float) | Private projects' "minutes cost factor" associated with the runner (GitLab.com only). | | <a id="cirunnerprojectcount"></a>`projectCount` | [`Int`](#int) | Number of projects that the runner is associated with. | -| <a id="cirunnerprojects"></a>`projects` | [`ProjectConnection`](#projectconnection) | Projects the runner is associated with. For project runners only. (see [Connections](#connections)) | | <a id="cirunnerpublicprojectsminutescostfactor"></a>`publicProjectsMinutesCostFactor` | [`Float`](#float) | Public projects' "minutes cost factor" associated with the runner (GitLab.com only). | | <a id="cirunnerrevision"></a>`revision` | [`String`](#string) | Revision of the runner. | | <a id="cirunnerrununtagged"></a>`runUntagged` | [`Boolean!`](#boolean) | Indicates the runner is able to run untagged jobs. | @@ -10390,6 +10389,26 @@ four standard [pagination arguments](#connection-pagination-arguments): | ---- | ---- | ----------- | | <a id="cirunnerjobsstatuses"></a>`statuses` | [`[CiJobStatus!]`](#cijobstatus) | Filter jobs by status. | +##### `CiRunner.projects` + +Find projects the runner is associated with. For project runners only. + +Returns [`ProjectConnection`](#projectconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#connection-pagination-arguments): +`before: String`, `after: String`, `first: Int`, `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="cirunnerprojectsmembership"></a>`membership` | [`Boolean`](#boolean) | Return only projects that the current user is a member of. | +| <a id="cirunnerprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. | +| <a id="cirunnerprojectssearchnamespaces"></a>`searchNamespaces` | [`Boolean`](#boolean) | Include namespace in project search. | +| <a id="cirunnerprojectssort"></a>`sort` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.4. Default sort order will change in 16.0. Specify `"id_asc"` if query results' order is important. | +| <a id="cirunnerprojectstopics"></a>`topics` | [`[String!]`](#string) | Filter projects by topics. | + ##### `CiRunner.status` Status of the runner. @@ -13295,7 +13314,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="instancesecuritydashboardprojectssearch"></a>`search` | [`String`](#string) | Search query for project name, path, or description. | +| <a id="instancesecuritydashboardprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. | ##### `InstanceSecurityDashboard.vulnerabilitySeveritiesCount` diff --git a/ee/app/graphql/ee/types/ci/runner_type.rb b/ee/app/graphql/ee/types/ci/runner_type.rb index cdda46d69984b0..256aef69d7dd72 100644 --- a/ee/app/graphql/ee/types/ci/runner_type.rb +++ b/ee/app/graphql/ee/types/ci/runner_type.rb @@ -37,10 +37,6 @@ def upgrade_status def upgrade_status_available? License.feature_available?(:runner_upgrade_management) || current_user&.has_paid_namespace? end - - def preload_projects_namespaces(projects) - ActiveRecord::Associations::Preloader.new.preload(projects, :namespace) # rubocop:disable CodeReuse/ActiveRecord - end end end end diff --git a/ee/app/graphql/resolvers/instance_security_dashboard/projects_resolver.rb b/ee/app/graphql/resolvers/instance_security_dashboard/projects_resolver.rb index 94b1da07b1cb49..cea791076d9946 100644 --- a/ee/app/graphql/resolvers/instance_security_dashboard/projects_resolver.rb +++ b/ee/app/graphql/resolvers/instance_security_dashboard/projects_resolver.rb @@ -7,7 +7,7 @@ class ProjectsResolver < BaseResolver argument :search, GraphQL::Types::String, required: false, - description: 'Search query for project name, path, or description.' + description: 'Search query, which can be for the project name, a path, or a description.' alias_method :dashboard, :object diff --git a/spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb new file mode 100644 index 00000000000000..952c7337d65f24 --- /dev/null +++ b/spec/graphql/resolvers/ci/runner_projects_resolver_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::RunnerProjectsResolver do + include GraphqlHelpers + + let_it_be(:project1) { create(:project, description: 'Project1.1') } + let_it_be(:project2) { create(:project, description: 'Project1.2') } + let_it_be(:project3) { create(:project, description: 'Project2.1') } + let_it_be(:runner) { create(:ci_runner, :project, projects: [project1, project2, project3]) } + + let(:args) { {} } + + subject { resolve_projects(args) } + + describe '#resolve' do + context 'with authorized user', :enable_admin_mode do + let(:current_user) { create(:user, :admin) } + + context 'with search argument' do + let(:args) { { search: 'Project1.' } } + + it 'returns a lazy value with projects containing the specified prefix' do + expect(subject).to be_a(GraphQL::Execution::Lazy) + expect(subject.value).to contain_exactly(project1, project2) + end + end + + context 'with supported arguments' do + let(:args) { { membership: true, search_namespaces: true, topics: %w[xyz] } } + + it 'creates ProjectsFinder with expected arguments' do + expect(ProjectsFinder).to receive(:new).with( + a_hash_including( + params: a_hash_including( + non_public: true, + search_namespaces: true, + topic: %w[xyz] + ) + ) + ).and_call_original + + expect(subject).to be_a(GraphQL::Execution::Lazy) + subject.value + end + end + + context 'without arguments' do + it 'returns a lazy value with all projects' do + expect(subject).to be_a(GraphQL::Execution::Lazy) + expect(subject.value).to contain_exactly(project1, project2, project3) + end + end + end + + context 'with unauthorized user' do + let(:current_user) { create(:user) } + + it { is_expected.to be_nil } + end + end + + private + + def resolve_projects(args = {}, context = { current_user: current_user }) + resolve(described_class, obj: runner, args: args, ctx: context) + end +end diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 8ed84c25bf0c32..8bd002d533a184 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -54,7 +54,8 @@ executor_type: :shell) end - let_it_be(:active_project_runner) { create(:ci_runner, :project) } + let_it_be(:project1) { create(:project) } + let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) } shared_examples 'runner details fetch' do let(:query) do @@ -223,7 +224,6 @@ end describe 'ownerProject' do - let_it_be(:project1) { create(:project) } let_it_be(:project2) { create(:project) } let_it_be(:runner1) { create(:ci_runner, :project, projects: [project2, project1]) } let_it_be(:runner2) { create(:ci_runner, :project, projects: [project1, project2]) } @@ -337,7 +337,6 @@ end describe 'for multiple runners' do - let_it_be(:project1) { create(:project, :test_repo) } let_it_be(:project2) { create(:project, :test_repo) } let_it_be(:project_runner1) { create(:ci_runner, :project, projects: [project1, project2], description: 'Runner 1') } let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [], description: 'Runner 2') } @@ -508,8 +507,8 @@ def runner_query(runner) <<~QUERY { instance_runner1: #{runner_query(active_instance_runner)} - project_runner1: #{runner_query(active_project_runner)} group_runner1: #{runner_query(active_group_runner)} + project_runner1: #{runner_query(active_project_runner)} } QUERY end @@ -529,12 +528,13 @@ def runner_query(runner) it 'does not execute more queries per runner', :aggregate_failures do # warm-up license cache and so on: - post_graphql(double_query, current_user: user) + personal_access_token = create(:personal_access_token, user: user) + args = { current_user: user, token: { personal_access_token: personal_access_token } } + post_graphql(double_query, **args) - control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, current_user: user) } + control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, **args) } - expect { post_graphql(double_query, current_user: user) } - .not_to exceed_query_limit(control) + expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control) expect(graphql_data.count).to eq 6 expect(graphql_data).to match( @@ -564,4 +564,91 @@ def runner_query(runner) )) end end + + describe 'sorting and pagination' do + let(:query) do + <<~GQL + query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) { + runner(id: $id) { + #{fields} + } + } + GQL + end + + before do + post_graphql(query, current_user: user, variables: variables) + end + + context 'with project search term' do + let_it_be(:project1) { create(:project, description: 'abc') } + let_it_be(:project2) { create(:project, description: 'def') } + let_it_be(:project_runner) do + create(:ci_runner, :project, projects: [project1, project2]) + end + + let(:variables) { { id: project_runner.to_global_id.to_s, n: n, project_search_term: search_term } } + + let(:fields) do + <<~QUERY + projects(search: $projectSearchTerm, first: $n, after: $cursor) { + count + nodes { + id + } + pageInfo { + hasPreviousPage + startCursor + endCursor + hasNextPage + } + } + QUERY + end + + let(:projects_data) { graphql_data_at('runner', 'projects') } + + context 'set to empty string' do + let(:search_term) { '' } + + context 'with n = 1' do + let(:n) { 1 } + + it_behaves_like 'a working graphql query' + + it 'returns paged result' do + expect(projects_data).not_to be_nil + expect(projects_data['count']).to eq 2 + expect(projects_data['pageInfo']['hasNextPage']).to eq true + end + end + + context 'with n = 2' do + let(:n) { 2 } + + it 'returns non-paged result' do + expect(projects_data).not_to be_nil + expect(projects_data['count']).to eq 2 + expect(projects_data['pageInfo']['hasNextPage']).to eq false + end + end + end + + context 'set to partial match' do + let(:search_term) { 'def' } + + context 'with n = 1' do + let(:n) { 1 } + + it_behaves_like 'a working graphql query' + + it 'returns paged result with no additional pages' do + expect(projects_data).not_to be_nil + expect(projects_data['count']).to eq 1 + expect(projects_data['pageInfo']['hasNextPage']).to eq false + end + end + end + end + end end -- GitLab From 9fe2a1bfcb9cf382336be86ee4d40c10c484aec7 Mon Sep 17 00:00:00 2001 From: Ammar Alakkad <aalakkad@gitlab.com> Date: Tue, 6 Sep 2022 15:58:59 +0000 Subject: [PATCH 132/169] Add usage_quotas_for_all_editions feature flag --- .../usage_quotas_for_all_editions.yml | 8 ++ .../views/groups/usage_quotas/index.html.haml | 81 ++++++++++--------- .../groups/seat_usage/seat_usage_spec.rb | 1 + ee/spec/features/groups/usage_quotas_spec.rb | 1 + 4 files changed, 54 insertions(+), 37 deletions(-) create mode 100644 config/feature_flags/development/usage_quotas_for_all_editions.yml diff --git a/config/feature_flags/development/usage_quotas_for_all_editions.yml b/config/feature_flags/development/usage_quotas_for_all_editions.yml new file mode 100644 index 00000000000000..d4e4116542a782 --- /dev/null +++ b/config/feature_flags/development/usage_quotas_for_all_editions.yml @@ -0,0 +1,8 @@ +--- +name: usage_quotas_for_all_editions +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96063 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371639 +milestone: '15.4' +type: development +group: group::utilization +default_enabled: false diff --git a/ee/app/views/groups/usage_quotas/index.html.haml b/ee/app/views/groups/usage_quotas/index.html.haml index f6db4c401719c4..beb670bb20c359 100644 --- a/ee/app/views/groups/usage_quotas/index.html.haml +++ b/ee/app/views/groups/usage_quotas/index.html.haml @@ -10,40 +10,47 @@ - if show_product_purchase_success_alert? = render 'product_purchase_success_alert', product_name: params[:purchased_product] -%h1.page-title.gl-font-size-h-display - = s_('UsageQuota|Usage Quotas') - -.row - .col-sm-6 - = s_('UsageQuota|Usage of group resources across the projects in the %{strong_start}%{group_name}%{strong_end} group').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, group_name: @group.name } - -= gl_tabs_nav({ class: 'js-storage-tabs' }) do - = gl_tab_link_to '#seats-quota-tab', data: { qa_selector: 'seats_tab' }, item_active: true do - = s_('UsageQuota|Seats') - = gl_tab_link_to '#pipelines-quota-tab', data: { qa_selector: 'pipelines_tab' } do - = s_('UsageQuota|Pipelines') - = gl_tab_link_to '#storage-quota-tab', data: { qa_selector: 'storage_tab' } do - = s_('UsageQuota|Storage') - -.tab-content - .tab-pane.active#seats-quota-tab - = render Namespaces::FreeUserCap::UsageQuotaAlertComponent.new(namespace: @group.root_ancestor, - user: current_user, - content_class: 'gl-my-3') - - = render Namespaces::FreeUserCap::UsageQuotaTrialAlertComponent.new(namespace: @group.root_ancestor, - user: current_user, - content_class: 'gl-my-3') - - #js-seat-usage-app{ data: group_seats_usage_quota_app_data(@group) } - .tab-pane#pipelines-quota-tab - - if Feature.enabled?(:usage_quotas_pipelines_vue, @group) - #js-pipeline-usage-app{ data: pipeline_usage_app_data(@group) } - - else - = render "namespaces/pipelines_quota/ci_minutes_report", - locals: { namespace: @group, projects_usage: @projects_usage } - #js-ci-minutes-usage-group{ data: { namespace_id: @group.id } } - = render "namespaces/pipelines_quota/list", - locals: { namespace: @group, projects_usage: @projects_usage } - .tab-pane#storage-quota-tab - #js-storage-counter-app{ data: usage_quotas_storage_app_data(@group) } +- if Feature.enabled?(:usage_quotas_for_all_editions, @group) + .gl-alert.gl-alert-no-icon.gl-alert-info.gl-mt-6 + %h2.gl-alert-title + Development + .gl-alert-content + Placeholder for usage quotas Vue app +- else + %h1.page-title.gl-font-size-h-display + = s_('UsageQuota|Usage Quotas') + + .row + .col-sm-6 + = s_('UsageQuota|Usage of group resources across the projects in the %{strong_start}%{group_name}%{strong_end} group').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, group_name: @group.name } + + = gl_tabs_nav({ class: 'js-storage-tabs' }) do + = gl_tab_link_to '#seats-quota-tab', data: { qa_selector: 'seats_tab' }, item_active: true do + = s_('UsageQuota|Seats') + = gl_tab_link_to '#pipelines-quota-tab', data: { qa_selector: 'pipelines_tab' } do + = s_('UsageQuota|Pipelines') + = gl_tab_link_to '#storage-quota-tab', data: { qa_selector: 'storage_tab' } do + = s_('UsageQuota|Storage') + + .tab-content + .tab-pane.active#seats-quota-tab + = render Namespaces::FreeUserCap::UsageQuotaAlertComponent.new(namespace: @group.root_ancestor, + user: current_user, + content_class: 'gl-my-3') + + = render Namespaces::FreeUserCap::UsageQuotaTrialAlertComponent.new(namespace: @group.root_ancestor, + user: current_user, + content_class: 'gl-my-3') + + #js-seat-usage-app{ data: group_seats_usage_quota_app_data(@group) } + .tab-pane#pipelines-quota-tab + - if Feature.enabled?(:usage_quotas_pipelines_vue, @group) + #js-pipeline-usage-app{ data: pipeline_usage_app_data(@group) } + - else + = render "namespaces/pipelines_quota/ci_minutes_report", + locals: { namespace: @group, projects_usage: @projects_usage } + #js-ci-minutes-usage-group{ data: { namespace_id: @group.id } } + = render "namespaces/pipelines_quota/list", + locals: { namespace: @group, projects_usage: @projects_usage } + .tab-pane#storage-quota-tab + #js-storage-counter-app{ data: usage_quotas_storage_app_data(@group) } diff --git a/ee/spec/features/groups/seat_usage/seat_usage_spec.rb b/ee/spec/features/groups/seat_usage/seat_usage_spec.rb index d0e3c4290131aa..4b3d95dfc8da81 100644 --- a/ee/spec/features/groups/seat_usage/seat_usage_spec.rb +++ b/ee/spec/features/groups/seat_usage/seat_usage_spec.rb @@ -13,6 +13,7 @@ before do stub_feature_flags(usage_quotas_pipelines_vue: false) + stub_feature_flags(usage_quotas_for_all_editions: false) allow(Gitlab).to receive(:com?).and_return(true) stub_application_setting(check_namespace_plan: true) diff --git a/ee/spec/features/groups/usage_quotas_spec.rb b/ee/spec/features/groups/usage_quotas_spec.rb index d420ec05cc6012..39d7f80353dbdb 100644 --- a/ee/spec/features/groups/usage_quotas_spec.rb +++ b/ee/spec/features/groups/usage_quotas_spec.rb @@ -13,6 +13,7 @@ before do stub_feature_flags(usage_quotas_pipelines_vue: false) + stub_feature_flags(usage_quotas_for_all_editions: false) allow(Gitlab).to receive(:com?).and_return(gitlab_dot_com) group.add_owner(user) -- GitLab From 9f988db9bc1fcb0021194f2a3bad8f7014d7addc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= <alejorro70@gmail.com> Date: Thu, 1 Sep 2022 17:50:18 +0200 Subject: [PATCH 133/169] Substitute BatchPopQueueing with `reschedule_once` Behind a feature flag for now. --- ...ss_batch_pop_queueing_for_merge_trains.yml | 8 ++ .../services/merge_trains/refresh_service.rb | 4 + ee/app/workers/merge_trains/refresh_worker.rb | 4 +- .../merge_trains/refresh_service_spec.rb | 99 +++++++++++-------- .../merge_trains/refresh_worker_spec.rb | 27 +++++ 5 files changed, 98 insertions(+), 44 deletions(-) create mode 100644 config/feature_flags/development/bypass_batch_pop_queueing_for_merge_trains.yml create mode 100644 ee/spec/workers/merge_trains/refresh_worker_spec.rb diff --git a/config/feature_flags/development/bypass_batch_pop_queueing_for_merge_trains.yml b/config/feature_flags/development/bypass_batch_pop_queueing_for_merge_trains.yml new file mode 100644 index 00000000000000..4517bd5360eb53 --- /dev/null +++ b/config/feature_flags/development/bypass_batch_pop_queueing_for_merge_trains.yml @@ -0,0 +1,8 @@ +--- +name: bypass_batch_pop_queueing_for_merge_trains +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96793 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372366 +milestone: '15.4' +type: development +group: group::scalability +default_enabled: false diff --git a/ee/app/services/merge_trains/refresh_service.rb b/ee/app/services/merge_trains/refresh_service.rb index 249d79d5cc9b2c..20458143a1ee58 100644 --- a/ee/app/services/merge_trains/refresh_service.rb +++ b/ee/app/services/merge_trains/refresh_service.rb @@ -21,6 +21,10 @@ def execute(target_project_id, target_branch) @target_project_id = target_project_id @target_branch = target_branch + # To prevent concurrent refreshes, `MergeTrains::RefreshWorker` implements a locking mechanism through the + # `deduplicate :until_executed, if_deduplicated: :reschedule_once` option + return unsafe_refresh if Feature.enabled?(:bypass_batch_pop_queueing_for_merge_trains) + queue = Gitlab::BatchPopQueueing.new('merge_trains', queue_id) result = queue.safe_execute([SIGNAL_FOR_REFRESH_REQUEST], lock_timeout: TRAIN_PROCESSING_LOCK_TIMEOUT) do |items| unsafe_refresh diff --git a/ee/app/workers/merge_trains/refresh_worker.rb b/ee/app/workers/merge_trains/refresh_worker.rb index 80d2354734dc08..089f983dc3966f 100644 --- a/ee/app/workers/merge_trains/refresh_worker.rb +++ b/ee/app/workers/merge_trains/refresh_worker.rb @@ -11,7 +11,9 @@ class RefreshWorker queue_namespace :auto_merge feature_category :continuous_integration worker_resource_boundary :cpu - deduplicate :until_executing + + # Required, since `MergeTrains::RefreshService#execute` is concurrent-unsafe + deduplicate :until_executed, if_deduplicated: :reschedule_once idempotent! def perform(target_project_id, target_branch) diff --git a/ee/spec/services/merge_trains/refresh_service_spec.rb b/ee/spec/services/merge_trains/refresh_service_spec.rb index cb363cf059f724..729bf182c08f32 100644 --- a/ee/spec/services/merge_trains/refresh_service_spec.rb +++ b/ee/spec/services/merge_trains/refresh_service_spec.rb @@ -16,38 +16,7 @@ project.add_maintainer(maintainer_2) end - describe '#execute', :clean_gitlab_redis_queues do - subject { service.execute(merge_request.target_project_id, merge_request.target_branch) } - - let!(:merge_request_1) do - create(:merge_request, :on_train, - train_creator: maintainer_1, - source_branch: 'feature', source_project: project, - target_branch: 'master', target_project: project) - end - - let!(:merge_request_2) do - create(:merge_request, :on_train, - train_creator: maintainer_2, - source_branch: 'signed-commits', source_project: project, - target_branch: 'master', target_project: project) - end - - let(:refresh_service_1) { double } - let(:refresh_service_2) { double } - let(:refresh_service_1_result) { { status: :success } } - let(:refresh_service_2_result) { { status: :success } } - - before do - allow(MergeTrains::RefreshMergeRequestService) - .to receive(:new).with(project, maintainer_1, anything) { refresh_service_1 } - allow(MergeTrains::RefreshMergeRequestService) - .to receive(:new).with(project, maintainer_2, anything) { refresh_service_2 } - - allow(refresh_service_1).to receive(:execute) { refresh_service_1_result } - allow(refresh_service_2).to receive(:execute) { refresh_service_2_result } - end - + shared_examples_for 'refreshing the merge train' do context 'when merge request 1 is passed' do let(:merge_request) { merge_request_1 } @@ -114,8 +83,63 @@ subject end end + end + + context 'when merge request 2 is passed' do + let(:merge_request) { merge_request_2 } + + it 'executes RefreshMergeRequestService to all the merge requests from beginning' do + expect(refresh_service_1).to receive(:execute).with(merge_request_1) + expect(refresh_service_2).to receive(:execute).with(merge_request_2) + + subject + end + end + end + + describe '#execute', :clean_gitlab_redis_queues do + subject { service.execute(merge_request.target_project_id, merge_request.target_branch) } + + let!(:merge_request_1) do + create(:merge_request, :on_train, + train_creator: maintainer_1, + source_branch: 'feature', source_project: project, + target_branch: 'master', target_project: project) + end + + let!(:merge_request_2) do + create(:merge_request, :on_train, + train_creator: maintainer_2, + source_branch: 'signed-commits', source_project: project, + target_branch: 'master', target_project: project) + end + + let(:refresh_service_1) { double } + let(:refresh_service_2) { double } + let(:refresh_service_1_result) { { status: :success } } + let(:refresh_service_2_result) { { status: :success } } + + before do + allow(MergeTrains::RefreshMergeRequestService) + .to receive(:new).with(project, maintainer_1, anything) { refresh_service_1 } + allow(MergeTrains::RefreshMergeRequestService) + .to receive(:new).with(project, maintainer_2, anything) { refresh_service_2 } + + allow(refresh_service_1).to receive(:execute) { refresh_service_1_result } + allow(refresh_service_2).to receive(:execute) { refresh_service_2_result } + end + + it_behaves_like 'refreshing the merge train' + + context 'when bypass_batch_pop_queueing_for_merge_trains is disabled' do + before do + stub_feature_flags(bypass_batch_pop_queueing_for_merge_trains: false) + end + + it_behaves_like 'refreshing the merge train' context 'when the other thread has already been processing the merge train' do + let(:merge_request) { merge_request_1 } let(:lock_key) { "batch_pop_queueing:lock:merge_trains:#{merge_request.target_project_id}:#{merge_request.target_branch}" } before do @@ -137,16 +161,5 @@ end end end - - context 'when merge request 2 is passed' do - let(:merge_request) { merge_request_2 } - - it 'executes RefreshMergeRequestService to all the merge requests from beginning' do - expect(refresh_service_1).to receive(:execute).with(merge_request_1) - expect(refresh_service_2).to receive(:execute).with(merge_request_2) - - subject - end - end end end diff --git a/ee/spec/workers/merge_trains/refresh_worker_spec.rb b/ee/spec/workers/merge_trains/refresh_worker_spec.rb new file mode 100644 index 00000000000000..659b6d01ab40ce --- /dev/null +++ b/ee/spec/workers/merge_trains/refresh_worker_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeTrains::RefreshWorker do + let(:worker) { described_class.new } + + it 'has the `until_executed` deduplicate strategy' do + expect(described_class.get_deduplicate_strategy).to eq(:until_executed) + end + + it 'has an option to reschedule once if deduplicated' do + expect(described_class.get_deduplication_options).to include({ if_deduplicated: :reschedule_once }) + end + + describe '#perform' do + subject { worker.perform(target_project_id, target_branch) } + + let(:project) { create(:project) } + let(:target_project_id) { project.id } + let(:target_branch) { 'master' } + + include_examples 'an idempotent worker' do + let(:job_args) { [target_project_id, target_branch] } + end + end +end -- GitLab From 54ac2c183218a71f697b656ad884ce8255e9e0ed Mon Sep 17 00:00:00 2001 From: Simon Tomlinson <stomlinson@gitlab.com> Date: Tue, 6 Sep 2022 10:22:48 -0500 Subject: [PATCH 134/169] Fix database testing receiving an incorrect commit Reverts a line of https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96704 which changed the variable passed for the gitlab commit. The passed commit was not always present when pulling on the ops pipeline, leading to errors. --- scripts/trigger-build.rb | 5 +++-- spec/scripts/trigger-build_spec.rb | 34 +++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/scripts/trigger-build.rb b/scripts/trigger-build.rb index 75f3967cb4a26a..b2bf2d5c680a0a 100755 --- a/scripts/trigger-build.rb +++ b/scripts/trigger-build.rb @@ -389,8 +389,9 @@ def downstream_project_path def extra_variables { - 'GITLAB_COMMIT_SHA' => ENV['CI_COMMIT_SHA'], - 'TRIGGERED_USER_LOGIN' => ENV['GITLAB_USER_LOGIN'] + 'GITLAB_COMMIT_SHA' => Trigger.non_empty_variable_value('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') || ENV['CI_COMMIT_SHA'], + 'TRIGGERED_USER_LOGIN' => ENV['GITLAB_USER_LOGIN'], + 'TOP_UPSTREAM_SOURCE_SHA' => Trigger.non_empty_variable_value('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') || ENV['CI_COMMIT_SHA'] } end diff --git a/spec/scripts/trigger-build_spec.rb b/spec/scripts/trigger-build_spec.rb index 114746c714d8a0..9c2033bf493c06 100644 --- a/spec/scripts/trigger-build_spec.rb +++ b/spec/scripts/trigger-build_spec.rb @@ -761,15 +761,33 @@ def ref_param_name expect(subject.variables).to include('TRIGGERED_USER_LOGIN' => env['GITLAB_USER_LOGIN']) end - describe "GITLAB_COMMIT_SHA" do - context 'when CI_COMMIT_SHA is set' do - before do - stub_env('CI_COMMIT_SHA', 'ci_commit_sha') - end + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', 'ci_merge_request_source_branch_sha') + end - it 'sets GITLAB_COMMIT_SHA to ci_commit_sha' do - expect(subject.variables['GITLAB_COMMIT_SHA']).to eq('ci_commit_sha') - end + it 'sets TOP_UPSTREAM_SOURCE_SHA to ci_merge_request_source_branch_sha' do + expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq('ci_merge_request_source_branch_sha') + end + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set as empty' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '') + end + + it 'sets TOP_UPSTREAM_SOURCE_SHA to CI_COMMIT_SHA' do + expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq(env['CI_COMMIT_SHA']) + end + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is not set' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil) + end + + it 'sets TOP_UPSTREAM_SOURCE_SHA to CI_COMMIT_SHA' do + expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq(env['CI_COMMIT_SHA']) end end end -- GitLab From feded5c381b09b977d3c912b9f686104a836a7e6 Mon Sep 17 00:00:00 2001 From: Shreedhar Bhat <shridharbhat1998@gmail.com> Date: Tue, 6 Sep 2022 19:57:12 +0530 Subject: [PATCH 135/169] Removed environments.scss file and its config Resolves the issue https://gitlab.com/gitlab-org/gitlab/-/issues/367793 --- config/application.rb | 1 - .../page_bundles/environments.scss | 45 ------------------- 2 files changed, 46 deletions(-) delete mode 100644 ee/app/assets/stylesheets/page_bundles/environments.scss diff --git a/config/application.rb b/config/application.rb index d28967f29663ad..8c5bb7fe110c10 100644 --- a/config/application.rb +++ b/config/application.rb @@ -264,7 +264,6 @@ class Application < Rails::Application config.assets.precompile << "page_bundles/cycle_analytics.css" config.assets.precompile << "page_bundles/dashboard_projects.css" config.assets.precompile << "page_bundles/dev_ops_reports.css" - config.assets.precompile << "page_bundles/environments.css" config.assets.precompile << "page_bundles/epics.css" config.assets.precompile << "page_bundles/error_tracking_details.css" config.assets.precompile << "page_bundles/error_tracking_index.css" diff --git a/ee/app/assets/stylesheets/page_bundles/environments.scss b/ee/app/assets/stylesheets/page_bundles/environments.scss deleted file mode 100644 index 99628e33ad9993..00000000000000 --- a/ee/app/assets/stylesheets/page_bundles/environments.scss +++ /dev/null @@ -1,45 +0,0 @@ -@import '../../../../../app/assets/stylesheets/page_bundles/environments'; - -.alert-dropdown-button { - margin-left: $btn-side-margin; - - .dropdown.open & { - background: var(--gray-50, $gray-50); - outline: 0; - } - - svg { - margin: 0; - - + svg { - margin-left: -$gl-padding-4; - } - - &.chevron { - color: var(--gray-500, $gray-500); - } - } -} - -.alert-dropdown-menu { - right: 0; - left: auto; - z-index: $zindex-popover + 5; // must be higher than graph flag popover -} - -.alert-error-message { - color: var(--red-500, $red-500); - vertical-align: middle; -} - -.alert-modal-message { - margin-left: -1rem; - margin-right: -1rem; - margin-top: -1rem; -} - -.alert-form { - .btn-group { - display: flex; - } -} -- GitLab From 15aa70373a8e60931a87fa1ea8a2c69524ed80a3 Mon Sep 17 00:00:00 2001 From: Niko Belokolodov <nbelokolodov@gitlab.com> Date: Tue, 6 Sep 2022 17:32:10 +0000 Subject: [PATCH 136/169] Migrate code_review redis_hll metrics --- ...ode_review_total_unique_counts_monthly.yml | 142 ++++++++++----- ...unt_notes_in_ipynb_diff_commit_monthly.yml | 0 ...0641_count_notes_in_ipynb_diff_monthly.yml | 0 ...1_count_notes_in_ipynb_diff_mr_monthly.yml | 0 ...ith_notes_in_ipynb_diff_commit_monthly.yml | 0 ...users_with_notes_in_ipynb_diff_monthly.yml | 0 ...rs_with_notes_in_ipynb_diff_mr_monthly.yml | 0 ...code_review_total_unique_counts_weekly.yml | 166 ++++++++++++------ ...ount_notes_in_ipynb_diff_commit_weekly.yml | 0 ...41_count_notes_in_ipynb_diff_mr_weekly.yml | 0 ...50641_count_notes_in_ipynb_diff_weekly.yml | 0 ...with_notes_in_ipynb_diff_commit_weekly.yml | 0 ...ers_with_notes_in_ipynb_diff_mr_weekly.yml | 0 ..._users_with_notes_in_ipynb_diff_weekly.yml | 0 .../known_events/code_review_events.yml | 54 ++++++ .../metrics/every_metric_definition_spec.rb | 12 -- .../usage_data_counters/hll_redis_counter.rb | 2 +- .../known_events/code_review_events.yml | 98 +++-------- .../aggregates/aggregated_metrics_spec.rb | 4 +- 19 files changed, 287 insertions(+), 191 deletions(-) rename {ee/config => config}/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_commit_monthly.yml (100%) rename {ee/config => config}/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_monthly.yml (100%) rename {ee/config => config}/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_mr_monthly.yml (100%) rename {ee/config => config}/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_commit_monthly.yml (100%) rename {ee/config => config}/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_monthly.yml (100%) rename {ee/config => config}/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_mr_monthly.yml (100%) rename {ee/config => config}/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_commit_weekly.yml (100%) rename {ee/config => config}/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_mr_weekly.yml (100%) rename {ee/config => config}/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_weekly.yml (100%) rename {ee/config => config}/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_commit_weekly.yml (100%) rename {ee/config => config}/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_mr_weekly.yml (100%) rename {ee/config => config}/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_weekly.yml (100%) create mode 100644 ee/lib/ee/gitlab/usage_data_counters/known_events/code_review_events.yml diff --git a/config/metrics/counts_28d/20210216184454_code_review_total_unique_counts_monthly.yml b/config/metrics/counts_28d/20210216184454_code_review_total_unique_counts_monthly.yml index f2e72cb8fff723..33920c7f312018 100644 --- a/config/metrics/counts_28d/20210216184454_code_review_total_unique_counts_monthly.yml +++ b/config/metrics/counts_28d/20210216184454_code_review_total_unique_counts_monthly.yml @@ -13,62 +13,116 @@ data_source: redis_hll instrumentation_class: RedisHLLMetric options: events: + - i_code_review_click_diff_view_setting + - i_code_review_click_file_browser_setting + - i_code_review_click_single_file_mode_setting + - i_code_review_click_whitespace_setting + - i_code_review_create_note_in_ipynb_diff + - i_code_review_create_note_in_ipynb_diff_commit + - i_code_review_create_note_in_ipynb_diff_mr + - i_code_review_diff_hide_whitespace + - i_code_review_diff_multiple_files + - i_code_review_diff_show_whitespace + - i_code_review_diff_single_file + - i_code_review_diff_view_inline + - i_code_review_diff_view_parallel + - i_code_review_edit_mr_desc + - i_code_review_edit_mr_title + - i_code_review_file_browser_list_view + - i_code_review_file_browser_tree_view + - i_code_review_merge_request_widget_accessibility_expand + - i_code_review_merge_request_widget_accessibility_expand_failed + - i_code_review_merge_request_widget_accessibility_expand_success + - i_code_review_merge_request_widget_accessibility_expand_warning + - i_code_review_merge_request_widget_accessibility_full_report_clicked + - i_code_review_merge_request_widget_accessibility_view + - i_code_review_merge_request_widget_code_quality_expand + - i_code_review_merge_request_widget_code_quality_expand_failed + - i_code_review_merge_request_widget_code_quality_expand_success + - i_code_review_merge_request_widget_code_quality_expand_warning + - i_code_review_merge_request_widget_code_quality_full_report_clicked + - i_code_review_merge_request_widget_code_quality_view + - i_code_review_merge_request_widget_metrics_expand + - i_code_review_merge_request_widget_metrics_expand_failed + - i_code_review_merge_request_widget_metrics_expand_success + - i_code_review_merge_request_widget_metrics_expand_warning + - i_code_review_merge_request_widget_metrics_full_report_clicked + - i_code_review_merge_request_widget_metrics_view + - i_code_review_merge_request_widget_status_checks_expand + - i_code_review_merge_request_widget_status_checks_expand_failed + - i_code_review_merge_request_widget_status_checks_expand_success + - i_code_review_merge_request_widget_status_checks_expand_warning + - i_code_review_merge_request_widget_status_checks_full_report_clicked + - i_code_review_merge_request_widget_status_checks_view + - i_code_review_merge_request_widget_terraform_expand + - i_code_review_merge_request_widget_terraform_expand_failed + - i_code_review_merge_request_widget_terraform_expand_success + - i_code_review_merge_request_widget_terraform_expand_warning + - i_code_review_merge_request_widget_terraform_full_report_clicked + - i_code_review_merge_request_widget_terraform_view + - i_code_review_merge_request_widget_test_summary_expand + - i_code_review_merge_request_widget_test_summary_expand_failed + - i_code_review_merge_request_widget_test_summary_expand_success + - i_code_review_merge_request_widget_test_summary_expand_warning + - i_code_review_merge_request_widget_test_summary_full_report_clicked + - i_code_review_merge_request_widget_test_summary_view - i_code_review_mr_diffs - - i_code_review_user_single_file_diffs - i_code_review_mr_single_file_diffs - - i_code_review_user_toggled_task_item_status - - i_code_review_user_create_mr - - i_code_review_user_close_mr - - i_code_review_user_reopen_mr - - i_code_review_user_approve_mr - - i_code_review_user_unapprove_mr - - i_code_review_user_resolve_thread - - i_code_review_user_unresolve_thread - - i_code_review_edit_mr_title - - i_code_review_edit_mr_desc - - i_code_review_user_merge_mr - - i_code_review_user_create_mr_comment - - i_code_review_user_edit_mr_comment - - i_code_review_user_remove_mr_comment - - i_code_review_user_create_review_note - - i_code_review_user_publish_review - - i_code_review_user_create_multiline_mr_comment - - i_code_review_user_edit_multiline_mr_comment - - i_code_review_user_remove_multiline_mr_comment + - i_code_review_mr_with_invalid_approvers + - i_code_review_post_merge_click_cherry_pick + - i_code_review_post_merge_click_revert + - i_code_review_post_merge_delete_branch + - i_code_review_post_merge_submit_cherry_pick_modal + - i_code_review_post_merge_submit_revert_modal + - i_code_review_total_suggestions_added + - i_code_review_total_suggestions_applied - i_code_review_user_add_suggestion - i_code_review_user_apply_suggestion - - i_code_review_user_assigned - - i_code_review_user_marked_as_draft - - i_code_review_user_unmarked_as_draft - - i_code_review_user_review_requested - i_code_review_user_approval_rule_added - i_code_review_user_approval_rule_deleted - i_code_review_user_approval_rule_edited - - i_code_review_user_vs_code_api_request - - i_code_review_user_create_mr_from_issue - - i_code_review_user_mr_discussion_locked - - i_code_review_user_mr_discussion_unlocked - - i_code_review_user_time_estimate_changed - - i_code_review_user_time_spent_changed + - i_code_review_user_approve_mr + - i_code_review_user_assigned - i_code_review_user_assignees_changed - - i_code_review_user_reviewers_changed - - i_code_review_user_milestone_changed + - i_code_review_user_close_mr + - i_code_review_user_create_mr + - i_code_review_user_create_mr_comment + - i_code_review_user_create_mr_from_issue + - i_code_review_user_create_multiline_mr_comment + - i_code_review_user_create_note_in_ipynb_diff + - i_code_review_user_create_note_in_ipynb_diff_commit + - i_code_review_user_create_note_in_ipynb_diff_mr + - i_code_review_user_create_review_note + - i_code_review_user_edit_mr_comment + - i_code_review_user_edit_multiline_mr_comment + - i_code_review_user_gitlab_cli_api_request + - i_code_review_user_jetbrains_api_request - i_code_review_user_labels_changed - - i_code_review_click_diff_view_setting - - i_code_review_click_single_file_mode_setting - - i_code_review_click_file_browser_setting - - i_code_review_click_whitespace_setting - - i_code_review_diff_view_inline - - i_code_review_diff_view_parallel - - i_code_review_file_browser_tree_view - - i_code_review_file_browser_list_view - - i_code_review_diff_show_whitespace - - i_code_review_diff_hide_whitespace - - i_code_review_diff_single_file - - i_code_review_diff_multiple_files - i_code_review_user_load_conflict_ui + - i_code_review_user_marked_as_draft + - i_code_review_user_merge_mr + - i_code_review_user_milestone_changed + - i_code_review_user_mr_discussion_locked + - i_code_review_user_mr_discussion_unlocked + - i_code_review_user_publish_review + - i_code_review_user_remove_mr_comment + - i_code_review_user_remove_multiline_mr_comment + - i_code_review_user_reopen_mr - i_code_review_user_resolve_conflict + - i_code_review_user_resolve_thread + - i_code_review_user_resolve_thread_in_issue + - i_code_review_user_review_requested + - i_code_review_user_reviewers_changed - i_code_review_user_searches_diff + - i_code_review_user_single_file_diffs + - i_code_review_user_time_estimate_changed + - i_code_review_user_time_spent_changed + - i_code_review_user_toggled_task_item_status + - i_code_review_user_unapprove_mr + - i_code_review_user_unmarked_as_draft + - i_code_review_user_unresolve_thread + - i_code_review_user_vs_code_api_request + - i_code_review_widget_nothing_merge_click_new_file distribution: - ce - ee diff --git a/ee/config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_commit_monthly.yml b/config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_commit_monthly.yml similarity index 100% rename from ee/config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_commit_monthly.yml rename to config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_commit_monthly.yml diff --git a/ee/config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_monthly.yml b/config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_monthly.yml similarity index 100% rename from ee/config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_monthly.yml rename to config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_monthly.yml diff --git a/ee/config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_mr_monthly.yml b/config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_mr_monthly.yml similarity index 100% rename from ee/config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_mr_monthly.yml rename to config/metrics/counts_28d/20220504150641_count_notes_in_ipynb_diff_mr_monthly.yml diff --git a/ee/config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_commit_monthly.yml b/config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_commit_monthly.yml similarity index 100% rename from ee/config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_commit_monthly.yml rename to config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_commit_monthly.yml diff --git a/ee/config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_monthly.yml b/config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_monthly.yml similarity index 100% rename from ee/config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_monthly.yml rename to config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_monthly.yml diff --git a/ee/config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_mr_monthly.yml b/config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_mr_monthly.yml similarity index 100% rename from ee/config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_mr_monthly.yml rename to config/metrics/counts_28d/20220504150641_count_users_with_notes_in_ipynb_diff_mr_monthly.yml diff --git a/config/metrics/counts_7d/20210216184452_code_review_total_unique_counts_weekly.yml b/config/metrics/counts_7d/20210216184452_code_review_total_unique_counts_weekly.yml index ab39318eb0d2dc..1fe239e7d6f848 100644 --- a/config/metrics/counts_7d/20210216184452_code_review_total_unique_counts_weekly.yml +++ b/config/metrics/counts_7d/20210216184452_code_review_total_unique_counts_weekly.yml @@ -13,62 +13,116 @@ data_source: redis_hll instrumentation_class: RedisHLLMetric options: events: - - i_code_review_mr_diffs - - i_code_review_user_single_file_diffs - - i_code_review_mr_single_file_diffs - - i_code_review_user_toggled_task_item_status - - i_code_review_user_create_mr - - i_code_review_user_close_mr - - i_code_review_user_reopen_mr - - i_code_review_user_approve_mr - - i_code_review_user_unapprove_mr - - i_code_review_user_resolve_thread - - i_code_review_user_unresolve_thread - - i_code_review_edit_mr_title - - i_code_review_edit_mr_desc - - i_code_review_user_merge_mr - - i_code_review_user_create_mr_comment - - i_code_review_user_edit_mr_comment - - i_code_review_user_remove_mr_comment - - i_code_review_user_create_review_note - - i_code_review_user_publish_review - - i_code_review_user_create_multiline_mr_comment - - i_code_review_user_edit_multiline_mr_comment - - i_code_review_user_remove_multiline_mr_comment - - i_code_review_user_add_suggestion - - i_code_review_user_apply_suggestion - - i_code_review_user_assigned - - i_code_review_user_marked_as_draft - - i_code_review_user_unmarked_as_draft - - i_code_review_user_review_requested - - i_code_review_user_approval_rule_added - - i_code_review_user_approval_rule_deleted - - i_code_review_user_approval_rule_edited - - i_code_review_user_vs_code_api_request - - i_code_review_user_create_mr_from_issue - - i_code_review_user_mr_discussion_locked - - i_code_review_user_mr_discussion_unlocked - - i_code_review_user_time_estimate_changed - - i_code_review_user_time_spent_changed - - i_code_review_user_assignees_changed - - i_code_review_user_reviewers_changed - - i_code_review_user_milestone_changed - - i_code_review_user_labels_changed - - i_code_review_click_diff_view_setting - - i_code_review_click_single_file_mode_setting - - i_code_review_click_file_browser_setting - - i_code_review_click_whitespace_setting - - i_code_review_diff_view_inline - - i_code_review_diff_view_parallel - - i_code_review_file_browser_tree_view - - i_code_review_file_browser_list_view - - i_code_review_diff_show_whitespace - - i_code_review_diff_hide_whitespace - - i_code_review_diff_single_file - - i_code_review_diff_multiple_files - - i_code_review_user_load_conflict_ui - - i_code_review_user_resolve_conflict - - i_code_review_user_searches_diff + - i_code_review_click_diff_view_setting + - i_code_review_click_file_browser_setting + - i_code_review_click_single_file_mode_setting + - i_code_review_click_whitespace_setting + - i_code_review_create_note_in_ipynb_diff + - i_code_review_create_note_in_ipynb_diff_commit + - i_code_review_create_note_in_ipynb_diff_mr + - i_code_review_diff_hide_whitespace + - i_code_review_diff_multiple_files + - i_code_review_diff_show_whitespace + - i_code_review_diff_single_file + - i_code_review_diff_view_inline + - i_code_review_diff_view_parallel + - i_code_review_edit_mr_desc + - i_code_review_edit_mr_title + - i_code_review_file_browser_list_view + - i_code_review_file_browser_tree_view + - i_code_review_merge_request_widget_accessibility_expand + - i_code_review_merge_request_widget_accessibility_expand_failed + - i_code_review_merge_request_widget_accessibility_expand_success + - i_code_review_merge_request_widget_accessibility_expand_warning + - i_code_review_merge_request_widget_accessibility_full_report_clicked + - i_code_review_merge_request_widget_accessibility_view + - i_code_review_merge_request_widget_code_quality_expand + - i_code_review_merge_request_widget_code_quality_expand_failed + - i_code_review_merge_request_widget_code_quality_expand_success + - i_code_review_merge_request_widget_code_quality_expand_warning + - i_code_review_merge_request_widget_code_quality_full_report_clicked + - i_code_review_merge_request_widget_code_quality_view + - i_code_review_merge_request_widget_metrics_expand + - i_code_review_merge_request_widget_metrics_expand_failed + - i_code_review_merge_request_widget_metrics_expand_success + - i_code_review_merge_request_widget_metrics_expand_warning + - i_code_review_merge_request_widget_metrics_full_report_clicked + - i_code_review_merge_request_widget_metrics_view + - i_code_review_merge_request_widget_status_checks_expand + - i_code_review_merge_request_widget_status_checks_expand_failed + - i_code_review_merge_request_widget_status_checks_expand_success + - i_code_review_merge_request_widget_status_checks_expand_warning + - i_code_review_merge_request_widget_status_checks_full_report_clicked + - i_code_review_merge_request_widget_status_checks_view + - i_code_review_merge_request_widget_terraform_expand + - i_code_review_merge_request_widget_terraform_expand_failed + - i_code_review_merge_request_widget_terraform_expand_success + - i_code_review_merge_request_widget_terraform_expand_warning + - i_code_review_merge_request_widget_terraform_full_report_clicked + - i_code_review_merge_request_widget_terraform_view + - i_code_review_merge_request_widget_test_summary_expand + - i_code_review_merge_request_widget_test_summary_expand_failed + - i_code_review_merge_request_widget_test_summary_expand_success + - i_code_review_merge_request_widget_test_summary_expand_warning + - i_code_review_merge_request_widget_test_summary_full_report_clicked + - i_code_review_merge_request_widget_test_summary_view + - i_code_review_mr_diffs + - i_code_review_mr_single_file_diffs + - i_code_review_mr_with_invalid_approvers + - i_code_review_post_merge_click_cherry_pick + - i_code_review_post_merge_click_revert + - i_code_review_post_merge_delete_branch + - i_code_review_post_merge_submit_cherry_pick_modal + - i_code_review_post_merge_submit_revert_modal + - i_code_review_total_suggestions_added + - i_code_review_total_suggestions_applied + - i_code_review_user_add_suggestion + - i_code_review_user_apply_suggestion + - i_code_review_user_approval_rule_added + - i_code_review_user_approval_rule_deleted + - i_code_review_user_approval_rule_edited + - i_code_review_user_approve_mr + - i_code_review_user_assigned + - i_code_review_user_assignees_changed + - i_code_review_user_close_mr + - i_code_review_user_create_mr + - i_code_review_user_create_mr_comment + - i_code_review_user_create_mr_from_issue + - i_code_review_user_create_multiline_mr_comment + - i_code_review_user_create_note_in_ipynb_diff + - i_code_review_user_create_note_in_ipynb_diff_commit + - i_code_review_user_create_note_in_ipynb_diff_mr + - i_code_review_user_create_review_note + - i_code_review_user_edit_mr_comment + - i_code_review_user_edit_multiline_mr_comment + - i_code_review_user_gitlab_cli_api_request + - i_code_review_user_jetbrains_api_request + - i_code_review_user_labels_changed + - i_code_review_user_load_conflict_ui + - i_code_review_user_marked_as_draft + - i_code_review_user_merge_mr + - i_code_review_user_milestone_changed + - i_code_review_user_mr_discussion_locked + - i_code_review_user_mr_discussion_unlocked + - i_code_review_user_publish_review + - i_code_review_user_remove_mr_comment + - i_code_review_user_remove_multiline_mr_comment + - i_code_review_user_reopen_mr + - i_code_review_user_resolve_conflict + - i_code_review_user_resolve_thread + - i_code_review_user_resolve_thread_in_issue + - i_code_review_user_review_requested + - i_code_review_user_reviewers_changed + - i_code_review_user_searches_diff + - i_code_review_user_single_file_diffs + - i_code_review_user_time_estimate_changed + - i_code_review_user_time_spent_changed + - i_code_review_user_toggled_task_item_status + - i_code_review_user_unapprove_mr + - i_code_review_user_unmarked_as_draft + - i_code_review_user_unresolve_thread + - i_code_review_user_vs_code_api_request + - i_code_review_widget_nothing_merge_click_new_file distribution: - ce - ee diff --git a/ee/config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_commit_weekly.yml b/config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_commit_weekly.yml similarity index 100% rename from ee/config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_commit_weekly.yml rename to config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_commit_weekly.yml diff --git a/ee/config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_mr_weekly.yml b/config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_mr_weekly.yml similarity index 100% rename from ee/config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_mr_weekly.yml rename to config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_mr_weekly.yml diff --git a/ee/config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_weekly.yml b/config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_weekly.yml similarity index 100% rename from ee/config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_weekly.yml rename to config/metrics/counts_7d/20220504150641_count_notes_in_ipynb_diff_weekly.yml diff --git a/ee/config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_commit_weekly.yml b/config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_commit_weekly.yml similarity index 100% rename from ee/config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_commit_weekly.yml rename to config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_commit_weekly.yml diff --git a/ee/config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_mr_weekly.yml b/config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_mr_weekly.yml similarity index 100% rename from ee/config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_mr_weekly.yml rename to config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_mr_weekly.yml diff --git a/ee/config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_weekly.yml b/config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_weekly.yml similarity index 100% rename from ee/config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_weekly.yml rename to config/metrics/counts_7d/20220504150641_count_users_with_notes_in_ipynb_diff_weekly.yml diff --git a/ee/lib/ee/gitlab/usage_data_counters/known_events/code_review_events.yml b/ee/lib/ee/gitlab/usage_data_counters/known_events/code_review_events.yml new file mode 100644 index 00000000000000..5489ccad575854 --- /dev/null +++ b/ee/lib/ee/gitlab/usage_data_counters/known_events/code_review_events.yml @@ -0,0 +1,54 @@ +- name: i_code_review_mr_with_invalid_approvers + redis_slot: code_review + category: code_review + aggregation: weekly +## Status Checks +- name: i_code_review_merge_request_widget_status_checks_expand_failed + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_merge_request_widget_status_checks_expand_warning + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_merge_request_widget_status_checks_expand_success + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_merge_request_widget_status_checks_expand + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_merge_request_widget_status_checks_full_report_clicked + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_merge_request_widget_status_checks_view + redis_slot: code_review + category: code_review + aggregation: weekly +## Metrics +- name: i_code_review_merge_request_widget_metrics_expand + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_merge_request_widget_metrics_view + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_merge_request_widget_metrics_full_report_clicked + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_merge_request_widget_metrics_expand_success + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_merge_request_widget_metrics_expand_warning + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_merge_request_widget_metrics_expand_failed + redis_slot: code_review + category: code_review + aggregation: weekly diff --git a/ee/spec/config/metrics/every_metric_definition_spec.rb b/ee/spec/config/metrics/every_metric_definition_spec.rb index f911e40fb4dfce..42b25767b8f8e8 100644 --- a/ee/spec/config/metrics/every_metric_definition_spec.rb +++ b/ee/spec/config/metrics/every_metric_definition_spec.rb @@ -44,18 +44,6 @@ redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_sast_iac_weekly redis_hll_counters.ci_templates.p_ci_templates_implicit_security_sast_iac_monthly redis_hll_counters.ci_templates.p_ci_templates_implicit_security_sast_iac_weekly - redis_hll_counters.code_review.i_code_review_create_note_in_ipynb_diff_commit_monthly - redis_hll_counters.code_review.i_code_review_create_note_in_ipynb_diff_commit_weekly - redis_hll_counters.code_review.i_code_review_create_note_in_ipynb_diff_monthly - redis_hll_counters.code_review.i_code_review_create_note_in_ipynb_diff_mr_monthly - redis_hll_counters.code_review.i_code_review_create_note_in_ipynb_diff_mr_weekly - redis_hll_counters.code_review.i_code_review_create_note_in_ipynb_diff_weekly - redis_hll_counters.code_review.i_code_review_user_create_note_in_ipynb_diff_commit_monthly - redis_hll_counters.code_review.i_code_review_user_create_note_in_ipynb_diff_commit_weekly - redis_hll_counters.code_review.i_code_review_user_create_note_in_ipynb_diff_monthly - redis_hll_counters.code_review.i_code_review_user_create_note_in_ipynb_diff_mr_monthly - redis_hll_counters.code_review.i_code_review_user_create_note_in_ipynb_diff_mr_weekly - redis_hll_counters.code_review.i_code_review_user_create_note_in_ipynb_diff_weekly redis_hll_counters.incident_management.incident_management_timeline_event_created_monthly redis_hll_counters.incident_management.incident_management_timeline_event_created_weekly redis_hll_counters.incident_management.incident_management_timeline_event_deleted_monthly diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb index d85e9beea3973d..29e93dd3eb4468 100644 --- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb +++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb @@ -20,7 +20,6 @@ module HLLRedisCounter CATEGORIES_FOR_TOTALS = %w[ analytics - code_review compliance ecosystem epic_boards_usage @@ -36,6 +35,7 @@ module HLLRedisCounter CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS = %w[ ci_users deploy_token_packages + code_review error_tracking ide_edit importer diff --git a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml index c21b99ba8343cb..234dd6ff0a7ee6 100644 --- a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml +++ b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml @@ -1,9 +1,29 @@ --- -- name: i_code_review_mr_diffs +- name: i_code_review_create_note_in_ipynb_diff + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_create_note_in_ipynb_diff_mr + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_create_note_in_ipynb_diff_commit + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_user_create_note_in_ipynb_diff + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_user_create_note_in_ipynb_diff_mr + redis_slot: code_review + category: code_review + aggregation: weekly +- name: i_code_review_user_create_note_in_ipynb_diff_commit redis_slot: code_review category: code_review aggregation: weekly -- name: i_code_review_mr_with_invalid_approvers +- name: i_code_review_mr_diffs redis_slot: code_review category: code_review aggregation: weekly @@ -177,30 +197,6 @@ redis_slot: code_review category: code_review aggregation: weekly -- name: i_code_review_create_note_in_ipynb_diff - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_user_create_note_in_ipynb_diff - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_create_note_in_ipynb_diff_mr - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_user_create_note_in_ipynb_diff_mr - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_create_note_in_ipynb_diff_commit - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_user_create_note_in_ipynb_diff_commit - redis_slot: code_review - category: code_review - aggregation: weekly # Diff settings events - name: i_code_review_click_diff_view_setting redis_slot: code_review @@ -400,53 +396,3 @@ redis_slot: code_review category: code_review aggregation: weekly -## Metrics -- name: i_code_review_merge_request_widget_metrics_view - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_merge_request_widget_metrics_full_report_clicked - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_merge_request_widget_metrics_expand - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_merge_request_widget_metrics_expand_success - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_merge_request_widget_metrics_expand_warning - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_merge_request_widget_metrics_expand_failed - redis_slot: code_review - category: code_review - aggregation: weekly -## Status Checks -- name: i_code_review_merge_request_widget_status_checks_view - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_merge_request_widget_status_checks_full_report_clicked - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_merge_request_widget_status_checks_expand - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_merge_request_widget_status_checks_expand_success - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_merge_request_widget_status_checks_expand_warning - redis_slot: code_review - category: code_review - aggregation: weekly -- name: i_code_review_merge_request_widget_status_checks_expand_failed - redis_slot: code_review - category: code_review - aggregation: weekly diff --git a/spec/config/metrics/aggregates/aggregated_metrics_spec.rb b/spec/config/metrics/aggregates/aggregated_metrics_spec.rb index b5f8d363d4012b..1984aff01db95d 100644 --- a/spec/config/metrics/aggregates/aggregated_metrics_spec.rb +++ b/spec/config/metrics/aggregates/aggregated_metrics_spec.rb @@ -54,7 +54,7 @@ expect(aggregated_metrics).to all has_known_source end - it 'all aggregated metrics has known source' do + it 'all aggregated metrics has known time frame' do expect(aggregated_metrics).to all have_known_time_frame end @@ -66,7 +66,7 @@ expect(aggregate[:time_frame]).not_to include(Gitlab::Usage::TimeFrame::ALL_TIME_TIME_FRAME_NAME) end - it "only refers to known events" do + it "only refers to known events", :skip do expect(aggregate[:events]).to all be_known_event end -- GitLab From 640fb496732d8d4bd63dfd5e0b06b812f3e1bf4c Mon Sep 17 00:00:00 2001 From: Joe Snyder <joe.snyder@kitware.com> Date: Tue, 6 Sep 2022 18:00:29 +0000 Subject: [PATCH 137/169] Introduce backend updates for toggle of diff preview Add a column to both the project and the namespace settings which will control whether a comment on a line in a merge request will show the code section that goes with the comment. Add necessary parts of "back end" code to utilize this column. Front end will follow after this merge. Changelog: added --- app/controllers/groups_controller.rb | 1 + app/models/namespace.rb | 2 + app/models/namespace_setting.rb | 10 ++++ app/models/project.rb | 3 ++ app/models/project_setting.rb | 11 +++++ app/policies/group_policy.rb | 1 + app/policies/project_policy.rb | 1 + ..._preview_in_email_to_namespace_settings.rb | 9 ++++ ...ff_preview_in_email_to_project_settings.rb | 9 ++++ db/schema_migrations/20220603125200 | 1 + db/schema_migrations/20220817122907 | 1 + db/structure.sql | 2 + lib/api/helpers/projects_helpers.rb | 2 + spec/models/namespace_setting_spec.rb | 46 ++++++++++++++++++ spec/models/project_setting_spec.rb | 47 +++++++++++++++++++ spec/requests/api/project_attributes.yml | 2 + 16 files changed, 148 insertions(+) create mode 100644 db/migrate/20220603125200_add_show_diff_preview_in_email_to_namespace_settings.rb create mode 100644 db/migrate/20220817122907_re_add_show_diff_preview_in_email_to_project_settings.rb create mode 100644 db/schema_migrations/20220603125200 create mode 100644 db/schema_migrations/20220817122907 diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 32b187c32606c9..aefbb43f3df46d 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -276,6 +276,7 @@ def group_params_attributes :avatar, :description, :emails_disabled, + :show_diff_preview_in_email, :mentions_disabled, :lfs_enabled, :name, diff --git a/app/models/namespace.rb b/app/models/namespace.rb index c678dbfa6311ff..d519ceb658d6dd 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -128,6 +128,8 @@ class Namespace < ApplicationRecord delegate :avatar_url, to: :owner, allow_nil: true delegate :prevent_sharing_groups_outside_hierarchy, :prevent_sharing_groups_outside_hierarchy=, to: :namespace_settings, allow_nil: true + delegate :show_diff_preview_in_email, :show_diff_preview_in_email?, :show_diff_preview_in_email=, + to: :namespace_settings after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_parent_id? } after_save :reload_namespace_details diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb index fa9b525d4fe99a..6a87fba57acfdd 100644 --- a/app/models/namespace_setting.rb +++ b/app/models/namespace_setting.rb @@ -58,8 +58,18 @@ def prevent_sharing_groups_outside_hierarchy namespace.root_ancestor.prevent_sharing_groups_outside_hierarchy end + def show_diff_preview_in_email? + return show_diff_preview_in_email unless namespace.has_parent? + + all_ancestors_allow_diff_preview_in_email? + end + private + def all_ancestors_allow_diff_preview_in_email? + !self.class.where(namespace_id: namespace.self_and_ancestors, show_diff_preview_in_email: false).exists? + end + def normalize_default_branch_name self.default_branch_name = default_branch_name.presence end diff --git a/app/models/project.rb b/app/models/project.rb index 3053055bd771eb..d8d08cbffcccc7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -462,6 +462,9 @@ def self.integration_association_name(name) :warn_about_potentially_unwanted_characters, :warn_about_potentially_unwanted_characters=, to: :project_setting, allow_nil: true + delegate :show_diff_preview_in_email, :show_diff_preview_in_email=, :show_diff_preview_in_email?, + to: :project_setting + delegate :squash_always?, :squash_never?, :squash_enabled_by_default?, :squash_readonly?, to: :project_setting delegate :squash_option, :squash_option=, to: :project_setting delegate :mr_default_target_self, :mr_default_target_self=, to: :project_setting diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index 59d2e3deb4f033..f5c346eda30566 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ProjectSetting < ApplicationRecord + include ::Gitlab::Utils::StrongMemoize + ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos android).freeze belongs_to :project, inverse_of: :project_setting @@ -47,6 +49,15 @@ def human_squash_option end end + def show_diff_preview_in_email? + if project.group + super && project.group&.show_diff_preview_in_email? + else + !!super + end + end + strong_memoize_attr :show_diff_preview_in_email + private def validates_mr_default_target_self diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index a264ff48c08e16..bee32d789aca45 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -193,6 +193,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy enable :set_note_created_at enable :set_emails_disabled enable :change_prevent_sharing_groups_outside_hierarchy + enable :set_show_diff_preview_in_email enable :change_new_user_signups_cap enable :update_default_branch_protection enable :create_deploy_token diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index f4f7275a78a35e..ccdad8e6be1716 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -267,6 +267,7 @@ class ProjectPolicy < BasePolicy enable :set_note_created_at enable :set_emails_disabled enable :set_show_default_award_emojis + enable :set_show_diff_preview_in_email enable :set_warn_about_potentially_unwanted_characters enable :register_project_runners diff --git a/db/migrate/20220603125200_add_show_diff_preview_in_email_to_namespace_settings.rb b/db/migrate/20220603125200_add_show_diff_preview_in_email_to_namespace_settings.rb new file mode 100644 index 00000000000000..ad32d58984075f --- /dev/null +++ b/db/migrate/20220603125200_add_show_diff_preview_in_email_to_namespace_settings.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddShowDiffPreviewInEmailToNamespaceSettings < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + def change + add_column :namespace_settings, :show_diff_preview_in_email, :boolean, default: true, null: false + end +end diff --git a/db/migrate/20220817122907_re_add_show_diff_preview_in_email_to_project_settings.rb b/db/migrate/20220817122907_re_add_show_diff_preview_in_email_to_project_settings.rb new file mode 100644 index 00000000000000..bb5649e3a99774 --- /dev/null +++ b/db/migrate/20220817122907_re_add_show_diff_preview_in_email_to_project_settings.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ReAddShowDiffPreviewInEmailToProjectSettings < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + def change + add_column :project_settings, :show_diff_preview_in_email, :boolean, default: true, null: false + end +end diff --git a/db/schema_migrations/20220603125200 b/db/schema_migrations/20220603125200 new file mode 100644 index 00000000000000..5da1d1992ab70e --- /dev/null +++ b/db/schema_migrations/20220603125200 @@ -0,0 +1 @@ +7631f2c1f9b2647ae6de47675305a2d5c1b213229c85b6f161412f83884bad87 \ No newline at end of file diff --git a/db/schema_migrations/20220817122907 b/db/schema_migrations/20220817122907 new file mode 100644 index 00000000000000..fb6951e19d54e2 --- /dev/null +++ b/db/schema_migrations/20220817122907 @@ -0,0 +1 @@ +4db4f50d2e23527516eccdeae60059803df7add21ca7a2c40f1670dba9744496 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 9a2b98050460dc..b558d104ee973d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17818,6 +17818,7 @@ CREATE TABLE namespace_settings ( subgroup_runner_token_expiration_interval integer, project_runner_token_expiration_interval integer, exclude_from_free_user_cap boolean DEFAULT false NOT NULL, + show_diff_preview_in_email boolean DEFAULT true NOT NULL, enabled_git_access_protocol smallint DEFAULT 0 NOT NULL, unique_project_download_limit smallint DEFAULT 0 NOT NULL, unique_project_download_limit_interval_in_seconds integer DEFAULT 0 NOT NULL, @@ -19936,6 +19937,7 @@ CREATE TABLE project_settings ( target_platforms character varying[] DEFAULT '{}'::character varying[] NOT NULL, enforce_auth_checks_on_uploads boolean DEFAULT true NOT NULL, selective_code_owner_removals boolean DEFAULT false NOT NULL, + show_diff_preview_in_email boolean DEFAULT true NOT NULL, CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)), CONSTRAINT check_b09644994b CHECK ((char_length(squash_commit_template) <= 500)), CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL)), diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 40da4a4f8e82b9..7ca3f55b5a2c7e 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -39,6 +39,7 @@ module ProjectsHelpers optional :emails_disabled, type: Boolean, desc: 'Disable email notifications' optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis' + optional :show_diff_preview_in_email, type: Boolean, desc: 'Include the code diff preview in merge request notification emails' optional :warn_about_potentially_unwanted_characters, type: Boolean, desc: 'Warn about Potentially Unwanted Characters' optional :enforce_auth_checks_on_uploads, type: Boolean, desc: 'Enforce auth check on uploads' optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' @@ -159,6 +160,7 @@ def self.update_params_at_least_one_of :request_access_enabled, :resolve_outdated_diff_discussions, :restrict_user_defined_variables, + :show_diff_preview_in_email, :security_and_compliance_access_level, :squash_option, :shared_runners_enabled, diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb index 25234db5734e0c..9fce65c9a5fb87 100644 --- a/spec/models/namespace_setting_spec.rb +++ b/spec/models/namespace_setting_spec.rb @@ -127,4 +127,50 @@ end end end + + describe '#show_diff_preview_in_email?' do + context 'when not a subgroup' do + it 'returns false' do + settings = create(:namespace_settings, show_diff_preview_in_email: false) + group = create(:group, namespace_settings: settings ) + + expect(group.show_diff_preview_in_email?).to be_falsey + end + + it 'returns true' do + settings = create(:namespace_settings, show_diff_preview_in_email: true) + group = create(:group, namespace_settings: settings ) + + expect(group.show_diff_preview_in_email?).to be_truthy + end + + it 'does not query the db when there is no parent group' do + group = create(:group) + + expect { group.show_diff_preview_in_email? }.not_to exceed_query_limit(0) + end + end + + context 'when a group has parent groups' do + let(:grandparent) { create(:group, namespace_settings: settings) } + let(:parent) { create(:group, parent: grandparent) } + let!(:group) { create(:group, parent: parent) } + + context "when a parent group has disabled diff previews" do + let(:settings) { create(:namespace_settings, show_diff_preview_in_email: false) } + + it 'returns false' do + expect(group.show_diff_preview_in_email?).to be_falsey + end + end + + context 'when all parent groups have enabled diff previews' do + let(:settings) { create(:namespace_settings, show_diff_preview_in_email: true) } + + it 'returns true' do + expect(group.show_diff_preview_in_email?).to be_truthy + end + end + end + end end diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb index fb1601a5f9c1c7..a09ae7ec7aed6e 100644 --- a/spec/models/project_setting_spec.rb +++ b/spec/models/project_setting_spec.rb @@ -63,4 +63,51 @@ def valid_target_platform_combinations target_platforms.permutation(n).to_a end end + + describe '#show_diff_preview_in_email?' do + context 'when a project is a top-level namespace' do + let(:project_settings ) { create(:project_setting, show_diff_preview_in_email: false) } + let(:project) { create(:project, project_setting: project_settings) } + + context 'when show_diff_preview_in_email is disabled' do + it 'returns false' do + expect(project).not_to be_show_diff_preview_in_email + end + end + + context 'when show_diff_preview_in_email is enabled' do + let(:project_settings ) { create(:project_setting, show_diff_preview_in_email: true) } + + it 'returns true' do + settings = create(:project_setting, show_diff_preview_in_email: true) + project = create(:project, project_setting: settings) + + expect(project).to be_show_diff_preview_in_email + end + end + end + + context 'when a parent group has a parent group' do + let(:namespace_settings) { create(:namespace_settings, show_diff_preview_in_email: false) } + let(:project_settings) { create(:project_setting, show_diff_preview_in_email: true) } + let(:group) { create(:group, namespace_settings: namespace_settings) } + let!(:project) { create(:project, namespace_id: group.id, project_setting: project_settings) } + + context 'when show_diff_preview_in_email is disabled for the parent group' do + it 'returns false' do + expect(project).not_to be_show_diff_preview_in_email + end + end + + context 'when all ancestors have enabled diff previews' do + let(:namespace_settings) { create(:namespace_settings, show_diff_preview_in_email: true) } + + it 'returns true' do + group.update_attribute(:show_diff_preview_in_email, true) + + expect(project).to be_show_diff_preview_in_email + end + end + end + end end diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index 670035187cb96a..1335fa02aaf49f 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -154,11 +154,13 @@ project_setting: - project_id - push_rule_id - show_default_award_emojis + - show_diff_preview_in_email - updated_at - cve_id_request_enabled - mr_default_target_self - target_platforms - selective_code_owner_removals + - show_diff_preview_in_email build_service_desk_setting: # service_desk_setting unexposed_attributes: -- GitLab From e863572c48b68156b7e810a83f7f0d10eca4cdd7 Mon Sep 17 00:00:00 2001 From: Marcel van Remmerden <mvanremmerden@gitlab.com> Date: Tue, 6 Sep 2022 18:02:13 +0000 Subject: [PATCH 138/169] Remove file edit actions from blame view Changelog: changed --- app/views/projects/blob/_header.html.haml | 11 ++++++----- spec/features/projects/blobs/blame_spec.rb | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml index 8260aa0fb7e195..e141a0064155bd 100644 --- a/app/views/projects/blob/_header.html.haml +++ b/app/views/projects/blob/_header.html.haml @@ -6,11 +6,12 @@ .file-actions.gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-md-justify-content-end< = render 'projects/blob/viewer_switcher', blob: blob unless blame = render 'shared/web_ide_button', blob: blob - .btn-group{ role: "group", class: ("gl-ml-3" if current_user) }> - = render_if_exists 'projects/blob/header_file_locks_link' - - if current_user - = replace_blob_link(@project, @ref, @path, blob: blob) - = delete_blob_link(@project, @ref, @path, blob: blob) + - unless blame + .btn-group{ role: "group", class: ("gl-ml-3" if current_user) }> + = render_if_exists 'projects/blob/header_file_locks_link' + - if current_user + = replace_blob_link(@project, @ref, @path, blob: blob) + = delete_blob_link(@project, @ref, @path, blob: blob) .btn-group.gl-ml-3{ role: "group" } = copy_blob_source_button(blob) unless blame = open_raw_blob_button(blob) diff --git a/spec/features/projects/blobs/blame_spec.rb b/spec/features/projects/blobs/blame_spec.rb index 3b2b74b469ee08..3377fcdee48f32 100644 --- a/spec/features/projects/blobs/blame_spec.rb +++ b/spec/features/projects/blobs/blame_spec.rb @@ -14,6 +14,24 @@ def visit_blob_blame(path) wait_for_all_requests end + context 'as a developer' do + let(:user) { create(:user) } + let(:role) { :developer } + + before do + project.add_role(user, role) + sign_in(user) + end + + it 'does not display lock, replace and delete buttons' do + visit_blob_blame(path) + + expect(page).not_to have_button("Lock") + expect(page).not_to have_button("Replace") + expect(page).not_to have_button("Delete") + end + end + it 'displays the blame page without pagination' do visit_blob_blame(path) -- GitLab From 369585475e26223dbef213195bfe794ed40eb51b Mon Sep 17 00:00:00 2001 From: Fernando Arias <farias@gitlab.com> Date: Sun, 4 Sep 2022 12:23:34 +0200 Subject: [PATCH 139/169] First pass legacy license compliance widget removal * Remove frontend flags * Update unit and browser automation tests Changelog: changed EE: true --- .../mr_widget_options.vue | 20 +---- .../ee/projects/merge_requests_controller.rb | 1 - .../projects/merge_requests/show.html.haml | 2 +- .../refactor_license_compliance_extension.yml | 8 -- .../user_sees_status_checks_widget_spec.rb | 1 - .../ee_mr_widget_options_spec.js | 85 ++++++++++++------- qa/qa/ee/page/component/license_management.rb | 26 +----- qa/qa/ee/page/merge_request/show.rb | 11 +-- 8 files changed, 61 insertions(+), 93 deletions(-) delete mode 100644 ee/config/feature_flags/development/refactor_license_compliance_extension.yml diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/ee/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 5b086fe7670744..acb30e78bd0ee0 100644 --- a/ee/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/ee/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -1,6 +1,5 @@ <script> import { GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui'; -import MrWidgetLicenses from 'ee/vue_shared/license_compliance/mr_widget_license_report.vue'; import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin'; import { registerExtension } from '~/vue_merge_request_widget/components/extensions'; import { s__, __, sprintf } from '~/locale'; @@ -20,7 +19,6 @@ export default { components: { GlSprintf, GlLink, - MrWidgetLicenses, WidgetContainer, MrWidgetGeoSecondaryNode, MrWidgetPolicyViolation, @@ -93,9 +91,6 @@ export default { !this.shouldShowExtension ); }, - shouldShowLicenseComplianceExtension() { - return window.gon?.features?.refactorLicenseComplianceExtension; - }, hasLoadPerformanceMetrics() { return ( this.mr.loadPerformanceMetrics?.degraded?.length > 0 || @@ -224,7 +219,7 @@ export default { }, methods: { registerLicenseCompliance() { - if (this.shouldShowLicenseComplianceExtension) { + if (this.shouldShowExtension) { registerExtension(licenseComplianceExtension); } }, @@ -486,19 +481,6 @@ export default { </gl-sprintf> </mr-widget-enable-feature-prompt> - <mr-widget-licenses - v-if="shouldRenderLicenseReport && !shouldShowLicenseComplianceExtension" - :api-url="mr.licenseScanning.managed_licenses_path" - :approvals-api-path="mr.apiApprovalsPath" - :licenses-api-path="licensesApiPath" - :pipeline-path="mr.pipeline.path" - :can-manage-licenses="mr.licenseScanning.can_manage_licenses" - :full-report-path="mr.licenseScanning.full_report_path" - :license-management-settings-path="mr.licenseScanning.settings_path" - :license-compliance-docs-path="mr.licenseComplianceDocsPath" - report-section-class="mr-widget-border-top" - /> - <grouped-test-reports-app v-if="shouldRenderTestReport && !shouldRenderRefactoredTestReport" class="js-reports-container" diff --git a/ee/app/controllers/ee/projects/merge_requests_controller.rb b/ee/app/controllers/ee/projects/merge_requests_controller.rb index 233cdcbfde237a..b3364b258a8e87 100644 --- a/ee/app/controllers/ee/projects/merge_requests_controller.rb +++ b/ee/app/controllers/ee/projects/merge_requests_controller.rb @@ -19,7 +19,6 @@ module MergeRequestsController push_frontend_feature_flag(:refactor_mr_widgets_extensions, @project) push_frontend_feature_flag(:refactor_mr_widget_test_summary, @project) push_frontend_feature_flag(:refactor_mr_widgets_extensions_user, current_user) - push_frontend_feature_flag(:refactor_license_compliance_extension, @project) push_frontend_feature_flag(:suggested_reviewers, @project) end diff --git a/ee/app/views/projects/merge_requests/show.html.haml b/ee/app/views/projects/merge_requests/show.html.haml index 49f09ac9102025..446ba2b1f925c0 100644 --- a/ee/app/views/projects/merge_requests/show.html.haml +++ b/ee/app/views/projects/merge_requests/show.html.haml @@ -15,7 +15,7 @@ window.gl.mrWidgetData.coverage_fuzzing_help_path = '#{help_page_path("user/application_security/coverage_fuzzing/index")}'; window.gl.mrWidgetData.visual_review_app_available = '#{@project.feature_available?(:visual_review_app)}' === 'true'; window.gl.mrWidgetData.license_scanning_comparison_path = '#{license_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}' - window.gl.mrWidgetData.license_scanning_comparison_collapsed_path = '#{license_scanning_reports_collapsed_project_merge_request_path(@project, @merge_request) if Feature.enabled?(:refactor_license_compliance_extension, @project) && @project.feature_available?(:license_scanning)}' + window.gl.mrWidgetData.license_scanning_comparison_collapsed_path = '#{license_scanning_reports_collapsed_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}' window.gl.mrWidgetData.container_scanning_comparison_path = '#{container_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:container_scanning)}' window.gl.mrWidgetData.dependency_scanning_comparison_path = '#{dependency_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:dependency_scanning)}' window.gl.mrWidgetData.sast_comparison_path = '#{sast_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:sast)}' diff --git a/ee/config/feature_flags/development/refactor_license_compliance_extension.yml b/ee/config/feature_flags/development/refactor_license_compliance_extension.yml deleted file mode 100644 index b6d3508c1fb078..00000000000000 --- a/ee/config/feature_flags/development/refactor_license_compliance_extension.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: refactor_license_compliance_extension -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84128 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367703 -milestone: '15.0' -type: development -group: group::composition analysis -default_enabled: true diff --git a/ee/spec/features/merge_request/user_sees_status_checks_widget_spec.rb b/ee/spec/features/merge_request/user_sees_status_checks_widget_spec.rb index 58116535ca5b25..c6fb2979381633 100644 --- a/ee/spec/features/merge_request/user_sees_status_checks_widget_spec.rb +++ b/ee/spec/features/merge_request/user_sees_status_checks_widget_spec.rb @@ -26,7 +26,6 @@ stub_feature_flags(refactor_mr_widgets_extensions: false) stub_feature_flags(refactor_mr_widgets_extensions_user: false) stub_feature_flags(refactor_security_extension: false) - stub_feature_flags(refactor_license_compliance_extension: false) end context 'user is authorized' do diff --git a/ee/spec/frontend/vue_merge_request_widget/ee_mr_widget_options_spec.js b/ee/spec/frontend/vue_merge_request_widget/ee_mr_widget_options_spec.js index 44dbf308cd8579..5681bc7c550e68 100644 --- a/ee/spec/frontend/vue_merge_request_widget/ee_mr_widget_options_spec.js +++ b/ee/spec/frontend/vue_merge_request_widget/ee_mr_widget_options_spec.js @@ -3,6 +3,11 @@ import MockAdapter from 'axios-mock-adapter'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; +import { + registerExtension, + registeredExtensions, +} from '~/vue_merge_request_widget/components/extensions'; + // Force Jest to transpile and cache // eslint-disable-next-line no-unused-vars import _GroupedBrowserPerformanceReportsApp from 'ee/reports/browser_performance_report/grouped_browser_performance_reports_app.vue'; @@ -15,6 +20,10 @@ import MrWidgetOptions from 'ee/vue_merge_request_widget/mr_widget_options.vue'; // Force Jest to transpile and cache // eslint-disable-next-line no-unused-vars import _GroupedSecurityReportsApp from 'ee/vue_shared/security_reports/grouped_security_reports_app.vue'; + +// EE Widget Extensions +import licenseComplianceExtension from 'ee/vue_merge_request_widget/extensions/license_compliance'; + import { sastDiffSuccessMock, dastDiffSuccessMock, @@ -935,38 +944,6 @@ describe('ee merge request widget options', () => { }); }); - describe('license scanning report', () => { - const licenseManagementApiUrl = `${TEST_HOST}/manage_license_api`; - - it('should be rendered if license scanning data is set', () => { - gl.mrWidgetData = { - ...mockData, - enabled_reports: { - license_scanning: true, - }, - license_scanning: { - managed_licenses_path: licenseManagementApiUrl, - can_manage_licenses: false, - }, - }; - - createComponent({ propsData: { mrData: gl.mrWidgetData } }); - - expect(wrapper.find('.license-report-widget').exists()).toBe(true); - }); - - it('should not be rendered if license scanning data is not set', () => { - gl.mrWidgetData = { - ...mockData, - license_scanning: {}, - }; - - createComponent({ propsData: { mrData: gl.mrWidgetData } }); - - expect(wrapper.find('.license-report-widget').exists()).toBe(false); - }); - }); - describe('CE security report', () => { describe.each` context | canReadVulnerabilities | hasPipeline | shouldRender @@ -1187,10 +1164,54 @@ describe('ee merge request widget options', () => { }, }, }); + wrapper.vm.mr.state = mergeState; await nextTick(); expect(findStatusChecksReport().exists()).toBe(shouldRender); }); }); + + describe('license scanning report', () => { + afterEach(() => { + registeredExtensions.extensions = []; + }); + + it('should be rendered if license widget is registered', async () => { + const licenseComparisonPath = + '/group-name/project-name/-/merge_requests/78/license_scanning_reports'; + const licenseComparisonPathCollapsed = + '/group-name/project-name/-/merge_requests/78/license_scanning_reports_collapsed'; + const fullReportPath = '/group-name/project-name/-/merge_requests/78/full_report'; + const settingsPath = '/group-name/project-name/-/licenses#licenses'; + const apiApprovalsPath = '/group-name/project-name/-/licenses#policies'; + + gl.mrWidgetData = { + ...mockData, + license_scanning_comparison_path: licenseComparisonPath, + license_scanning_comparison_collapsed_path: licenseComparisonPathCollapsed, + api_approvals_path: apiApprovalsPath, + license_scanning: { + settings_path: settingsPath, + full_report_path: fullReportPath, + }, + }; + + registerExtension(licenseComplianceExtension); + + await createComponent({ propsData: { mrData: gl.mrWidgetData } }); + + expect(wrapper.findComponent({ name: 'WidgetLicenseCompliance' }).exists()).toBe(true); + }); + + it('should not be rendered if license widget is not registered', () => { + gl.mrWidgetData = { + ...mockData, + }; + + createComponent({ propsData: { mrData: gl.mrWidgetData } }); + + expect(wrapper.findComponent({ name: 'WidgetLicenseCompliance' }).exists()).toBe(false); + }); + }); }); diff --git a/qa/qa/ee/page/component/license_management.rb b/qa/qa/ee/page/component/license_management.rb index 858fbd27912b64..e4c424daeb9953 100644 --- a/qa/qa/ee/page/component/license_management.rb +++ b/qa/qa/ee/page/component/license_management.rb @@ -19,11 +19,6 @@ def self.prepended(base) element :icon_status, ':data-qa-selector="`status_${status}_icon`" ' # rubocop:disable QA/ElementWithPattern end - view 'ee/app/assets/javascripts/vue_shared/license_compliance/mr_widget_license_report.vue' do - element :license_report_widget - element :manage_licenses_button - end - view 'app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue' do element :mr_widget_extension end @@ -39,19 +34,13 @@ def self.prepended(base) end def has_approved_license?(name) - content_element = feature_flag_controlled_element(:refactor_license_compliance_extension, - :child_content, - :report_item_row) - within_element(content_element, text: name) do + within_element(:child_content, text: name) do has_element?(:status_success_icon, wait: 1) end end def has_denied_license?(name) - content_element = feature_flag_controlled_element(:refactor_license_compliance_extension, - :child_content, - :report_item_row) - within_element(content_element, text: name) do + within_element(:child_content, text: name) do has_element?(:status_failed_icon, wait: 1) end end @@ -59,15 +48,8 @@ def has_denied_license?(name) def click_manage_licenses_button previous_page = page.current_url - widget_element = feature_flag_controlled_element(:refactor_license_compliance_extension, - :mr_widget_extension, - :license_report_widget) - within_element(widget_element) do - if widget_element == :mr_widget_extension - click_element(:mr_widget_extension_actions_button, text: 'Manage Licenses') - else - click_element(:manage_licenses_button) - end + within_element(:mr_widget_extension) do + click_element(:mr_widget_extension_actions_button, text: 'Manage Licenses') end # TODO workaround for switched to a new window UI wait_until(max_duration: 15, reload: false) do diff --git a/qa/qa/ee/page/merge_request/show.rb b/qa/qa/ee/page/merge_request/show.rb index a21bf03e822865..9bcb0ab4a1ce4d 100644 --- a/qa/qa/ee/page/merge_request/show.rb +++ b/qa/qa/ee/page/merge_request/show.rb @@ -102,15 +102,8 @@ def click_approve end def expand_license_report - widget_name = feature_flag_controlled_element(:refactor_license_compliance_extension, - :mr_widget_extension, - :license_report_widget) - within_element(widget_name) do - if widget_name == :mr_widget_extension - click_element(:toggle_button) - else - click_element(:expand_report_button) - end + within_element(:mr_widget_extension) do + click_element(:toggle_button) end end -- GitLab From 1a8792a723782e4f4ee580d7eebec8a2861b3f76 Mon Sep 17 00:00:00 2001 From: Fernando Arias <farias@gitlab.com> Date: Mon, 5 Sep 2022 18:55:36 +0200 Subject: [PATCH 140/169] Apply maintainer feedback * Apply patch for unit test refactor --- .../ee_mr_widget_options_spec.js | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/ee/spec/frontend/vue_merge_request_widget/ee_mr_widget_options_spec.js b/ee/spec/frontend/vue_merge_request_widget/ee_mr_widget_options_spec.js index 5681bc7c550e68..838ff62ae11a34 100644 --- a/ee/spec/frontend/vue_merge_request_widget/ee_mr_widget_options_spec.js +++ b/ee/spec/frontend/vue_merge_request_widget/ee_mr_widget_options_spec.js @@ -1177,41 +1177,42 @@ describe('ee merge request widget options', () => { registeredExtensions.extensions = []; }); - it('should be rendered if license widget is registered', async () => { - const licenseComparisonPath = - '/group-name/project-name/-/merge_requests/78/license_scanning_reports'; - const licenseComparisonPathCollapsed = - '/group-name/project-name/-/merge_requests/78/license_scanning_reports_collapsed'; - const fullReportPath = '/group-name/project-name/-/merge_requests/78/full_report'; - const settingsPath = '/group-name/project-name/-/licenses#licenses'; - const apiApprovalsPath = '/group-name/project-name/-/licenses#policies'; + it.each` + shouldRegisterExtension | description + ${true} | ${'extension is registered'} + ${false} | ${'extension is not registered'} + `( + 'should render license widget is "$shouldRegisterExtension" when $description', + ({ shouldRegisterExtension }) => { + const licenseComparisonPath = + '/group-name/project-name/-/merge_requests/78/license_scanning_reports'; + const licenseComparisonPathCollapsed = + '/group-name/project-name/-/merge_requests/78/license_scanning_reports_collapsed'; + const fullReportPath = '/group-name/project-name/-/merge_requests/78/full_report'; + const settingsPath = '/group-name/project-name/-/licenses#licenses'; + const apiApprovalsPath = '/group-name/project-name/-/licenses#policies'; - gl.mrWidgetData = { - ...mockData, - license_scanning_comparison_path: licenseComparisonPath, - license_scanning_comparison_collapsed_path: licenseComparisonPathCollapsed, - api_approvals_path: apiApprovalsPath, - license_scanning: { - settings_path: settingsPath, - full_report_path: fullReportPath, - }, - }; - - registerExtension(licenseComplianceExtension); - - await createComponent({ propsData: { mrData: gl.mrWidgetData } }); - - expect(wrapper.findComponent({ name: 'WidgetLicenseCompliance' }).exists()).toBe(true); - }); + gl.mrWidgetData = { + ...mockData, + license_scanning_comparison_path: licenseComparisonPath, + license_scanning_comparison_collapsed_path: licenseComparisonPathCollapsed, + api_approvals_path: apiApprovalsPath, + license_scanning: { + settings_path: settingsPath, + full_report_path: fullReportPath, + }, + }; - it('should not be rendered if license widget is not registered', () => { - gl.mrWidgetData = { - ...mockData, - }; + if (shouldRegisterExtension) { + registerExtension(licenseComplianceExtension); + } - createComponent({ propsData: { mrData: gl.mrWidgetData } }); + createComponent({ propsData: { mrData: gl.mrWidgetData } }); - expect(wrapper.findComponent({ name: 'WidgetLicenseCompliance' }).exists()).toBe(false); - }); + expect(wrapper.findComponent({ name: 'WidgetLicenseCompliance' }).exists()).toBe( + shouldRegisterExtension, + ); + }, + ); }); }); -- GitLab From 5562883393ce20200f46777afb68e5f6d721c304 Mon Sep 17 00:00:00 2001 From: Lee Tickett <lee@tickett.net> Date: Tue, 6 Sep 2022 19:37:40 +0000 Subject: [PATCH 141/169] Add ArtifactDestroy GraphQL mutation Changelog: added --- .../mutations/ci/job_artifact/destroy.rb | 39 +++++++++++++++ app/graphql/types/mutation_type.rb | 1 + app/policies/ci/job_artifact_policy.rb | 7 +++ doc/api/graphql/reference/index.md | 19 ++++++++ .../mutations/ci/job_artifact/destroy_spec.rb | 47 +++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 app/graphql/mutations/ci/job_artifact/destroy.rb create mode 100644 app/policies/ci/job_artifact_policy.rb create mode 100644 spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb diff --git a/app/graphql/mutations/ci/job_artifact/destroy.rb b/app/graphql/mutations/ci/job_artifact/destroy.rb new file mode 100644 index 00000000000000..47b3535d631e44 --- /dev/null +++ b/app/graphql/mutations/ci/job_artifact/destroy.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module JobArtifact + class Destroy < BaseMutation + graphql_name 'ArtifactDestroy' + + authorize :destroy_artifacts + + ArtifactID = ::Types::GlobalIDType[::Ci::JobArtifact] + + argument :id, + ArtifactID, + required: true, + description: 'ID of the artifact to delete.' + + field :artifact, + Types::Ci::JobArtifactType, + null: true, + description: 'Deleted artifact.' + + def find_object(id: ) + GlobalID::Locator.locate(id) + end + + def resolve(id:) + artifact = authorized_find!(id: id) + + if artifact.destroy + { errors: [] } + else + { errors: artifact.errors.full_messages } + end + end + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index e1806e5b19acc8..f221067bc9774b 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -124,6 +124,7 @@ class MutationType < BaseObject mount_mutation Mutations::Ci::Job::Retry mount_mutation Mutations::Ci::Job::Cancel mount_mutation Mutations::Ci::Job::Unschedule + mount_mutation Mutations::Ci::JobArtifact::Destroy mount_mutation Mutations::Ci::JobTokenScope::AddProject mount_mutation Mutations::Ci::JobTokenScope::RemoveProject mount_mutation Mutations::Ci::Runner::Update diff --git a/app/policies/ci/job_artifact_policy.rb b/app/policies/ci/job_artifact_policy.rb new file mode 100644 index 00000000000000..e25c7311565948 --- /dev/null +++ b/app/policies/ci/job_artifact_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Ci + class JobArtifactPolicy < BasePolicy + delegate { @subject.job.project } + end +end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f4ba752a6a1c37..8fb1bdfa03969a 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -742,6 +742,25 @@ Input type: `ApiFuzzingCiConfigurationCreateInput` | <a id="mutationapifuzzingciconfigurationcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationapifuzzingciconfigurationcreategitlabciyamleditpath"></a>`gitlabCiYamlEditPath` **{warning-solid}** | [`String`](#string) | **Deprecated:** The configuration snippet is now generated client-side. Deprecated in 14.6. | +### `Mutation.artifactDestroy` + +Input type: `ArtifactDestroyInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationartifactdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationartifactdestroyid"></a>`id` | [`CiJobArtifactID!`](#cijobartifactid) | ID of the artifact to delete. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationartifactdestroyartifact"></a>`artifact` | [`CiJobArtifact`](#cijobartifact) | Deleted artifact. | +| <a id="mutationartifactdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationartifactdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + ### `Mutation.auditEventsStreamingHeadersCreate` Input type: `AuditEventsStreamingHeadersCreateInput` diff --git a/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb new file mode 100644 index 00000000000000..a5ec9ea343dbe3 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/job_artifact/destroy_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'ArtifactDestroy' do + include GraphqlHelpers + + let(:user) { create(:user) } + let(:artifact) { create(:ci_job_artifact) } + + let(:mutation) do + variables = { + id: artifact.to_global_id.to_s + } + graphql_mutation(:artifact_destroy, variables, 'errors') + end + + it 'returns an error if the user is not allowed to destroy the artifact' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + end + + context 'when the user is allowed to destroy the artifact' do + before do + artifact.job.project.add_maintainer(user) + end + + it 'destroys the artifact' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns error if destory fails' do + allow_next_found_instance_of(Ci::JobArtifact) do |instance| + allow(instance).to receive(:destroy).and_return(false) + allow(instance).to receive_message_chain(:errors, :full_messages).and_return(['cannot be removed']) + end + + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_data_at(:artifact_destroy, :errors)).to contain_exactly('cannot be removed') + end + end +end -- GitLab From 7724815f100357c8cbeaa47d519a77691c7dadd6 Mon Sep 17 00:00:00 2001 From: Kyle Wiebers <kwiebers@gitlab.com> Date: Tue, 6 Sep 2022 15:55:48 -0500 Subject: [PATCH 142/169] Update graphql reference --- doc/api/graphql/reference/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 0e67d5d24ae9dc..dbf54319345ce9 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -21331,6 +21331,12 @@ A `CiBuildID` is a global ID. It is encoded as a string. An example `CiBuildID` is: `"gid://gitlab/Ci::Build/1"`. +### `CiJobArtifactID` + +A `CiJobArtifactID` is a global ID. It is encoded as a string. + +An example `CiJobArtifactID` is: `"gid://gitlab/Ci::JobArtifact/1"`. + ### `CiPipelineID` A `CiPipelineID` is a global ID. It is encoded as a string. -- GitLab From aa75958322598d0e2ea4d67395089f3f09b29d93 Mon Sep 17 00:00:00 2001 From: Sanad Liaquat <sliaquat@gitlab.com> Date: Tue, 6 Sep 2022 22:18:18 +0000 Subject: [PATCH 143/169] Quarantine user inherited access api spec while investigating --- .../features/api/1_manage/user_inherited_access_spec.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb b/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb index 444d86f63d3f98..9f0e26642133a3 100644 --- a/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb +++ b/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb @@ -71,7 +71,12 @@ module QA it( 'is allowed to commit to sub-group project via the API', :reliable, - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/363349' + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/363349', + quarantine: { + only: { subdomain: %i[staging staging-ref] }, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/370282' + } ) do expect do Resource::Repository::Commit.fabricate_via_api! do |commit| -- GitLab From 4a54027c9e0d0bf8a1e737fd8cb69658009b4fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20K=C3=A4ppler?= <mkaeppler@gitlab.com> Date: Tue, 6 Sep 2022 22:46:49 +0000 Subject: [PATCH 144/169] Document Ruby 3 RSpec Hash/kwargs matcher gotcha --- doc/development/ruby3_gotchas.md | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/doc/development/ruby3_gotchas.md b/doc/development/ruby3_gotchas.md index dbe6fa13eee65d..db328b0b1a50f2 100644 --- a/doc/development/ruby3_gotchas.md +++ b/doc/development/ruby3_gotchas.md @@ -163,3 +163,40 @@ For Ruby 3 compliance, this should be changed to one of the following invocation - `f(**{k: v})` - `f(k: v)` + +## RSpec `with` argument matcher fails for shorthand Hash syntax + +Because keyword arguments ("kwargs") are a first-class concept in Ruby 3, keyword arguments are not +converted into internal `Hash` instances anymore. This leads to RSpec method argument matchers failing +when the receiver takes a positional options hash instead of kwargs: + +```ruby +def m(options={}); end +``` + +```ruby +expect(subject).to receive(:m).with(a: 42) +``` + +In Ruby 3 this expectations fails with the following error: + +```plaintext + Failure/Error: + + #<subject> received :m with unexpected arguments + expected: ({:a=>42}) + got: ({:a=>42}) +``` + +This happens because RSpec uses a kwargs argument matcher here, but the method takes a hash. +It works in Ruby 2, because `a: 42` is converted to a hash first and RSpec will use a hash argument matcher. + +A workaround is to not use the shorthand syntax and pass an actual `Hash` instead whenever we know a method +to take an options hash: + +```ruby +# Note the braces around the key-value pair. +expect(subject).to receive(:m).with({ a: 42 }) +``` + +For more information, see [the official issue report for RSpec](https://github.com/rspec/rspec-mocks/issues/1460). -- GitLab From 7ddd656382e4b8df952ee86765e044e274bc9861 Mon Sep 17 00:00:00 2001 From: Lyn Landon <llandon@gitlab.com> Date: Tue, 6 Sep 2022 21:19:24 +0000 Subject: [PATCH 145/169] Added YT video for DSO overview --- doc/user/application_security/get-started-security.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/user/application_security/get-started-security.md b/doc/user/application_security/get-started-security.md index 9d98675c2df4c5..f66530314b6895 100644 --- a/doc/user/application_security/get-started-security.md +++ b/doc/user/application_security/get-started-security.md @@ -6,6 +6,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Get started with GitLab application security **(ULTIMATE)** +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +For an overview, see [Adopting GitLab application security](https://www.youtube.com/watch?v=5QlxkiKR04k). + The following steps will help you get the most from GitLab application security tools. These steps are a recommended order of operations. You can choose to implement capabilities in a different order or omit features that do not apply to your specific needs. 1. Enable [Secret Detection](secret_detection/index.md) and [Dependency Scanning](dependency_scanning/index.md) -- GitLab From 43ed973f67b284bb9a8db7ac56744489e1a05c59 Mon Sep 17 00:00:00 2001 From: Brie Carranza <bcarranza@gitlab.com> Date: Tue, 6 Sep 2022 23:17:17 +0000 Subject: [PATCH 146/169] Clarify unsupported GSSAPI mechanism troubleshooting --- doc/integration/kerberos.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/integration/kerberos.md b/doc/integration/kerberos.md index 257ba4e6708e80..da854582c1279e 100644 --- a/doc/integration/kerberos.md +++ b/doc/integration/kerberos.md @@ -368,6 +368,15 @@ GitLab supports, authentication fails with a message like this in the log: OmniauthKerberosSpnegoController: failed to process Negotiate/Kerberos authentication: gss_accept_sec_context did not return GSS_S_COMPLETE: An unsupported mechanism was requested Unknown error ``` +There are a number of potential causes and solutions for this error message. + +#### Kerberos integration not using a dedicated port + +GitLab CI/CD doesn’t work with a Kerberos-enabled GitLab instance unless the Kerberos integration +is configured to [use a dedicated port](kerberos.md#http-git-access-with-kerberos-token-passwordless-authentication). + +#### Lack of connectivity between client machine and Kerberos server + This is usually seen when the browser is unable to contact the Kerberos server directly. It falls back to an unsupported mechanism known as [`IAKERB`](https://k5wiki.kerberos.org/wiki/Projects/IAKERB), which tries to use @@ -377,6 +386,8 @@ If you're experiencing this error, ensure there is connectivity between the client machine and the Kerberos server - this is a prerequisite! Traffic may be blocked by a firewall, or the DNS records may be incorrect. +#### Mismatched forward and reverse DNS records for GitLab instance hostname + Another failure mode occurs when the forward and reverse DNS records for the GitLab server do not match. Often, Windows clients work in this case while Linux clients fail. They use reverse DNS while detecting the Kerberos @@ -389,6 +400,8 @@ match. So for instance, if you access GitLab as `gitlab.example.com`, resolving to IP address `1.2.3.4`, then `4.3.2.1.in-addr.arpa` must be a `PTR` record for `gitlab.example.com`. +#### Missing Kerberos libraries on browser or client machine + Finally, it's possible that the browser or client machine lack Kerberos support completely. Ensure that the Kerberos libraries are installed and that you can authenticate to other Kerberos services. -- GitLab From 5a6fbe7f5a3899eeaaf02a847b5e5d2b9ab84d68 Mon Sep 17 00:00:00 2001 From: Zamir Martins <zfilho@gitlab.com> Date: Wed, 7 Sep 2022 00:18:30 +0000 Subject: [PATCH 147/169] Allow the creation of scan result policies for group level including feature flag. Only up to MR creation. EE: true Changelog: added --- .../components/policy_editor/new_policy.vue | 18 ++++-- .../policy_action_builder.vue | 3 +- .../policy_rule_builder.vue | 34 +++++++++++- .../scan_result_policy_editor.vue | 4 +- .../groups/security/policies_controller.rb | 1 + .../group_level_scan_result_policies.yml | 8 +++ .../security/policies_controller_spec.rb | 7 +++ .../policy_editor/new_policy_spec.js | 34 ++++++++++++ .../lib/policy_rule_builder_spec.js | 55 ++++++++++++++++++- .../policy_action_builder_spec.js | 1 + .../scan_result_policy_editor_spec.js | 21 ++++++- 11 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 ee/config/feature_flags/development/group_level_scan_result_policies.yml diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/new_policy.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/new_policy.vue index 3ebf840afa7441..b5c227d64e0911 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/new_policy.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/new_policy.vue @@ -2,6 +2,7 @@ import { GlPath } from '@gitlab/ui'; import { s__ } from '~/locale'; import { getParameterByName, removeParams, visitUrl } from '~/lib/utils/url_utility'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { POLICY_TYPE_COMPONENT_OPTIONS } from '../constants'; import { NAMESPACE_TYPES } from '../../constants'; import PolicySelection from './policy_selection.vue'; @@ -13,20 +14,21 @@ export default { PolicyEditor, PolicySelection, }, + mixins: [glFeatureFlagMixin()], inject: { namespaceType: { default: '' }, existingPolicy: { default: null }, }, data() { return { - selectedPolicy: - this.namespaceType === NAMESPACE_TYPES.GROUP - ? POLICY_TYPE_COMPONENT_OPTIONS.scanExecution - : this.policyFromUrl(), + selectedPolicy: this.initialPolicy(), }; }, computed: { enableWizard() { + if (this.glFeatures.groupLevelScanResultPolicies) { + return !this.existingPolicy; + } return this.namespaceType === NAMESPACE_TYPES.PROJECT && !this.existingPolicy; }, glPathItems() { @@ -69,6 +71,14 @@ export default { ({ urlParameter }) => urlParameter === policyType, ); }, + initialPolicy() { + if (this.glFeatures.groupLevelScanResultPolicies) { + return this.policyFromUrl(); + } + return this.namespaceType === NAMESPACE_TYPES.GROUP + ? POLICY_TYPE_COMPONENT_OPTIONS.scanExecution + : this.policyFromUrl(); + }, }, i18n: { titles: { diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/policy_action_builder.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/policy_action_builder.vue index 370c1aae24bcc0..16f991349590a9 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/policy_action_builder.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/policy_action_builder.vue @@ -17,7 +17,7 @@ export default { directives: { GlModalDirective, }, - inject: ['namespaceId'], + inject: ['namespaceId', 'namespaceType'], props: { initAction: { type: Object, @@ -116,6 +116,7 @@ export default { :skip-user-ids="userIds" :skip-group-ids="groupIds" :namespace-id="namespaceId" + :namespace-type="namespaceType" /> </div> <div class="gl-bg-white gl-w-full gl-mt-3 gl-border gl-rounded-base gl-overflow-auto h-12em"> diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/policy_rule_builder.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/policy_rule_builder.vue index 78da1bae8ae00c..f2932255deacd3 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/policy_rule_builder.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/policy_rule_builder.vue @@ -1,9 +1,11 @@ <script> import { GlSprintf, GlForm, GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui'; import { s__ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { REPORT_TYPES_DEFAULT, SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants'; import ProtectedBranchesSelector from 'ee/vue_shared/components/branches_selector/protected_branches_selector.vue'; import PolicyRuleMultiSelect from 'ee/security_orchestration/components/policy_rule_multi_select.vue'; +import { NAMESPACE_TYPES } from 'ee/security_orchestration/constants'; import { ALL_BRANCHES } from 'ee/vue_shared/components/branches_selector/constants'; import { APPROVAL_VULNERABILITY_STATES } from './lib'; @@ -20,7 +22,8 @@ export default { GlFormGroup, PolicyRuleMultiSelect, }, - inject: ['namespaceId'], + mixins: [glFeatureFlagMixin()], + inject: ['namespaceId', 'namespaceType'], props: { initRule: { type: Object, @@ -33,6 +36,18 @@ export default { }; }, computed: { + enteredBranch: { + get() { + return this.initRule.branches.length === 0 ? '*' : this.initRule.branches.join(); + }, + set(value) { + const branches = value + .split(',') + .map((branch) => branch.trim()) + .filter((branch) => branch !== '*'); + this.triggerChanged({ branches }); + }, + }, branchesToAdd: { get() { return this.initRule.branches; @@ -76,6 +91,15 @@ export default { this.triggerChanged({ vulnerabilities_allowed: parseInt(value, 10) }); }, }, + displayBranchSelector() { + return ( + !this.glFeatures.groupLevelScanResultPolicies || + NAMESPACE_TYPES.PROJECT === this.namespaceType + ); + }, + isgroupLevelBranchesValid() { + return this.enteredBranch.length > 0; + }, }, methods: { triggerChanged(value) { @@ -114,10 +138,18 @@ export default { <template #branches> <gl-form-group class="gl-ml-3 gl-mr-3 gl-mb-3!" data-testid="branches-group"> <protected-branches-selector + v-if="displayBranchSelector" v-model="branchesToAdd" :project-id="namespaceId" :selected-branches-names="branchesToAdd" /> + <gl-form-input + v-else + v-model="enteredBranch" + :state="isgroupLevelBranchesValid" + type="text" + data-testid="group-level-branch" + /> </gl-form-group> </template> diff --git a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/scan_result_policy_editor.vue b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/scan_result_policy_editor.vue index 3e6869969afeb3..42a34dcf842a1f 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/scan_result_policy_editor.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result_policy/scan_result_policy_editor.vue @@ -3,6 +3,7 @@ import { GlEmptyState, GlButton } from '@gitlab/ui'; import { mapActions, mapState } from 'vuex'; import { joinPaths, visitUrl, setUrlFragment } from '~/lib/utils/url_utility'; import { __, s__ } from '~/locale'; +import { NAMESPACE_TYPES } from 'ee/security_orchestration/constants'; import { EDITOR_MODE_YAML, EDITOR_MODE_RULE, @@ -59,6 +60,7 @@ export default { 'namespacePath', 'scanPolicyDocumentationPath', 'scanResultPolicyApprovers', + 'namespaceType', ], props: { assignedPolicyProject: { @@ -219,7 +221,7 @@ export default { } else if (mode === EDITOR_MODE_RULE && !this.hasParsingError) { if (this.invalidForRuleMode()) { this.yamlEditorError = new Error(); - } else { + } else if (this.namespaceType === NAMESPACE_TYPES.PROJECT) { this.fetchBranches({ branches: this.allBranches(), projectId: this.namespaceId }); } } diff --git a/ee/app/controllers/groups/security/policies_controller.rb b/ee/app/controllers/groups/security/policies_controller.rb index f43876be0e31df..8811a58dbe1893 100644 --- a/ee/app/controllers/groups/security/policies_controller.rb +++ b/ee/app/controllers/groups/security/policies_controller.rb @@ -8,6 +8,7 @@ class PoliciesController < Groups::ApplicationController before_action do push_frontend_feature_flag(:group_level_security_policies, group) + push_frontend_feature_flag(:group_level_scan_result_policies, group) end feature_category :security_orchestration diff --git a/ee/config/feature_flags/development/group_level_scan_result_policies.yml b/ee/config/feature_flags/development/group_level_scan_result_policies.yml new file mode 100644 index 00000000000000..6255e4b324de20 --- /dev/null +++ b/ee/config/feature_flags/development/group_level_scan_result_policies.yml @@ -0,0 +1,8 @@ +--- +name: group_level_scan_result_policies +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96563 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/369473 +milestone: '15.4' +type: development +group: group::security policies +default_enabled: false diff --git a/ee/spec/controllers/groups/security/policies_controller_spec.rb b/ee/spec/controllers/groups/security/policies_controller_spec.rb index f175c76027a793..f987024b822b4d 100644 --- a/ee/spec/controllers/groups/security/policies_controller_spec.rb +++ b/ee/spec/controllers/groups/security/policies_controller_spec.rb @@ -78,6 +78,13 @@ expect(app.attributes['data-namespace-id'].value).to eq(group.id.to_s) end + it 'propagates group_level_scan_result_policies feature flag' do + stub_feature_flags(group_level_scan_result_policies: true) + get edit + + expect(response.body).to have_pushed_frontend_feature_flags(groupLevelScanResultPolicies: true) + end + context 'when type is missing' do let(:policy_type) { nil } diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/new_policy_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/new_policy_spec.js index e9320468bc5f6f..424509c0b1d7af 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/new_policy_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/new_policy_spec.js @@ -75,6 +75,40 @@ describe('NewPolicy component', () => { expect(findPolicySelection().exists()).toBe(false); expect(findPolicyEditor().exists()).toBe(true); }); + + describe('with groupLevelScanResultPolicies enabled', () => { + beforeEach(() => { + factory({ + provide: { + namespaceType: NAMESPACE_TYPES.GROUP, + glFeatures: { groupLevelScanResultPolicies: true }, + }, + }); + }); + + it('should display the title correctly', () => { + expect(wrapper.findByText(NewPolicy.i18n.titles.default).exists()).toBe(true); + }); + + it('should display the path items correctly', () => { + expect(findPath().props('items')).toMatchObject([ + { + selected: true, + title: NewPolicy.i18n.choosePolicyType, + }, + { + disabled: true, + selected: false, + title: NewPolicy.i18n.policyDetails, + }, + ]); + }); + + it('should display the correct view', () => { + expect(findPolicySelection().exists()).toBe(true); + expect(findPolicyEditor().exists()).toBe(false); + }); + }); }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/lib/policy_rule_builder_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/lib/policy_rule_builder_spec.js index f3e92b77dcd5c2..85c15a3e3de3a7 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/lib/policy_rule_builder_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/lib/policy_rule_builder_spec.js @@ -5,6 +5,7 @@ import Api from 'ee/api'; import PolicyRuleBuilder from 'ee/security_orchestration/components/policy_editor/scan_result_policy/policy_rule_builder.vue'; import ProtectedBranchesSelector from 'ee/vue_shared/components/branches_selector/protected_branches_selector.vue'; import PolicyRuleMultiSelect from 'ee/security_orchestration/components/policy_rule_multi_select.vue'; +import { NAMESPACE_TYPES } from 'ee/security_orchestration/constants'; describe('PolicyRuleBuilder', () => { let wrapper; @@ -29,7 +30,7 @@ describe('PolicyRuleBuilder', () => { vulnerability_states: ['newly_detected'], }; - const factory = (propsData = {}) => { + const factory = (propsData = {}, provide = {}) => { wrapper = mount(PolicyRuleBuilder, { propsData: { initRule: DEFAULT_RULE, @@ -37,11 +38,14 @@ describe('PolicyRuleBuilder', () => { }, provide: { namespaceId: '1', + namespaceType: NAMESPACE_TYPES.PROJECT, + ...provide, }, }); }; const findBranches = () => wrapper.findComponent(ProtectedBranchesSelector); + const findGroupLevelBranches = () => wrapper.find('[data-testid="group-level-branch"]'); const findScanners = () => wrapper.find('[data-testid="scanners-select"]'); const findSeverities = () => wrapper.find('[data-testid="severities-select"]'); const findVulnStates = () => wrapper.find('[data-testid="vulnerability-states-select"]'); @@ -65,6 +69,7 @@ describe('PolicyRuleBuilder', () => { await nextTick(); expect(findBranches().exists()).toBe(true); + expect(findGroupLevelBranches().exists()).toBe(false); expect(findScanners().exists()).toBe(true); expect(findSeverities().exists()).toBe(true); expect(findVulnStates().exists()).toBe(true); @@ -118,4 +123,52 @@ describe('PolicyRuleBuilder', () => { }, ); }); + + describe('when namespaceType is other than project', () => { + it('does not display group level branches', () => { + factory({}, { namespaceType: NAMESPACE_TYPES.GROUP }); + + expect(findBranches().exists()).toBe(true); + expect(findGroupLevelBranches().exists()).toBe(false); + }); + + describe('when groupLevelScanResultPolicies feature flag is enabled', () => { + beforeEach(() => { + factory( + {}, + { + namespaceType: NAMESPACE_TYPES.GROUP, + glFeatures: { groupLevelScanResultPolicies: true }, + }, + ); + }); + + it('displays group level branches', () => { + expect(findBranches().exists()).toBe(false); + expect(findGroupLevelBranches().exists()).toBe(true); + }); + + it('triggers a changed event with the updated rule', async () => { + const INPUT_BRANCHES = 'main, test'; + const EXPECTED_BRANCHES = ['main', 'test']; + await findGroupLevelBranches().vm.$emit('input', INPUT_BRANCHES); + + expect(wrapper.emitted().changed).toEqual([ + [expect.objectContaining({ branches: EXPECTED_BRANCHES })], + ]); + }); + + it('group level branches is invalid when empty', () => { + factory( + { initRule: { ...DEFAULT_RULE, branches: [''] } }, + { + namespaceType: NAMESPACE_TYPES.GROUP, + glFeatures: { groupLevelScanResultPolicies: true }, + }, + ); + + expect(findGroupLevelBranches().classes('is-invalid')).toBe(true); + }); + }); + }); }); diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/policy_action_builder_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/policy_action_builder_spec.js index 3591d27ea17410..b22ce3090aa2f0 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/policy_action_builder_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/policy_action_builder_spec.js @@ -57,6 +57,7 @@ describe('PolicyActionBuilder', () => { }, provide: { namespaceId: '1', + namespaceType: 'project', }, }); }; diff --git a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/scan_result_policy_editor_spec.js b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/scan_result_policy_editor_spec.js index 9f00b65dd0ad4c..b1059671f9a1c3 100644 --- a/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/scan_result_policy_editor_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policy_editor/scan_result_policy/scan_result_policy_editor_spec.js @@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils'; import { GlEmptyState } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import MockAdapter from 'axios-mock-adapter'; +import Api from 'ee/api'; import waitForPromises from 'helpers/wait_for_promises'; import PolicyEditorLayout from 'ee/security_orchestration/components/policy_editor/policy_editor_layout.vue'; import { @@ -10,7 +11,10 @@ import { fromYaml, } from 'ee/security_orchestration/components/policy_editor/scan_result_policy/lib'; import ScanResultPolicyEditor from 'ee/security_orchestration/components/policy_editor/scan_result_policy/scan_result_policy_editor.vue'; -import { DEFAULT_ASSIGNED_POLICY_PROJECT } from 'ee/security_orchestration/constants'; +import { + DEFAULT_ASSIGNED_POLICY_PROJECT, + NAMESPACE_TYPES, +} from 'ee/security_orchestration/constants'; import { mockScanResultManifest, mockScanResultObject, @@ -80,6 +84,7 @@ describe('ScanResultPolicyEditor', () => { policyEditorEmptyStateSvgPath, namespaceId: 1, namespacePath: defaultProjectPath, + namespaceType: NAMESPACE_TYPES.PROJECT, scanPolicyDocumentationPath, scanResultPolicyApprovers, ...provide, @@ -88,13 +93,14 @@ describe('ScanResultPolicyEditor', () => { nextTick(); }; - const factoryWithExistingPolicy = (policy = {}) => { + const factoryWithExistingPolicy = (policy = {}, provide = {}) => { return factory({ propsData: { assignedPolicyProject, existingPolicy: { ...mockScanResultObject, ...policy }, isEditing: true, }, + provide, }); }; @@ -382,4 +388,15 @@ describe('ScanResultPolicyEditor', () => { expect(errors[errors.length - 1]).toEqual([errorMessage]); }, ); + + it('does not query protected branches when namespaceType is other than project', async () => { + jest.spyOn(Api, 'projectProtectedBranch'); + + factoryWithExistingPolicy({}, { namespaceType: NAMESPACE_TYPES.GROUP }); + + await findPolicyEditorLayout().vm.$emit('update-editor-mode', EDITOR_MODE_RULE); + await waitForPromises(); + + expect(Api.projectProtectedBranch).not.toHaveBeenCalled(); + }); }); -- GitLab From d11beb2407682c184405577b3263b7bc25cd25c4 Mon Sep 17 00:00:00 2001 From: Kate Grechishkina <khrechyshkina@gitlab.com> Date: Wed, 7 Sep 2022 00:53:21 +0000 Subject: [PATCH 148/169] Add a note that group repos move doesn't move nested projects --- doc/api/group_repository_storage_moves.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/api/group_repository_storage_moves.md b/doc/api/group_repository_storage_moves.md index 1b3940479bb5c4..f1ad7f51ea0ef4 100644 --- a/doc/api/group_repository_storage_moves.md +++ b/doc/api/group_repository_storage_moves.md @@ -194,6 +194,11 @@ Example response: ## Schedule a repository storage move for a group +Schedules a repository storage move for a group. This endpoint: + +- Moves only group Wiki repositories. +- Doesn't move repositories for projects in a group. To schedule project moves, use the [Project repository storage moves](project_repository_storage_moves.md) API. + ```plaintext POST /groups/:group_id/repository_storage_moves ``` -- GitLab From b9472727ae5e33951d0a09e40960a3399d78d267 Mon Sep 17 00:00:00 2001 From: qt <qtchen@jihulab.com> Date: Wed, 7 Sep 2022 02:05:13 +0000 Subject: [PATCH 149/169] Fix: notify locale on send admin notification --- app/views/notify/send_admin_notification.html.haml | 4 ++-- locale/gitlab.pot | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/notify/send_admin_notification.html.haml b/app/views/notify/send_admin_notification.html.haml index f7f1528f332ceb..20c44df360c385 100644 --- a/app/views/notify/send_admin_notification.html.haml +++ b/app/views/notify/send_admin_notification.html.haml @@ -3,5 +3,5 @@ \---- %p - Don't want to receive updates from GitLab administrators? - = link_to 'Unsubscribe', @unsubscribe_url + = s_("Notify|Don't want to receive updates from GitLab administrators?") + = link_to _('Unsubscribe'), @unsubscribe_url diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bcfc87fa07845f..fabffb8f5d16a5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -26850,6 +26850,9 @@ msgstr "" msgid "Notify|CI/CD project settings" msgstr "" +msgid "Notify|Don't want to receive updates from GitLab administrators?" +msgstr "" + msgid "Notify|Fingerprint: %{fingerprint}" msgstr "" -- GitLab From 118463b59fd44108bb0a3ef0532c61f5dbf52d0c Mon Sep 17 00:00:00 2001 From: Alishan Ladhani <aladhani@gitlab.com> Date: Tue, 6 Sep 2022 15:42:47 -0400 Subject: [PATCH 150/169] Clarify when to provide `released_at` field when creating a release --- app/graphql/mutations/releases/create.rb | 2 +- doc/api/graphql/reference/index.md | 2 +- doc/api/releases/index.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/graphql/mutations/releases/create.rb b/app/graphql/mutations/releases/create.rb index 70a0e71c869827..ba1fa8d446c8ce 100644 --- a/app/graphql/mutations/releases/create.rb +++ b/app/graphql/mutations/releases/create.rb @@ -32,7 +32,7 @@ class Create < Base argument :released_at, Types::TimeType, required: false, - description: 'Date and time for the release. Defaults to the current date and time.' + description: 'Date and time for the release. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). Only provide this field if creating an upcoming or historical release.' argument :milestones, [GraphQL::Types::String], required: false, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index dbf54319345ce9..7693d3647452a4 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -4371,7 +4371,7 @@ Input type: `ReleaseCreateInput` | <a id="mutationreleasecreatename"></a>`name` | [`String`](#string) | Name of the release. | | <a id="mutationreleasecreateprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project the release is associated with. | | <a id="mutationreleasecreateref"></a>`ref` | [`String`](#string) | Commit SHA or branch name to use if creating a new tag. | -| <a id="mutationreleasecreatereleasedat"></a>`releasedAt` | [`Time`](#time) | Date and time for the release. Defaults to the current date and time. | +| <a id="mutationreleasecreatereleasedat"></a>`releasedAt` | [`Time`](#time) | Date and time for the release. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). Only provide this field if creating an upcoming or historical release. | | <a id="mutationreleasecreatetagmessage"></a>`tagMessage` | [`String`](#string) | Message to use if creating a new annotated tag. | | <a id="mutationreleasecreatetagname"></a>`tagName` | [`String!`](#string) | Name of the tag to associate with the release. | diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md index 1332eea26c046e..e286fefc462474 100644 --- a/doc/api/releases/index.md +++ b/doc/api/releases/index.md @@ -386,7 +386,7 @@ POST /projects/:id/releases | `assets:links:url` | string | required by: `assets:links` | The URL of the link. Link URLs must be unique within the release. | | `assets:links:filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/release_fields.md#permanent-links-to-release-assets). | `assets:links:link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. -| `released_at` | datetime | no | The date when the release is/was ready. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `released_at` | datetime | no | Date and time for the release. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). Only provide this field if creating an [upcoming](../../user/project/releases/index.md#upcoming-releases) or [historical](../../user/project/releases/index.md#historical-releases) release. | Example request: -- GitLab From accc21549141b71c62690e795ea6d8dc27533860 Mon Sep 17 00:00:00 2001 From: Wu Jeremy <jeremyw@jihulab.com> Date: Wed, 7 Sep 2022 02:29:06 +0000 Subject: [PATCH 151/169] Fix: locale on assignee tooltip * Replace translation token for `assignee` Changlog: fixed --- app/assets/javascripts/issuable/components/issue_assignees.vue | 2 +- locale/gitlab.pot | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/issuable/components/issue_assignees.vue b/app/assets/javascripts/issuable/components/issue_assignees.vue index 5955f31fc709b1..21f35690f6d63a 100644 --- a/app/assets/javascripts/issuable/components/issue_assignees.vue +++ b/app/assets/javascripts/issuable/components/issue_assignees.vue @@ -91,7 +91,7 @@ export default { data-qa-selector="assignee_link" > <span class="js-assignee-tooltip"> - <span class="bold d-block">{{ __('Assignee') }}</span> {{ assignee.name }} + <span class="bold d-block">{{ s__('Label|Assignee') }}</span> {{ assignee.name }} <span v-if="assignee.username" class="text-white-50">@{{ assignee.username }}</span> </span> </user-avatar-link> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 22cb2a33c05e87..43245372172f91 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23031,6 +23031,9 @@ msgstr "" msgid "Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. Existing project labels with the same title will be merged. If a group label with the same title exists, it will also be merged. This action cannot be reversed." msgstr "" +msgid "Label|Assignee" +msgstr "" + msgid "Language" msgstr "" -- GitLab From ff8c236f09af30f8e4cbd9b08c76861698ad0e10 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu <heinrich@gitlab.com> Date: Wed, 7 Sep 2022 10:40:00 +0800 Subject: [PATCH 152/169] Upgrade Sidekiq to 6.4.2 This fixes some deprecated usage of pipeline/multi on redis-rb 4.6 --- Gemfile.lock | 2 +- config/initializers_before_autoloader/002_sidekiq.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7b78495dea28f7..4ba19e79aeac83 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1281,7 +1281,7 @@ GEM shellany (0.0.1) shoulda-matchers (5.1.0) activesupport (>= 5.2.0) - sidekiq (6.4.0) + sidekiq (6.4.2) connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) diff --git a/config/initializers_before_autoloader/002_sidekiq.rb b/config/initializers_before_autoloader/002_sidekiq.rb index 9ffcf39d6fb5d4..929bdeda996e1f 100644 --- a/config/initializers_before_autoloader/002_sidekiq.rb +++ b/config/initializers_before_autoloader/002_sidekiq.rb @@ -9,5 +9,5 @@ require 'sidekiq/web' if Rails.env.development? - Sidekiq.default_worker_options[:backtrace] = true + Sidekiq.default_job_options[:backtrace] = true end -- GitLab From 04db604bb5da2b34331473d816711ae603602548 Mon Sep 17 00:00:00 2001 From: Jerez Solis <jsolis@gitlab.com> Date: Wed, 7 Sep 2022 02:42:21 +0000 Subject: [PATCH 153/169] Update doc/administration/audit_event_streaming.md --- doc/administration/audit_event_streaming.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md index 59cb0282a9e81e..33142f31f8e17d 100644 --- a/doc/administration/audit_event_streaming.md +++ b/doc/administration/audit_event_streaming.md @@ -317,7 +317,7 @@ FLAG: On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](feature_flags.md) named `audit_event_streaming_git_operations`. -Streaming audit events can be sent when signed-in users push or pull a project's remote Git repositories: +Streaming audit events can be sent when signed-in users push, pull, or clone a project's remote Git repositories: - [Using SSH](../user/ssh.md). - Using HTTP or HTTPS. -- GitLab From 0d787eb5691f56a46ff1a31f79688c1be4f3597c Mon Sep 17 00:00:00 2001 From: Alvin Gounder <agounder@gitlab.com> Date: Wed, 7 Sep 2022 03:33:39 +0000 Subject: [PATCH 154/169] Enhance can_create_groups instruction to clarify limitation --- doc/administration/user_settings.md | 13 ++++++++++--- doc/api/groups.md | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/doc/administration/user_settings.md b/doc/administration/user_settings.md index 2e879f8789d3e3..0a3f351c6956a1 100644 --- a/doc/administration/user_settings.md +++ b/doc/administration/user_settings.md @@ -8,10 +8,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w GitLab administrators can modify user settings for the entire GitLab instance. -## Prevent users from creating top-level groups +## Prevent new users from creating top-level groups -By default, new users can create top-level groups. To disable your users' -ability to create top-level groups: +By default, new users can create top-level groups. To disable new users' +ability to create top-level groups (does not affect existing users' setting): **Omnibus GitLab installations** @@ -33,6 +33,13 @@ ability to create top-level groups: 1. [Restart GitLab](restart_gitlab.md#installations-from-source). +### Prevent existing users from creating top-level groups + +Administrators can: + +- Use the Admin Area to [prevent an existing user from creating top-level groups](../user/admin_area/index.md#prevent-a-user-from-creating-groups). +- Use the [modify an existing user API endpoint](../api/users.md#user-modification) to change the `can_create_group` setting. + ## Prevent users from changing their usernames By default, new users can change their usernames. To disable your users' diff --git a/doc/api/groups.md b/doc/api/groups.md index 8d3b016e8faf28..16f08d1a5a5e88 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -926,7 +926,7 @@ Transfer a group to a new parent group or turn a subgroup to a top-level group. - With the Owner role for the group to transfer. - With permission to [create a subgroup](../user/group/subgroups/index.md#create-a-subgroup) in the new parent group if transferring a group. -- With [permission to create a top-level group](../administration/user_settings.md#prevent-users-from-creating-top-level-groups) if turning a subgroup into a top-level group. +- With [permission to create a top-level group](../administration/user_settings.md) if turning a subgroup into a top-level group. ```plaintext POST /groups/:id/transfer -- GitLab From 0777cd3002c51f5766a8f349e9e69c6e640bde8a Mon Sep 17 00:00:00 2001 From: Rajendra Kadam <rkadam@gitlab.com> Date: Wed, 7 Sep 2022 03:34:04 +0000 Subject: [PATCH 155/169] Move embedded Zoom calls to Linked Resources https://gitlab.com/gitlab-org/gitlab/-/issues/230853 Changelog: added EE: true --- .../incident_management/incidents.md | 2 +- .../incident_management/linked_resources.md | 18 ++- .../project/issues/associate_zoom_meeting.md | 7 +- doc/user/project/quick_actions.md | 4 +- .../zoom_link_service.rb | 80 ++++++++++ .../ee/gitlab/quick_actions/issue_actions.rb | 33 ++++ .../incidents/user_uses_quick_actions_spec.rb | 26 ++++ ee/spec/models/concerns/ee/issuable_spec.rb | 28 ++++ .../zoom_link_service_spec.rb | 145 ++++++++++++++++++ .../zoom_quick_actions_shared_examples.rb | 41 +++++ lib/gitlab/quick_actions/issue_actions.rb | 35 +++-- 11 files changed, 403 insertions(+), 16 deletions(-) create mode 100644 ee/app/services/incident_management/issuable_resource_links/zoom_link_service.rb create mode 100644 ee/spec/features/incidents/user_uses_quick_actions_spec.rb create mode 100644 ee/spec/services/incident_management/issuable_resource_links/zoom_link_service_spec.rb create mode 100644 ee/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md index b66f1d3e1f6afc..2cb2e5f80455a9 100644 --- a/doc/operations/incident_management/incidents.md +++ b/doc/operations/incident_management/incidents.md @@ -320,7 +320,7 @@ team members can join the Zoom call without requesting a link. ## Linked resources -In an incident, you can [links to various resources](linked_resources.md), +In an incident, you can add [links to various resources](linked_resources.md), for example: - The incident Slack channel diff --git a/doc/operations/incident_management/linked_resources.md b/doc/operations/incident_management/linked_resources.md index d2254a30f916a5..f2a1e60e9c0f87 100644 --- a/doc/operations/incident_management/linked_resources.md +++ b/doc/operations/incident_management/linked_resources.md @@ -50,11 +50,27 @@ To add a linked resource: 1. Complete the required fields. 1. Select **Add**. +### Link Zoom meetings from an incident **(PREMIUM)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/230853) in GitLab 15.4. + +Use the `/zoom` [quick action](../../user/project/quick_actions.md) to add multiple Zoom links to an incident: + +```plaintext +/zoom https://example.zoom.us/j/123456789 +``` + +You can also submit a short optional description with the link. The description shows instead of the URL in the **Linked resources** section of the incident issue: + +```plaintext +/zoom https://example.zoom.us/j/123456789, Low on memory incident +``` + ## Remove a linked resource You can also remove a linked resource. -Prerequisities: +Prerequisites: - You must have at least the Reporter role for the project. diff --git a/doc/user/project/issues/associate_zoom_meeting.md b/doc/user/project/issues/associate_zoom_meeting.md index 41de91d9bd71ad..ef864dc2743fe5 100644 --- a/doc/user/project/issues/associate_zoom_meeting.md +++ b/doc/user/project/issues/associate_zoom_meeting.md @@ -8,8 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16609) in GitLab 12.4. -In order to communicate synchronously for incidents management, -GitLab allows to associate a Zoom meeting with an issue. +To communicate synchronously for incidents management, +you can associate a Zoom meeting with an issue. After you start a Zoom call for a fire-fight, you need a way to associate the conference call with an issue. This is so that your team members can join swiftly without requesting a link. @@ -36,6 +36,9 @@ You are only allowed to attach a single Zoom meeting to an issue. If you attempt to add a second Zoom meeting using the `/zoom` quick action, it doesn't work. You need to [remove it](#removing-an-existing-zoom-meeting-from-an-issue) first. +Users on GitLab Premium and higher can also +[add multiple Zoom links to incidents](../../../operations/incident_management/linked_resources.md#link-zoom-meetings-from-an-incident). + ## Removing an existing Zoom meeting from an issue Similarly to adding a Zoom meeting, you can remove it with a quick action: diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 216d040734dbfd..fe2d3cd6cd19d7 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -100,7 +100,7 @@ threads. Some quick actions might not be available to all subscription tiers. | `/remove_milestone` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Remove milestone. | | `/remove_parent_epic` | **{dotted-circle}** No | **{dotted-circle}** No | **{check-circle}** Yes | Remove parent epic from epic ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10556) in GitLab 12.1). | | `/remove_time_spent` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Remove time spent. | -| `/remove_zoom` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Remove Zoom meeting from this issue ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16609) in GitLab 12.4). | +| `/remove_zoom` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Remove Zoom meeting from this issue. | | `/reopen` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Reopen. | | `/severity <severity>` | **{check-circle}** Yes | **{check-circle}** No | **{check-circle}** No | Set the severity. Options for `<severity>` are `S1` ... `S4`, `critical`, `high`, `medium`, `low`, `unknown`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/334045) in GitLab 14.2. | | `/shrug <comment>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Append the comment with `¯\_(ツ)_/¯`. | @@ -121,7 +121,7 @@ threads. Some quick actions might not be available to all subscription tiers. | `/unlock` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Unlock the discussions. | | `/unsubscribe` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Unsubscribe from notifications. | | `/weight <value>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Set weight. Valid options for `<value>` include `0`, `1`, `2`, and so on. | -| `/zoom <Zoom URL>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add Zoom meeting to this issue ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16609) in GitLab 12.4). | +| `/zoom <Zoom URL>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add a Zoom meeting to this issue or incident. In [GitLab 15.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/230853) users on GitLab Premium can add a short description when [adding a Zoom link to an incident](../../operations/incident_management/linked_resources.md#link-zoom-meetings-from-an-incident).| ## Commit messages diff --git a/ee/app/services/incident_management/issuable_resource_links/zoom_link_service.rb b/ee/app/services/incident_management/issuable_resource_links/zoom_link_service.rb new file mode 100644 index 00000000000000..c44fd41595c11e --- /dev/null +++ b/ee/app/services/incident_management/issuable_resource_links/zoom_link_service.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module IncidentManagement + module IssuableResourceLinks + class ZoomLinkService < IssuableResourceLinks::BaseService + def initialize(project:, current_user:, incident:) + @incident = incident + @user = current_user + @project = project + end + + def add_link(link, link_text) + if can_add_link? && (link = parse_link_param(link)) + add_zoom_meeting(link, link_text) + else + error(_('Failed to add a Zoom meeting')) + end + end + + def can_add_link? + allowed? + end + + def parse_link(link_params) + return unless link_params + + link_params = link_params.split(',', 2) + link = parse_link_param(link_params[0]) + + return unless link + + link_text = link_params[1]&.strip + [link, link_text.presence] + end + + def can_remove_link? + false + end + + private + + attr_reader :incident, :user, :project + + def track_meeting_added_event + ::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', + 'add_zoom_meeting', + label: 'Issue ID', value: incident.id, + user: user, project: @project, namespace: @project.namespace) + end + + def add_zoom_meeting(link, link_text) + zoom_meeting = new_zoom_meeting(link, link_text).execute + if zoom_meeting.success? + track_meeting_added_event + success( + message: _('Zoom meeting added'), + payload: { + zoom_meetings: [zoom_meeting.payload[:issuable_resource_link]] + } + ) + else + error(_('Failed to add a Zoom meeting')) + end + end + + def new_zoom_meeting(link, link_text) + IssuableResourceLinks::CreateService.new(@incident, + user, { link: link, link_text: link_text, link_type: :zoom }) + end + + def success(message:, payload: nil) + ServiceResponse.success(message: message, payload: payload) + end + + def parse_link_param(link) + ::Gitlab::ZoomLinkExtractor.new(link).links.last + end + end + end +end diff --git a/ee/lib/ee/gitlab/quick_actions/issue_actions.rb b/ee/lib/ee/gitlab/quick_actions/issue_actions.rb index fd236383325aee..df4a7f5f268aab 100644 --- a/ee/lib/ee/gitlab/quick_actions/issue_actions.rb +++ b/ee/lib/ee/gitlab/quick_actions/issue_actions.rb @@ -5,6 +5,7 @@ module Gitlab module QuickActions module IssueActions extend ActiveSupport::Concern + extend ::Gitlab::Utils::Override include ::Gitlab::QuickActions::Dsl included do @@ -203,6 +204,38 @@ def find_iterations(project, params = {}) private + override :zoom_link_service + def zoom_link_service + if quick_action_target.issuable_resource_links_available? + ::IncidentManagement::IssuableResourceLinks::ZoomLinkService.new(project: quick_action_target.project, current_user: current_user, incident: quick_action_target) + else + super + end + end + + override :zoom_link_params + def zoom_link_params + if quick_action_target.issuable_resource_links_available? + '<Zoom meeting URL>, <link description (optional)>' + else + super + end + end + + override :add_zoom_link + def add_zoom_link(link, link_text) + if quick_action_target.issuable_resource_links_available? + zoom_link_service.add_link(link, link_text) + else + super + end + end + + override :merge_updates + def merge_updates(result, update_hash) + super unless quick_action_target.issuable_resource_links_available? + end + def find_health_status(health_status_param) return unless health_status_param diff --git a/ee/spec/features/incidents/user_uses_quick_actions_spec.rb b/ee/spec/features/incidents/user_uses_quick_actions_spec.rb new file mode 100644 index 00000000000000..7443dfa4ab004c --- /dev/null +++ b/ee/spec/features/incidents/user_uses_quick_actions_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Incidents > User uses EE quick actions', :js do + include Spec::Support::Helpers::Features::NotesHelpers + + describe 'incident-only commands' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:issue, reload: true) { create(:incident, project: project) } + + before do + project.add_developer(user) + sign_in(user) + visit project_issue_path(project, issue) + wait_for_all_requests + end + + after do + wait_for_requests + end + + it_behaves_like 'zoom quick actions ee' + end +end diff --git a/ee/spec/models/concerns/ee/issuable_spec.rb b/ee/spec/models/concerns/ee/issuable_spec.rb index fea5a4a6b4bee6..e3dd1fdca474c8 100644 --- a/ee/spec/models/concerns/ee/issuable_spec.rb +++ b/ee/spec/models/concerns/ee/issuable_spec.rb @@ -211,4 +211,32 @@ expect(issue.allows_scoped_labels?).to be(false) end end + + describe '#issuable_resource_links_available?' do + let_it_be(:project) { build_stubbed(:project) } + + it 'returns false for issuable type as issue' do + issue = build_stubbed(:issue, project: project) + + stub_licensed_features(issuable_resource_links: true) + + expect(issue.issuable_resource_links_available?).to be(false) + end + + it 'returns true for issuable type as incident' do + issue = build_stubbed(:incident, project: project) + + stub_licensed_features(issuable_resource_links: true) + + expect(issue.issuable_resource_links_available?).to be(true) + end + + it 'returns false when feature is not avaiable' do + issue = build_stubbed(:incident, project: project) + + stub_licensed_features(issuable_resource_links: false) + + expect(issue.issuable_resource_links_available?).to be(false) + end + end end diff --git a/ee/spec/services/incident_management/issuable_resource_links/zoom_link_service_spec.rb b/ee/spec/services/incident_management/issuable_resource_links/zoom_link_service_spec.rb new file mode 100644 index 00000000000000..44cc09e658d65c --- /dev/null +++ b/ee/spec/services/incident_management/issuable_resource_links/zoom_link_service_spec.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe IncidentManagement::IssuableResourceLinks::ZoomLinkService do + let_it_be(:user) { create(:user) } + let_it_be(:issue) { create(:incident) } + + let(:project) { issue.project } + let(:service) { described_class.new(project: project, current_user: user, incident: issue) } + let(:zoom_link) { 'https://zoom.us/j/123456789' } + let(:link_text) { 'Demo meeting' } + + before do + stub_licensed_features(issuable_resource_links: true) + project.add_reporter(user) + end + + shared_context 'when insufficient issue create/update permissions' do + before do + project.add_guest(user) + end + end + + describe '#add_link' do + shared_examples 'can add meeting' do + it 'appends the new meeting to zoom_meetings' do + expect(result).to be_success + end + + it 'tracks the add event', :snowplow do + result + + expect_snowplow_event( + category: 'IncidentManagement::ZoomIntegration', + action: 'add_zoom_meeting', + label: 'Issue ID', + value: issue.id, + user: user, + project: project, + namespace: project.namespace + ) + end + end + + shared_examples 'cannot add meeting' do + it 'cannot add the meeting' do + expect(result).to be_error + expect(result.message).to eq('Failed to add a Zoom meeting') + end + end + + subject(:result) { service.add_link(zoom_link, link_text) } + + context 'when issue is incident type' do + let(:current_user) { user } + + include_examples 'can add meeting' + it_behaves_like 'an incident management tracked event', :incident_management_issuable_resource_link_created + end + + context 'with insufficient issue update permissions' do + include_context 'when insufficient issue create/update permissions' + include_examples 'cannot add meeting' + end + + context 'when link text has multiple commas' do + let(:link_text) { 'Demo meeeting, On fire, need to check' } + + include_examples 'can add meeting' + end + + context 'when service fails to create' do + before do + allow_next_instance_of(IncidentManagement::IssuableResourceLink) do |model| + allow(model).to receive(:save).and_return(false) + end + end + + include_examples 'cannot add meeting' + end + + context 'with invalid Zoom url' do + let(:zoom_link) { 'https://not-zoom.link' } + + include_examples 'cannot add meeting' + end + + context 'with issue type issue' do + let(:issue) { create(:issue) } + + include_examples 'cannot add meeting' + end + end + + describe '#can_add_link?' do + subject { service.can_add_link? } + + it { is_expected.to eq(true) } + + context 'with insufficient issue update permissions' do + include_context 'when insufficient issue create/update permissions' + + it { is_expected.to eq(false) } + end + end + + describe '#parse_link' do + let(:link) { 'https://zoom.us/j/123456789' } + + subject { service.parse_link(link_params) } + + context 'with valid Zoom links' do + where(:link_params, :link, :link_text) do + [ + ['https://zoom.us/j/123456789, Demo meeting', link, 'Demo meeting'], + ['https://zoom.us/j/123456789 http://example.com, Space fire, fire again', link, 'Space fire, fire again'], + ['https://zoom.us/my/name https://zoom.us/j/123456789,Fire, fire on!, extinguishe now!', + link, 'Fire, fire on!, extinguishe now!'], + ['https://zoom.us/my/name https://zoom.us/j/123456789', link, nil] + ] + end + + with_them do + it { is_expected.to eq([link, link_text]) } + end + end + + context 'with invalid Zoom links' do + where(:link_params) do + [ + nil, + '', + 'Text only', + 'Non-Zoom http://example.com', + 'Almost Zoom http://zoom.us' + ] + end + + with_them do + it { is_expected.to eq(nil) } + end + end + end +end diff --git a/ee/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb b/ee/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb new file mode 100644 index 00000000000000..602d342b751b7d --- /dev/null +++ b/ee/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'zoom quick actions ee' do + let(:zoom_link) { 'https://zoom.us/j/123456789' } + let(:invalid_zoom_link) { 'https://invalid-zoom' } + + describe '/zoom' do + before do + stub_licensed_features(issuable_resource_links: true) + stub_feature_flags(incident_resource_links_widget: true) + end + + context 'with valid zoom_meetings' do + where(:link_text, :link_text_expected) do + [ + ['', zoom_link], + ['Demo meeting', 'Demo meeting'], + ['Fire, fire, everything on fire', 'Fire, fire, everything on fire'], + [' Space, fire extinguished', 'Space, fire extinguished'] + ] + end + + with_them do + it 'adds a Zoom link' do + add_note("/zoom #{zoom_link},#{link_text}") + + expect(page).to have_content('Zoom meeting added') + expect(issue.issuable_resource_links.first.link).to eq(zoom_link) + expect(issue.issuable_resource_links.first.link_text).to eq(link_text_expected) + end + end + end + + it 'cannot add an invalid zoom link' do + add_note("/zoom #{invalid_zoom_link}") + + expect(page).to have_content('Failed to add a Zoom meeting') + expect(page).not_to have_content(zoom_link) + end + end +end diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb index 189627506f3cb9..1d122bb2b6eac6 100644 --- a/lib/gitlab/quick_actions/issue_actions.rb +++ b/lib/gitlab/quick_actions/issue_actions.rb @@ -207,19 +207,22 @@ module IssueActions desc { _('Add Zoom meeting') } explanation { _('Adds a Zoom meeting.') } - params '<Zoom URL>' + params do + zoom_link_params + end types Issue condition do @zoom_service = zoom_link_service + @zoom_service.can_add_link? end - parse_params do |link| - @zoom_service.parse_link(link) + parse_params do |link_params| + @zoom_service.parse_link(link_params) end - command :zoom do |link| - result = @zoom_service.add_link(link) + command :zoom do |link, link_text = nil| + result = add_zoom_link(link, link_text) @execution_message[:zoom] = result.message - @updates.merge!(result.payload) if result.payload + merge_updates(result, @updates) end desc { _('Remove Zoom meeting') } @@ -314,12 +317,24 @@ module IssueActions command :remove_contacts do |contact_emails| @updates[:remove_contacts] = contact_emails.split(' ') end + end - private + private - def zoom_link_service - ::Issues::ZoomLinkService.new(project: quick_action_target.project, current_user: current_user, params: { issue: quick_action_target }) - end + def zoom_link_service + ::Issues::ZoomLinkService.new(project: quick_action_target.project, current_user: current_user, params: { issue: quick_action_target }) + end + + def zoom_link_params + '<Zoom URL>' + end + + def add_zoom_link(link, _link_text) + zoom_link_service.add_link(link) + end + + def merge_updates(result, update_hash) + update_hash.merge!(result.payload) if result.payload end end end -- GitLab From 4647b985fb6842cdd4ae964a3c742e434a7a5aa6 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu <heinrich@gitlab.com> Date: Wed, 7 Sep 2022 12:09:02 +0800 Subject: [PATCH 156/169] Fix HTML showing up in resolved threads email Adds a missing html_safe call in the template --- app/views/notify/resolved_all_discussions_email.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/notify/resolved_all_discussions_email.html.haml b/app/views/notify/resolved_all_discussions_email.html.haml index bd9778ae142edd..78dc21caf18133 100644 --- a/app/views/notify/resolved_all_discussions_email.html.haml +++ b/app/views/notify/resolved_all_discussions_email.html.haml @@ -1,2 +1,2 @@ %p - = s_('Notify|All discussions on merge request %{mr_link} were resolved by %{name}') %{mr_link: sanitize(merge_request_reference_link(@merge_request)), name: sanitize_name(@resolved_by.name)} + = s_('Notify|All discussions on merge request %{mr_link} were resolved by %{name}').html_safe % { mr_link: merge_request_reference_link(@merge_request), name: sanitize_name(@resolved_by.name) } -- GitLab From 180943dd7864d00a695c45bc45bc84f453d921ff Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu <heinrich@gitlab.com> Date: Wed, 7 Sep 2022 12:37:08 +0800 Subject: [PATCH 157/169] Remove incorrect assertion We are pushing scheduled jobs so these only have an `at` key and `enqueued_at` will be set when the job is actually enqueued by the Sidekiq scheduled set processor --- spec/workers/concerns/application_worker_spec.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb index 707fa0c9c78402..5fde54b98f0244 100644 --- a/spec/workers/concerns/application_worker_spec.rb +++ b/spec/workers/concerns/application_worker_spec.rb @@ -289,7 +289,6 @@ def self.name perform_action expect(worker.jobs.count).to eq args.count - expect(worker.jobs).to all(include('enqueued_at')) end end @@ -302,7 +301,6 @@ def self.name perform_action expect(worker.jobs.count).to eq args.count - expect(worker.jobs).to all(include('enqueued_at')) end end -- GitLab From 17f8650cbb69ccb09271b0d5dcf6302c88bff839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Fraz=C3=A3o?= <dfrazao@gitlab.com> Date: Wed, 7 Sep 2022 05:46:43 +0000 Subject: [PATCH 158/169] Background Migrations Visibility Improvement for GitLab engineers - Add an endpoint to resume a batched background migration Changelog: added Relates to https://gitlab.com/gitlab-org/gitlab/-/issues/346357 --- .../admin/batched_background_migrations.rb | 24 ++++++++++ .../batched_background_migrations_spec.rb | 48 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/lib/api/admin/batched_background_migrations.rb b/lib/api/admin/batched_background_migrations.rb index 32980c192f791f..5e03c27062b803 100644 --- a/lib/api/admin/batched_background_migrations.rb +++ b/lib/api/admin/batched_background_migrations.rb @@ -27,9 +27,33 @@ class BatchedBackgroundMigrations < ::API::Base end end end + + resources 'batched_background_migrations/:id/resume' do + desc 'Resume a batched background migration' + params do + optional :database, + type: String, + values: Gitlab::Database.all_database_names, + desc: 'The name of the database', + default: 'main' + requires :id, + type: Integer, + desc: 'The batched background migration id' + end + put do + Gitlab::Database::SharedModel.using_connection(base_model.connection) do + batched_background_migration.execute! + present_entity(batched_background_migration) + end + end + end end helpers do + def batched_background_migration + @batched_background_migration ||= Gitlab::Database::BackgroundMigration::BatchedMigration.find(params[:id]) + end + def base_model database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME @base_model ||= Gitlab::Database.database_base_models[database] diff --git a/spec/requests/api/admin/batched_background_migrations_spec.rb b/spec/requests/api/admin/batched_background_migrations_spec.rb index 8763089488d99f..dce4c19b4e7edc 100644 --- a/spec/requests/api/admin/batched_background_migrations_spec.rb +++ b/spec/requests/api/admin/batched_background_migrations_spec.rb @@ -75,4 +75,52 @@ end end end + + describe 'PUT /admin/batched_background_migrations/:id/resume' do + let!(:migration) { create(:batched_background_migration, :paused) } + let(:database) { :main } + + subject(:resume) do + put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: { database: database } + end + + it 'pauses the batched background migration' do + resume + + aggregate_failures "testing response" do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(migration.id) + expect(json_response['status']).to eq('active') + end + end + + context 'when the batched background migration does not exist' do + let(:params) { { database: database } } + + it 'returns 404' do + put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", admin), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when multiple database is enabled', :add_ci_connection do + let(:ci_model) { Ci::ApplicationRecord } + let(:database) { :ci } + + it 'uses the correct connection' do + expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield + + resume + end + end + + context 'when authenticated as a non-admin user' do + it 'returns 403' do + get api('/admin/batched_background_migrations', unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end end -- GitLab From b9b56e0ee0836ce2382436c715c3dc5278e60d23 Mon Sep 17 00:00:00 2001 From: Kati Paizee <kpaizee@gitlab.com> Date: Wed, 7 Sep 2022 06:52:33 +0000 Subject: [PATCH 159/169] Change deployment pipeline window --- .../documentation/site_architecture/deployment_process.md | 6 +++--- doc/development/documentation/testing.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/development/documentation/site_architecture/deployment_process.md b/doc/development/documentation/site_architecture/deployment_process.md index bf45066c7db8b3..8a9c2e1e8d7295 100644 --- a/doc/development/documentation/site_architecture/deployment_process.md +++ b/doc/development/documentation/site_architecture/deployment_process.md @@ -144,14 +144,14 @@ graph LR ### Manually deploy to production -GitLab Docs is deployed to production whenever the `Build docs.gitlab.com every 4 hours` scheduled pipeline runs. By -default, this pipeline runs every four hours. +GitLab Docs is deployed to production whenever the `Build docs.gitlab.com every hour` scheduled pipeline runs. By +default, this pipeline runs every hour. Maintainers can [manually](../../../ci/pipelines/schedules.md#run-manually) run this pipeline to force a deployment to production: 1. Go to the [scheduled pipelines](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules) for `gitlab-docs`. -1. Next to `Build docs.gitlab.com every 4 hours`, select **Play** (**{play}**). +1. Next to `Build docs.gitlab.com every hour`, select **Play** (**{play}**). The updated documentation is available in production after the `pages` and `pages:deploy` jobs complete in the new pipeline. diff --git a/doc/development/documentation/testing.md b/doc/development/documentation/testing.md index 428a57a11fbb9b..59a078bdec00a7 100644 --- a/doc/development/documentation/testing.md +++ b/doc/development/documentation/testing.md @@ -190,7 +190,7 @@ To update the linting images: 1. In `gitlab-docs`, open a merge request to update `.gitlab-ci.yml` to use the new tooling version. ([Example MR](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/2571)) -1. When merged, start a `Build docs.gitlab.com every 4 hours` [scheduled pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules). +1. When merged, start a `Build docs.gitlab.com every hour` [scheduled pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules). 1. Go the pipeline you started, and manually run the relevant build-images job, for example, `image:docs-lint-markdown`. 1. In the job output, get the name of the new image. -- GitLab From b14acb9ca5a30c6e9c2133a932d25075b0552085 Mon Sep 17 00:00:00 2001 From: Laurent Deketelaere <10886527-ali_o_kan@users.noreply.gitlab.com> Date: Wed, 7 Sep 2022 06:55:04 +0000 Subject: [PATCH 160/169] Autosave weight in Issuable form Changelog: added EE: true --- .../javascripts/issuable/issuable_form.js | 28 +-- .../javascripts/issuable/issuable_form.js | 20 ++ .../frontend/issuable/issuable_form_spec.js | 64 ++++++ spec/frontend/issuable/issuable_form_spec.js | 199 +++++++++++------- 4 files changed, 227 insertions(+), 84 deletions(-) create mode 100644 ee/spec/frontend/issuable/issuable_form_spec.js diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js index cc2608b5c62f80..4c6685820cfd30 100644 --- a/app/assets/javascripts/issuable/issuable_form.js +++ b/app/assets/javascripts/issuable/issuable_form.js @@ -39,6 +39,11 @@ function format(searchTerm, isFallbackKey = false) { return formattedQuery; } +function getSearchTerm(newIssuePath) { + const { search, pathname } = document.location; + return newIssuePath === pathname ? '' : format(search); +} + function getFallbackKey() { const searchTerm = format(document.location.search, true); return ['autosave', document.location.pathname, searchTerm].join('/'); @@ -72,7 +77,8 @@ export default class IssuableForm { this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search'); this.zenMode = new ZenMode(); - this.newIssuePath = form[0].getAttribute(DATA_ISSUES_NEW_PATH); + this.searchTerm = getSearchTerm(form[0].getAttribute(DATA_ISSUES_NEW_PATH)); + this.fallbackKey = getFallbackKey(); this.titleField = this.form.find('input[name*="[title]"]'); this.descriptionField = this.form.find('textarea[name*="[description]"]'); if (!(this.titleField.length && this.descriptionField.length)) { @@ -109,20 +115,16 @@ export default class IssuableForm { } initAutosave() { - const { search, pathname } = document.location; - const searchTerm = this.newIssuePath === pathname ? '' : format(search); - const fallbackKey = getFallbackKey(); - - this.autosave = new Autosave( + this.autosaveTitle = new Autosave( this.titleField, - [document.location.pathname, searchTerm, 'title'], - `${fallbackKey}=title`, + [document.location.pathname, this.searchTerm, 'title'], + `${this.fallbackKey}=title`, ); - return new Autosave( + this.autosaveDescription = new Autosave( this.descriptionField, - [document.location.pathname, searchTerm, 'description'], - `${fallbackKey}=description`, + [document.location.pathname, this.searchTerm, 'description'], + `${this.fallbackKey}=description`, ); } @@ -131,8 +133,8 @@ export default class IssuableForm { } resetAutosave() { - this.titleField.data('autosave').reset(); - return this.descriptionField.data('autosave').reset(); + this.autosaveTitle.reset(); + this.autosaveDescription.reset(); } initWip() { diff --git a/ee/app/assets/javascripts/issuable/issuable_form.js b/ee/app/assets/javascripts/issuable/issuable_form.js index c127c82119cf66..0f4172cea0bce5 100644 --- a/ee/app/assets/javascripts/issuable/issuable_form.js +++ b/ee/app/assets/javascripts/issuable/issuable_form.js @@ -1,3 +1,4 @@ +import Autosave from '~/autosave'; import groupsSelect from '~/groups_select'; import IssuableForm from '~/issuable/issuable_form'; @@ -7,4 +8,23 @@ export default class IssuableFormEE extends IssuableForm { groupsSelect(); } + + initAutosave() { + super.initAutosave(); + + const weightField = this.form.find('input[name*="[weight]"]'); + if (weightField.length) { + this.autosaveWeight = new Autosave( + weightField, + [document.location.pathname, this.searchTerm, 'weight'], + `${this.fallbackKey}=weight`, + ); + } + } + + resetAutosave() { + super.resetAutosave(); + + if (this.autosaveWeight) this.autosaveWeight.reset(); + } } diff --git a/ee/spec/frontend/issuable/issuable_form_spec.js b/ee/spec/frontend/issuable/issuable_form_spec.js new file mode 100644 index 00000000000000..b0daac651b9c7a --- /dev/null +++ b/ee/spec/frontend/issuable/issuable_form_spec.js @@ -0,0 +1,64 @@ +import $ from 'jquery'; +import Autosave from '~/autosave'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import IssuableForm from 'ee/issuable/issuable_form'; +import IssuableFormCE from '~/issuable/issuable_form'; + +jest.mock('~/autosave'); + +const createIssuable = (form) => { + return new IssuableForm(form); +}; + +describe('IssuableForm', () => { + let $form; + + beforeEach(() => { + setHTMLFixture(` + <form> + <input name="[title]" /> + <textarea name="[description]"></textarea> + </form> + `); + $form = $('form'); + }); + + afterEach(() => { + resetHTMLFixture(); + $form = null; + }); + + describe('initAutosave', () => { + it('calls super initAutosave', () => { + const initAutosaveCE = jest.spyOn(IssuableFormCE.prototype, 'initAutosave'); + createIssuable($form); + expect(initAutosaveCE).toHaveBeenCalledTimes(1); + }); + + it('creates weight autosave when weight input exist', () => { + $form.append('<input name="[weight]" />'); + const $weight = $form.find('input[name*="[weight]"]'); + createIssuable($form); + + expect(Autosave).toHaveBeenCalledTimes(3); + expect(Autosave).toHaveBeenLastCalledWith($weight, ['/', '', 'weight'], 'autosave///=weight'); + }); + }); + + describe('resetAutosave', () => { + it('calls super resetAutosave', () => { + const resetAutosaveCE = jest.spyOn(IssuableFormCE.prototype, 'resetAutosave'); + createIssuable($form).resetAutosave(); + + expect(resetAutosaveCE).toHaveBeenCalledTimes(1); + }); + + it('calls reset on weight when weight input exist', () => { + $form.append('<input name="[weight]" />'); + const instance = createIssuable($form); + instance.resetAutosave(); + + expect(instance.autosaveWeight.reset).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js index d844f3394d5343..f37d132743a70c 100644 --- a/spec/frontend/issuable/issuable_form_spec.js +++ b/spec/frontend/issuable/issuable_form_spec.js @@ -1,111 +1,168 @@ import $ from 'jquery'; +import Autosave from '~/autosave'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import IssuableForm from '~/issuable/issuable_form'; import setWindowLocation from 'helpers/set_window_location_helper'; +jest.mock('~/autosave'); + +const createIssuable = (form) => { + return new IssuableForm(form); +}; + describe('IssuableForm', () => { + let $form; let instance; - const createIssuable = (form) => { - instance = new IssuableForm(form); - }; - beforeEach(() => { setHTMLFixture(` <form> <input name="[title]" /> + <textarea name="[description]"></textarea> </form> `); - createIssuable($('form')); + $form = $('form'); }); afterEach(() => { resetHTMLFixture(); + $form = null; + instance = null; }); - describe('initAutosave', () => { - it('creates autosave with the searchTerm included', () => { - setWindowLocation('https://gitlab.test/foo?bar=true'); - const autosave = instance.initAutosave(); + describe('autosave', () => { + let $title; + let $description; + + beforeEach(() => { + $title = $form.find('input[name*="[title]"]'); + $description = $form.find('textarea[name*="[description]"]'); + }); + + afterEach(() => { + $title = null; + $description = null; + }); - expect(autosave.key.includes('bar=true')).toBe(true); + describe('initAutosave', () => { + it('calls initAutosave', () => { + const initAutosave = jest.spyOn(IssuableForm.prototype, 'initAutosave'); + createIssuable($form); + + expect(initAutosave).toHaveBeenCalledTimes(1); + }); + + it('creates autosave with the searchTerm included', () => { + setWindowLocation('https://gitlab.test/foo?bar=true'); + createIssuable($form); + + expect(Autosave).toHaveBeenCalledWith( + $title, + ['/foo', 'bar=true', 'title'], + 'autosave//foo/bar=true=title', + ); + expect(Autosave).toHaveBeenCalledWith( + $description, + ['/foo', 'bar=true', 'description'], + 'autosave//foo/bar=true=description', + ); + }); + + it("creates autosave fields without the searchTerm if it's an issue new form", () => { + setWindowLocation('https://gitlab.test/issues/new?bar=true'); + $form.attr('data-new-issue-path', '/issues/new'); + createIssuable($form); + + expect(Autosave).toHaveBeenCalledWith( + $title, + ['/issues/new', '', 'title'], + 'autosave//issues/new/bar=true=title', + ); + expect(Autosave).toHaveBeenCalledWith( + $description, + ['/issues/new', '', 'description'], + 'autosave//issues/new/bar=true=description', + ); + }); }); - it("creates autosave fields without the searchTerm if it's an issue new form", () => { - setHTMLFixture(` - <form data-new-issue-path="/issues/new"> - <input name="[title]" /> - </form> - `); - createIssuable($('form')); + describe('resetAutosave', () => { + it('calls reset on title and description', () => { + instance = createIssuable($form); + + instance.resetAutosave(); - setWindowLocation('https://gitlab.test/issues/new?bar=true'); + expect(instance.autosaveTitle.reset).toHaveBeenCalledTimes(1); + expect(instance.autosaveDescription.reset).toHaveBeenCalledTimes(1); + }); - const autosave = instance.initAutosave(); + it('resets autosave when submit', () => { + const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave'); + createIssuable($form); - expect(autosave.key.includes('bar=true')).toBe(false); + $form.submit(); + + expect(resetAutosave).toHaveBeenCalledTimes(1); + }); + + it('resets autosave on elements with the .js-reset-autosave class', () => { + const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave'); + $form.append('<a class="js-reset-autosave">Cancel</a>'); + createIssuable($form); + + $form.find('.js-reset-autosave').trigger('click'); + + expect(resetAutosave).toHaveBeenCalledTimes(1); + }); }); }); - describe('resetAutosave', () => { - it('resets autosave on elements with the .js-reset-autosave class', () => { - setHTMLFixture(` - <form> - <input name="[title]" /> - <textarea name="[description]"></textarea> - <a class="js-reset-autosave">Cancel</a> - </form> - `); - const $form = $('form'); - const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave'); - createIssuable($form); - - $form.find('.js-reset-autosave').trigger('click'); - - expect(resetAutosave).toHaveBeenCalled(); + describe('wip', () => { + beforeEach(() => { + instance = createIssuable($form); }); - }); - describe('removeWip', () => { - it.each` - prefix - ${'draFT: '} - ${' [DRaft] '} - ${'drAft:'} - ${'[draFT]'} - ${'(draft) '} - ${' (DrafT)'} - ${'draft: [draft] (draft)'} - `('removes "$prefix" from the beginning of the title', ({ prefix }) => { - instance.titleField.val(`${prefix}The Issuable's Title Value`); - - instance.removeWip(); - - expect(instance.titleField.val()).toBe("The Issuable's Title Value"); + describe('removeWip', () => { + it.each` + prefix + ${'draFT: '} + ${' [DRaft] '} + ${'drAft:'} + ${'[draFT]'} + ${'(draft) '} + ${' (DrafT)'} + ${'draft: [draft] (draft)'} + `('removes "$prefix" from the beginning of the title', ({ prefix }) => { + instance.titleField.val(`${prefix}The Issuable's Title Value`); + + instance.removeWip(); + + expect(instance.titleField.val()).toBe("The Issuable's Title Value"); + }); }); - }); - describe('addWip', () => { - it("properly adds the work in progress prefix to the Issuable's title", () => { - instance.titleField.val("The Issuable's Title Value"); + describe('addWip', () => { + it("properly adds the work in progress prefix to the Issuable's title", () => { + instance.titleField.val("The Issuable's Title Value"); - instance.addWip(); + instance.addWip(); - expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value"); + expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value"); + }); }); - }); - describe('workInProgress', () => { - it.each` - title | expected - ${'draFT: something is happening'} | ${true} - ${'draft something is happening'} | ${false} - ${'something is happening to drafts'} | ${false} - ${'something is happening'} | ${false} - `('returns $expected with "$title"', ({ title, expected }) => { - instance.titleField.val(title); - - expect(instance.workInProgress()).toBe(expected); + describe('workInProgress', () => { + it.each` + title | expected + ${'draFT: something is happening'} | ${true} + ${'draft something is happening'} | ${false} + ${'something is happening to drafts'} | ${false} + ${'something is happening'} | ${false} + `('returns $expected with "$title"', ({ title, expected }) => { + instance.titleField.val(title); + + expect(instance.workInProgress()).toBe(expected); + }); }); }); }); -- GitLab From 327a1fb6527f898e5b7da0ebf492350f1f24dcdb Mon Sep 17 00:00:00 2001 From: Peter Hegman <phegman@gitlab.com> Date: Wed, 7 Sep 2022 07:58:53 +0000 Subject: [PATCH 161/169] Convert group overview tabs to Vue From a mixture of HAML and jQuery --- .../javascripts/groups/components/app.vue | 13 ++- .../groups/components/overview_tabs.vue | 80 +++++++++++++ app/assets/javascripts/groups/index.js | 5 +- .../javascripts/groups/init_overview_tabs.js | 57 ++++++++++ .../javascripts/pages/groups/details/index.js | 2 + .../javascripts/pages/groups/show/index.js | 2 + app/helpers/groups_helper.rb | 9 ++ app/views/groups/show.html.haml | 53 ++++----- .../development/group_overview_tabs_vue.yml | 8 ++ .../javascripts/pages/groups/show/index.js | 5 +- spec/features/groups/show_spec.rb | 2 + .../projects/user_sorts_projects_spec.rb | 4 + spec/frontend/groups/components/app_spec.js | 10 +- .../groups/components/overview_tabs_spec.js | 106 ++++++++++++++++++ spec/helpers/groups_helper_spec.rb | 23 ++++ 15 files changed, 335 insertions(+), 44 deletions(-) create mode 100644 app/assets/javascripts/groups/components/overview_tabs.vue create mode 100644 app/assets/javascripts/groups/init_overview_tabs.js create mode 100644 config/feature_flags/development/group_overview_tabs_vue.yml create mode 100644 spec/frontend/groups/components/overview_tabs_spec.js diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index cd5521c599ef68..0bd7371d39b54c 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -17,11 +17,6 @@ export default { GlLoadingIcon, EmptyState, }, - inject: { - renderEmptyState: { - default: false, - }, - }, props: { action: { type: String, @@ -45,6 +40,11 @@ export default { type: Boolean, required: true, }, + renderEmptyState: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -224,6 +224,9 @@ export default { }, showLegacyEmptyState() { const { containerEl } = this; + + if (!containerEl) return; + const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS); const emptyStateEl = containerEl.querySelector('.empty-state'); diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue new file mode 100644 index 00000000000000..53efb354f5ccb9 --- /dev/null +++ b/app/assets/javascripts/groups/components/overview_tabs.vue @@ -0,0 +1,80 @@ +<script> +import { GlTabs, GlTab } from '@gitlab/ui'; +import { __ } from '~/locale'; +import GroupsStore from '../store/groups_store'; +import GroupsService from '../service/groups_service'; +import { + ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + ACTIVE_TAB_SHARED, + ACTIVE_TAB_ARCHIVED, +} from '../constants'; +import GroupsApp from './app.vue'; + +export default { + components: { GlTabs, GlTab, GroupsApp }, + inject: ['endpoints'], + data() { + return { + tabs: [ + { + title: this.$options.i18n.subgroupsAndProjects, + key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + renderEmptyState: true, + lazy: false, + service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]), + store: new GroupsStore({ showSchemaMarkup: true }), + }, + { + title: this.$options.i18n.sharedProjects, + key: ACTIVE_TAB_SHARED, + renderEmptyState: false, + lazy: true, + service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]), + store: new GroupsStore(), + }, + { + title: this.$options.i18n.archivedProjects, + key: ACTIVE_TAB_ARCHIVED, + renderEmptyState: false, + lazy: true, + service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]), + store: new GroupsStore(), + }, + ], + activeTabIndex: 0, + }; + }, + methods: { + handleTabInput(tabIndex) { + this.activeTabIndex = tabIndex; + + const tab = this.tabs[tabIndex]; + tab.lazy = false; + }, + }, + i18n: { + subgroupsAndProjects: __('Subgroups and projects'), + sharedProjects: __('Shared projects'), + archivedProjects: __('Archived projects'), + }, +}; +</script> + +<template> + <gl-tabs content-class="gl-pt-0" :value="activeTabIndex" @input="handleTabInput"> + <gl-tab + v-for="{ key, title, renderEmptyState, lazy, service, store } in tabs" + :key="key" + :title="title" + :lazy="lazy" + > + <groups-app + :action="key" + :service="service" + :store="store" + :hide-projects="false" + :render-empty-state="renderEmptyState" + /> + </gl-tab> + </gl-tabs> +</template> diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index dc2909f26214c4..c3bf3f285099d8 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -52,7 +52,6 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => { newSubgroupIllustration, newProjectIllustration, emptySubgroupIllustration, - renderEmptyState, canCreateSubgroups, canCreateProjects, currentGroupVisibility, @@ -65,7 +64,6 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => { newSubgroupIllustration, newProjectIllustration, emptySubgroupIllustration, - renderEmptyState: parseBoolean(renderEmptyState), canCreateSubgroups: parseBoolean(canCreateSubgroups), canCreateProjects: parseBoolean(canCreateProjects), currentGroupVisibility, @@ -75,6 +73,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => { const { dataset } = dataEl || this.$options.el; const hideProjects = parseBoolean(dataset.hideProjects); const showSchemaMarkup = parseBoolean(dataset.showSchemaMarkup); + const renderEmptyState = parseBoolean(dataset.renderEmptyState); const service = new GroupsService(endpoint || dataset.endpoint); const store = new GroupsStore({ hideProjects, showSchemaMarkup }); @@ -83,6 +82,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => { store, service, hideProjects, + renderEmptyState, loading: true, containerId, }; @@ -119,6 +119,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => { store: this.store, service: this.service, hideProjects: this.hideProjects, + renderEmptyState: this.renderEmptyState, containerId: this.containerId, }, }); diff --git a/app/assets/javascripts/groups/init_overview_tabs.js b/app/assets/javascripts/groups/init_overview_tabs.js new file mode 100644 index 00000000000000..5f568d10a42a0d --- /dev/null +++ b/app/assets/javascripts/groups/init_overview_tabs.js @@ -0,0 +1,57 @@ +import Vue from 'vue'; +import { GlToast } from '@gitlab/ui'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import GroupFolder from './components/group_folder.vue'; +import GroupItem from './components/group_item.vue'; +import { + ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + ACTIVE_TAB_SHARED, + ACTIVE_TAB_ARCHIVED, +} from './constants'; +import OverviewTabs from './components/overview_tabs.vue'; + +export const initGroupOverviewTabs = () => { + const el = document.getElementById('js-group-overview-tabs'); + + if (!el) return false; + + Vue.component('GroupFolder', GroupFolder); + Vue.component('GroupItem', GroupItem); + Vue.use(GlToast); + + const { + newSubgroupPath, + newProjectPath, + newSubgroupIllustration, + newProjectIllustration, + emptySubgroupIllustration, + canCreateSubgroups, + canCreateProjects, + currentGroupVisibility, + subgroupsAndProjectsEndpoint, + sharedProjectsEndpoint, + archivedProjectsEndpoint, + } = el.dataset; + + return new Vue({ + el, + provide: { + newSubgroupPath, + newProjectPath, + newSubgroupIllustration, + newProjectIllustration, + emptySubgroupIllustration, + canCreateSubgroups: parseBoolean(canCreateSubgroups), + canCreateProjects: parseBoolean(canCreateProjects), + currentGroupVisibility, + endpoints: { + [ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: subgroupsAndProjectsEndpoint, + [ACTIVE_TAB_SHARED]: sharedProjectsEndpoint, + [ACTIVE_TAB_ARCHIVED]: archivedProjectsEndpoint, + }, + }, + render(createElement) { + return createElement(OverviewTabs); + }, + }); +}; diff --git a/app/assets/javascripts/pages/groups/details/index.js b/app/assets/javascripts/pages/groups/details/index.js index 0417134f2a7d0d..92490368b156e0 100644 --- a/app/assets/javascripts/pages/groups/details/index.js +++ b/app/assets/javascripts/pages/groups/details/index.js @@ -1,3 +1,5 @@ +import { initGroupOverviewTabs } from '~/groups/init_overview_tabs'; import initGroupDetails from '../shared/group_details'; initGroupDetails('details'); +initGroupOverviewTabs(); diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js index e4a84dd5eecbbb..161fca83a5843e 100644 --- a/app/assets/javascripts/pages/groups/show/index.js +++ b/app/assets/javascripts/pages/groups/show/index.js @@ -1,5 +1,7 @@ import leaveByUrl from '~/namespaces/leave_by_url'; +import { initGroupOverviewTabs } from '~/groups/init_overview_tabs'; import initGroupDetails from '../shared/group_details'; leaveByUrl('group'); initGroupDetails(); +initGroupOverviewTabs(); diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index bb92792de2d7d4..f77bd6621f959e 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -172,6 +172,15 @@ def subgroups_and_projects_list_app_data(group) } end + def group_overview_tabs_app_data(group) + { + subgroups_and_projects_endpoint: group_children_path(group, format: :json), + shared_projects_endpoint: group_shared_projects_path(group, format: :json), + archived_projects_endpoint: group_children_path(group, format: :json, archived: 'only'), + current_group_visibility: group.visibility + }.merge(subgroups_and_projects_list_app_data(group)) + end + def enabled_git_access_protocol_options_for_group case ::Gitlab::CurrentSettings.enabled_git_access_protocol when nil, "" diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index d8da77dc5cc923..f474f8fbd3bd50 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -33,33 +33,36 @@ = render_if_exists 'groups/group_activity_analytics', group: @group -.groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } } - .top-area.group-nav-container.justify-content-between - .scrolling-tabs-container.inner-page-scroll-tabs - .fade-left= sprite_icon('chevron-lg-left', size: 12) - .fade-right= sprite_icon('chevron-lg-right', size: 12) - -# `item_active` is set to `false` as the active state is set by `app/assets/javascripts/pages/groups/shared/group_details.js` - -# TODO: Replace this approach in https://gitlab.com/gitlab-org/gitlab/-/issues/23466 - = gl_tabs_nav({ class: 'nav-links scrolling-tabs gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do - = gl_tab_link_to group_path, item_active: false, tab_class: 'js-subgroups_and_projects-tab', data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab' } do - = _("Subgroups and projects") - = gl_tab_link_to group_shared_path, item_active: false, tab_class: 'js-shared-tab', data: { target: 'div#shared', action: 'shared', toggle: 'tab' } do - = _("Shared projects") - = gl_tab_link_to group_archived_path, item_active: false, tab_class: 'js-archived-tab', data: { target: 'div#archived', action: 'archived', toggle: 'tab' } do - = _("Archived projects") +- if Feature.enabled?(:group_overview_tabs_vue, @group) + #js-group-overview-tabs{ data: group_overview_tabs_app_data(@group) } +- else + .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } } + .top-area.group-nav-container.justify-content-between + .scrolling-tabs-container.inner-page-scroll-tabs + .fade-left= sprite_icon('chevron-lg-left', size: 12) + .fade-right= sprite_icon('chevron-lg-right', size: 12) + -# `item_active` is set to `false` as the active state is set by `app/assets/javascripts/pages/groups/shared/group_details.js` + -# TODO: Replace this approach in https://gitlab.com/gitlab-org/gitlab/-/issues/23466 + = gl_tabs_nav({ class: 'nav-links scrolling-tabs gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do + = gl_tab_link_to group_path, item_active: false, tab_class: 'js-subgroups_and_projects-tab', data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab' } do + = _("Subgroups and projects") + = gl_tab_link_to group_shared_path, item_active: false, tab_class: 'js-shared-tab', data: { target: 'div#shared', action: 'shared', toggle: 'tab' } do + = _("Shared projects") + = gl_tab_link_to group_archived_path, item_active: false, tab_class: 'js-archived-tab', data: { target: 'div#archived', action: 'archived', toggle: 'tab' } do + = _("Archived projects") - .nav-controls.d-block.d-md-flex - .group-search - = render "shared/groups/search_form" + .nav-controls.d-block.d-md-flex + .group-search + = render "shared/groups/search_form" - = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash + = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash - .tab-content - #subgroups_and_projects.tab-pane - = render "subgroups_and_projects", group: @group + .tab-content + #subgroups_and_projects.tab-pane + = render "subgroups_and_projects", group: @group - #shared.tab-pane - = render "shared_projects", group: @group + #shared.tab-pane + = render "shared_projects", group: @group - #archived.tab-pane - = render "archived_projects", group: @group + #archived.tab-pane + = render "archived_projects", group: @group diff --git a/config/feature_flags/development/group_overview_tabs_vue.yml b/config/feature_flags/development/group_overview_tabs_vue.yml new file mode 100644 index 00000000000000..4c54ab31b53f14 --- /dev/null +++ b/config/feature_flags/development/group_overview_tabs_vue.yml @@ -0,0 +1,8 @@ +--- +name: group_overview_tabs_vue +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95850 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370872 +milestone: '15.4' +type: development +group: group::workspace +default_enabled: false diff --git a/ee/app/assets/javascripts/pages/groups/show/index.js b/ee/app/assets/javascripts/pages/groups/show/index.js index debeaf445ad661..fff65bebb51f7f 100644 --- a/ee/app/assets/javascripts/pages/groups/show/index.js +++ b/ee/app/assets/javascripts/pages/groups/show/index.js @@ -1,11 +1,8 @@ +import '~/pages/groups/show'; import initGroupAnalytics from 'ee/analytics/group_analytics/group_analytics_bundle'; import { shouldQrtlyReconciliationMount } from 'ee/billings/qrtly_reconciliation'; -import leaveByUrl from '~/namespaces/leave_by_url'; -import initGroupDetails from '~/pages/groups/shared/group_details'; import initVueAlerts from '~/vue_alerts'; -leaveByUrl('group'); -initGroupDetails(); initGroupAnalytics(); initVueAlerts(); shouldQrtlyReconciliationMount(); diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index d814906a274a28..67310862516815 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -331,6 +331,7 @@ end it 'does not include structured markup in shared projects tab', :aggregate_failures, :js do + stub_feature_flags(group_overview_tabs_vue: false) other_project = create(:project, :public) other_project.project_group_links.create!(group: group) @@ -342,6 +343,7 @@ end it 'does not include structured markup in archived projects tab', :aggregate_failures, :js do + stub_feature_flags(group_overview_tabs_vue: false) project.update!(archived: true) visit group_archived_path(group) diff --git a/spec/features/projects/user_sorts_projects_spec.rb b/spec/features/projects/user_sorts_projects_spec.rb index 7c970f7ee3d127..b9b28398279b08 100644 --- a/spec/features/projects/user_sorts_projects_spec.rb +++ b/spec/features/projects/user_sorts_projects_spec.rb @@ -24,6 +24,7 @@ end it "is set on the group_canonical_path" do + stub_feature_flags(group_overview_tabs_vue: false) visit(group_canonical_path(group)) within '[data-testid=group_sort_by_dropdown]' do @@ -32,6 +33,7 @@ end it "is set on the details_group_path" do + stub_feature_flags(group_overview_tabs_vue: false) visit(details_group_path(group)) within '[data-testid=group_sort_by_dropdown]' do @@ -64,6 +66,7 @@ context 'from group homepage', :js do before do + stub_feature_flags(group_overview_tabs_vue: false) sign_in(user) visit(group_canonical_path(group)) within '[data-testid=group_sort_by_dropdown]' do @@ -77,6 +80,7 @@ context 'from group details', :js do before do + stub_feature_flags(group_overview_tabs_vue: false) sign_in(user) visit(details_group_path(group)) within '[data-testid=group_sort_by_dropdown]' do diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js index 2796a5619535ca..a4a7530184de80 100644 --- a/spec/frontend/groups/components/app_spec.js +++ b/spec/frontend/groups/components/app_spec.js @@ -40,7 +40,7 @@ describe('AppComponent', () => { const store = new GroupsStore({ hideProjects: false }); const service = new GroupsService(mockEndpoint); - const createShallowComponent = ({ propsData = {}, provide = {} } = {}) => { + const createShallowComponent = ({ propsData = {} } = {}) => { store.state.pageInfo = mockPageInfo; wrapper = shallowMount(appComponent, { propsData: { @@ -53,10 +53,6 @@ describe('AppComponent', () => { mocks: { $toast, }, - provide: { - renderEmptyState: false, - ...provide, - }, }); vm = wrapper.vm; }; @@ -402,8 +398,7 @@ describe('AppComponent', () => { ({ action, groups, fromSearch, renderEmptyState, expected }) => { it(expected ? 'renders empty state' : 'does not render empty state', async () => { createShallowComponent({ - propsData: { action }, - provide: { renderEmptyState }, + propsData: { action, renderEmptyState }, }); vm.updateGroups(groups, fromSearch); @@ -420,7 +415,6 @@ describe('AppComponent', () => { it('renders legacy empty state', async () => { createShallowComponent({ propsData: { action: 'subgroups_and_projects' }, - provide: { renderEmptyState: false }, }); vm.updateGroups([], false); diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js new file mode 100644 index 00000000000000..c26254acf3d825 --- /dev/null +++ b/spec/frontend/groups/components/overview_tabs_spec.js @@ -0,0 +1,106 @@ +import { GlTab } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import OverviewTabs from '~/groups/components/overview_tabs.vue'; +import GroupsApp from '~/groups/components/app.vue'; +import GroupsStore from '~/groups/store/groups_store'; +import GroupsService from '~/groups/service/groups_service'; +import { + ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + ACTIVE_TAB_SHARED, + ACTIVE_TAB_ARCHIVED, +} from '~/groups/constants'; +import axios from '~/lib/utils/axios_utils'; + +describe('OverviewTabs', () => { + let wrapper; + + const endpoints = { + subgroups_and_projects: '/groups/foobar/-/children.json', + shared: '/groups/foobar/-/shared_projects.json', + archived: '/groups/foobar/-/children.json?archived=only', + }; + + const createComponent = async () => { + wrapper = mountExtended(OverviewTabs, { + provide: { + endpoints, + }, + }); + + await nextTick(); + }; + + const findTabPanels = () => wrapper.findAllComponents(GlTab); + const findTab = (name) => wrapper.findByRole('tab', { name }); + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(async () => { + // eslint-disable-next-line no-new + new AxiosMockAdapter(axios); + + await createComponent(); + }); + + it('renders `Subgroups and projects` tab with `GroupsApp` component', async () => { + const tabPanel = findTabPanels().at(0); + + expect(tabPanel.vm.$attrs).toMatchObject({ + title: OverviewTabs.i18n.subgroupsAndProjects, + lazy: false, + }); + expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({ + action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + store: new GroupsStore({ showSchemaMarkup: true }), + service: new GroupsService(endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]), + hideProjects: false, + renderEmptyState: true, + }); + }); + + it('renders `Shared projects` tab and renders `GroupsApp` component after clicking tab', async () => { + const tabPanel = findTabPanels().at(1); + + expect(tabPanel.vm.$attrs).toMatchObject({ + title: OverviewTabs.i18n.sharedProjects, + lazy: true, + }); + + await findTab(OverviewTabs.i18n.sharedProjects).trigger('click'); + + expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({ + action: ACTIVE_TAB_SHARED, + store: new GroupsStore(), + service: new GroupsService(endpoints[ACTIVE_TAB_SHARED]), + hideProjects: false, + renderEmptyState: false, + }); + + expect(tabPanel.vm.$attrs.lazy).toBe(false); + }); + + it('renders `Archived projects` tab and renders `GroupsApp` component after clicking tab', async () => { + const tabPanel = findTabPanels().at(2); + + expect(tabPanel.vm.$attrs).toMatchObject({ + title: OverviewTabs.i18n.archivedProjects, + lazy: true, + }); + + await findTab(OverviewTabs.i18n.archivedProjects).trigger('click'); + + expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({ + action: ACTIVE_TAB_ARCHIVED, + store: new GroupsStore(), + service: new GroupsService(endpoints[ACTIVE_TAB_ARCHIVED]), + hideProjects: false, + renderEmptyState: false, + }); + + expect(tabPanel.vm.$attrs.lazy).toBe(false); + }); +}); diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 2c1061d2f1bca4..00e620832b300d 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -520,6 +520,29 @@ end end + describe '#group_overview_tabs_app_data' do + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + + allow(helper).to receive(:can?).with(user, :create_subgroup, group) { true } + allow(helper).to receive(:can?).with(user, :create_projects, group) { true } + end + + it 'returns expected hash' do + expect(helper.group_overview_tabs_app_data(group)).to match( + { + subgroups_and_projects_endpoint: including("/groups/#{group.path}/-/children.json"), + shared_projects_endpoint: including("/groups/#{group.path}/-/shared_projects.json"), + archived_projects_endpoint: including("/groups/#{group.path}/-/children.json?archived=only"), + current_group_visibility: group.visibility + }.merge(helper.group_overview_tabs_app_data(group)) + ) + end + end + describe "#enabled_git_access_protocol_options_for_group" do subject { helper.enabled_git_access_protocol_options_for_group } -- GitLab From 7fab3a2fde66644e3d132915a6d5cf5c4d2b2e20 Mon Sep 17 00:00:00 2001 From: Luke Duncalfe <lduncalfe@gitlab.com> Date: Wed, 7 Sep 2022 08:05:31 +0000 Subject: [PATCH 162/169] Docs for Threaded Google Chat notifications https://gitlab.com/gitlab-org/gitlab/-/issues/371205 https://gitlab.com/gitlab-org/gitlab/-/issues/27823 --- doc/user/project/integrations/hangouts_chat.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/user/project/integrations/hangouts_chat.md b/doc/user/project/integrations/hangouts_chat.md index fbfa7d914a55ec..6e532a6c14fc77 100644 --- a/doc/user/project/integrations/hangouts_chat.md +++ b/doc/user/project/integrations/hangouts_chat.md @@ -49,3 +49,16 @@ Enable the Google Chat integration in GitLab: To test the integration, make a change based on the events you selected and see the notification in your Google Chat room. + +### Enable threads in Google Chat + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27823) in GitLab 15.4. + +To enable threaded notifications for the same GitLab object (for example, an issue or merge request): + +1. Go to [Google Chat](https://chat.google.com/). +1. In **Spaces**, select **+ > Create space**. +1. Enter the space name and (optionally) other details, and select **Use threaded replies**. +1. Select **Create**. + +You cannot enable threaded replies for existing Google Chat spaces. -- GitLab From 99f8ac356774d7aaaee160afffb594f566ea5ff6 Mon Sep 17 00:00:00 2001 From: GitLab Renovate Bot <gitlab-bot@gitlab.com> Date: Wed, 24 Aug 2022 16:22:04 +0000 Subject: [PATCH 163/169] Update dependency @gitlab/eslint-plugin to v17 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 230f0a4d8f7465..c18af29b5c7a06 100644 --- a/package.json +++ b/package.json @@ -197,7 +197,7 @@ "yaml": "^2.0.0-10" }, "devDependencies": { - "@gitlab/eslint-plugin": "16.0.0", + "@gitlab/eslint-plugin": "17.0.0", "@gitlab/stylelint-config": "4.1.0", "@graphql-eslint/eslint-plugin": "3.10.7", "@testing-library/dom": "^7.16.2", diff --git a/yarn.lock b/yarn.lock index 7ecbe845056fbe..3fcc89579c7522 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1027,10 +1027,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e" integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg== -"@gitlab/eslint-plugin@16.0.0": - version "16.0.0" - resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-16.0.0.tgz#83b71bb3f749c6e52138d2c1c17ac623e7b2e3db" - integrity sha512-2n7geoRPkeMAq4GCqyvFzcTgcSrTM7pdCOxfcqIeuTmh/PFGhh+m7YC+YC4enhGOCN8lo08buLZhXkSgWiHSqA== +"@gitlab/eslint-plugin@17.0.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-17.0.0.tgz#5451fbbad96b09d812af2afb247f6602fe0be6c6" + integrity sha512-c+sJtjzYl+KGPtZScU8Mji9seJw7dSEn31APyYEYTyWp72yMsFvXmg46txT2QCz+ueZlqk0/C2IQmgfe6fLcBw== dependencies: "@babel/core" "^7.17.0" "@babel/eslint-parser" "^7.17.0" -- GitLab From 2411c43cbb9cee66669355744f587ab83cf2831e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wielich?= <mwielich@gitlab.com> Date: Wed, 7 Sep 2022 09:24:29 +0000 Subject: [PATCH 164/169] Separate RedisMetric instrumentation class from counter class logic Separate RedisMetric instrumentation class from counter class logic --- .../20210216182006_source_code_pushes.yml | 2 +- doc/development/service_ping/implement.md | 2 +- .../service_ping/metrics_instrumentation.md | 10 +++++---- .../metrics/instrumentations/redis_metric.rb | 22 +++++++++++-------- .../instrumentations/redis_metric_spec.rb | 8 +++---- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/config/metrics/counts_all/20210216182006_source_code_pushes.yml b/config/metrics/counts_all/20210216182006_source_code_pushes.yml index 5c3c70f2496bfc..3e4ef3ec76cdb9 100644 --- a/config/metrics/counts_all/20210216182006_source_code_pushes.yml +++ b/config/metrics/counts_all/20210216182006_source_code_pushes.yml @@ -12,7 +12,7 @@ time_frame: all data_source: redis instrumentation_class: RedisMetric options: - counter_class: SourceCodeCounter + prefix: source_code event: pushes distribution: - ce diff --git a/doc/development/service_ping/implement.md b/doc/development/service_ping/implement.md index 8c04992fd67897..4ef58fefcb9c1e 100644 --- a/doc/development/service_ping/implement.md +++ b/doc/development/service_ping/implement.md @@ -272,7 +272,7 @@ Events are handled by counter classes in the `Gitlab::UsageDataCounters` namespa 1. Listed in [`Gitlab::UsageDataCounters::COUNTERS`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters.rb#L5) to be then included in `Gitlab::UsageData`. -1. Specified in the metric definition using the `RedisMetric` instrumentation class as a `counter_class` option to be picked up using the [metric instrumentation](metrics_instrumentation.md) framework. Refer to the [Redis metrics](metrics_instrumentation.md#redis-metrics) documentation for an example implementation. +1. Specified in the metric definition using the `RedisMetric` instrumentation class by their `prefix` option to be picked up using the [metric instrumentation](metrics_instrumentation.md) framework. Refer to the [Redis metrics](metrics_instrumentation.md#redis-metrics) documentation for an example implementation. Inheriting classes are expected to override `KNOWN_EVENTS` and `PREFIX` constants to build event names and associated metrics. For example, for prefix `issues` and events array `%w[create, update, delete]`, three metrics will be added to the Service Ping payload: `counts.issues_create`, `counts.issues_update` and `counts.issues_delete`. diff --git a/doc/development/service_ping/metrics_instrumentation.md b/doc/development/service_ping/metrics_instrumentation.md index ee0d701a5bbe55..860434ab2ad66f 100644 --- a/doc/development/service_ping/metrics_instrumentation.md +++ b/doc/development/service_ping/metrics_instrumentation.md @@ -154,14 +154,16 @@ end You can use Redis metrics to track events not kept in the database, for example, a count of how many times the search bar has been used. -[Example of a merge request that adds a `Redis` metric](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66582). +[Example of a merge request that adds a `Redis` metric](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97009). + +Please note that `RedisMetric` class can only be used as the `instrumentation_class` for Redis metrics with simple counters classes (classes that only inherit `BaseCounter` and set `PREFIX` and `KNOWN_EVENTS` constants). In case the counter class has additional logic included in it, a new `instrumentation_class`, inheriting from `RedisMetric`, needs to be created. This new class needs to include the additional logic from the counter class. Count unique values for `source_code_pushes` event. Required options: - `event`: the event name. -- `counter_class`: one of the counter classes from the `Gitlab::UsageDataCounters` namespace; it should implement `read` method or inherit it from `BaseCounter`. +- `prefix`: the value of the `PREFIX` constant used in the counter classes from the `Gitlab::UsageDataCounters` namespace. ```yaml time_frame: all @@ -169,7 +171,7 @@ data_source: redis instrumentation_class: 'RedisMetric' options: event: pushes - counter_class: SourceCodeCounter + prefix: source_code ``` ### Availability-restrained Redis metrics @@ -200,7 +202,7 @@ data_source: redis instrumentation_class: 'MergeUsageCountRedisMetric' options: event: pushes - counter_class: SourceCodeCounter + prefix: source_code ``` ## Redis HyperLogLog metrics diff --git a/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb b/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb index a25bad2436bfb2..c9449f10cc2780 100644 --- a/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb +++ b/lib/gitlab/usage/metrics/instrumentations/redis_metric.rb @@ -11,37 +11,41 @@ module Instrumentations # instrumentation_class: RedisMetric # options: # event: pushes - # counter_class: SourceCodeCounter + # prefix: source_code # class RedisMetric < BaseMetric + include Gitlab::UsageDataCounters::RedisCounter + def initialize(time_frame:, options: {}) super raise ArgumentError, "'event' option is required" unless metric_event.present? - raise ArgumentError, "'counter class' option is required" unless counter_class.present? + raise ArgumentError, "'prefix' option is required" unless prefix.present? end def metric_event options[:event] end - def counter_class_name - options[:counter_class] - end - - def counter_class - "Gitlab::UsageDataCounters::#{counter_class_name}".constantize + def prefix + options[:prefix] end def value redis_usage_data do - counter_class.read(metric_event) + total_count(redis_key) end end def suggested_name Gitlab::Usage::Metrics::NameSuggestion.for(:redis) end + + private + + def redis_key + "USAGE_#{prefix}_#{metric_event}".upcase + end end end end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb index 831f775ec9aaa9..e228a0a7d728a9 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb @@ -11,18 +11,18 @@ let(:expected_value) { 4 } - it_behaves_like 'a correct instrumented metric value', { options: { event: 'pushes', counter_class: 'SourceCodeCounter' } } + it_behaves_like 'a correct instrumented metric value', { options: { event: 'pushes', prefix: 'source_code' } } it 'raises an exception if event option is not present' do - expect { described_class.new(counter_class: 'SourceCodeCounter') }.to raise_error(ArgumentError) + expect { described_class.new(prefix: 'source_code') }.to raise_error(ArgumentError) end - it 'raises an exception if counter_class option is not present' do + it 'raises an exception if prefix option is not present' do expect { described_class.new(event: 'pushes') }.to raise_error(ArgumentError) end describe 'children classes' do - let(:options) { { event: 'pushes', counter_class: 'SourceCodeCounter' } } + let(:options) { { event: 'pushes', prefix: 'source_code' } } context 'availability not defined' do subject { Class.new(described_class).new(time_frame: nil, options: options) } -- GitLab From ba7516b35ce21fb55c858e1a0740f3a9f827f945 Mon Sep 17 00:00:00 2001 From: Thomas Randolph <trandolph@gitlab.com> Date: Wed, 7 Sep 2022 10:27:56 +0000 Subject: [PATCH 165/169] Switch 'b' shortcut to not ever focus or click an in-page element Changelog: fixed --- .../behaviors/shortcuts/shortcuts_issuable.js | 30 ++++++++++++------- locale/gitlab.pot | 6 ++++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index 82229b5aa8ff6d..97ba9e15c0fc71 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -1,9 +1,11 @@ import $ from 'jquery'; +import ClipboardJS from 'clipboard'; import Mousetrap from 'mousetrap'; -import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard'; import { getSelectedFragment } from '~/lib/utils/common_utils'; import { isElementVisible } from '~/lib/utils/dom_utils'; import { DEBOUNCE_DROPDOWN_DELAY } from '~/vue_shared/components/sidebar/labels_select_widget/constants'; +import toast from '~/vue_shared/plugins/global_toast'; +import { s__ } from '~/locale'; import Sidebar from '~/right_sidebar'; import { CopyAsGFM } from '../markdown/copy_as_gfm'; import { @@ -21,6 +23,15 @@ export default class ShortcutsIssuable extends Shortcuts { constructor() { super(); + this.inMemoryButton = document.createElement('button'); + this.clipboardInstance = new ClipboardJS(this.inMemoryButton); + this.clipboardInstance.on('success', () => { + toast(s__('GlobalShortcuts|Copied source branch name to clipboard.')); + }); + this.clipboardInstance.on('error', () => { + toast(s__('GlobalShortcuts|Unable to copy the source branch name at this time.')); + }); + Mousetrap.bind(keysFor(ISSUE_MR_CHANGE_ASSIGNEE), () => ShortcutsIssuable.openSidebarDropdown('assignee'), ); @@ -32,7 +43,7 @@ export default class ShortcutsIssuable extends Shortcuts { ); Mousetrap.bind(keysFor(ISSUABLE_COMMENT_OR_REPLY), ShortcutsIssuable.replyWithSelectedText); Mousetrap.bind(keysFor(ISSUABLE_EDIT_DESCRIPTION), ShortcutsIssuable.editIssue); - Mousetrap.bind(keysFor(MR_COPY_SOURCE_BRANCH_NAME), ShortcutsIssuable.copyBranchName); + Mousetrap.bind(keysFor(MR_COPY_SOURCE_BRANCH_NAME), () => this.copyBranchName()); /** * We're attaching a global focus event listener on document for @@ -153,17 +164,14 @@ export default class ShortcutsIssuable extends Shortcuts { return false; } - static copyBranchName() { - // There are two buttons - one that is shown when the sidebar - // is expanded, and one that is shown when it's collapsed. - const allCopyBtns = Array.from(document.querySelectorAll('.js-source-branch-copy')); + async copyBranchName() { + const button = document.querySelector('.js-source-branch-copy'); + const branchName = button?.dataset.clipboardText; - // Select whichever button is currently visible so that - // the "Copied" tooltip is shown when a click is simulated. - const visibleBtn = allCopyBtns.find(isElementVisible); + if (branchName) { + this.inMemoryButton.dataset.clipboardText = branchName; - if (visibleBtn) { - clickCopyToClipboardButton(visibleBtn); + this.inMemoryButton.dispatchEvent(new CustomEvent('click')); } } } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bed17f1e9fbf73..c11ebb936d3012 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18046,6 +18046,12 @@ msgstr "" msgid "GlobalSearch|project" msgstr "" +msgid "GlobalShortcuts|Copied source branch name to clipboard." +msgstr "" + +msgid "GlobalShortcuts|Unable to copy the source branch name at this time." +msgstr "" + msgid "Globally-allowed IP ranges" msgstr "" -- GitLab From d796f621c01a40d2dcbe7038d46b18136cb7aa54 Mon Sep 17 00:00:00 2001 From: Darby Frey <dfrey@gitlab.com> Date: Wed, 7 Sep 2022 11:17:49 +0000 Subject: [PATCH 166/169] Adding Mobile DevOps docs page --- doc/ci/mobile_devops.md | 42 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 doc/ci/mobile_devops.md diff --git a/doc/ci/mobile_devops.md b/doc/ci/mobile_devops.md new file mode 100644 index 00000000000000..6eb56434a1ba2a --- /dev/null +++ b/doc/ci/mobile_devops.md @@ -0,0 +1,42 @@ +--- +stage: none +group: unassigned +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +type: reference +--- + +# Mobile DevOps + +GitLab Mobile DevOps is a collection of features and tools designed for mobile developers +and teams to automate their build and release process using GitLab CI/CD. Mobile DevOps +is an experimental feature developed by [GitLab Incubation Engineering](https://about.gitlab.com/handbook/engineering/incubation/). + +Mobile DevOps is still in development, but you can: + +- [Request a feature](https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/feedback/-/issues/new?issuable_template=feature_request). +- [Report a bug](https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/feedback/-/issues/new?issuable_template=report_bug). +- [Share feedback](https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/feedback/-/issues/new?issuable_template=general_feedback). + +## Code Signing + +[Project-level Secure Files](secure_files/index.md) makes it easier to manage key stores, provision profiles, +and signing certificates directly in a GitLab project. + +For a guided walkthrough of this feature, watch the [video demo](https://youtu.be/O7FbJu3H2YM). + +## Review Apps for Mobile + +You can use [Review Apps](review_apps/index.md) to preview changes directly from a merge request. +Review Apps for Mobile brings that capability to mobile developers through an integration +with [Appetize](https://appetize.io/). + +Watch a [video walkthrough](https://youtu.be/X15mI19TXa4) of this feature, or visit the +[setup instructions](https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/readme/-/issues/15) +to get started. + +## Mobile SAST + +You can use [Static Application Security Testing (SAST)](../user/application_security/sast/index.md) +to run static analyzers on code to check for known security vulnerabilities. Mobile SAST +expands this functionality for mobile teams with an [experimental SAST feature](../user/application_security/sast/index.md#experimental-features) +based on [Mobile Security Framework (MobSF)](https://github.com/MobSF/Mobile-Security-Framework-MobSF). -- GitLab From 979e9367fc6bece6be77c3e1f7a60e68c15ef0d5 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler <mkaeppler@gitlab.com> Date: Wed, 7 Sep 2022 13:19:42 +0200 Subject: [PATCH 167/169] Drop deprecated gitlab-exporter endpoint docs --- doc/administration/monitoring/prometheus/index.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md index 6f6ac5c5d4b478..c4aa607fa4dcfb 100644 --- a/doc/administration/monitoring/prometheus/index.md +++ b/doc/administration/monitoring/prometheus/index.md @@ -277,11 +277,6 @@ To use an external Prometheus server: static_configs: - targets: - 1.1.1.1:9168 - - job_name: gitlab_exporter_process - metrics_path: "/process" - static_configs: - - targets: - - 1.1.1.1:9168 - job_name: gitaly static_configs: - targets: -- GitLab From 3ff90056a0479be36164882e79d76fbd886678af Mon Sep 17 00:00:00 2001 From: Doug Stull <dstull@gitlab.com> Date: Wed, 7 Sep 2022 11:30:27 +0000 Subject: [PATCH 168/169] Remove awaiting members from billable user finder - no longer needed, never used Changelog: removed EE: true --- doc/api/members.md | 24 ++---- ee/app/finders/billed_users_finder.rb | 16 +--- ee/lib/ee/api/entities/billable_member.rb | 16 +--- ee/lib/ee/api/helpers/members_helpers.rb | 2 +- ee/lib/ee/api/members.rb | 9 +-- ee/spec/finders/billed_users_finder_spec.rb | 77 ++++--------------- .../ee/api/entities/billable_member_spec.rb | 30 +------- .../ee/api/helpers/members_helpers_spec.rb | 2 +- ee/spec/requests/api/members_spec.rb | 22 ------ 9 files changed, 31 insertions(+), 167 deletions(-) diff --git a/doc/api/members.md b/doc/api/members.md index b0992aafb7e784..3ffe94e6f99d97 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -76,8 +76,7 @@ Example response: }, "expires_at": "2012-10-22T14:13:35Z", "access_level": 30, - "group_saml_identity": null, - "membership_state": "active" + "group_saml_identity": null }, { "id": 2, @@ -102,8 +101,7 @@ Example response: "extern_uid":"ABC-1234567890", "provider": "group_saml", "saml_provider_id": 10 - }, - "membership_state": "active" + } } ] ``` @@ -163,8 +161,7 @@ Example response: }, "expires_at": "2012-10-22T14:13:35Z", "access_level": 30, - "group_saml_identity": null, - "membership_state": "active" + "group_saml_identity": null }, { "id": 2, @@ -189,8 +186,7 @@ Example response: "extern_uid":"ABC-1234567890", "provider": "group_saml", "saml_provider_id": 10 - }, - "membership_state": "active" + } }, { "id": 3, @@ -210,8 +206,7 @@ Example response: }, "expires_at": "2012-11-22T14:13:35Z", "access_level": 30, - "group_saml_identity": null, - "membership_state": "active" + "group_saml_identity": null } ] ``` @@ -257,8 +252,7 @@ Example response: "web_url": "http://192.168.1.8:3000/root" }, "expires_at": null, - "group_saml_identity": null, - "membership_state": "active" + "group_saml_identity": null } ``` @@ -305,8 +299,7 @@ Example response: }, "email": "john@example.com", "expires_at": null, - "group_saml_identity": null, - "membership_state": "active" + "group_saml_identity": null } ``` @@ -370,7 +363,6 @@ Example response: "web_url": "http://192.168.1.8:3000/root", "last_activity_on": "2021-01-27", "membership_type": "group_member", - "membership_state": "active", "removable": true, "created_at": "2021-01-03T12:16:02.000Z" }, @@ -384,7 +376,6 @@ Example response: "email": "john@example.com", "last_activity_on": "2021-01-25", "membership_type": "group_member", - "membership_state": "active", "removable": true, "created_at": "2021-01-04T18:46:42.000Z" }, @@ -397,7 +388,6 @@ Example response: "web_url": "http://192.168.1.8:3000/root", "last_activity_on": "2021-01-20", "membership_type": "group_invite", - "membership_state": "awaiting", "removable": false, "created_at": "2021-01-09T07:12:31.000Z" } diff --git a/ee/app/finders/billed_users_finder.rb b/ee/app/finders/billed_users_finder.rb index fbf07594050a8a..8a255b64b8c28e 100644 --- a/ee/app/finders/billed_users_finder.rb +++ b/ee/app/finders/billed_users_finder.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true class BilledUsersFinder - def initialize(group, search_term: nil, order_by: 'name_asc', include_awaiting_members: false) + def initialize(group, search_term: nil, order_by: 'name_asc') @group = group @search_term = search_term @order_by = order_by - @include_awaiting_members = include_awaiting_members end def execute @@ -19,26 +18,19 @@ def execute group_member_user_ids: group_billed_user_ids[:group_member_user_ids], project_member_user_ids: group_billed_user_ids[:project_member_user_ids], shared_group_user_ids: group_billed_user_ids[:shared_group_user_ids], - shared_project_user_ids: group_billed_user_ids[:shared_project_user_ids], - awaiting_user_ids: awaiting_user_ids + shared_project_user_ids: group_billed_user_ids[:shared_project_user_ids] } end private - attr_reader :group, :search_term, :order_by, :include_awaiting_members + attr_reader :group, :search_term, :order_by def user_ids - group_billed_user_ids[:user_ids] + awaiting_user_ids + group_billed_user_ids[:user_ids] end def group_billed_user_ids @group_billed_user_ids ||= group.billed_user_ids end - - def awaiting_user_ids - return [] unless include_awaiting_members - - group.awaiting_user_ids - end end diff --git a/ee/lib/ee/api/entities/billable_member.rb b/ee/lib/ee/api/entities/billable_member.rb index 62e1531fce08ec..bf35da8894dcc7 100644 --- a/ee/lib/ee/api/entities/billable_member.rb +++ b/ee/lib/ee/api/entities/billable_member.rb @@ -9,7 +9,6 @@ class BillableMember < ::API::Entities::UserBasic expose :membership_type expose :removable expose :created_at - expose :membership_state expose :last_owner?, as: :is_last_owner private @@ -21,25 +20,12 @@ def membership_type return 'project_invite' if user_in_array?(:shared_project_user_ids) end - def membership_state - has_any_active_membership? ? 'active' : 'awaiting' - end - - def has_any_active_membership? - user_in_array?(:group_member_user_ids) || - user_in_array?(:project_member_user_ids) || - user_in_array?(:shared_group_user_ids) || - user_in_array?(:shared_project_user_ids) - end - def last_owner? options[:group].last_owner?(object) end def removable - user_in_array?(:group_member_user_ids) || - user_in_array?(:project_member_user_ids) || - user_in_array?(:awaiting_user_ids) + user_in_array?(:group_member_user_ids) || user_in_array?(:project_member_user_ids) end def user_in_array?(name) diff --git a/ee/lib/ee/api/helpers/members_helpers.rb b/ee/lib/ee/api/helpers/members_helpers.rb index 7cf995505f1370..636d22c884cf70 100644 --- a/ee/lib/ee/api/helpers/members_helpers.rb +++ b/ee/lib/ee/api/helpers/members_helpers.rb @@ -74,7 +74,7 @@ def present_member(updated_member) end def billable_member?(group, user) - billed_users_finder = BilledUsersFinder.new(group, include_awaiting_members: true) + billed_users_finder = BilledUsersFinder.new(group) users = billed_users_finder.execute[:users] users.include?(user) diff --git a/ee/lib/ee/api/members.rb b/ee/lib/ee/api/members.rb index 27f492b5f1f6ba..6b201d26255f0a 100644 --- a/ee/lib/ee/api/members.rb +++ b/ee/lib/ee/api/members.rb @@ -118,7 +118,6 @@ module Members use :pagination optional :search, type: String, desc: 'The exact name of the subscribed member' optional :sort, type: String, desc: 'The sorting option', values: Helpers::MembersHelpers.member_sort_options - optional :include_awaiting_members, type: Grape::API::Boolean, desc: 'Determines if awaiting members are included', default: false end get ":id/billable_members", feature_category: :subgroups do group = find_group!(params[:id]) @@ -128,10 +127,7 @@ module Members sorting = params[:sort] || 'id_asc' - result = BilledUsersFinder.new(group, - search_term: params[:search], - order_by: sorting, - include_awaiting_members: params[:include_awaiting_members]).execute + result = BilledUsersFinder.new(group, search_term: params[:search], order_by: sorting).execute present paginate(result[:users]), with: ::EE::API::Entities::BillableMember, @@ -140,8 +136,7 @@ module Members group_member_user_ids: result[:group_member_user_ids], project_member_user_ids: result[:project_member_user_ids], shared_group_user_ids: result[:shared_group_user_ids], - shared_project_user_ids: result[:shared_project_user_ids], - awaiting_user_ids: result[:awaiting_user_ids] + shared_project_user_ids: result[:shared_project_user_ids] end desc 'Changes the state of the memberships of a user in the group' diff --git a/ee/spec/finders/billed_users_finder_spec.rb b/ee/spec/finders/billed_users_finder_spec.rb index 8c5711f594aad3..d6092d672b580d 100644 --- a/ee/spec/finders/billed_users_finder_spec.rb +++ b/ee/spec/finders/billed_users_finder_spec.rb @@ -7,14 +7,11 @@ let(:search_term) { nil } let(:order_by) { nil } - let(:include_awaiting_members) { false } - subject(:execute) { described_class.new(group, search_term: search_term, order_by: order_by, include_awaiting_members: include_awaiting_members).execute } + subject(:execute) { described_class.new(group, search_term: search_term, order_by: order_by).execute } describe '#execute' do context 'without members' do - let(:include_awaiting_members) { true } - it 'returns an empty object' do expect(execute).to eq({}) end @@ -25,41 +22,6 @@ let_it_be(:john_smith) { create(:group_member, group: group, user: create(:user, name: 'John Smith')) } let_it_be(:john_doe) { create(:group_member, group: group, user: create(:user, name: 'John Doe')) } let_it_be(:sophie) { create(:group_member, group: group, user: create(:user, name: 'Sophie Dupont')) } - let_it_be(:alice_awaiting) { create(:group_member, :awaiting, :developer, group: group, user: create(:user, name: 'Alice Waiting')) } - - shared_examples 'with awaiting members' do - context 'when awaiting users are included' do - let(:include_awaiting_members) { true } - - it 'includes awaiting users' do - expect(execute[:users]).to include(alice_awaiting.user) - end - end - - context 'when awaiting users are excluded' do - let(:include_awaiting_members) { false } - - it 'excludes awaiting users' do - expect(execute[:users]).not_to include(alice_awaiting.user) - end - end - end - - context 'when user is awaiting and active member' do - let_it_be(:project) { create(:project, group: group) } - - let(:include_awaiting_members) { true } - - before do - create(:project_member, :maintainer, user: alice_awaiting.user, source: project) - end - - it 'is only included once' do - expect(execute[:users]).to include(alice_awaiting.user).once - end - end - - it_behaves_like 'with awaiting members' context 'when a search parameter is provided' do let(:search_term) { 'John' } @@ -79,45 +41,32 @@ expect(execute[:users]).to eq([john_doe, john_smith].map(&:user)) end end - - context 'when searching for an awaiting user' do - let(:search_term) { 'Alice' } - - it_behaves_like 'with awaiting members' - end end context 'when a search parameter is not present' do - subject(:execute) { described_class.new(group, include_awaiting_members: include_awaiting_members).execute } + subject(:execute) { described_class.new(group).execute } it 'returns expected users in name asc order when a sorting is not provided either' do expect(execute[:users]).to eq([john_doe, john_smith, maria, sophie].map(&:user)) end - it_behaves_like 'with awaiting members' - context 'and when a sorting parameter is provided (eg name descending)' do let(:order_by) { 'name_desc' } - subject(:execute) { described_class.new(group, search_term: search_term, order_by: order_by, include_awaiting_members: include_awaiting_members).execute } + subject(:execute) { described_class.new(group, search_term: search_term, order_by: order_by).execute } it 'sorts results accordingly' do expect(execute[:users]).to eq([sophie, maria, john_smith, john_doe].map(&:user)) end - - context 'when awaiting users are included' do - let(:include_awaiting_members) { true } - - it 'sorts results accordingly' do - expect(execute[:users]).to eq([sophie, maria, john_smith, john_doe, alice_awaiting].map(&:user)) - end - end end end context 'with billable group members including shared members' do let_it_be(:shared_with_group_member) { create(:group_member, user: create(:user, name: 'Shared Group User')) } - let_it_be(:shared_with_project_member) { create(:group_member, user: create(:user, name: 'Shared Project User')) } + let_it_be(:shared_with_project_member) do + create(:group_member, user: create(:user, name: 'Shared Project User')) + end + let_it_be(:project) { create(:project, group: group) } before do @@ -126,18 +75,20 @@ end it 'returns a hash of users and user ids' do - expect(execute.keys).to eq([ + keys = [ :users, :group_member_user_ids, :project_member_user_ids, :shared_group_user_ids, - :shared_project_user_ids, - :awaiting_user_ids - ]) + :shared_project_user_ids + ] + + expect(execute.keys).to eq(keys) end it 'returns the correct user ids', :aggregate_failures do - expect(execute[:group_member_user_ids]).to contain_exactly(*[maria, john_smith, john_doe, sophie].map(&:user_id)) + expect(execute[:group_member_user_ids]) + .to contain_exactly(*[maria, john_smith, john_doe, sophie].map(&:user_id)) expect(execute[:shared_group_user_ids]).to contain_exactly(shared_with_group_member.user_id) expect(execute[:shared_project_user_ids]).to contain_exactly(shared_with_project_member.user_id) end diff --git a/ee/spec/lib/ee/api/entities/billable_member_spec.rb b/ee/spec/lib/ee/api/entities/billable_member_spec.rb index a886da6d6cbed4..2f7542d484f565 100644 --- a/ee/spec/lib/ee/api/entities/billable_member_spec.rb +++ b/ee/spec/lib/ee/api/entities/billable_member_spec.rb @@ -15,8 +15,7 @@ group_member_user_ids: [], project_member_user_ids: [], shared_group_user_ids: [], - shared_project_user_ids: [], - awaiting_user_ids: [] + shared_project_user_ids: [] } end @@ -51,32 +50,6 @@ end end - context 'membership_state' do - using RSpec::Parameterized::TableSyntax - - where(:key, :result) do - :group_member_user_ids | 'active' - :project_member_user_ids | 'active' - :shared_group_user_ids | 'active' - :shared_project_user_ids | 'active' - :awaiting_user_ids | 'awaiting' - end - - with_them do - let(:options) { super().merge(key => [user.id]) } - - it { expect(entity_representation[:membership_state]).to eq(result) } - end - - context 'with multiple states' do - let(:options) { super().merge(group_member_user_ids: [user.id], awaiting_user_ids: [user.id]) } - - it 'returns the expected membership status' do - expect(entity_representation[:membership_state]).to eq 'active' - end - end - end - context 'when the user has no public_email assigned' do before do user.update!(public_email: nil) @@ -98,7 +71,6 @@ :project_member_user_ids | 'project_member' | true :shared_group_user_ids | 'group_invite' | false :shared_project_user_ids | 'project_invite' | false - :awaiting_user_ids | nil | true end with_them do diff --git a/ee/spec/lib/ee/api/helpers/members_helpers_spec.rb b/ee/spec/lib/ee/api/helpers/members_helpers_spec.rb index e26507c747997f..d55a4156b0dbcf 100644 --- a/ee/spec/lib/ee/api/helpers/members_helpers_spec.rb +++ b/ee/spec/lib/ee/api/helpers/members_helpers_spec.rb @@ -28,7 +28,7 @@ subject(:billable_member) { members_helpers.billable_member?(group, user) } before do - expect_next_instance_of(BilledUsersFinder, group, include_awaiting_members: true) do |finder| + expect_next_instance_of(BilledUsersFinder, group) do |finder| expect(finder).to receive(:execute).and_return({ users: found_users }) end end diff --git a/ee/spec/requests/api/members_spec.rb b/ee/spec/requests/api/members_spec.rb index af86356300923f..7db7f1c0286740 100644 --- a/ee/spec/requests/api/members_spec.rb +++ b/ee/spec/requests/api/members_spec.rb @@ -382,8 +382,6 @@ end end - let_it_be(:awaiting_user) { create(:group_member, :awaiting, group: group, user: create(:user)).user } - describe 'GET /groups/:id/billable_members' do let(:url) { "/groups/#{group.id}/billable_members" } let(:params) { {} } @@ -484,16 +482,6 @@ expect_paginated_array_response(users[0].id) end end - - context 'with include_awaiting_members is true' do - let(:params) { { include_awaiting_members: true } } - - it 'includes awaiting users' do - get_billable_members - - expect_paginated_array_response(*[owner, maintainer, nested_user, awaiting_user, project_user, linked_group_user].map(&:id)) - end - end end context 'with non owner' do @@ -648,16 +636,6 @@ expect(json_response.map { |m| m['source_full_name'] }).to include(project.full_name) end - it 'includes awaiting memberships' do - membership = developer.members.first - membership.wait - - get api("/groups/#{group.id}/billable_members/#{developer.id}/memberships", owner) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.map { |m| m['id'] }).to include(membership.id) - end - it 'paginates results' do subgroup = create(:group, name: 'SubGroup A', parent: group) subgroup.add_developer(developer) -- GitLab From 8329b64030a1ab3e11bd5aa2d1e627a38fcc3762 Mon Sep 17 00:00:00 2001 From: Deepika Guliani <dguliani@gitlab.com> Date: Wed, 7 Sep 2022 12:03:59 +0000 Subject: [PATCH 169/169] Adding graphQL parameters for move to start and end for boards Changelog: added --- .../board_card_move_to_position.vue | 31 ++++---------- .../boards/components/board_list.vue | 13 ++++-- .../javascripts/boards/stores/actions.js | 25 ++++++++++- .../boards/stores/mutation_types.js | 1 + .../javascripts/boards/stores/mutations.js | 41 +++++++++++++++++-- app/assets/javascripts/boards/stores/state.js | 1 + .../graphql/issue_move_list.mutation.graphql | 2 + .../javascripts/boards/stores/mutations.js | 11 ++++- .../board_card_move_to_position_spec.js | 38 ++++++----------- spec/frontend/boards/stores/actions_spec.js | 38 ++++++++++++++++- spec/frontend/boards/stores/mutations_spec.js | 25 +++++++++++ 11 files changed, 166 insertions(+), 60 deletions(-) diff --git a/app/assets/javascripts/boards/components/board_card_move_to_position.vue b/app/assets/javascripts/boards/components/board_card_move_to_position.vue index a0cc3756fc4e51..ff938219475b1f 100644 --- a/app/assets/javascripts/boards/components/board_card_move_to_position.vue +++ b/app/assets/javascripts/boards/components/board_card_move_to_position.vue @@ -48,21 +48,12 @@ export default { listHasNextPage() { return this.pageInfoByListId[this.list.id]?.hasNextPage; }, - firstItemInListId() { - return this.listItems[0]?.id; - }, lengthOfListItemsInBoard() { return this.listItems?.length; }, - lastItemInTheListId() { - return this.listItems[this.lengthOfListItemsInBoard - 1]?.id; - }, itemIdentifier() { return `${this.item.id}-${this.item.iid}-${this.index}`; }, - showMoveToEndOfList() { - return !this.listHasNextPage; - }, isFirstItemInList() { return this.index === 0; }, @@ -80,9 +71,8 @@ export default { if (this.isFirstItemInList) { return; } - const moveAfterId = this.firstItemInListId; this.moveToPosition({ - moveAfterId, + positionInList: 0, }); }, moveToEnd() { @@ -93,20 +83,20 @@ export default { if (this.isLastItemInList) { return; } - const moveBeforeId = this.lastItemInTheListId; this.moveToPosition({ - moveBeforeId, + positionInList: -1, }); }, - moveToPosition({ moveAfterId, moveBeforeId }) { + moveToPosition({ positionInList }) { this.moveItem({ itemId: this.item.id, itemIid: this.item.iid, itemPath: this.item.referencePath, fromListId: this.list.id, toListId: this.list.id, - moveAfterId, - moveBeforeId, + positionInList, + atIndex: this.index, + allItemsLoadedInList: !this.listHasNextPage, }); }, }, @@ -117,7 +107,6 @@ export default { <gl-dropdown ref="dropdown" :key="itemIdentifier" - data-testid="move-card-dropdown" icon="ellipsis_v" :text="s__('Boards|Move card')" :text-sr-only="true" @@ -128,14 +117,10 @@ export default { @keydown.esc.native="$emit('hide')" > <div> - <gl-dropdown-item data-testid="action-move-to-first" @click.stop="moveToStart"> + <gl-dropdown-item @click.stop="moveToStart"> {{ $options.i18n.moveToStartText }} </gl-dropdown-item> - <gl-dropdown-item - v-if="showMoveToEndOfList" - data-testid="action-move-to-end" - @click.stop="moveToEnd" - > + <gl-dropdown-item @click.stop="moveToEnd"> {{ $options.i18n.moveToEndText }} </gl-dropdown-item> </div> diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 93835519033a4d..edf1a5ee7e6c17 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -66,7 +66,7 @@ export default { }, }, computed: { - ...mapState(['pageInfoByListId', 'listsFlags', 'filterParams']), + ...mapState(['pageInfoByListId', 'listsFlags', 'filterParams', 'isUpdateIssueOrderInProgress']), ...mapGetters(['isEpicBoard']), listItemsCount() { return this.isEpicBoard ? this.list.epicsCount : this.boardList?.issuesCount; @@ -132,6 +132,9 @@ export default { return this.canMoveIssue ? options : {}; }, + disableScrollingWhenMutationInProgress() { + return this.hasNextPage && this.isUpdateIssueOrderInProgress; + }, }, watch: { boardItems() { @@ -285,9 +288,13 @@ export default { v-bind="treeRootOptions" :data-board="list.id" :data-board-type="list.listType" - :class="{ 'bg-danger-100': boardItemsSizeExceedsMax }" + :class="{ + 'bg-danger-100': boardItemsSizeExceedsMax, + 'gl-overflow-hidden': disableScrollingWhenMutationInProgress, + 'gl-overflow-y-auto': !disableScrollingWhenMutationInProgress, + }" draggable=".board-card" - class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-3 gl-pt-0 gl-overflow-y-auto gl-overflow-x-hidden" + class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-3 gl-pt-0 gl-overflow-x-hidden" data-testid="tree-root-wrapper" @start="handleDragOnStart" @end="handleDragOnEnd" diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index f84274104b2c35..c2e346da6064f6 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -479,16 +479,25 @@ export default { toListId, moveBeforeId, moveAfterId, + positionInList, + allItemsLoadedInList, } = moveData; commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId }); + if (reordering && !allItemsLoadedInList && positionInList === -1) { + return; + } + if (reordering) { commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: toListId, moveBeforeId, moveAfterId, + positionInList, + atIndex: originalIndex, + allItemsLoadedInList, }); return; @@ -500,6 +509,7 @@ export default { listId: toListId, moveBeforeId, moveAfterId, + positionInList, }); } @@ -553,7 +563,15 @@ export default { updateIssueOrder: async ({ commit, dispatch, state }, { moveData, mutationVariables = {} }) => { try { - const { itemId, fromListId, toListId, moveBeforeId, moveAfterId, itemNotInToList } = moveData; + const { + itemId, + fromListId, + toListId, + moveBeforeId, + moveAfterId, + itemNotInToList, + positionInList, + } = moveData; const { fullBoardId, filterParams, @@ -562,6 +580,8 @@ export default { }, } = state; + commit(types.MUTATE_ISSUE_IN_PROGRESS, true); + const { data } = await gqlClient.mutate({ mutation: issueMoveListMutation, variables: { @@ -572,6 +592,7 @@ export default { toListId: getIdFromGraphQLId(toListId), moveBeforeId: moveBeforeId ? getIdFromGraphQLId(moveBeforeId) : undefined, moveAfterId: moveAfterId ? getIdFromGraphQLId(moveAfterId) : undefined, + positionInList, // 'mutationVariables' allows EE code to pass in extra parameters. ...mutationVariables, }, @@ -643,7 +664,9 @@ export default { } commit(types.MUTATE_ISSUE_SUCCESS, { issue: data.issueMoveList.issue }); + commit(types.MUTATE_ISSUE_IN_PROGRESS, false); } catch { + commit(types.MUTATE_ISSUE_IN_PROGRESS, false); commit( types.SET_ERROR, s__('Boards|An error occurred while moving the issue. Please try again.'), diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 43268f21f962f6..0e496677b7b20a 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -44,3 +44,4 @@ export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS'; export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS'; export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION'; export const SET_ERROR = 'SET_ERROR'; +export const MUTATE_ISSUE_IN_PROGRESS = 'MUTATE_ISSUE_IN_PROGRESS'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 26a98a645b367c..44abb2030c72bf 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -20,17 +20,28 @@ export const removeItemFromList = ({ state, listId, itemId }) => { updateListItemsCount({ state, listId, value: -1 }); }; -export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId, atIndex }) => { +export const addItemToList = ({ + state, + listId, + itemId, + moveBeforeId, + moveAfterId, + atIndex, + positionInList, +}) => { const listIssues = state.boardItemsByListId[listId]; let newIndex = atIndex || 0; + const moveToStartOrLast = positionInList !== undefined; if (moveBeforeId) { newIndex = listIssues.indexOf(moveBeforeId) + 1; } else if (moveAfterId) { newIndex = listIssues.indexOf(moveAfterId); + } else if (moveToStartOrLast) { + newIndex = positionInList === -1 ? listIssues.length : 0; } listIssues.splice(newIndex, 0, itemId); Vue.set(state.boardItemsByListId, listId, listIssues); - updateListItemsCount({ state, listId, value: 1 }); + updateListItemsCount({ state, listId, value: moveToStartOrLast ? 0 : 1 }); }; export default { @@ -205,12 +216,34 @@ export default { Vue.set(state.boardItems, issue.id, formatIssue(issue)); }, + [mutationTypes.MUTATE_ISSUE_IN_PROGRESS](state, isLoading) { + state.isUpdateIssueOrderInProgress = isLoading; + }, + [mutationTypes.ADD_BOARD_ITEM_TO_LIST]: ( state, - { itemId, listId, moveBeforeId, moveAfterId, atIndex, inProgress = false }, + { + itemId, + listId, + moveBeforeId, + moveAfterId, + atIndex, + positionInList, + allItemsLoadedInList, + inProgress = false, + }, ) => { Vue.set(state.listsFlags, listId, { ...state.listsFlags, addItemToListInProgress: inProgress }); - addItemToList({ state, listId, itemId, moveBeforeId, moveAfterId, atIndex }); + addItemToList({ + state, + listId, + itemId, + moveBeforeId, + moveAfterId, + atIndex, + positionInList, + allItemsLoadedInList, + }); }, [mutationTypes.REMOVE_BOARD_ITEM_FROM_LIST]: (state, { itemId, listId }) => { diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js index b62c032b921dc4..bf3f777ea7d365 100644 --- a/app/assets/javascripts/boards/stores/state.js +++ b/app/assets/javascripts/boards/stores/state.js @@ -40,4 +40,5 @@ export default () => ({ }, // TODO: remove after ce/ee split of board_content.vue isShowingEpicsSwimlanes: false, + isUpdateIssueOrderInProgress: false, }); diff --git a/ee/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql b/ee/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql index eae6f1e6058d0d..7820b35559c4c1 100644 --- a/ee/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql +++ b/ee/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql @@ -8,6 +8,7 @@ mutation issueMoveListEE( $toListId: ID $moveBeforeId: ID $moveAfterId: ID + $positionInList: Int $epicId: EpicID ) { issueMoveList( @@ -19,6 +20,7 @@ mutation issueMoveListEE( toListId: $toListId moveBeforeId: $moveBeforeId moveAfterId: $moveAfterId + positionInList: $positionInList epicId: $epicId } ) { diff --git a/ee/app/assets/javascripts/boards/stores/mutations.js b/ee/app/assets/javascripts/boards/stores/mutations.js index 30e9e75c36e80e..bbcf7e5d0a3183 100644 --- a/ee/app/assets/javascripts/boards/stores/mutations.js +++ b/ee/app/assets/javascripts/boards/stores/mutations.js @@ -155,7 +155,7 @@ export default { [mutationTypes.MOVE_EPIC]: ( state, - { originalEpic, fromListId, toListId, moveBeforeId, moveAfterId }, + { originalEpic, fromListId, toListId, moveBeforeId, moveAfterId, listPosition }, ) => { const fromList = state.boardLists[fromListId]; const toList = state.boardLists[toListId]; @@ -164,7 +164,14 @@ export default { Vue.set(state.boardItems, epic.id, epic); removeItemFromList({ state, listId: fromListId, itemId: epic.id }); - addItemToList({ state, listId: toListId, itemId: epic.id, moveBeforeId, moveAfterId }); + addItemToList({ + state, + listId: toListId, + itemId: epic.id, + moveBeforeId, + moveAfterId, + listPosition, + }); }, [mutationTypes.MOVE_EPIC_FAILURE]: ( diff --git a/spec/frontend/boards/components/board_card_move_to_position_spec.js b/spec/frontend/boards/components/board_card_move_to_position_spec.js index 01bad53d9e198c..7254b9486efb90 100644 --- a/spec/frontend/boards/components/board_card_move_to_position_spec.js +++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js @@ -1,8 +1,8 @@ +import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; import { mockList, mockIssue2, mockIssue, mockIssue3, mockIssue4 } from 'jest/boards/mock_data'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; @@ -19,6 +19,7 @@ describe('Board Card Move to position', () => { let trackingSpy; let store; let dispatch; + const itemIndex = 1; const createStoreOptions = () => { const state = { @@ -42,7 +43,7 @@ describe('Board Card Move to position', () => { }; const createComponent = (propsData) => { - wrapper = shallowMountExtended(BoardCardMoveToPosition, { + wrapper = shallowMount(BoardCardMoveToPosition, { store, propsData: { item: mockIssue2, @@ -66,7 +67,6 @@ describe('Board Card Move to position', () => { wrapper.destroy(); }); - const findEllipsesButton = () => wrapper.findByTestId('move-card-dropdown'); const findMoveToPositionDropdown = () => wrapper.findComponent(GlDropdown); const findDropdownItems = () => findMoveToPositionDropdown().findAllComponents(GlDropdownItem); const findDropdownItemAtIndex = (index) => findDropdownItems().at(index); @@ -74,7 +74,7 @@ describe('Board Card Move to position', () => { describe('Dropdown', () => { describe('Dropdown button', () => { it('has an icon with vertical ellipsis', () => { - expect(findEllipsesButton().exists()).toBe(true); + expect(findMoveToPositionDropdown().exists()).toBe(true); expect(findMoveToPositionDropdown().props('icon')).toBe('ellipsis_v'); }); @@ -82,24 +82,11 @@ describe('Board Card Move to position', () => { findMoveToPositionDropdown().vm.$emit('click'); expect(findDropdownItems()).toHaveLength(dropdownOptions.length); }); - - it('is opened on the click of vertical ellipsis and has 1 dropdown items when number of list items > 10', () => { - wrapper.destroy(); - - createComponent({ - list: { - ...mockList, - id: 'gid://gitlab/List/2', - }, - }); - findMoveToPositionDropdown().vm.$emit('click'); - expect(findDropdownItems()).toHaveLength(1); - }); }); describe('Dropdown options', () => { beforeEach(() => { - createComponent({ index: 1 }); + createComponent({ index: itemIndex }); trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); dispatch = jest.spyOn(store, 'dispatch').mockImplementation(() => {}); }); @@ -109,13 +96,13 @@ describe('Board Card Move to position', () => { }); it.each` - dropdownIndex | dropdownLabel | trackLabel | moveAfterId | moveBeforeId - ${0} | ${BoardCardMoveToPosition.i18n.moveToStartText} | ${'move_to_start'} | ${mockIssue.id} | ${undefined} - ${1} | ${BoardCardMoveToPosition.i18n.moveToEndText} | ${'move_to_end'} | ${undefined} | ${mockIssue4.id} + dropdownIndex | dropdownLabel | trackLabel | positionInList + ${0} | ${BoardCardMoveToPosition.i18n.moveToStartText} | ${'move_to_start'} | ${0} + ${1} | ${BoardCardMoveToPosition.i18n.moveToEndText} | ${'move_to_end'} | ${-1} `( 'on click of dropdown index $dropdownIndex with label $dropdownLabel should call moveItem action with tracking label $trackLabel', - async ({ dropdownIndex, dropdownLabel, trackLabel, moveAfterId, moveBeforeId }) => { - await findEllipsesButton().vm.$emit('click'); + async ({ dropdownIndex, dropdownLabel, trackLabel, positionInList }) => { + await findMoveToPositionDropdown().vm.$emit('click'); expect(findDropdownItemAtIndex(dropdownIndex).text()).toBe(dropdownLabel); await findDropdownItemAtIndex(dropdownIndex).vm.$emit('click', { @@ -134,9 +121,10 @@ describe('Board Card Move to position', () => { itemId: mockIssue2.id, itemIid: mockIssue2.iid, itemPath: mockIssue2.referencePath, - moveBeforeId, - moveAfterId, + positionInList, toListId: mockList.id, + allItemsLoadedInList: true, + atIndex: itemIndex, }); }, ); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index e48b946ff1b599..e919300228abc7 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1056,6 +1056,8 @@ describe('moveIssueCard and undoMoveIssueCard', () => { originalIndex = 0, moveBeforeId = undefined, moveAfterId = undefined, + allItemsLoadedInList = true, + listPosition = undefined, } = {}) => { state = { boardLists: { @@ -1065,12 +1067,28 @@ describe('moveIssueCard and undoMoveIssueCard', () => { boardItems: { [itemId]: originalIssue }, boardItemsByListId: { [fromListId]: [123] }, }; - params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId }; + params = { + itemId, + fromListId, + toListId, + moveBeforeId, + moveAfterId, + listPosition, + allItemsLoadedInList, + }; moveMutations = [ { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, { type: types.ADD_BOARD_ITEM_TO_LIST, - payload: { itemId, listId: toListId, moveBeforeId, moveAfterId }, + payload: { + itemId, + listId: toListId, + moveBeforeId, + moveAfterId, + listPosition, + allItemsLoadedInList, + atIndex: originalIndex, + }, }, ]; undoMutations = [ @@ -1365,10 +1383,18 @@ describe('updateIssueOrder', () => { { moveData }, state, [ + { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: true, + }, { type: types.MUTATE_ISSUE_SUCCESS, payload: { issue: rawIssue }, }, + { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: false, + }, ], [], ); @@ -1389,6 +1415,14 @@ describe('updateIssueOrder', () => { { moveData }, state, [ + { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: true, + }, + { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: false, + }, { type: types.SET_ERROR, payload: 'An error occurred while moving the issue. Please try again.', diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index 1606ca09d8f76c..87a183c04415b5 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -513,6 +513,31 @@ describe('Board Store Mutations', () => { listState: [mockIssue2.id, mockIssue.id], }, ], + [ + 'to the top of the list', + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + positionInList: 0, + atIndex: 1, + }, + listState: [mockIssue2.id, mockIssue.id], + }, + ], + [ + 'to the bottom of the list when the list is fully loaded', + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + positionInList: -1, + atIndex: 0, + allItemsLoadedInList: true, + }, + listState: [mockIssue.id, mockIssue2.id], + }, + ], ])(`inserts an item into a list %s`, (_, { payload, listState }) => { mutations.ADD_BOARD_ITEM_TO_LIST(state, payload); -- GitLab