diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-23 18:08:01 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-23 18:08:01 +0300 |
commit | abdb550f6937ce69ec38954f24ef221d07637438 (patch) | |
tree | a8f2136768637c628f544f567fa57d63c5cf6e3b | |
parent | e6d048d769240760008f0dbb6b811e1ebc675292 (diff) |
Add latest changes from gitlab-org/gitlab@master
99 files changed, 1314 insertions, 352 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index c6d2d48db9b..1630d59c17a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1017,3 +1017,6 @@ RSpec/FactoryBot/LocalStaticAssignment: Include: - spec/factories/**/*.rb - ee/spec/factories/**/*.rb + +Rails/TransactionExitStatement: + Enabled: true diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml index 3a81130a47c..98cedc10bbe 100644 --- a/.rubocop_todo/gitlab/namespaced_class.yml +++ b/.rubocop_todo/gitlab/namespaced_class.yml @@ -1274,5 +1274,5 @@ Gitlab/NamespacedClass: - 'spec/support/helpers/require_migration.rb' - 'spec/support/models/merge_request_without_merge_request_diff.rb' - 'spec/support/renameable_upload.rb' - - 'spec/tasks/gitlab/task_helpers_spec.rb' + - 'spec/lib/gitlab/task_helpers_spec.rb' - 'spec/uploaders/object_storage_spec.rb' diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index bdd3a15489d..b74cb450320 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -2378,7 +2378,7 @@ Layout/ArgumentAlignment: - 'spec/support/shared_examples/integrations/integration_settings_form.rb' - 'spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb' - 'spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb' - - 'spec/tasks/cache/clear/redis_spec.rb' + - 'spec/tasks/cache_rake_spec.rb' - 'spec/tasks/gitlab/cleanup_rake_spec.rb' - 'spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb' - 'spec/tasks/gitlab/db/truncate_legacy_tables_rake_spec.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 3e29279693e..221b5cc0f1e 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -5195,7 +5195,7 @@ Layout/LineLength: - 'spec/tasks/gitlab/db/validate_config_rake_spec.rb' - 'spec/tasks/gitlab/db_rake_spec.rb' - 'spec/tasks/gitlab/external_diffs_rake_spec.rb' - - 'spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb' + - 'spec/tasks/gitlab/generate_sample_prometheus_data_rake_spec.rb' - 'spec/tasks/gitlab/gitaly_rake_spec.rb' - 'spec/tasks/gitlab/ldap_rake_spec.rb' - 'spec/tasks/gitlab/lfs/check_rake_spec.rb' @@ -5205,7 +5205,7 @@ Layout/LineLength: - 'spec/tasks/gitlab/smtp_rake_spec.rb' - 'spec/tasks/gitlab/snippets_rake_spec.rb' - 'spec/tasks/gitlab/storage_rake_spec.rb' - - 'spec/tasks/gitlab/task_helpers_spec.rb' + - 'spec/lib/gitlab/task_helpers_spec.rb' - 'spec/tasks/gitlab/terraform/migrate_rake_spec.rb' - 'spec/tasks/gitlab/uploads/check_rake_spec.rb' - 'spec/tasks/gitlab/workhorse_rake_spec.rb' diff --git a/.rubocop_todo/lint/mixed_regexp_capture_types.yml b/.rubocop_todo/lint/mixed_regexp_capture_types.yml index 70f3773eb31..b2af57158de 100644 --- a/.rubocop_todo/lint/mixed_regexp_capture_types.yml +++ b/.rubocop_todo/lint/mixed_regexp_capture_types.yml @@ -2,6 +2,7 @@ Lint/MixedRegexpCaptureTypes: Exclude: - 'app/models/alert_management/alert.rb' + - 'app/models/integrations/clickup.rb' - 'app/models/integrations/ewm.rb' - 'app/uploaders/file_uploader.rb' - 'ee/lib/gitlab/code_owners/reference_extractor.rb' diff --git a/.rubocop_todo/rails/file_path.yml b/.rubocop_todo/rails/file_path.yml index e536c22e518..0214b471e59 100644 --- a/.rubocop_todo/rails/file_path.yml +++ b/.rubocop_todo/rails/file_path.yml @@ -126,5 +126,5 @@ Rails/FilePath: - 'spec/support/shared_examples/models/application_setting_shared_examples.rb' - 'spec/support/shared_examples/models/wiki_shared_examples.rb' - 'spec/tasks/gitlab/db_rake_spec.rb' - - 'spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb' + - 'spec/tasks/gitlab/generate_sample_prometheus_data_rake_spec.rb' - 'spec/tasks/gitlab/usage_data_rake_spec.rb' diff --git a/.rubocop_todo/rails/transaction_exit_statement.yml b/.rubocop_todo/rails/transaction_exit_statement.yml new file mode 100644 index 00000000000..695e696f776 --- /dev/null +++ b/.rubocop_todo/rails/transaction_exit_statement.yml @@ -0,0 +1,3 @@ +--- +Rails/TransactionExitStatement: + Details: grace period diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index a2886ff9679..3355be60b49 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -3118,7 +3118,7 @@ RSpec/ContextWording: - 'spec/support_specs/helpers/migrations_helpers_spec.rb' - 'spec/support_specs/helpers/stub_feature_flags_spec.rb' - 'spec/support_specs/helpers/stub_method_calls_spec.rb' - - 'spec/tasks/cache/clear/redis_spec.rb' + - 'spec/tasks/cache_rake_spec.rb' - 'spec/tasks/dev_rake_spec.rb' - 'spec/tasks/gitlab/cleanup_rake_spec.rb' - 'spec/tasks/gitlab/db/validate_config_rake_spec.rb' @@ -3128,7 +3128,7 @@ RSpec/ContextWording: - 'spec/tasks/gitlab/lfs/migrate_rake_spec.rb' - 'spec/tasks/gitlab/packages/migrate_rake_spec.rb' - 'spec/tasks/gitlab/storage_rake_spec.rb' - - 'spec/tasks/gitlab/task_helpers_spec.rb' + - 'spec/lib/gitlab/task_helpers_spec.rb' - 'spec/tasks/gitlab/terraform/migrate_rake_spec.rb' - 'spec/tasks/gitlab/workhorse_rake_spec.rb' - 'spec/tooling/danger/project_helper_spec.rb' diff --git a/.rubocop_todo/rspec/expect_in_hook.yml b/.rubocop_todo/rspec/expect_in_hook.yml index 4003908c97e..ceeffd1f098 100644 --- a/.rubocop_todo/rspec/expect_in_hook.yml +++ b/.rubocop_todo/rspec/expect_in_hook.yml @@ -469,7 +469,7 @@ RSpec/ExpectInHook: - 'spec/tasks/gitlab/cleanup_rake_spec.rb' - 'spec/tasks/gitlab/gitaly_rake_spec.rb' - 'spec/tasks/gitlab/praefect_rake_spec.rb' - - 'spec/tasks/gitlab/task_helpers_spec.rb' + - 'spec/lib/gitlab/task_helpers_spec.rb' - 'spec/tooling/danger/feature_flag_spec.rb' - 'spec/tooling/rspec_flaky/listener_spec.rb' - 'spec/uploaders/file_mover_spec.rb' diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index 0a77e2d8b15..f4ecd85bd47 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -5607,7 +5607,7 @@ RSpec/MissingFeatureCategory: - 'spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb' - 'spec/support_specs/time_travel_spec.rb' - 'spec/tasks/admin_mode_spec.rb' - - 'spec/tasks/config_lint_spec.rb' + - 'spec/tasks/config_lint_rake_spec.rb' - 'spec/tasks/dev_rake_spec.rb' - 'spec/tasks/gitlab/artifacts/check_rake_spec.rb' - 'spec/tasks/gitlab/artifacts/migrate_rake_spec.rb' @@ -5615,7 +5615,7 @@ RSpec/MissingFeatureCategory: - 'spec/tasks/gitlab/container_registry_rake_spec.rb' - 'spec/tasks/gitlab/dependency_proxy/migrate_rake_spec.rb' - 'spec/tasks/gitlab/external_diffs_rake_spec.rb' - - 'spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb' + - 'spec/tasks/gitlab/generate_sample_prometheus_data_rake_spec.rb' - 'spec/tasks/gitlab/git_rake_spec.rb' - 'spec/tasks/gitlab/gitaly_rake_spec.rb' - 'spec/tasks/gitlab/ldap_rake_spec.rb' @@ -5631,7 +5631,7 @@ RSpec/MissingFeatureCategory: - 'spec/tasks/gitlab/sidekiq_rake_spec.rb' - 'spec/tasks/gitlab/smtp_rake_spec.rb' - 'spec/tasks/gitlab/snippets_rake_spec.rb' - - 'spec/tasks/gitlab/task_helpers_spec.rb' + - 'spec/lib/gitlab/task_helpers_spec.rb' - 'spec/tasks/gitlab/terraform/migrate_rake_spec.rb' - 'spec/tasks/gitlab/uploads/check_rake_spec.rb' - 'spec/tasks/gitlab/uploads/migrate_rake_spec.rb' @@ -5640,7 +5640,7 @@ RSpec/MissingFeatureCategory: - 'spec/tasks/gitlab/x509/update_rake_spec.rb' - 'spec/tasks/migrate/schema_check_rake_spec.rb' - 'spec/tasks/rubocop_rake_spec.rb' - - 'spec/tasks/tokens_spec.rb' + - 'spec/tasks/tokens_rake_spec.rb' - 'spec/tooling/danger/config_files_spec.rb' - 'spec/tooling/danger/customer_success_spec.rb' - 'spec/tooling/danger/datateam_spec.rb' diff --git a/.rubocop_todo/style/numbered_parameters.yml b/.rubocop_todo/style/numbered_parameters.yml index fc08515bc2f..d73fab2a86a 100644 --- a/.rubocop_todo/style/numbered_parameters.yml +++ b/.rubocop_todo/style/numbered_parameters.yml @@ -15,6 +15,7 @@ Style/NumberedParameters: - 'app/models/concerns/integrations/reset_secret_fields.rb' - 'app/models/hooks/web_hook.rb' - 'app/models/integration.rb' + - 'app/models/integrations/clickup.rb' - 'app/models/integrations/datadog.rb' - 'app/models/integrations/youtrack.rb' - 'app/models/project.rb' diff --git a/.rubocop_todo/style/percent_literal_delimiters.yml b/.rubocop_todo/style/percent_literal_delimiters.yml index 496f05c7cab..9ad2fb06ae3 100644 --- a/.rubocop_todo/style/percent_literal_delimiters.yml +++ b/.rubocop_todo/style/percent_literal_delimiters.yml @@ -1081,7 +1081,7 @@ Style/PercentLiteralDelimiters: - 'spec/support_specs/helpers/active_record/query_recorder_spec.rb' - 'spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb' - 'spec/tasks/gitlab/db_rake_spec.rb' - - 'spec/tasks/gitlab/task_helpers_spec.rb' + - 'spec/lib/gitlab/task_helpers_spec.rb' - 'spec/tooling/danger/customer_success_spec.rb' - 'spec/tooling/danger/datateam_spec.rb' - 'spec/tooling/danger/sidekiq_queues_spec.rb' diff --git a/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js index fa1c70c1994..ed5be66d07a 100644 --- a/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js +++ b/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js @@ -7,30 +7,36 @@ import getPipelineEtag from './queries/client/pipeline_etag.query.graphql'; export const resolvers = { Mutation: { lintCI: (_, { endpoint, content, dry_run }) => { - return axios.post(endpoint, { content, dry_run }).then(({ data }) => ({ - valid: data.valid, - errors: data.errors, - warnings: data.warnings, - jobs: data.jobs.map((job) => { - const only = job.only ? { refs: job.only.refs, __typename: 'CiLintJobOnlyPolicy' } : null; + return axios.post(endpoint, { content, dry_run }).then(({ data }) => { + const { errors, warnings, valid, jobs } = data; - return { - name: job.name, - stage: job.stage, - beforeScript: job.before_script, - script: job.script, - afterScript: job.after_script, - tags: job.tag_list, - environment: job.environment, - when: job.when, - allowFailure: job.allow_failure, - only, - except: job.except, - __typename: 'CiLintJob', - }; - }), - __typename: 'CiLintContent', - })); + return { + valid, + errors, + warnings, + jobs: jobs.map((job) => { + const only = job.only + ? { refs: job.only.refs, __typename: 'CiLintJobOnlyPolicy' } + : null; + + return { + name: job.name, + stage: job.stage, + beforeScript: job.before_script, + script: job.script, + afterScript: job.after_script, + tags: job.tag_list, + environment: job.environment, + when: job.when, + allowFailure: job.allow_failure, + only, + except: job.except, + __typename: 'CiLintJob', + }; + }), + __typename: 'CiLintContent', + }; + }); }, updateAppStatus: (_, { appStatus }, { cache }) => { cache.writeQuery({ diff --git a/app/assets/javascripts/ci/runner/components/runner_details.vue b/app/assets/javascripts/ci/runner/components/runner_details.vue index 6eba8f2e49f..0608d63897b 100644 --- a/app/assets/javascripts/ci/runner/components/runner_details.vue +++ b/app/assets/javascripts/ci/runner/components/runner_details.vue @@ -1,11 +1,16 @@ <script> -import { GlIntersperse, GlLink } from '@gitlab/ui'; +import { GlIcon, GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; -import { s__ } from '~/locale'; +import { s__, formatNumber } from '~/locale'; import HelpPopover from '~/vue_shared/components/help_popover.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; -import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants'; +import { + ACCESS_LEVEL_REF_PROTECTED, + GROUP_TYPE, + PROJECT_TYPE, + RUNNER_MANAGERS_HELP_URL, +} from '../constants'; import RunnerDetail from './runner_detail.vue'; import RunnerGroups from './runner_groups.vue'; import RunnerProjects from './runner_projects.vue'; @@ -13,8 +18,10 @@ import RunnerTags from './runner_tags.vue'; export default { components: { + GlIcon, GlIntersperse, GlLink, + GlSprintf, HelpPopover, RunnerDetail, RunnerMaintenanceNoteDetail: () => @@ -74,8 +81,12 @@ export default { anchor: 'authentication-token-security', }); }, + runnerManagersCount() { + return formatNumber(this.runner?.managers?.count || 0); + }, }, ACCESS_LEVEL_REF_PROTECTED, + RUNNER_MANAGERS_HELP_URL, }; </script> @@ -150,6 +161,34 @@ export default { class="gl-pt-4 gl-border-t-gray-100 gl-border-t-1 gl-border-t-solid" :value="runner.maintenanceNoteHtml" /> + + <runner-detail> + <template #label> + {{ s__('Runners|Runners') }} + <help-popover> + <gl-sprintf + :message=" + s__( + 'Runners|Runners are grouped when they have the same authentication token. This happens when you re-use a runner configuration in more than one runner manager. %{linkStart}How does this work?%{linkEnd}', + ) + " + > + <template #link="{ content }" + ><gl-link + :href="$options.RUNNER_MANAGERS_HELP_URL" + target="_blank" + class="gl-reset-font-size" + >{{ content }}</gl-link + ></template + > + </gl-sprintf> + </help-popover> + </template> + <template #value> + <gl-icon name="container-image" class="gl-text-secondary" /> + {{ runnerManagersCount }} + </template> + </runner-detail> </dl> </div> diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js index 4e36a410a66..b5fcc14be37 100644 --- a/app/assets/javascripts/ci/runner/constants.js +++ b/app/assets/javascripts/ci/runner/constants.js @@ -256,3 +256,5 @@ export const SERVICE_COMMANDS_HELP_URL = export const CHANGELOG_URL = 'https://gitlab.com/gitlab-org/gitlab-runner/blob/main/CHANGELOG.md'; export const DOCKER_HELP_URL = 'https://docs.gitlab.com/runner/install/docker.html'; export const KUBERNETES_HELP_URL = 'https://docs.gitlab.com/runner/install/kubernetes.html'; +export const RUNNER_MANAGERS_HELP_URL = + 'https://docs.gitlab.com/runner/fleet_scaling/#workers-executors-and-autoscaling-capabilities'; diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql index 87d92b8e263..1a2ad59650e 100644 --- a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql +++ b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql @@ -20,6 +20,9 @@ fragment RunnerDetailsShared on CiRunner { tokenExpiresAt version editAdminUrl + managers { + count + } userPermissions { updateRunner deleteRunner diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue index 25cf5335fb5..ad78de8728f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue @@ -187,7 +187,7 @@ export default { this.updateApproval( () => this.service.approveMergeRequestWithAuth(data), (error) => { - if (error && error.response && error.response.status === HTTP_STATUS_UNAUTHORIZED) { + if (error?.response?.status === HTTP_STATUS_UNAUTHORIZED) { this.hasApprovalAuthError = true; return; } diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index 10dd18c0c86..9d7569047f6 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -26,7 +26,6 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController if project.licensed_feature_available?(:cycle_analytics_for_projects) push_licensed_feature(:cycle_analytics_for_projects) - push_frontend_feature_flag(:vsa_group_and_project_parity, @project) end end diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb index b9e777f27a0..1cb030c67c3 100644 --- a/app/models/ci/catalog/listing.rb +++ b/app/models/ci/catalog/listing.rb @@ -14,16 +14,25 @@ module Ci @current_user = current_user end - def resources - Ci::Catalog::Resource - .joins(:project).includes(:project) - .merge(projects_in_namespace_visible_to_user) + def resources(sort: nil) + case sort.to_s + when 'name_desc' then all_resources.order_by_name_desc + when 'name_asc' then all_resources.order_by_name_asc + else + all_resources.order_by_created_at_desc + end end private attr_reader :namespace, :current_user + def all_resources + Ci::Catalog::Resource + .joins(:project).includes(:project) + .merge(projects_in_namespace_visible_to_user) + end + def projects_in_namespace_visible_to_user Project .in_namespace(namespace.self_and_descendant_ids) diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb index bb4584aacae..f400b8fe046 100644 --- a/app/models/ci/catalog/resource.rb +++ b/app/models/ci/catalog/resource.rb @@ -13,6 +13,9 @@ module Ci belongs_to :project scope :for_projects, ->(project_ids) { where(project_id: project_ids) } + scope :order_by_created_at_desc, -> { reorder(created_at: :desc) } + scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) } + scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) } delegate :avatar_path, :description, :name, to: :project diff --git a/app/models/integration.rb b/app/models/integration.rb index 860739fe5aa..0f716862032 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -18,7 +18,7 @@ class Integration < ApplicationRecord self.inheritance_column = :type_new INTEGRATION_NAMES = %w[ - asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord + asana assembla bamboo bugzilla buildkite campfire clickup confluence custom_issue_tracker datadog discord drone_ci emails_on_push ewm external_wiki hangouts_chat harbor irker jira mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands squash_tm teamcity diff --git a/app/models/integrations/clickup.rb b/app/models/integrations/clickup.rb new file mode 100644 index 00000000000..7cc05d41e14 --- /dev/null +++ b/app/models/integrations/clickup.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Integrations + class Clickup < BaseIssueTracker + include HasIssueTrackerFields + + validates :project_url, :issues_url, presence: true, public_url: true, if: :activated? + + def reference_pattern(*) + @reference_pattern ||= /((#|CU-)(?<issue>[a-z0-9]+)|(?<issue>[A-Z0-9_]{2,10}-\d+))\b/ + end + + def title + 'ClickUp' + end + + def description + s_("IssueTracker|Use Clickup as this project's issue tracker.") + end + + def help + docs_link = ActionController::Base.helpers.link_to _('Learn more.'), + Rails.application.routes.url_helpers.help_page_url('user/project/integrations/clickup'), + target: '_blank', + rel: 'noopener noreferrer' + format(s_( + "IssueTracker|Use ClickUp as this project's issue tracker. %{docs_link}" + ).html_safe, docs_link: docs_link.html_safe) + end + + def self.to_param + 'clickup' + end + + def fields + super.select { _1.name.in?(%w[project_url issues_url]) } + end + end +end diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index b4f2282fd7a..f10b7872277 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -14,6 +14,14 @@ module Integrations ATLASSIAN_REFERRER_GITLAB_COM = { atlOrigin: 'eyJpIjoiY2QyZTJiZDRkNGZhNGZlMWI3NzRkNTBmZmVlNzNiZTkiLCJwIjoianN3LWdpdGxhYi1pbnQifQ' }.freeze ATLASSIAN_REFERRER_SELF_MANAGED = { atlOrigin: 'eyJpIjoiYjM0MTA4MzUyYTYxNDVkY2IwMzVjOGQ3ZWQ3NzMwM2QiLCJwIjoianN3LWdpdGxhYlNNLWludCJ9' }.freeze + API_ENDPOINTS = { + find_issue: "/rest/api/2/issue/%s", + server_info: "/rest/api/2/serverInfo", + transition_issue: "/rest/api/2/issue/%s/transitions", + issue_comments: "/rest/api/2/issue/%s/comment", + link_remote_issue: "/rest/api/2/issue/%s/remotelink" + }.freeze + SECTION_TYPE_JIRA_TRIGGER = 'jira_trigger' SECTION_TYPE_JIRA_ISSUES = 'jira_issues' @@ -277,7 +285,9 @@ module Integrations expands << 'transitions' if transitions options = { expand: expands.join(',') } if expands.any? - jira_request { client.Issue.find(issue_key, options || {}) } + path = API_ENDPOINTS[:find_issue] % issue_key + + jira_request(path) { client.Issue.find(issue_key, options || {}) } end def close_issue(entity, external_issue, current_user) @@ -389,7 +399,7 @@ module Integrations def server_info strong_memoize(:server_info) do - client_url.present? ? jira_request { client.ServerInfo.all.attrs } : nil + client_url.present? ? jira_request(API_ENDPOINTS[:server_info]) { client.ServerInfo.all.attrs } : nil end end @@ -419,7 +429,8 @@ module Integrations true rescue StandardError => e - log_exception(e, message: 'Issue transition failed', client_url: client_url) + path = API_ENDPOINTS[:transition_issue] % issue.id + log_exception(e, message: 'Issue transition failed', client_url: client_url, client_path: path, client_status: '400') false end @@ -518,7 +529,8 @@ module Integrations end def comment_exists?(issue, message) - comments = jira_request { issue.comments } + path = API_ENDPOINTS[:issue_comments] % issue.id + comments = jira_request(path) { issue.comments } comments.present? && comments.any? { |comment| comment.body.include?(message) } end @@ -526,14 +538,16 @@ module Integrations def send_message(issue, message, remote_link_props) return unless client_url.present? - jira_request do + path = API_ENDPOINTS[:link_remote_issue] % issue.id + + jira_request(path) do remote_link = find_remote_link(issue, remote_link_props[:object][:url]) create_issue_comment(issue, message) unless remote_link remote_link ||= issue.remotelink.build remote_link.save!(remote_link_props) - log_info("Successfully posted", client_url: client_url) + log_info("Successfully posted", client_url: client_url, client_path: path) "SUCCESS: Successfully posted to #{client_url}." end end @@ -545,7 +559,8 @@ module Integrations end def find_remote_link(issue, url) - links = jira_request { issue.remotelink.all } + path = API_ENDPOINTS[:link_remote_issue] % issue.id + links = jira_request(path) { issue.remotelink.all } return unless links links.find { |link| link.object["url"] == url } @@ -612,11 +627,11 @@ module Integrations end # Handle errors when doing Jira API calls - def jira_request + def jira_request(path) yield rescue StandardError => e @error = e - log_exception(e, message: 'Error sending message', client_url: client_url) + log_exception(e, message: 'Error sending message', client_url: client_url, client_path: path, client_status: e.code) nil end diff --git a/app/models/issue.rb b/app/models/issue.rb index 51deb8dea68..66c62ddf0b5 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -220,8 +220,20 @@ class Issue < ApplicationRecord project: [:project_namespace, :project_feature, :route, { group: :route }, { namespace: :route }], duplicated_to: { project: [:project_feature] }) } - scope :with_issue_type, ->(types) { where(issue_type: types) } - scope :without_issue_type, ->(types) { where.not(issue_type: types) } + scope :with_issue_type, ->(types) { + if Feature.enabled?(:issue_type_uses_work_item_types_table) + joins(:work_item_type).where(work_item_types: { base_type: types }) + else + where(issue_type: types) + end + } + scope :without_issue_type, ->(types) { + if Feature.enabled?(:issue_type_uses_work_item_types_table) + joins(:work_item_type).where.not(work_item_types: { base_type: types }) + else + where.not(issue_type: types) + end + } scope :public_only, -> { where(confidential: false) } diff --git a/app/models/project.rb b/app/models/project.rb index 462a169ea95..ef87b611fca 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -185,6 +185,7 @@ class Project < ApplicationRecord has_one :bugzilla_integration, class_name: 'Integrations::Bugzilla' has_one :buildkite_integration, class_name: 'Integrations::Buildkite' has_one :campfire_integration, class_name: 'Integrations::Campfire' + has_one :clickup_integration, class_name: 'Integrations::Clickup' has_one :confluence_integration, class_name: 'Integrations::Confluence' has_one :custom_issue_tracker_integration, class_name: 'Integrations::CustomIssueTracker' has_one :datadog_integration, class_name: 'Integrations::Datadog' @@ -602,6 +603,42 @@ class Project < ApplicationRecord .or(arel_table[:storage_version].eq(nil))) end + scope :sorted_by_name_desc, -> { + keyset_order = Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :name, + column_expression: Project.arel_table[:name], + order_expression: Project.arel_table[:name].desc, + distinct: false, + nullable: :nulls_last + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :id, + order_expression: Project.arel_table[:id].desc + ) + ]) + + reorder(keyset_order) + } + + scope :sorted_by_name_asc, -> { + keyset_order = Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :name, + column_expression: Project.arel_table[:name], + order_expression: Project.arel_table[:name].asc, + distinct: false, + nullable: :nulls_last + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: :id, + order_expression: Project.arel_table[:id].asc + ) + ]) + + reorder(keyset_order) + } + scope :sorted_by_updated_asc, -> { reorder(self.arel_table['updated_at'].asc) } scope :sorted_by_updated_desc, -> { reorder(self.arel_table['updated_at'].desc) } scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) } diff --git a/app/services/ci/delete_unit_tests_service.rb b/app/services/ci/delete_unit_tests_service.rb index 230661a107d..a2fb44ff3fc 100644 --- a/app/services/ci/delete_unit_tests_service.rb +++ b/app/services/ci/delete_unit_tests_service.rb @@ -25,9 +25,7 @@ module Ci klass.transaction do ids = klass.deletable.lock('FOR UPDATE SKIP LOCKED').limit(BATCH_SIZE).pluck(:id) - break if ids.empty? - - deleted = klass.where(id: ids).delete_all + deleted = klass.where(id: ids).delete_all if ids.any? end deleted > 0 diff --git a/app/services/concerns/update_repository_storage_methods.rb b/app/services/concerns/update_repository_storage_methods.rb index a0b4040cff7..bb43cab79bb 100644 --- a/app/services/concerns/update_repository_storage_methods.rb +++ b/app/services/concerns/update_repository_storage_methods.rb @@ -14,12 +14,16 @@ module UpdateRepositoryStorageMethods end def execute - repository_storage_move.with_lock do - return ServiceResponse.success unless repository_storage_move.scheduled? # rubocop:disable Cop/AvoidReturnFromBlocks + response = repository_storage_move.with_lock do + next ServiceResponse.success unless repository_storage_move.scheduled? repository_storage_move.start! + + nil end + return response if response + mirror_repositories unless same_filesystem? repository_storage_move.transaction do diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 3a7b577d59a..b8853e8bcbc 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -334,7 +334,7 @@ module MergeRequests strong_memoize(:issue_iid) do @params_issue_iid || begin id = if target_project.external_issue_tracker - source_branch.match(target_project.external_issue_reference_pattern).try(:[], 0) + target_project.external_issue_reference_pattern.match(source_branch).try(:[], 0) end id || source_branch.match(/\A(\d+)-/).try(:[], 1) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 8a51b210d93..e37b6516d21 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -231,7 +231,7 @@ module Projects @project.create_labels unless @project.gitlab_project_import? - break if @project.import? + next if @project.import? unless @project.create_repository(default_branch: default_branch) raise 'Failed to create repository' diff --git a/app/services/work_items/delete_task_service.rb b/app/services/work_items/delete_task_service.rb index 3d66716543a..4c0ee2f827d 100644 --- a/app/services/work_items/delete_task_service.rb +++ b/app/services/work_items/delete_task_service.rb @@ -22,7 +22,7 @@ module WorkItems current_user: @current_user ).execute - break ::ServiceResponse.error(message: replacement_result.errors, http_status: 422) if replacement_result.error? + next ::ServiceResponse.error(message: replacement_result.errors, http_status: 422) if replacement_result.error? delete_result = ::WorkItems::DeleteService.new( container: @task.project, diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index c8d4f02274b..b31e8919832 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -8,11 +8,10 @@ = form_tag network_path, method: :get, class: 'form-inline network-form' do |f| = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: _("Git revision"), class: 'search-input form-control gl-form-input input-mx-250 search-sha gl-mr-2' = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, icon: 'search') - .inline.gl-ml-5 - .form-check.light - = check_box_tag :filter_ref, 1, @options[:filter_ref], class: 'form-check-input' - = label_tag :filter_ref, class: 'form-check-label' do - %span= _("Begin with the selected commit") + .form-group{ class: 'gl-ml-5 gl-mb-n3!' } + = render Pajamas::CheckboxTagComponent.new(name: :filter_ref, checked: @options[:filter_ref]) do |c| + = c.label do + = _("Begin with the selected commit") - if @commit .network-graph.gl-bg-white.gl-overflow-scroll.gl-overflow-x-hidden{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } } diff --git a/config/feature_flags/development/record_issue_and_mr_assignee_events.yml b/config/feature_flags/development/record_issue_and_mr_assignee_events.yml deleted file mode 100644 index 83660a2a4a6..00000000000 --- a/config/feature_flags/development/record_issue_and_mr_assignee_events.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: record_issue_and_mr_assignee_events -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117545 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/397050 -milestone: '15.11' -type: development -group: group::optimize -default_enabled: true diff --git a/config/feature_flags/development/vsa_group_and_project_parity.yml b/config/feature_flags/development/vsa_group_and_project_parity.yml deleted file mode 100644 index d75621a8e5d..00000000000 --- a/config/feature_flags/development/vsa_group_and_project_parity.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: vsa_group_and_project_parity -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114056 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/396865 -milestone: '15.11' -type: development -group: group::optimize -default_enabled: true diff --git a/config/initializers/00_deprecations.rb b/config/initializers/00_deprecations.rb index 915e8d704c9..8aff095d88b 100644 --- a/config/initializers/00_deprecations.rb +++ b/config/initializers/00_deprecations.rb @@ -30,7 +30,9 @@ else # https://gitlab.com/gitlab-org/gitlab/-/issues/333086 /default_hash is deprecated/, # https://gitlab.com/gitlab-org/gitlab/-/issues/369970 - /Passing an Active Record object to `\w+` directly is deprecated/ + /Passing an Active Record object to `\w+` directly is deprecated/, + # https://gitlab.com/gitlab-org/gitlab/-/issues/410086 + /Using `return`, `break` or `throw` to exit a transaction block/ ] ActiveSupport::Deprecation.disallowed_warnings = rails7_deprecation_warnings diff --git a/config/metrics/counts_all/20230515153810_groups_clickup_active.yml b/config/metrics/counts_all/20230515153810_groups_clickup_active.yml new file mode 100644 index 00000000000..b5d999e76fd --- /dev/null +++ b/config/metrics/counts_all/20230515153810_groups_clickup_active.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.groups_clickup_active +description: Count of groups with active integrations for ClickUp +product_section: dev +product_stage: manage +product_group: integrations +value_type: number +status: active +milestone: "16.1" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120732 +time_frame: all +data_source: database +data_category: optional +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20230515153826_groups_inheriting_clickup_active.yml b/config/metrics/counts_all/20230515153826_groups_inheriting_clickup_active.yml new file mode 100644 index 00000000000..66a1efdc47b --- /dev/null +++ b/config/metrics/counts_all/20230515153826_groups_inheriting_clickup_active.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.groups_inheriting_clickup_active +description: Count of active groups inheriting integrations for ClickUp +product_section: dev +product_stage: manage +product_group: integrations +value_type: number +status: active +milestone: "16.1" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120732 +time_frame: all +data_source: database +data_category: optional +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20230515153827_instances_clickup_active.yml b/config/metrics/counts_all/20230515153827_instances_clickup_active.yml new file mode 100644 index 00000000000..de631475e5d --- /dev/null +++ b/config/metrics/counts_all/20230515153827_instances_clickup_active.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.instances_clickup_active +description: Count of active instance-level integrations for ClickUp +product_section: dev +product_stage: manage +product_group: integrations +value_type: number +status: active +milestone: "16.1" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120732 +time_frame: all +data_source: database +data_category: optional +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20230515153829_projects_clickup_active.yml b/config/metrics/counts_all/20230515153829_projects_clickup_active.yml new file mode 100644 index 00000000000..4f7c8775614 --- /dev/null +++ b/config/metrics/counts_all/20230515153829_projects_clickup_active.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.projects_clickup_active +description: Count of projects with active integrations for ClickUp +product_section: dev +product_stage: manage +product_group: integrations +value_type: number +status: active +milestone: "16.1" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120732 +time_frame: all +data_source: database +data_category: optional +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20230515153834_projects_inheriting_clickup_active.yml b/config/metrics/counts_all/20230515153834_projects_inheriting_clickup_active.yml new file mode 100644 index 00000000000..1e45317f2a5 --- /dev/null +++ b/config/metrics/counts_all/20230515153834_projects_inheriting_clickup_active.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.projects_inheriting_clickup_active +description: Count of active projects inheriting integrations for ClickUp +product_section: dev +product_stage: manage +product_group: integrations +value_type: number +status: active +milestone: "16.1" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120732 +time_frame: all +data_source: database +data_category: optional +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/data/deprecations/15-9-rails-error-tracking.yml b/data/deprecations/15-9-rails-error-tracking.yml deleted file mode 100644 index 4d9fc371213..00000000000 --- a/data/deprecations/15-9-rails-error-tracking.yml +++ /dev/null @@ -1,16 +0,0 @@ -- title: "Error Tracking UI in GitLab Rails is deprecated" - announcement_milestone: "15.9" - removal_milestone: "16.6" - breaking_change: true - reporter: kbychu - stage: monitor - issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389991 - body: | - The [Error Tracking UI](https://docs.gitlab.com/ee/operations/error_tracking.html) is deprecated in 15.9 and will be removed in 16.6 (milestone might change) once GitLab Observability UI is made available. In future versions, you should use the [GitLab Observability UI](https://gitlab.com/gitlab-org/opstrace/opstrace-ui/), which will gradually be made available on GitLab.com over the next few releases. - - During the transition to the GitLab Observability UI, we will migrate the [GitLab Observability Backend](https://gitlab.com/gitlab-org/opstrace/opstrace) from a per-cluster deployment model to a per-tenant deployment model. Because [Integrated Error Tracking](https://docs.gitlab.com/ee/operations/error_tracking.html#integrated-error-tracking) is in Open Beta, we will not migrate any existing user data. For more details about the migration, see the direction pages for: - - - [Observability](https://about.gitlab.com/direction/monitor/observability/data-visualization/). - - The [Observability Backend](https://about.gitlab.com/direction/monitor/observability/data-management/). - - [Data visualization](https://about.gitlab.com/direction/monitor/observability/data-visualization/). - documentation_url: https://docs.gitlab.com/ee/operations/error_tracking.html diff --git a/db/docs/batched_background_migrations/backfill_resource_link_events.yml b/db/docs/batched_background_migrations/backfill_resource_link_events.yml new file mode 100644 index 00000000000..224cf5fe06b --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_resource_link_events.yml @@ -0,0 +1,6 @@ +--- +migration_job_name: BackfillResourceLinkEvents +description: Backfills resource_link_events table based off system_note_metadata and notes +feature_category: team_planning +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118605 +milestone: 16.1 diff --git a/db/fixtures/development/24_forks.rb b/db/fixtures/development/24_forks.rb index 1476681f4d7..f92b7972de5 100644 --- a/db/fixtures/development/24_forks.rb +++ b/db/fixtures/development/24_forks.rb @@ -8,7 +8,7 @@ Sidekiq::Testing.inline! do ## # 03_project.rb might not have created a public project because # we use randomized approach (e.g. `Array#sample`). - return unless source_project + next unless source_project Sidekiq::Worker.skipping_transaction_check do fork_project = Projects::ForkService.new( diff --git a/db/post_migrate/20230426085615_queue_backfill_resource_link_events.rb b/db/post_migrate/20230426085615_queue_backfill_resource_link_events.rb new file mode 100644 index 00000000000..fe4ea099d2e --- /dev/null +++ b/db/post_migrate/20230426085615_queue_backfill_resource_link_events.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class QueueBackfillResourceLinkEvents < Gitlab::Database::Migration[2.1] + MIGRATION = "BackfillResourceLinkEvents" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 5000 + SUB_BATCH_SIZE = 10 + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + class SystemNoteMetadata < MigrationRecord + self.table_name = 'system_note_metadata' + + def self.batch_start_id + SystemNoteMetadata + .select(:id) + .where("action='relate_to_parent' OR action='unrelate_from_parent'") + .order(id: :asc) + .limit(1) + .first&.id + end + end + + def up + batch_min_value = SystemNoteMetadata.batch_start_id + + return unless batch_min_value + + queue_batched_background_migration( + MIGRATION, + :system_note_metadata, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE, + batch_min_value: batch_min_value + ) + end + + def down + delete_batched_background_migration(MIGRATION, :system_note_metadata, :id, []) + end +end diff --git a/db/schema_migrations/20230426085615 b/db/schema_migrations/20230426085615 new file mode 100644 index 00000000000..a0166399442 --- /dev/null +++ b/db/schema_migrations/20230426085615 @@ -0,0 +1 @@ +e331d6f2d5934cce0ac78862abb3eac5c0567be95b0865f5ac38299ee7e147ca
\ No newline at end of file diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 6384d896292..2b633e37fc3 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -74,6 +74,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | <a id="querycicatalogresourcesprojectpath"></a>`projectPath` | [`ID`](#id) | Project with the namespace catalog. | +| <a id="querycicatalogresourcessort"></a>`sort` | [`CiCatalogResourceSort`](#cicatalogresourcesort) | Sort Catalog Resources by given criteria. | ### `Query.ciConfig` @@ -23995,6 +23996,23 @@ Types of blob viewers. | <a id="blobviewerstyperich"></a>`rich` | Rich blob viewers type. | | <a id="blobviewerstypesimple"></a>`simple` | Simple blob viewers type. | +### `CiCatalogResourceSort` + +Values for sorting catalog resources. + +| Value | Description | +| ----- | ----------- | +| <a id="cicatalogresourcesortcreated_asc"></a>`CREATED_ASC` | Created at ascending order. | +| <a id="cicatalogresourcesortcreated_desc"></a>`CREATED_DESC` | Created at descending order. | +| <a id="cicatalogresourcesortname_asc"></a>`NAME_ASC` | Name by ascending order. | +| <a id="cicatalogresourcesortname_desc"></a>`NAME_DESC` | Name by descending order. | +| <a id="cicatalogresourcesortupdated_asc"></a>`UPDATED_ASC` | Updated at ascending order. | +| <a id="cicatalogresourcesortupdated_desc"></a>`UPDATED_DESC` | Updated at descending order. | +| <a id="cicatalogresourcesortcreated_asc"></a>`created_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_ASC`. | +| <a id="cicatalogresourcesortcreated_desc"></a>`created_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_DESC`. | +| <a id="cicatalogresourcesortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. | +| <a id="cicatalogresourcesortupdated_desc"></a>`updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. | + ### `CiConfigIncludeType` Include type. @@ -25652,6 +25670,7 @@ State of a Sentry error. | <a id="servicetypebugzilla_service"></a>`BUGZILLA_SERVICE` | BugzillaService type. | | <a id="servicetypebuildkite_service"></a>`BUILDKITE_SERVICE` | BuildkiteService type. | | <a id="servicetypecampfire_service"></a>`CAMPFIRE_SERVICE` | CampfireService type. | +| <a id="servicetypeclickup_service"></a>`CLICKUP_SERVICE` | ClickupService type. | | <a id="servicetypeconfluence_service"></a>`CONFLUENCE_SERVICE` | ConfluenceService type. | | <a id="servicetypecustom_issue_tracker_service"></a>`CUSTOM_ISSUE_TRACKER_SERVICE` | CustomIssueTrackerService type. | | <a id="servicetypedatadog_service"></a>`DATADOG_SERVICE` | DatadogService type. | diff --git a/doc/api/integrations.md b/doc/api/integrations.md index 5b6c4d17915..0a05759c8c2 100644 --- a/doc/api/integrations.md +++ b/doc/api/integrations.md @@ -334,6 +334,43 @@ Get Campfire integration settings for a project. GET /projects/:id/integrations/campfire ``` +## ClickUp + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120732) in GitLab 16.1. + +ClickUp issue tracker. + +### Create or edit ClickUp integration + +Set up ClickUp integration for a project. + +```plaintext +PUT /projects/:id/integrations/clickup +``` + +Parameters: + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issues_url` | string | true | Issue URL | +| `project_url` | string | true | Project URL | + +### Disable ClickUp integration + +Disable the ClickUp integration for a project. Integration settings are reset. + +```plaintext +DELETE /projects/:id/integrations/clickup +``` + +### Get ClickUp integration settings + +Get ClickUp integration settings for a project. + +```plaintext +GET /projects/:id/integrations/clickup +``` + ## Datadog Datadog system monitoring. diff --git a/doc/development/ai_features.md b/doc/development/ai_features.md index f8d19dc26bf..c1493e3f843 100644 --- a/doc/development/ai_features.md +++ b/doc/development/ai_features.md @@ -115,6 +115,7 @@ Gitlab::CurrentSettings.update(tofa_credentials: File.read('/YOUR_FILE.json')) # Note: These credential examples will not work locally for all models Gitlab::CurrentSettings.update(tofa_host: "<root-domain>") # Example: us-central1-aiplatform.googleapis.com Gitlab::CurrentSettings.update(tofa_url: "<full-api-endpoint>") # Example: https://ROOT-DOMAIN/v1/projects/MY-COOL-PROJECT/locations/us-central1/publishers/google/models/MY-SPECIAL-MODEL:predict +Gitlab::CurrentSettings.update(vertex_project: "<project-id>") # Example: cloud-large-language-models ``` Internal team members can [use this snippet](https://gitlab.com/gitlab-com/gl-infra/production/-/snippets/2541742) for help configuring these endpoints. diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index c63c2e3fd24..04073ad611f 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -28,6 +28,7 @@ To enable an external issue tracker, you must configure the appropriate [integra The following external issue tracker integrations are available: - [Bugzilla](../user/project/integrations/bugzilla.md) +- [ClickUp](../user/project/integrations/clickup.md) - [Custom Issue Tracker](../user/project/integrations/custom_issue_tracker.md) - [Engineering Workflow Management](../user/project/integrations/ewm.md) - [Jira](../integration/jira/index.md) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 18303a5f45c..5cb7b00ddc5 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -288,7 +288,7 @@ To prepare the new server: Edit `/etc/gitlab/gitlab.rb` and set the following: ```ruby - nginx['custom_gitlab_server_config'] = "location /api/v4/jobs/request {\n deny all;\n return 503;\n}\n" + nginx['custom_gitlab_server_config'] = "location = /api/v4/jobs/request {\n deny all;\n return 503;\n }\n" ``` 1. Reconfigure GitLab: @@ -319,7 +319,7 @@ To prepare the new server: 1. Edit `/etc/gitlab/gitlab.rb`, and set the following: ```ruby - nginx['custom_gitlab_server_config'] = "location /api/v4/jobs/request {\n deny all;\n return 503;\n}\n" + nginx['custom_gitlab_server_config'] = "location = /api/v4/jobs/request {\n deny all;\n return 503;\n }\n" ``` 1. Reconfigure GitLab: @@ -425,7 +425,7 @@ To prepare the new server: ```ruby # The following line must be removed - nginx['custom_gitlab_server_config'] = "location /api/v4/jobs/request {\n deny all;\n return 503;\n}\n" + nginx['custom_gitlab_server_config'] = "location = /api/v4/jobs/request {\n deny all;\n return 503;\n }\n" ``` 1. Reconfigure GitLab: diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index 8d4b45209c9..0d9e399f25c 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -744,31 +744,6 @@ Previous work helped [align the vulnerabilities calls for pipeline security tabs </div> </div> -<div class="milestone-wrapper" data-milestone="16.6"> - -## GitLab 16.6 - -<div class="deprecation breaking-change" data-milestone="16.6"> - -### Error Tracking UI in GitLab Rails is deprecated - -<div class="deprecation-notes"> -- Announced in: GitLab <span class="milestone">15.9</span> -- This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/). -- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/389991). -</div> - -The [Error Tracking UI](https://docs.gitlab.com/ee/operations/error_tracking.html) is deprecated in 15.9 and will be removed in 16.6 (milestone might change) once GitLab Observability UI is made available. In future versions, you should use the [GitLab Observability UI](https://gitlab.com/gitlab-org/opstrace/opstrace-ui/), which will gradually be made available on GitLab.com over the next few releases. - -During the transition to the GitLab Observability UI, we will migrate the [GitLab Observability Backend](https://gitlab.com/gitlab-org/opstrace/opstrace) from a per-cluster deployment model to a per-tenant deployment model. Because [Integrated Error Tracking](https://docs.gitlab.com/ee/operations/error_tracking.html#integrated-error-tracking) is in Open Beta, we will not migrate any existing user data. For more details about the migration, see the direction pages for: - -- [Observability](https://about.gitlab.com/direction/monitor/observability/data-visualization/). -- The [Observability Backend](https://about.gitlab.com/direction/monitor/observability/data-management/). -- [Data visualization](https://about.gitlab.com/direction/monitor/observability/data-visualization/). - -</div> -</div> - <div class="milestone-wrapper" data-milestone="16.5"> ## GitLab 16.5 diff --git a/doc/user/project/integrations/clickup.md b/doc/user/project/integrations/clickup.md new file mode 100644 index 00000000000..255f0c3f56b --- /dev/null +++ b/doc/user/project/integrations/clickup.md @@ -0,0 +1,53 @@ +--- +stage: Manage +group: Import and Integrate +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# ClickUp **(FREE)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120732) in GitLab 16.1. + +You can use [ClickUp](https://clickup.com/) as an external issue tracker. +To enable the ClickUp integration in a project: + +1. On the top bar, select **Main menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. +1. Select **ClickUp**. +1. Select the checkbox under **Enable integration**. +1. Fill in the required fields: + + - **Project URL**: The URL to the ClickUp project to link to this GitLab project. + - **Issue URL**: The URL to the ClickUp project issue to link to this GitLab project. + The URL must contain `:id`. GitLab replaces this ID with the issue number. + +1. Select **Save changes** or optionally select **Test settings**. + +After you have configured and enabled ClickUp, you see the ClickUp link on the GitLab project pages, +which takes you to your ClickUp project. + +For example, this is a configuration for a project named `gitlab-ci`: + +- Project URL: `https://app.clickup.com/1234567` +- Issue URL: `https://app.clickup.com/t/:id` + +You can also disable [GitLab internal issue tracking](../issues/index.md) in this project. +For more information about the steps and consequences of disabling GitLab issues, see +[Configure project visibility, features, and permissions](../settings/index.md#configure-project-visibility-features-and-permissions). + +## Reference ClickUp issues in GitLab + +You can reference your ClickUp issues using: + +- `#<ID>`, where `<ID>` is a alphanumerical string (example `#8wrtcd932`). +- `CU-<ID>`, where `<ID>` is a alphanumerical string (example `CU-8wrtcd932`). +- `<PROJECT>-<ID>`, for example `API_32-143`, where: + - `<PROJECT>` is a ClickUp list custom prefix ID. + - `<ID>` is a number. + +In links, the `CU-` part is ignored and it links to the global URL of the issue. When a custom +prefix is used in a ClickUp list, the prefix part is part of the link. + +We suggest using the `CU-` format (`CU-<ID>`) if you have both internal and external issue +trackers enabled. If you use the shorter format, and an issue with the same ID exists in the +internal issue tracker, the internal issue is linked. diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md index f7019b2eeb2..3bdb6ada00e 100644 --- a/doc/user/project/integrations/index.md +++ b/doc/user/project/integrations/index.md @@ -50,6 +50,7 @@ You can configure the following integrations. | [Bugzilla](bugzilla.md) | Use Bugzilla as the issue tracker. | **{dotted-circle}** No | | Buildkite | Run CI/CD pipelines with Buildkite. | **{check-circle}** Yes | | Campfire | Connect to chat. | **{dotted-circle}** No | +| [ClickUp](clickup.md) | Use ClickUp as the issue tracker. | **{dotted-circle}** No | | [Confluence Workspace](../../../api/integrations.md#confluence-integration) | Use Confluence Cloud Workspace as an internal wiki. | **{dotted-circle}** No | | [Custom issue tracker](custom_issue_tracker.md) | Use a custom issue tracker. | **{dotted-circle}** No | | [Datadog](../../../integration/datadog.md) | Trace your GitLab pipelines with Datadog. | **{check-circle}** Yes | diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index 8c2bcb250a9..3255c38fd25 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -276,6 +276,8 @@ Members who can push to this branch can now also force push. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35097) in GitLab 13.5, users and groups who can push to protected branches do not have to use a merge request to merge their feature branches. This means they can skip merge request approval rules. For a protected branch, you can require at least one approval by a [Code Owner](codeowners/index.md). +If a branch is protected by multiple rules, code owner approval is required if _any_ of +the applicable rules have **Required approval from code owners** enabled. To protect a new branch and enable Code Owner's approval: diff --git a/doc/user/workspace/index.md b/doc/user/workspace/index.md index 9d8fb45b5f7..647960459bf 100644 --- a/doc/user/workspace/index.md +++ b/doc/user/workspace/index.md @@ -29,31 +29,27 @@ Each workspace includes its own set of dependencies, libraries, and tools, which - In the Kubernetes cluster, install an Ingress controller of your choice (for example, `ingress-nginx`), and make that controller accessible over a domain. For example, point `*.workspaces.example.dev` and `workspaces.example.dev` to the load balancer exposed by the Ingress controller. - In the Kubernetes cluster, [install `gitlab-workspaces-proxy`](https://gitlab.com/gitlab-org/remote-development/gitlab-workspaces-proxy#installation-instructions). - In the Kubernetes cluster, [install the GitLab agent for Kubernetes](../clusters/agent/install/index.md). -- Configure remote development settings for the GitLab agent with this snippet: +- Configure remote development settings for the GitLab agent with this snippet, and update `dns_zone` as needed: - ```yaml - remote_development: - enabled: true - dns_zone: "workspaces.example.dev" - ``` + ```yaml + remote_development: + enabled: true + dns_zone: "workspaces.example.dev" + ``` - Update `dns_zone` as needed. - -- In each public project you want to use this feature for, define a [devfile](#devfile). Ensure the container images used in the devfile support [arbitrary user IDs](#arbitrary-user-ids). +- In each public project you want to use this feature for, create a [devfile](#devfile): + 1. On the top bar, select **Main menu > Projects** and find your project. + 1. In the root directory of your project, create a file named `.devfile.yaml`. You can use one of the [example configurations](#example-configurations). +- Ensure the container images used in the devfile support [arbitrary user IDs](#arbitrary-user-ids). ### Create a workspace -Prepare your projects and create a [devfile](#devfile): - -1. On the top bar, select **Main menu > Projects** and find your project. -1. In the root directory of your project, create a file named `.devfile.yaml`. - - Optional: For a quick demo, you can use one of the [example configurations](#example-definition) below. - -Next, go to the main menu to create a workspace: +To create a workspace: -1. Go to **Main Menu > Your Work > Workspaces**. -1. In the upper right, select **New workspace**. -1. From the **Select project** dropdown list, select a project with a `.devfile.yaml` file. You can only create workspaces for public projects. +1. On the top bar, select **Main menu > Your work**. +1. On the left sidebar, select **Workspaces**. +1. Select **New workspace**. +1. From the **Select project** dropdown list, [select a project with a `.devfile.yaml` file](#prerequisites). You can only create workspaces for public projects. 1. From the **Select cluster agent** dropdown list, select a cluster agent owned by the group the project belongs to. 1. In **Time before automatic termination**, enter the number of hours until the workspace automatically terminates. This timeout is a safety measure to prevent a workspace from consuming excessive resources or running indefinitely. 1. Select **Create workspace**. @@ -88,9 +84,9 @@ Only these properties are relevant to the GitLab implementation of the `containe | `endpoints` | Port mappings to expose from the container. | | `volumeMounts` | Storage volume to mount in the container. | -### Example definition +### Example configurations -The following is an example devfile: +The following is an example devfile configuration: ```yaml schemaVersion: 2.2.0 diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb index 4c37a2a5aba..8154ba36072 100644 --- a/lib/api/helpers/integrations_helpers.rb +++ b/lib/api/helpers/integrations_helpers.rb @@ -839,6 +839,20 @@ module API desc: 'The issues URL' } ], + 'clickup' => [ + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + } + ], 'slack' => [ chat_notification_settings, chat_notification_flags, @@ -968,6 +982,7 @@ module API ::Integrations::Bugzilla, ::Integrations::Buildkite, ::Integrations::Campfire, + ::Integrations::Clickup, ::Integrations::Confluence, ::Integrations::CustomIssueTracker, ::Integrations::Datadog, diff --git a/lib/gitlab/background_migration/backfill_resource_link_events.rb b/lib/gitlab/background_migration/backfill_resource_link_events.rb new file mode 100644 index 00000000000..a2499e90e1f --- /dev/null +++ b/lib/gitlab/background_migration/backfill_resource_link_events.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # Backfills resource_link_events from system_note_metadata and notes records + class BackfillResourceLinkEvents < BatchedMigrationJob + operation_name :backfill_resource_link_events + feature_category :team_planning + + # AR model for resource_link_events inlined + class ResourceLinkEvent < ApplicationRecord + self.table_name = 'resource_link_events' + + enum action: { + add: 1, + remove: 2 + } + end + + scope_to ->(relation) { relation.where("action='relate_to_parent' OR action='unrelate_from_parent'") } + + def perform + each_sub_batch do |sub_batch| + values_subquery = resource_link_event_values_query(sub_batch.select(:id).to_sql) + + connection.execute(<<~SQL) + INSERT INTO resource_link_events (action, issue_id, child_work_item_id, user_id, created_at, system_note_metadata_id) + #{values_subquery} + ON CONFLICT (system_note_metadata_id) DO NOTHING; + SQL + end + end + + def resource_link_event_values_query(ids_subquery) + <<~SQL + SELECT + CASE WHEN system_note_metadata.action='relate_to_parent' THEN #{ResourceLinkEvent.actions[:add]} + ELSE #{ResourceLinkEvent.actions[:remove]} + END AS action, + parent_issues.id AS issue_id, + notes.noteable_id AS child_work_item_id, + notes.author_id AS user_id, + system_note_metadata.created_at AS created_at, + system_note_metadata.id AS system_note_metadata_id + FROM system_note_metadata + INNER JOIN notes ON system_note_metadata.note_id = notes.id + INNER JOIN issues as work_items ON work_items.id = notes.noteable_id, + LATERAL ( + -- This lateral join searches for the id of the parent issue. + -- + -- When a child work item is added to its parent, + -- "relate_to_parent" is recorded as `system_note_metadata.action` + -- and a note records to which parent the child work item is added e.g, "added #1 (iid) as parent". + -- + -- Based on the iid of the parent extracted from the note and using the child work item's project id, + -- we can find out the id of the parent issue. + SELECT issues.id + FROM issues + WHERE + issues.project_id = work_items.project_id + AND issues.iid = CASE WHEN system_note_metadata.action='relate_to_parent' THEN substring(notes.note from 'added #(\\d+) as parent')::bigint + ELSE substring(notes.note from 'removed parent \\S+ #(\\d+)')::bigint + END + ) parent_issues + WHERE + system_note_metadata.id IN (#{ids_subquery}) + SQL + end + end + end +end diff --git a/lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml index 6e8cf15204a..de3f688bdb6 100644 --- a/lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/CF-Provision.gitlab-ci.yml @@ -7,7 +7,7 @@ cloud_formation: script: - gl-cloudformation create-stack rules: - - if: '($AUTO_DEVOPS_PLATFORM_TARGET != "EC2") || ($AUTO_DEVOPS_PLATFORM_TARGET != "ECS")' + - if: '($AUTO_DEVOPS_PLATFORM_TARGET != "EC2") && ($AUTO_DEVOPS_PLATFORM_TARGET != "ECS")' when: never - if: '$CI_KUBERNETES_ACTIVE || $KUBECONFIG' when: never diff --git a/lib/gitlab/database/background_migration/batched_job.rb b/lib/gitlab/database/background_migration/batched_job.rb index 523ab2a9f27..458b099924b 100644 --- a/lib/gitlab/database/background_migration/batched_job.rb +++ b/lib/gitlab/database/background_migration/batched_job.rb @@ -139,7 +139,7 @@ module Gitlab new_batch_size = batch_size / 2 - break update!(attempts: 0) if new_batch_size < 1 + next update!(attempts: 0) if new_batch_size < 1 batching_strategy = batched_migration.batch_class.new(connection: self.class.connection) next_batch_bounds = batching_strategy.next_batch( diff --git a/lib/gitlab/resource_events/assignment_event_recorder.rb b/lib/gitlab/resource_events/assignment_event_recorder.rb index 94bd05a17ba..0f1ceeb2a66 100644 --- a/lib/gitlab/resource_events/assignment_event_recorder.rb +++ b/lib/gitlab/resource_events/assignment_event_recorder.rb @@ -11,8 +11,6 @@ module Gitlab end def record - return if Feature.disabled?(:record_issue_and_mr_assignee_events, parent.project) - case parent when Issue record_for_parent( diff --git a/lib/sidebars/projects/menus/monitor_menu.rb b/lib/sidebars/projects/menus/monitor_menu.rb index f1fc9f70ef8..a74448d0bdc 100644 --- a/lib/sidebars/projects/menus/monitor_menu.rb +++ b/lib/sidebars/projects/menus/monitor_menu.rb @@ -8,7 +8,6 @@ module Sidebars def configure_menu_items return false unless feature_enabled? - add_item(metrics_dashboard_menu_item) add_item(error_tracking_menu_item) add_item(alert_management_menu_item) add_item(incidents_menu_item) @@ -49,23 +48,6 @@ module Sidebars context.project.feature_available?(:monitor, context.current_user) end - def metrics_dashboard_menu_item - return ::Sidebars::NilMenuItem.new(item_id: :metrics) if Feature.enabled?(:remove_monitor_metrics) - - unless can?(context.current_user, :metrics_dashboard, context.project) - return ::Sidebars::NilMenuItem.new(item_id: :metrics) - end - - ::Sidebars::MenuItem.new( - title: _('Metrics'), - link: project_metrics_dashboard_path(context.project), - super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::MonitorMenu, - active_routes: { path: 'metrics_dashboard#show' }, - container_html_options: { class: 'shortcuts-metrics' }, - item_id: :metrics - ) - end - def error_tracking_menu_item unless can?(context.current_user, :read_sentry_issue, context.project) return ::Sidebars::NilMenuItem.new(item_id: :error_tracking) diff --git a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb index fb56f6f3792..6e64ac01ffa 100644 --- a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb +++ b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb @@ -17,7 +17,6 @@ module Sidebars override :configure_menu_items def configure_menu_items [ - :metrics, :error_tracking, :alert_management, :incidents, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 437f090921d..accf2afca30 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -24926,6 +24926,12 @@ msgstr "" msgid "IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}" msgstr "" +msgid "IssueTracker|Use ClickUp as this project's issue tracker. %{docs_link}" +msgstr "" + +msgid "IssueTracker|Use Clickup as this project's issue tracker." +msgstr "" + msgid "IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker." msgstr "" @@ -39395,6 +39401,9 @@ msgstr "" msgid "Runners|Runners are either:" msgstr "" +msgid "Runners|Runners are grouped when they have the same authentication token. This happens when you re-use a runner configuration in more than one runner manager. %{linkStart}How does this work?%{linkEnd}" +msgstr "" + msgid "Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner." msgstr "" diff --git a/scripts/verify-tff-mapping b/scripts/verify-tff-mapping index 86ab7548b19..496e0b75f84 100755 --- a/scripts/verify-tff-mapping +++ b/scripts/verify-tff-mapping @@ -220,6 +220,17 @@ tests = [ explanation: 'https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/1683#note_1385966977', source: 'app/finders/members_finder.rb', expected: ['spec/finders/members_finder_spec.rb', 'spec/graphql/types/project_member_relation_enum_spec.rb'] + }, + + { + explanation: 'Map FOSS rake tasks', + source: 'lib/tasks/import.rake', + expected: ['spec/tasks/import_rake_spec.rb'] + }, + { + explanation: 'Map EE rake tasks', + source: 'ee/lib/tasks/geo.rake', + expected: ['ee/spec/tasks/geo_rake_spec.rb'] } ] diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb index 10568d7f1cd..74545c6eec1 100644 --- a/spec/factories/integrations.rb +++ b/spec/factories/integrations.rb @@ -197,6 +197,12 @@ FactoryBot.define do issue_tracker end + factory :clickup_integration, class: 'Integrations::Clickup' do + project + active { true } + issue_tracker + end + trait :issue_tracker do transient do create_data { true } diff --git a/spec/features/monitor_sidebar_link_spec.rb b/spec/features/monitor_sidebar_link_spec.rb index 6a1413c04f6..6e464cb8752 100644 --- a/spec/features/monitor_sidebar_link_spec.rb +++ b/spec/features/monitor_sidebar_link_spec.rb @@ -11,7 +11,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category before do project.add_role(user, role) if role sign_in(user) - stub_feature_flags(remove_monitor_metrics: false) end shared_examples 'shows Monitor menu based on the access level' do @@ -53,7 +52,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category expect(page).to have_link('Incidents', href: project_incidents_path(project)) expect(page).to have_link('Environments', href: project_environments_path(project)) - expect(page).not_to have_link('Metrics', href: project_metrics_dashboard_path(project)) expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project)) expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project)) expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project)) @@ -85,7 +83,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category expect(page).to have_link('Incidents', href: project_incidents_path(project)) expect(page).to have_link('Environments', href: project_environments_path(project)) - expect(page).not_to have_link('Metrics', href: project_metrics_dashboard_path(project)) expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project)) expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project)) expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project)) @@ -99,7 +96,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category it 'has the correct `Monitor` menu items' do visit project_issues_path(project) - expect(page).to have_link('Metrics', href: project_metrics_dashboard_path(project)) expect(page).to have_link('Incidents', href: project_incidents_path(project)) expect(page).to have_link('Environments', href: project_environments_path(project)) expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project)) @@ -116,7 +112,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category it 'has the correct `Monitor` menu items' do visit project_issues_path(project) - expect(page).to have_link('Metrics', href: project_metrics_dashboard_path(project)) expect(page).to have_link('Alerts', href: project_alert_management_index_path(project)) expect(page).to have_link('Incidents', href: project_incidents_path(project)) expect(page).to have_link('Environments', href: project_environments_path(project)) @@ -132,7 +127,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures, feature_category it 'has the correct `Monitor` menu items' do visit project_issues_path(project) - expect(page).to have_link('Metrics', href: project_metrics_dashboard_path(project)) expect(page).to have_link('Alerts', href: project_alert_management_index_path(project)) expect(page).to have_link('Incidents', href: project_incidents_path(project)) expect(page).to have_link('Environments', href: project_environments_path(project)) diff --git a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb index d2c48cb2af0..9fc91e03c94 100644 --- a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb +++ b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb @@ -89,4 +89,5 @@ RSpec.describe 'User activates issue tracker', :js, feature_category: :integrati it_behaves_like 'external issue tracker activation', tracker: 'Bugzilla' it_behaves_like 'external issue tracker activation', tracker: 'Custom issue tracker' it_behaves_like 'external issue tracker activation', tracker: 'EWM', skip_test: true + it_behaves_like 'external issue tracker activation', tracker: 'ClickUp', skip_new_issue_url: true end diff --git a/spec/features/projects/integrations/user_activates_jira_spec.rb b/spec/features/projects/integrations/user_activates_jira_spec.rb index e4b10aeb340..03d5e68d2aa 100644 --- a/spec/features/projects/integrations/user_activates_jira_spec.rb +++ b/spec/features/projects/integrations/user_activates_jira_spec.rb @@ -48,7 +48,7 @@ RSpec.describe 'User activates Jira', :js, feature_category: :integrations do it 'activates the Jira integration' do stub_request(:get, test_url).with(basic_auth: %w(username password)) - .to_raise(JIRA::HTTPError.new(double(message: 'message'))) + .to_raise(JIRA::HTTPError.new(double(message: 'message', code: '200'))) visit_project_integration('Jira') fill_form diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb index 532dd7d0a84..31f4e9dcf95 100644 --- a/spec/features/projects/navbar_spec.rb +++ b/spec/features/projects/navbar_spec.rb @@ -20,7 +20,6 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :projects do stub_config(registry: { enabled: false }) stub_feature_flags(harbor_registry_integration: false) stub_feature_flags(ml_experiment_tracking: false) - stub_feature_flags(remove_monitor_metrics: false) insert_package_nav(_('Deployments')) insert_infrastructure_registry_nav insert_infrastructure_google_cloud_nav diff --git a/spec/features/projects/settings/monitor_settings_spec.rb b/spec/features/projects/settings/monitor_settings_spec.rb index 1367ffb0009..89fee1cdb49 100644 --- a/spec/features/projects/settings/monitor_settings_spec.rb +++ b/spec/features/projects/settings/monitor_settings_spec.rb @@ -19,8 +19,8 @@ RSpec.describe 'Projects > Settings > For a forked project', :js, feature_catego visit project_path(project) wait_for_requests - expect(page).to have_selector('.sidebar-sub-level-items a[aria-label="Monitor"]', - text: 'Monitor', visible: :hidden) + expect(page).to have_selector('.sidebar-sub-level-items a[aria-label="Error Tracking"]', + text: 'Error Tracking', visible: :hidden) end end diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb index 1d4ab242308..e90e540ae32 100644 --- a/spec/features/projects/user_uses_shortcuts_spec.rb +++ b/spec/features/projects/user_uses_shortcuts_spec.rb @@ -9,7 +9,6 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :projects do before do sign_in(user) - stub_feature_flags(remove_monitor_metrics: false) visit(project_path(project)) @@ -183,16 +182,6 @@ RSpec.describe 'User uses shortcuts', :js, feature_category: :projects do end end - context 'when navigating to the Monitor pages' do - it 'redirects to the Metrics page' do - find('body').native.send_key('g') - find('body').native.send_key('l') - - expect(page).to have_active_navigation('Monitor') - expect(page).to have_active_sub_navigation('Metrics') - end - end - context 'when navigating to the Infrastructure pages' do it 'redirects to the Kubernetes page' do find('body').native.send_key('g') diff --git a/spec/frontend/ci/ci_lint/mock_data.js b/spec/frontend/ci/ci_lint/mock_data.js index 05582470dfa..1a9888817d0 100644 --- a/spec/frontend/ci/ci_lint/mock_data.js +++ b/spec/frontend/ci/ci_lint/mock_data.js @@ -1,4 +1,5 @@ import { mockJobs } from 'jest/ci/pipeline_editor/mock_data'; +import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils'; export const mockLintDataError = { data: { @@ -6,7 +7,11 @@ export const mockLintDataError = { errors: ['Error message'], warnings: ['Warning message'], valid: false, - jobs: mockJobs, + jobs: mockJobs.map((j) => { + const job = { ...j, tags: j.tagList }; + delete job.tagList; + return job; + }), }, }, }; @@ -17,7 +22,21 @@ export const mockLintDataValid = { errors: [], warnings: [], valid: true, - jobs: mockJobs, + jobs: mockJobs.map((j) => { + const job = { ...j, tags: j.tagList }; + delete job.tagList; + return job; + }), }, }, }; + +export const mockLintDataErrorRest = { + ...mockLintDataError.data.lintCI, + jobs: mockJobs.map((j) => convertObjectPropsToSnakeCase(j)), +}; + +export const mockLintDataValidRest = { + ...mockLintDataValid.data.lintCI, + jobs: mockJobs.map((j) => convertObjectPropsToSnakeCase(j)), +}; diff --git a/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js index 2349816fa86..f2818277c59 100644 --- a/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js @@ -1,15 +1,20 @@ +import Vue from 'vue'; import { GlAlert, GlDisclosureDropdown, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/ui'; -import { nextTick } from 'vue'; -import { createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; +import MockAdapter from 'axios-mock-adapter'; + import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; + +import axios from '~/lib/utils/axios_utils'; +import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers'; import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue'; import CiValidate, { i18n } from '~/ci/pipeline_editor/components/validate/ci_validate.vue'; import ValidatePipelinePopover from '~/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue'; import getBlobContent from '~/ci/pipeline_editor/graphql/queries/blob_content.query.graphql'; -import lintCIMutation from '~/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql'; import { pipelineEditorTrackingOptions } from '~/ci/pipeline_editor/constants'; import { mockBlobContentQueryResponse, @@ -17,68 +22,45 @@ import { mockCiYml, mockSimulatePipelineHelpPagePath, } from '../../mock_data'; -import { mockLintDataError, mockLintDataValid } from '../../../ci_lint/mock_data'; +import { + mockLintDataError, + mockLintDataValid, + mockLintDataErrorRest, + mockLintDataValidRest, +} from '../../../ci_lint/mock_data'; + +let mockAxios; + +Vue.use(VueApollo); -const localVue = createLocalVue(); -localVue.use(VueApollo); +const defaultProvide = { + ciConfigPath: '/path/to/ci-config', + ciLintPath: mockCiLintPath, + currentBranch: 'main', + projectFullPath: '/path/to/project', + validateTabIllustrationPath: '/path/to/img', + simulatePipelineHelpPagePath: mockSimulatePipelineHelpPagePath, +}; describe('Pipeline Editor Validate Tab', () => { let wrapper; - let mockApollo; let mockBlobContentData; let trackingSpy; - const createComponent = ({ - props, - stubs, - options, - isBlobLoading = false, - isSimulationLoading = false, - } = {}) => { + const createComponent = ({ props, stubs } = {}) => { + const handlers = [[getBlobContent, mockBlobContentData]]; + const mockApollo = createMockApollo(handlers, resolvers); + wrapper = shallowMountExtended(CiValidate, { propsData: { ciFileContent: mockCiYml, ...props, }, - provide: { - ciConfigPath: '/path/to/ci-config', - ciLintPath: mockCiLintPath, - currentBranch: 'main', - projectFullPath: '/path/to/project', - validateTabIllustrationPath: '/path/to/img', - simulatePipelineHelpPagePath: mockSimulatePipelineHelpPagePath, - }, - stubs, - mocks: { - $apollo: { - queries: { - initialBlobContent: { - loading: isBlobLoading, - }, - }, - mutations: { - lintCiMutation: { - loading: isSimulationLoading, - }, - }, - }, - }, - ...options, - }); - }; - - const createComponentWithApollo = ({ props, stubs } = {}) => { - const handlers = [[getBlobContent, mockBlobContentData]]; - mockApollo = createMockApollo(handlers); - - createComponent({ - props, stubs, - options: { - localVue, - apolloProvider: mockApollo, - mocks: {}, + provide: { + ...defaultProvide, }, + apolloProvider: mockApollo, }); }; @@ -96,12 +78,21 @@ describe('Pipeline Editor Validate Tab', () => { const findResultsCta = () => wrapper.findByTestId('resimulate-pipeline-button'); beforeEach(() => { + mockAxios = new MockAdapter(axios); + mockAxios.onPost(defaultProvide.ciLintPath).reply(HTTP_STATUS_OK, mockLintDataValidRest); + mockBlobContentData = jest.fn(); }); + afterEach(() => { + mockAxios.restore(); + }); + describe('while initial CI content is loading', () => { beforeEach(() => { - createComponent({ isBlobLoading: true }); + mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); + + createComponent(); }); it('renders disabled CTA with tooltip', () => { @@ -113,7 +104,7 @@ describe('Pipeline Editor Validate Tab', () => { describe('after initial CI content is loaded', () => { beforeEach(async () => { mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); - await createComponentWithApollo({ stubs: { GlPopover, ValidatePipelinePopover } }); + await createComponent({ stubs: { GlPopover, ValidatePipelinePopover } }); }); it('renders disabled pipeline source dropdown', () => { @@ -137,10 +128,9 @@ describe('Pipeline Editor Validate Tab', () => { describe('simulating the pipeline', () => { beforeEach(async () => { mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); - await createComponentWithApollo(); + await createComponent(); trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); }); afterEach(() => { @@ -158,32 +148,32 @@ describe('Pipeline Editor Validate Tab', () => { }); it('renders loading state while simulation is ongoing', async () => { - findCta().vm.$emit('click'); - await nextTick(); + await findCta().vm.$emit('click'); expect(findLoadingIcon().exists()).toBe(true); expect(findCancelBtn().exists()).toBe(true); expect(findCta().props('loading')).toBe(true); }); - it('calls mutation with the correct input', async () => { - await findCta().vm.$emit('click'); + it('calls endpoint with the correct input', async () => { + findCta().vm.$emit('click'); + + await waitForPromises(); - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: lintCIMutation, - variables: { - dry: true, + expect(mockAxios.history.post).toHaveLength(1); + expect(mockAxios.history.post[0].data).toBe( + JSON.stringify({ content: mockCiYml, - endpoint: mockCiLintPath, - }, - }); + dry_run: true, + }), + ); }); describe('when results are successful', () => { beforeEach(async () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); - await findCta().vm.$emit('click'); + findCta().vm.$emit('click'); + + await waitForPromises(); }); it('renders success alert', () => { @@ -210,8 +200,10 @@ describe('Pipeline Editor Validate Tab', () => { describe('when results have errors', () => { beforeEach(async () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataError); - await findCta().vm.$emit('click'); + mockAxios.onPost(defaultProvide.ciLintPath).reply(HTTP_STATUS_OK, mockLintDataErrorRest); + findCta().vm.$emit('click'); + + await waitForPromises(); }); it('renders error alert', () => { @@ -236,11 +228,11 @@ describe('Pipeline Editor Validate Tab', () => { describe('when CI content has changed after a simulation', () => { beforeEach(async () => { mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); - await createComponentWithApollo(); + await createComponent(); trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); - await findCta().vm.$emit('click'); + findCta().vm.$emit('click'); + await waitForPromises(); }); afterEach(() => { @@ -267,25 +259,26 @@ describe('Pipeline Editor Validate Tab', () => { }); it('calls mutation with new content', async () => { - await wrapper.setProps({ ciFileContent: 'new yaml content' }); - await findResultsCta().vm.$emit('click'); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(2); - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: lintCIMutation, - variables: { - dry: true, - content: 'new yaml content', - endpoint: mockCiLintPath, - }, - }); + const newContent = 'new yaml content'; + await wrapper.setProps({ ciFileContent: newContent }); + findResultsCta().vm.$emit('click'); + + await waitForPromises(); + + expect(mockAxios.history.post).toHaveLength(2); + expect(mockAxios.history.post[1].data).toBe( + JSON.stringify({ + content: newContent, + dry_run: true, + }), + ); }); }); describe('canceling a simulation', () => { beforeEach(async () => { mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); - await createComponentWithApollo(); + await createComponent(); }); it('returns to init state', async () => { @@ -294,9 +287,7 @@ describe('Pipeline Editor Validate Tab', () => { expect(findCiLintResults().exists()).toBe(false); // mutations should have successful results - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid); - findCta().vm.$emit('click'); - await nextTick(); + await findCta().vm.$emit('click'); // cancel before simulation succeeds expect(findCancelBtn().exists()).toBe(true); diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js index 865dd34fbfe..dddf2dd7602 100644 --- a/spec/frontend/ci/pipeline_editor/mock_data.js +++ b/spec/frontend/ci/pipeline_editor/mock_data.js @@ -43,7 +43,7 @@ job_build: export const mockCiTemplateQueryResponse = { data: { project: { - id: 'project-1', + id: 'gid://gitlab/Project/1', ciTemplate: { content: mockCiYml, }, @@ -54,7 +54,7 @@ export const mockCiTemplateQueryResponse = { export const mockBlobContentQueryResponse = { data: { project: { - id: 'project-1', + id: 'gid://gitlab/Project/1', repository: { blobs: { nodes: [{ id: 'blob-1', rawBlob: mockCiYml }] } }, }, }, @@ -62,13 +62,13 @@ export const mockBlobContentQueryResponse = { export const mockBlobContentQueryResponseNoCiFile = { data: { - project: { id: 'project-1', repository: { blobs: { nodes: [] } } }, + project: { id: 'gid://gitlab/Project/1', repository: { blobs: { nodes: [] } } }, }, }; export const mockBlobContentQueryResponseEmptyCiFile = { data: { - project: { id: 'project-1', repository: { blobs: { nodes: [{ rawBlob: '' }] } } }, + project: { id: 'gid://gitlab/Project/1', repository: { blobs: { nodes: [{ rawBlob: '' }] } } }, }, }; diff --git a/spec/frontend/ci/runner/components/runner_details_spec.js b/spec/frontend/ci/runner/components/runner_details_spec.js index c2d9e86aa91..47e09a1a7fd 100644 --- a/spec/frontend/ci/runner/components/runner_details_spec.js +++ b/spec/frontend/ci/runner/components/runner_details_spec.js @@ -1,4 +1,5 @@ import { GlSprintf, GlIntersperse } from '@gitlab/ui'; +import { s__ } from '~/locale'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { useFakeDate } from 'helpers/fake_date'; @@ -24,6 +25,7 @@ describe('RunnerDetails', () => { useFakeDate(mockNow); const findDetailGroups = () => wrapper.findComponent(RunnerGroups); + const findDdContent = (label) => findDd(label, wrapper).text().replace(/\s+/g, ' '); const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => { wrapper = mountFn(RunnerDetails, { @@ -61,6 +63,7 @@ describe('RunnerDetails', () => { ${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'} ${'Token expiry'} | ${{ tokenExpiresAt: mockOneHourAgo }} | ${'1 hour ago'} ${'Token expiry'} | ${{ tokenExpiresAt: null }} | ${'Never expires'} + ${'Runners'} | ${{ managers: { count: 2 } }} | ${'2'} `('"$field" field', ({ field, runner, expectedValue }) => { beforeEach(() => { createComponent({ @@ -94,7 +97,7 @@ describe('RunnerDetails', () => { stubs, }); - expect(findDd('Tags', wrapper).text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2'); + expect(findDdContent(s__('Runners|Tags'))).toBe('tag-1 tag-2'); }); it('displays "None" when runner has no tags', () => { @@ -105,7 +108,29 @@ describe('RunnerDetails', () => { stubs, }); - expect(findDd('Tags', wrapper).text().replace(/\s+/g, ' ')).toBe('None'); + expect(findDdContent(s__('Runners|Tags'))).toBe('None'); + }); + }); + + describe('"Runners" field', () => { + it.each` + count | expected + ${0} | ${'0'} + ${1} | ${'1'} + ${1000} | ${'1,000'} + `('displays runner managers count of $count', ({ count, expected }) => { + createComponent({ + props: { + runner: { + ...mockRunner, + managers: { + count, + }, + }, + }, + }); + + expect(findDdContent(s__('Runners|Runners'))).toBe(expected); }); }); diff --git a/spec/graphql/types/projects/service_type_enum_spec.rb b/spec/graphql/types/projects/service_type_enum_spec.rb index 8b444a08c3b..a5b1ba24a44 100644 --- a/spec/graphql/types/projects/service_type_enum_spec.rb +++ b/spec/graphql/types/projects/service_type_enum_spec.rb @@ -15,6 +15,7 @@ RSpec.describe GitlabSchema.types['ServiceType'] do BUGZILLA_SERVICE BUILDKITE_SERVICE CAMPFIRE_SERVICE + CLICKUP_SERVICE CONFLUENCE_SERVICE CUSTOM_ISSUE_TRACKER_SERVICE DATADOG_SERVICE diff --git a/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb index d40041d890e..79500f43394 100644 --- a/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb @@ -184,6 +184,44 @@ RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter, feature end end + context "clickup project" do + before_all do + create(:clickup_integration, project: project) + end + + before do + project.update!(issues_enabled: false) + end + + context "with right markdown" do + let(:issue) { ExternalIssue.new("PRJ-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with underscores in the prefix" do + let(:issue) { ExternalIssue.new("PRJ_1-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with a hash prefix and alphanumeric" do + let(:issue) { ExternalIssue.new("#abcd123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + + context "with prefix and alphanumeric" do + let(:issue) { ExternalIssue.new("CU-abcd123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end + end + context "jira project" do let_it_be(:service) { create(:jira_integration, project: project) } diff --git a/spec/lib/gitlab/background_migration/backfill_resource_link_events_spec.rb b/spec/lib/gitlab/background_migration/backfill_resource_link_events_spec.rb new file mode 100644 index 00000000000..4b8495cc004 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_resource_link_events_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillResourceLinkEvents, schema: 20230426085615, feature_category: :team_planning do + include MigrationHelpers::WorkItemTypesHelper + + let(:users) { table(:users) } + let(:namespaces) { table(:namespaces) } + let(:notes) { table(:notes) } + let(:system_note_metadata) { table(:system_note_metadata) } + let(:resource_link_events) { table(:resource_link_events) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + let(:work_item_issue_type_id) { table(:work_item_types).find_by(namespace_id: nil, name: 'Issue').id } + let(:work_item_task_type_id) { table(:work_item_types).find_by(namespace_id: nil, name: 'Task').id } + + # rubocop:disable Layout/LineLength + let!(:namespace) { namespaces.create!(name: "namespace", path: "namespace") } + let!(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) } + let!(:issue) { issues.create!(iid: 100, project_id: project.id, namespace_id: project.project_namespace_id, work_item_type_id: work_item_issue_type_id) } + let!(:work_item) { issues.create!(iid: 200, project_id: project.id, namespace_id: project.project_namespace_id, work_item_type_id: work_item_task_type_id) } + let!(:user) { users.create!(name: 'user', projects_limit: 10) } + + # Given a system note generated for a child work item, "Added #100 as parent issue", + # the migration searches for the parent issue with iid #100 using the child work item's project scope. + # Creating antoher issue that has the identical iid under another project ensures the migration is picking up the correct issue. + let!(:other_namespace) { namespaces.create!(name: "other_namespace", path: "other_namespace") } + let!(:other_project) { projects.create!(namespace_id: other_namespace.id, project_namespace_id: other_namespace.id) } + let!(:other_issue) { issues.create!(iid: issue.iid, project_id: other_project.id, namespace_id: other_project.project_namespace_id, work_item_type_id: work_item_issue_type_id) } + let!(:other_work_item) { issues.create!(iid: 200, project_id: other_project.id, namespace_id: other_project.project_namespace_id, work_item_type_id: work_item_task_type_id) } + # rubocop:enable Layout/LineLength + + subject(:migration) do + described_class.new( + start_id: system_note_metadata.minimum(:id), + end_id: system_note_metadata.maximum(:id), + batch_table: :system_note_metadata, + batch_column: :id, + sub_batch_size: 1, + pause_ms: 0, + connection: ActiveRecord::Base.connection + ) + end + + describe '#perform' do + it 'does nothing when relevant notes do not exist' do + expect { migration.perform } + .to not_change { resource_link_events.count } + end + + shared_examples 'a resource_link_event is correctly created' do + it "correctly backfills a resource_link_event record", :aggregate_failures do + expect { migration.perform } + .to change { resource_link_events.count }.from(0).to(1) + + expect(resource_link_events.last.attributes).to match(a_hash_including(expected_attributes)) + expect(resource_link_events.last.created_at).to be_like_time(system_note.created_at) + end + end + + context "for 'relate_to_parent' system_note_metadata record" do + let!(:system_note) do + create_relate_to_parent_note(parent: issue, child: work_item, issue_type_name: issue_type_name) + end + + let(:expected_attributes) do + { + "action" => described_class::ResourceLinkEvent.actions[:add], + "user_id" => user.id, + "issue_id" => issue.id, + "child_work_item_id" => work_item.id, + "system_note_metadata_id" => system_note.id + } + end + + context 'when issue_type_name is `issue`' do + let(:issue_type_name) { 'issue' } + + it_behaves_like 'a resource_link_event is correctly created' + end + + context "when issue_type_name is not `issue`" do + let(:issue_type_name) { 'objective' } + + it_behaves_like 'a resource_link_event is correctly created' + end + end + + context "for 'unrelate_to_parent' system_note_metadata record" do + let!(:system_note) do + create_unrelate_from_parent_note(parent: issue, child: work_item, issue_type_name: issue_type_name) + end + + let(:expected_attributes) do + { + "action" => described_class::ResourceLinkEvent.actions[:remove], + "user_id" => user.id, + "issue_id" => issue.id, + "child_work_item_id" => work_item.id, + "system_note_metadata_id" => system_note.id + } + end + + context 'when issue_type_name is `issue`' do + let(:issue_type_name) { 'issue' } + + it_behaves_like 'a resource_link_event is correctly created' + end + + context "when issue_type_name is not `issue`" do + let(:issue_type_name) { 'objective' } + + it_behaves_like 'a resource_link_event is correctly created' + end + end + + context "when a backfilled note exists" do + let!(:backfilled_system_note) do + create_relate_to_parent_note(parent: other_issue, child: other_work_item, issue_type_name: 'issue') + end + + let!(:backfilled_resource_link_event) do + resource_link_events.create!( + action: described_class::ResourceLinkEvent.actions[:add], + user_id: user.id, + issue_id: other_issue.id, + child_work_item_id: other_work_item.id, + created_at: backfilled_system_note.created_at, + system_note_metadata_id: backfilled_system_note.id) + end + + before do + # Create two system notes for which resource_link_events should be created (backfilled) + create_relate_to_parent_note(parent: issue, child: work_item, issue_type_name: 'issue') + create_unrelate_from_parent_note(parent: issue, child: work_item, issue_type_name: 'objective') + + # A backfilled resource_link_event exists for `backfilled_system_note` + # No resource_link_event record should be created for `backfilled_system_note` + # To test, update `backfilled_system_note` and check `backfilled_resource_link_event` does not change + backfilled_system_note.update!(created_at: 1.week.ago) + end + + it "correctly backfills the system notes without those that have been backfilled" do + expect { migration.perform } + .to change { resource_link_events.count }.from(1).to(3) + .and not_change { backfilled_resource_link_event } + end + end + + context 'with unexpected note content' do + context 'when note iid is prefixed' do + before do + note = notes.create!( + noteable_type: 'Issue', + noteable_id: work_item.id, + author_id: user.id, + # Cross-project linking is not supported currently. + # When an issue is referenced not in its own project, + # the iid is prefixed by the project name like gitlab#1 + # Test the scenario to ensure no resource_link_event is wrongly created. + note: "added gitlab##{issue.iid} as parent issue" + ) + + system_note_metadata.create!(action: 'relate_to_parent', note_id: note.id) + end + + it 'does not create resource_link_events record' do + expect { migration.perform } + .to not_change { resource_link_events.count } + end + end + end + end + + def create_relate_to_parent_note(parent:, child:, issue_type_name:) + note = notes.create!( + noteable_type: 'Issue', + noteable_id: child.id, + author_id: user.id, + note: "added ##{parent.iid} as parent #{issue_type_name}" + ) + + system_note_metadata.create!(action: 'relate_to_parent', note_id: note.id) + end + + def create_unrelate_from_parent_note(parent:, child:, issue_type_name:) + note = notes.create!( + noteable_type: 'Issue', + noteable_id: child.id, + author_id: user.id, + note: "removed parent #{issue_type_name} ##{parent.iid}" + ) + + system_note_metadata.create!(action: 'unrelate_from_parent', note_id: note.id) + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index e6829363922..72073ced21f 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -581,6 +581,7 @@ project: - custom_issue_tracker_integration - bugzilla_integration - ewm_integration +- clickup_integration - external_wiki_integration - mock_ci_integration - mock_monitoring_integration diff --git a/spec/lib/gitlab/jira_import_spec.rb b/spec/lib/gitlab/jira_import_spec.rb index c0c1a28b9ff..64a5758d152 100644 --- a/spec/lib/gitlab/jira_import_spec.rb +++ b/spec/lib/gitlab/jira_import_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Gitlab::JiraImport do context 'when Jira connection is not valid' do before do WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/serverInfo') - .to_raise(JIRA::HTTPError.new(double(message: 'Some failure.'))) + .to_raise(JIRA::HTTPError.new(double(message: 'Some failure.', code: '400'))) end it_behaves_like 'raise Jira import error', 'Unable to connect to the Jira instance. Please check your Jira integration configuration.' diff --git a/spec/lib/gitlab/resource_events/assignment_event_recorder_spec.rb b/spec/lib/gitlab/resource_events/assignment_event_recorder_spec.rb index b15f95dbd9c..768ff368602 100644 --- a/spec/lib/gitlab/resource_events/assignment_event_recorder_spec.rb +++ b/spec/lib/gitlab/resource_events/assignment_event_recorder_spec.rb @@ -76,16 +76,4 @@ RSpec.describe Gitlab::ResourceEvents::AssignmentEventRecorder, feature_category end.to change { ResourceEvents::MergeRequestAssignmentEvent.count }.by(1) end end - - context 'when the record_issue_and_mr_assignee_events FF is off' do - before do - stub_feature_flags(record_issue_and_mr_assignee_events: false) - end - - it 'does nothing' do - expect do - described_class.new(parent: mr_with_one_assignee, old_assignees: [user2, user3]).record - end.not_to change { mr_with_one_assignee.assignment_events.count } - end - end end diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/lib/gitlab/task_helpers_spec.rb index 0c43dd15e8c..0c43dd15e8c 100644 --- a/spec/tasks/gitlab/task_helpers_spec.rb +++ b/spec/lib/gitlab/task_helpers_spec.rb diff --git a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb index aa1e67085cd..8bb7e8c8518 100644 --- a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb @@ -57,10 +57,6 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu do end context 'Menu items' do - before do - stub_feature_flags(remove_monitor_metrics: false) - end - subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } } shared_examples 'access rights checks' do @@ -73,12 +69,6 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu do end end - describe 'Metrics Dashboard' do - let(:item_id) { :metrics } - - it_behaves_like 'access rights checks' - end - describe 'Error Tracking' do let(:item_id) { :error_tracking } diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb index 9344bbc76db..e59062c7eaf 100644 --- a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb +++ b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb @@ -15,7 +15,6 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::MonitorMenu, feature_categ it 'defines list of NilMenuItem placeholders' do expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem]) expect(items.map(&:item_id)).to eq([ - :metrics, :error_tracking, :alert_management, :incidents, diff --git a/spec/migrations/20230426085615_queue_backfill_resource_link_events_spec.rb b/spec/migrations/20230426085615_queue_backfill_resource_link_events_spec.rb new file mode 100644 index 00000000000..d0d948dad9d --- /dev/null +++ b/spec/migrations/20230426085615_queue_backfill_resource_link_events_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueBackfillResourceLinkEvents, feature_category: :team_planning do + include MigrationHelpers::WorkItemTypesHelper + + let(:users) { table(:users) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + let(:notes) { table(:notes) } + let(:system_note_metadata) { table(:system_note_metadata) } + + let(:namespace) { namespaces.create!(name: "namespace", path: "namespace") } + let(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) } + let(:work_item_issue_type_id) { table(:work_item_types).find_by(namespace_id: nil, name: 'Issue').id } + let(:issue) { issues.create!(project_id: project.id, namespace_id: project.project_namespace_id, work_item_type_id: work_item_issue_type_id) } # rubocop:disable Layout/LineLength + let(:user) { users.create!(name: 'user', projects_limit: 10) } + + let!(:system_note_metadata_record1) do + note = notes.create!(noteable_type: 'Issue', noteable_id: issue.id, author_id: user.id, note: "foobar") + + system_note_metadata.create!(action: 'foobar', note_id: note.id) + end + + let!(:batched_migration) { described_class::MIGRATION } + + describe '#up' do + %w[relate_to_parent unrelate_from_parent].each do |action_value| + context 'when system_note_metadata table has a row with targeted action values' do + let!(:system_note_metadata_record2) do + note = notes.create!(noteable_type: 'Issue', noteable_id: issue.id, author_id: user.id, note: "foobar") + + system_note_metadata.create!(action: action_value, note_id: note.id) + end + + let!(:system_note_metadata_record3) do + note = notes.create!(noteable_type: 'Issue', noteable_id: issue.id, author_id: user.id, note: "foobar") + + system_note_metadata.create!(action: action_value, note_id: note.id) + end + + it 'schedules a new batched migration with the lowest system_note_metadat record id' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :system_note_metadata, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE, + batch_min_value: system_note_metadata_record2.id + ) + } + end + end + end + end + + context 'when system_note_metadata table does not ahve a row with the targeted action values' do + it 'does not a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + end + end + end + end + + describe '#down' do + it 'deletes all batched migration records' do + migrate! + schema_migrate_down! + + expect(batched_migration).not_to have_scheduled_batched_migration + end + end +end diff --git a/spec/models/ci/catalog/listing_spec.rb b/spec/models/ci/catalog/listing_spec.rb index 93d70a3f63e..159b70d7f8f 100644 --- a/spec/models/ci/catalog/listing_spec.rb +++ b/spec/models/ci/catalog/listing_spec.rb @@ -4,8 +4,8 @@ require 'spec_helper' RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do let_it_be(:namespace) { create(:group) } - let_it_be(:project_1) { create(:project, namespace: namespace) } - let_it_be(:project_2) { create(:project, namespace: namespace) } + let_it_be(:project_1) { create(:project, namespace: namespace, name: 'X Project') } + let_it_be(:project_2) { create(:project, namespace: namespace, name: 'B Project') } let_it_be(:project_3) { create(:project) } let_it_be(:user) { create(:user) } @@ -34,11 +34,32 @@ RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do end context 'when the namespace has catalog resources' do - let!(:resource) { create(:catalog_resource, project: project_1) } - let!(:other_namespace_resource) { create(:catalog_resource, project: project_3) } + let_it_be(:resource) { create(:catalog_resource, project: project_1) } + let_it_be(:resource_2) { create(:catalog_resource, project: project_2) } + let_it_be(:other_namespace_resource) { create(:catalog_resource, project: project_3) } it 'contains only catalog resources for projects in that namespace' do - is_expected.to contain_exactly(resource) + is_expected.to contain_exactly(resource, resource_2) + end + + context 'with a sort parameter' do + subject(:resources) { list.resources(sort: sort) } + + context 'when the sort is name ascending' do + let_it_be(:sort) { :name_asc } + + it 'contains catalog resources for projects sorted by name' do + is_expected.to eq([resource_2, resource]) + end + end + + context 'when the sort is name descending' do + let_it_be(:sort) { :name_desc } + + it 'contains catalog resources for projects sorted by name' do + is_expected.to eq([resource, resource_2]) + end + end end end end diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb index a239bbad857..dfb7b311d96 100644 --- a/spec/models/ci/catalog/resource_spec.rb +++ b/spec/models/ci/catalog/resource_spec.rb @@ -3,8 +3,12 @@ require 'spec_helper' RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, name: 'A') } + let_it_be(:project_2) { build(:project, name: 'Z') } + let_it_be(:project_3) { build(:project, name: 'L') } let_it_be(:resource) { create(:catalog_resource, project: project) } + let_it_be(:resource_2) { create(:catalog_resource, project: project_2) } + let_it_be(:resource_3) { create(:catalog_resource, project: project_3) } let_it_be(:releases) do [ @@ -28,6 +32,30 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do end end + describe '.order_by_created_at_desc' do + it 'returns catalog resources sorted by descending created at' do + ordered_resources = described_class.order_by_created_at_desc + + expect(ordered_resources.to_a).to eq([resource_3, resource_2, resource]) + end + end + + describe '.order_by_name_desc' do + it 'returns catalog resources sorted by descending name' do + ordered_resources = described_class.order_by_name_desc + + expect(ordered_resources.pluck(:name)).to eq(%w[Z L A]) + end + end + + describe '.order_by_name_asc' do + it 'returns catalog resources sorted by ascending name' do + ordered_resources = described_class.order_by_name_asc + + expect(ordered_resources.pluck(:name)).to eq(%w[A L Z]) + end + end + describe '#versions' do it 'returns releases ordered by released date descending' do expect(resource.versions).to eq(releases.reverse) diff --git a/spec/models/integrations/clickup_spec.rb b/spec/models/integrations/clickup_spec.rb new file mode 100644 index 00000000000..f83fb3ddabc --- /dev/null +++ b/spec/models/integrations/clickup_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Integrations::Clickup, feature_category: :integrations do + describe 'Validations' do + context 'when integration is active' do + before do + subject.active = true + end + + it { is_expected.to validate_presence_of(:project_url) } + it { is_expected.to validate_presence_of(:issues_url) } + + it_behaves_like 'issue tracker integration URL attribute', :project_url + it_behaves_like 'issue tracker integration URL attribute', :issues_url + end + + context 'when integration is inactive' do + before do + subject.active = false + end + + it { is_expected.not_to validate_presence_of(:project_url) } + it { is_expected.not_to validate_presence_of(:issues_url) } + end + end + + describe '#reference_pattern' do + it 'does allow project prefix on the reference' do + expect(subject.reference_pattern.match('PRJ-123')[:issue]).to eq('PRJ-123') + end + + it 'allows a hash with an alphanumeric key on the reference' do + expect(subject.reference_pattern.match('#abcd123')[:issue]).to eq('abcd123') + end + + it 'allows a global prefix with an alphanumeric key on the reference' do + expect(subject.reference_pattern.match('CU-abcd123')[:issue]).to eq('abcd123') + end + end + + describe '#fields' do + it 'only returns the project_url and issues_url fields' do + expect(subject.fields.pluck(:name)).to eq(%w[project_url issues_url]) + end + end +end diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb index 71dd543b3ec..9bb77f6d6d4 100644 --- a/spec/models/integrations/jira_spec.rb +++ b/spec/models/integrations/jira_spec.rb @@ -871,6 +871,8 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do expect(jira_integration).to have_received(:log_exception).with( kind_of(StandardError), message: 'Issue transition failed', + client_path: '/rest/api/2/issue/JIRA-123/transitions', + client_status: '400', client_url: "http://jira.example.com" ) end @@ -1175,12 +1177,14 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do error_message = 'Some specific failure.' WebMock.stub_request(:get, test_url).with(basic_auth: [username, password]) - .to_raise(JIRA::HTTPError.new(double(message: error_message))) + .to_raise(JIRA::HTTPError.new(double(message: error_message, code: '403'))) expect(jira_integration).to receive(:log_exception).with( kind_of(JIRA::HTTPError), message: 'Error sending message', - client_url: 'http://jira.example.com' + client_url: 'http://jira.example.com', + client_path: '/rest/api/2/serverInfo', + client_status: '403' ) expect(jira_integration.test(nil)).to eq(success: false, result: error_message) diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index a3d015fd768..9bab6fcd942 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -423,15 +423,77 @@ RSpec.describe Issue, feature_category: :team_planning do let_it_be(:issue) { create(:issue, project: reusable_project) } let_it_be(:incident) { create(:incident, project: reusable_project) } - it 'gives issues with the given issue type' do + it 'returns issues with the given issue type' do expect(described_class.with_issue_type('issue')) .to contain_exactly(issue) end - it 'gives issues with the given issue type' do + it 'returns issues with the given issue types' do expect(described_class.with_issue_type(%w(issue incident))) .to contain_exactly(issue, incident) end + + it 'uses the work_item_types table for filtering' do + expect do + described_class.with_issue_type(:issue).to_a + end.to make_queries_matching( + %r{ + INNER\sJOIN\s"work_item_types"\sON\s"work_item_types"\."id"\s=\s"issues"\."work_item_type_id" + \sWHERE\s"work_item_types"\."base_type"\s=\s0 + }x + ) + end + + context 'when the issue_type_uses_work_item_types_table feature flag is disabled' do + before do + stub_feature_flags(issue_type_uses_work_item_types_table: false) + end + + it 'uses the issue_type column for filtering' do + expect do + described_class.with_issue_type(:issue).to_a + end.to make_queries_matching(/"issues"\."issue_type" = 0/) + end + end + end + + describe '.without_issue_type' do + let_it_be(:issue) { create(:issue, project: reusable_project) } + let_it_be(:incident) { create(:incident, project: reusable_project) } + let_it_be(:task) { create(:issue, :task, project: reusable_project) } + + it 'returns issues without the given issue type' do + expect(described_class.without_issue_type('issue')) + .to contain_exactly(incident, task) + end + + it 'returns issues without the given issue types' do + expect(described_class.without_issue_type(%w(issue incident))) + .to contain_exactly(task) + end + + it 'uses the work_item_types table for filtering' do + expect do + described_class.without_issue_type(:issue).to_a + end.to make_queries_matching( + %r{ + INNER\sJOIN\s"work_item_types"\sON\s"work_item_types"\."id"\s=\s"issues"\."work_item_type_id" + \sWHERE\s"work_item_types"\."base_type"\s!=\s0 + }x + ) + end + + context 'when the issue_type_uses_work_item_types_table feature flag is disabled' do + before do + stub_feature_flags(issue_type_uses_work_item_types_table: false) + end + + it 'uses the issue_type column for filtering' do + expect do + described_class.without_issue_type(:issue).to_a + end.to make_queries_matching(/"issues"\."issue_type" != 0/) + end + end end describe '.order_severity' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e9bb01f4b23..32158ef9509 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -76,6 +76,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do it { is_expected.to have_one(:harbor_integration) } it { is_expected.to have_one(:redmine_integration) } it { is_expected.to have_one(:youtrack_integration) } + it { is_expected.to have_one(:clickup_integration) } it { is_expected.to have_one(:custom_issue_tracker_integration) } it { is_expected.to have_one(:bugzilla_integration) } it { is_expected.to have_one(:ewm_integration) } @@ -2047,6 +2048,28 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do end end + describe 'sorting by name' do + let_it_be(:project1) { create(:project, name: 'A') } + let_it_be(:project2) { create(:project, name: 'Z') } + let_it_be(:project3) { create(:project, name: 'L') } + + context 'when using .sort_by_name_desc' do + it 'reorders the projects by descending name order' do + projects = described_class.sorted_by_name_desc + + expect(projects.pluck(:name)).to eq(%w[Z L A]) + end + end + + context 'when using .sort_by_name_asc' do + it 'reorders the projects by ascending name order' do + projects = described_class.sorted_by_name_asc + + expect(projects.pluck(:name)).to eq(%w[A L Z]) + end + end + end + describe '.with_shared_runners_enabled' do subject { described_class.with_shared_runners_enabled } diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 7b839594816..efb4d244c10 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -82,7 +82,6 @@ RSpec.shared_context 'project navbar structure' do { nav_item: _('Monitor'), nav_sub_items: [ - _('Metrics'), _('Error Tracking'), _('Alerts'), _('Incidents') diff --git a/spec/tasks/cache/clear/redis_spec.rb b/spec/tasks/cache_rake_spec.rb index 375d01bf2ba..375d01bf2ba 100644 --- a/spec/tasks/cache/clear/redis_spec.rb +++ b/spec/tasks/cache_rake_spec.rb diff --git a/spec/tasks/config_lint_spec.rb b/spec/tasks/config_lint_rake_spec.rb index 34899c84888..34899c84888 100644 --- a/spec/tasks/config_lint_spec.rb +++ b/spec/tasks/config_lint_rake_spec.rb diff --git a/spec/tasks/gitlab/db/decomposition/connection_status_spec.rb b/spec/tasks/gitlab/db/decomposition/connection_status_rake_spec.rb index 78f86049ebb..78f86049ebb 100644 --- a/spec/tasks/gitlab/db/decomposition/connection_status_spec.rb +++ b/spec/tasks/gitlab/db/decomposition/connection_status_rake_spec.rb diff --git a/spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb b/spec/tasks/gitlab/generate_sample_prometheus_data_rake_spec.rb index 67bf512c6da..67bf512c6da 100644 --- a/spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb +++ b/spec/tasks/gitlab/generate_sample_prometheus_data_rake_spec.rb diff --git a/spec/tasks/gitlab/metrics_exporter_task_spec.rb b/spec/tasks/gitlab/metrics_exporter_rake_spec.rb index ca37fc1b5d7..ca37fc1b5d7 100644 --- a/spec/tasks/gitlab/metrics_exporter_task_spec.rb +++ b/spec/tasks/gitlab/metrics_exporter_rake_spec.rb diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_rake_spec.rb index 3f7271d4be1..3f7271d4be1 100644 --- a/spec/tasks/tokens_spec.rb +++ b/spec/tasks/tokens_rake_spec.rb 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 94ea9043857..3ec731c8eb7 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -382,34 +382,12 @@ RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do end describe 'Monitor' do - before do - stub_feature_flags(remove_monitor_metrics: false) - end - it 'top level navigation link is visible for user with permissions' do render expect(rendered).to have_link('Monitor') end - describe 'Metrics Dashboard' do - it 'has a link to the metrics dashboard page' do - render - - expect(rendered).to have_link('Metrics', href: project_metrics_dashboard_path(project)) - end - - describe 'when the user does not have access' do - let(:user) { nil } - - it 'does not have a link to the metrics page' do - render - - expect(rendered).not_to have_link('Metrics') - end - end - end - describe 'Error Tracking' do it 'has a link to the error tracking page' do render diff --git a/tests.yml b/tests.yml index b2d8311fb7e..dee78aae486 100644 --- a/tests.yml +++ b/tests.yml @@ -24,6 +24,10 @@ mapping: - source: lib/(.+)\.rb test: spec/lib/%s_spec.rb + # Map rake tasks to its respective specs + - source: '(ee/)?lib/tasks/(.+)\.rake' + test: '%sspec/tasks/%s_rake_spec.rb' + # See https://gitlab.com/gitlab-org/gitlab/-/issues/368628 - source: lib/gitlab/usage_data_counters/(.+)\.rb test: spec/lib/gitlab/usage_data_spec.rb |