diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 3e0064961d9..229f4032b7d 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -620,6 +620,16 @@ upload-knapsack-report: stage: report when: always +export-test-metrics: + extends: + - .qa-install + - .ruby-image + - .rules:report:process-results + stage: report + when: always + script: + - bundle exec rake "ci:export_test_metrics[$CI_PROJECT_DIR/gitlab-qa-run-*/**/test-metrics-*.json]" + relate-test-failures: extends: - .qa-install diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index ab1d57ea71a..f6668d7864e 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -8,7 +8,6 @@ variables: USE_BUNDLE_INSTALL: "false" SETUP_DB: "false" - QA_EXPORT_TEST_METRICS: "false" before_script: - !reference [.default-before_script, before_script] - cd qa && bundle install diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index 4ffc30296bb..c631161aa08 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -178,7 +178,7 @@ notify-slack: extends: - .notify-slack-qa - .qa-cache - - .rules:notify-slack + - .rules:main-run stage: post-qa variables: RUN_WITH_BUNDLE: "true" @@ -188,6 +188,16 @@ notify-slack: STATUS_SYM: ☠️ STATUS: failed TYPE: "(review-app) " + when: on_failure script: - bundle exec gitlab-qa-report --prepare-stage-reports "$CI_PROJECT_DIR/qa/tmp/rspec-*.xml" # generate summary - !reference [.notify-slack-qa, script] + +export-test-metrics: + extends: + - .bundle-base + - .rules:main-run + stage: post-qa + when: always + script: + - bundle exec rake "ci:export_test_metrics[tmp/test-metrics-*.json]" diff --git a/.gitlab/ci/review-apps/rules.gitlab-ci.yml b/.gitlab/ci/review-apps/rules.gitlab-ci.yml index ba646c53870..49343c98547 100644 --- a/.gitlab/ci/review-apps/rules.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/rules.gitlab-ci.yml @@ -98,7 +98,6 @@ rules: - when: always -.rules:notify-slack: +.rules:main-run: rules: - - <<: *default-branch - when: on_failure + - *default-branch diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 00124d1b80b..34e44303c6e 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -1137,6 +1137,8 @@ SKIP_REPORT_IN_ISSUES: "false" PROCESS_TEST_RESULTS: "true" KNAPSACK_GENERATE_REPORT: "true" + QA_SAVE_TEST_METRICS: "true" + QA_EXPORT_TEST_METRICS: "false" # on main runs, metrics are exported to separate bucket via rake task for better consistency - <<: *if-force-ci when: manual allow_failure: true @@ -1899,6 +1901,8 @@ allow_failure: true variables: KNAPSACK_GENERATE_REPORT: "true" + QA_SAVE_TEST_METRICS: "true" + QA_EXPORT_TEST_METRICS: "false" # on main runs, metrics are exported to separate bucket via rake task for better consistency .review:rules:review-build-cng: rules: diff --git a/Gemfile b/Gemfile index a7e349e59f6..f4ef72beeab 100644 --- a/Gemfile +++ b/Gemfile @@ -477,7 +477,7 @@ gem 'html2text' gem 'stackprof', '~> 0.2.21', require: false gem 'rbtrace', '~> 0.4', require: false -gem 'memory_profiler', '~> 0.9', require: false +gem 'memory_profiler', '~> 1.0', require: false gem 'activerecord-explain-analyze', '~> 0.1', require: false # OAuth diff --git a/Gemfile.checksum b/Gemfile.checksum index fb44be70f8b..c9d8615cf5e 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -50,7 +50,7 @@ {"name":"benchmark","version":"0.2.0","platform":"ruby","checksum":"5f7087b794613abdd3ac9c13f4351f65b164bcb15ced2ad29508e365f9b28c77"}, {"name":"benchmark-ips","version":"2.3.0","platform":"ruby","checksum":"12443aa327d3129aa965244f79d7d5cb0f692f0f92ba7db76fba61526a40062e"}, {"name":"benchmark-malloc","version":"0.2.0","platform":"ruby","checksum":"37c68f0435261634026f584d79956a35325a3027e3e6b4cc8d7575aa10537e6b"}, -{"name":"benchmark-memory","version":"0.1.2","platform":"ruby","checksum":"aa7bfe6776174d0ddefe6fb39945d88fff6d76eac165690188391d9acd441c87"}, +{"name":"benchmark-memory","version":"0.2.0","platform":"ruby","checksum":"ca1e436433b09535ee8f64f80600a5edb407cff1f6ac70e089ca238118e6ab5c"}, {"name":"benchmark-perf","version":"0.6.0","platform":"ruby","checksum":"fe2b01959f3de0f9dd34820d54ef881eb4f3589fccb7d17b63068ac92d7f9621"}, {"name":"benchmark-trend","version":"0.4.0","platform":"ruby","checksum":"de5a02a9f443babefbbd97784759820decee8554a0c273d859c02a0990845d81"}, {"name":"better_errors","version":"2.9.1","platform":"ruby","checksum":"39efc116ab04d6c4200052c5782936e4bd99906978d098992bce6bf81d054284"}, @@ -103,7 +103,7 @@ {"name":"declarative_policy","version":"1.1.0","platform":"ruby","checksum":"9af4cf299ade03f2bbf63908f2ce6a117d132fc714c39a128596667fb13331cb"}, {"name":"default_value_for","version":"3.4.0","platform":"ruby","checksum":"35d2dc51675a6bedfa875778628d44b823e0d7336da9432519477174ebb0f40f"}, {"name":"deprecation_toolkit","version":"1.5.1","platform":"ruby","checksum":"a8a1ab1a19ae40ea12560b65010e099f3459ebde390b76621ef0c21c516a04ba"}, -{"name":"derailed_benchmarks","version":"2.1.1","platform":"ruby","checksum":"25b0ba79dc52c715ea6cce35fd8afbbf84511ef1c7f2bbe1d8b7a30addc6aab3"}, +{"name":"derailed_benchmarks","version":"2.1.2","platform":"ruby","checksum":"eaadc6206ceeb5538ff8f5e04a0023d54ebdd95d04f33e8960fb95a5f189a14f"}, {"name":"device_detector","version":"1.0.0","platform":"ruby","checksum":"b800fb3150b00c23e87b6768011808ac1771fffaae74c3238ebaf2b782947a7d"}, {"name":"devise","version":"4.8.1","platform":"ruby","checksum":"fdd48bbe79a89e7c1152236a70479842ede48bea4fa7f4f2d8da1f872559803e"}, {"name":"devise-two-factor","version":"4.0.2","platform":"ruby","checksum":"6548d2696ed090d27046f888f4fa7380f151e0f823902d46fd9b91e7d0cac511"}, @@ -331,7 +331,7 @@ {"name":"marcel","version":"1.0.2","platform":"ruby","checksum":"a013b677ef46cbcb49fd5c59b3d35803d2ee04dd75d8bfdc43533fc5a31f7e4e"}, {"name":"marginalia","version":"1.11.1","platform":"ruby","checksum":"cb63212ab63e42746e27595e912cb20408a1a28bcd0edde55d15b7c45fa289cf"}, {"name":"memoist","version":"0.16.2","platform":"ruby","checksum":"a52c53a3f25b5875151670b2f3fd44388633486dc0f09f9a7150ead1e3bf3c45"}, -{"name":"memory_profiler","version":"0.9.14","platform":"ruby","checksum":"de558cf6525d8d56d2c0ea465b1664517fbe45560f892dc7a898d3b8c2863b12"}, +{"name":"memory_profiler","version":"1.0.1","platform":"ruby","checksum":"38cdb42f22d9100df2eba0365c199724b58b05c38e765cd764a07392916901b1"}, {"name":"method_source","version":"1.0.0","platform":"ruby","checksum":"d779455a2b5666a079ce58577bfad8534f571af7cec8107f4dce328f0981dede"}, {"name":"mime-types","version":"3.4.1","platform":"ruby","checksum":"6bcf8b0e656b6ae9977bdc1351ef211d0383252d2f759a59ef4bcf254542fc46"}, {"name":"mime-types-data","version":"3.2022.0105","platform":"ruby","checksum":"d8c401ba9ea8b648b7145b90081789ec714e91fd625d82c5040079c5ea696f00"}, diff --git a/Gemfile.lock b/Gemfile.lock index a24e2877d55..33cf0d9dae9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -219,8 +219,8 @@ GEM benchmark (0.2.0) benchmark-ips (2.3.0) benchmark-malloc (0.2.0) - benchmark-memory (0.1.2) - memory_profiler (~> 0.9) + benchmark-memory (0.2.0) + memory_profiler (~> 1) benchmark-perf (0.6.0) benchmark-trend (0.4.0) better_errors (2.9.1) @@ -330,7 +330,7 @@ GEM activerecord (>= 3.2.0, < 7.0) deprecation_toolkit (1.5.1) activesupport (>= 4.2) - derailed_benchmarks (2.1.1) + derailed_benchmarks (2.1.2) benchmark-ips (~> 2) dead_end get_process_mem (~> 0) @@ -892,7 +892,7 @@ GEM actionpack (>= 5.2) activerecord (>= 5.2) memoist (0.16.2) - memory_profiler (0.9.14) + memory_profiler (1.0.1) method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) @@ -1727,7 +1727,7 @@ DEPENDENCIES mail (= 2.7.1) mail-smtp_pool (~> 0.1.0)! marginalia (~> 1.11.1) - memory_profiler (~> 0.9) + memory_profiler (~> 1.0) microsoft_graph_mailer (~> 0.1.0)! mini_magick (~> 4.10.1) minitest (~> 5.11.0) diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 34e984a9bb9..fb0c47fe018 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,11 +1,38 @@ +import Vue from 'vue'; import $ from 'jquery'; import { escape } from 'lodash'; +import GroupSelect from '~/vue_shared/components/group_select/group_select.vue'; import { groupsPath } from '~/vue_shared/components/group_select/utils'; import { __ } from '~/locale'; import Api from './api'; import { loadCSSFile } from './lib/utils/css_utils'; import { select2AxiosTransport } from './lib/utils/select2_utils'; +const initVueSelect = () => { + [...document.querySelectorAll('.ajax-groups-select')].forEach((el) => { + const { parentId: parentGroupID, groupsFilter, inputId } = el.dataset; + + return new Vue({ + el, + components: { + GroupSelect, + }, + render(createElement) { + return createElement(GroupSelect, { + props: { + inputName: el.name, + initialSelection: el.value || null, + parentGroupID, + groupsFilter, + inputId, + clearable: el.classList.contains('allowClear'), + }, + }); + }, + }); + }); +}; + const groupsSelect = () => { loadCSSFile(gon.select2_css_path) .then(() => { @@ -84,8 +111,12 @@ const groupsSelect = () => { export default () => { if ($('.ajax-groups-select').length) { - import(/* webpackChunkName: 'select2' */ 'select2/select2') - .then(groupsSelect) - .catch(() => {}); + if (gon.features?.vueGroupSelect) { + initVueSelect(); + } else { + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(groupsSelect) + .catch(() => {}); + } } }; diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js index 2aec0617b5a..52124865bcc 100644 --- a/app/assets/javascripts/pages/groups/shared/group_details.js +++ b/app/assets/javascripts/pages/groups/shared/group_details.js @@ -1,23 +1,12 @@ /* eslint-disable no-new */ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; -import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants'; import initInviteMembersBanner from '~/groups/init_invite_members_banner'; import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; -import { getPagePath, getDashPath } from '~/lib/utils/common_utils'; import initNotificationsDropdown from '~/notifications'; import ProjectsList from '~/projects_list'; -import GroupTabs from './group_tabs'; -export default function initGroupDetails(actionName = 'show') { - const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED]; - const dashPath = getDashPath(); - let action = loadableActions.includes(dashPath) ? dashPath : getPagePath(1); - if (actionName && action === actionName) { - action = 'show'; // 'show' resets GroupTabs to default action through base class - } - - new GroupTabs({ parentEl: '.groups-listing', action }); +export default function initGroupDetails() { new ShortcutsNavigation(); initNotificationsDropdown(); diff --git a/app/assets/javascripts/pages/groups/shared/group_tabs.js b/app/assets/javascripts/pages/groups/shared/group_tabs.js deleted file mode 100644 index 73d810007dc..00000000000 --- a/app/assets/javascripts/pages/groups/shared/group_tabs.js +++ /dev/null @@ -1,136 +0,0 @@ -import $ from 'jquery'; -import createGroupTree from '~/groups'; -import { - ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, - ACTIVE_TAB_SHARED, - ACTIVE_TAB_ARCHIVED, - CONTENT_LIST_CLASS, - GROUPS_LIST_HOLDER_CLASS, - GROUPS_FILTER_FORM_CLASS, -} from '~/groups/constants'; -import GroupFilterableList from '~/groups/groups_filterable_list'; -import { removeParams } from '~/lib/utils/url_utility'; -import UserTabs from '~/pages/users/user_tabs'; - -export default class GroupTabs extends UserTabs { - constructor({ defaultAction = 'subgroups_and_projects', action, parentEl }) { - super({ defaultAction, action, parentEl }); - } - - bindEvents() { - this.$parentEl - .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') - .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', (event) => this.tabShown(event)); - } - - tabShown(event) { - const $target = $(event.target); - const action = $target.data('action') || $target.data('targetSection'); - const source = $target.attr('href') || $target.data('targetPath'); - - document.querySelector(GROUPS_FILTER_FORM_CLASS).action = source; - - this.setTab(action); - return this.setCurrentAction(source); - } - - setTab(action) { - const loadableActions = [ - ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, - ACTIVE_TAB_SHARED, - ACTIVE_TAB_ARCHIVED, - ]; - this.enableSearchBar(action); - this.action = action; - - if (this.loaded[action]) { - return; - } - - if (loadableActions.includes(action)) { - this.cleanFilterState(); - this.loadTab(action); - } - } - - loadTab(action) { - const elId = `js-groups-${action}-tree`; - const endpoint = this.getEndpoint(action); - - this.toggleLoading(true); - - createGroupTree(elId, endpoint, action); - this.loaded[action] = true; - - this.toggleLoading(false); - } - - getEndpoint(action) { - const { endpointsDefault, endpointsShared } = this.$parentEl.data(); - let endpoint; - - switch (action) { - case ACTIVE_TAB_ARCHIVED: - endpoint = `${endpointsDefault}?archived=only`; - break; - case ACTIVE_TAB_SHARED: - endpoint = endpointsShared; - break; - default: - // ACTIVE_TAB_SUBGROUPS_AND_PROJECTS - endpoint = endpointsDefault; - break; - } - - return endpoint; - } - - enableSearchBar(action) { - const containerEl = document.getElementById(action); - const form = document.querySelector(GROUPS_FILTER_FORM_CLASS); - const filter = form.querySelector('.js-groups-list-filter'); - const holder = containerEl.querySelector(GROUPS_LIST_HOLDER_CLASS); - const dataEl = containerEl.querySelector(CONTENT_LIST_CLASS); - const endpoint = this.getEndpoint(action); - - if (!dataEl) { - return; - } - - const { dataset } = dataEl; - const opts = { - form, - filter, - holder, - filterEndpoint: endpoint || dataset.endpoint, - pagePath: null, - dropdownSel: '.js-group-filter-dropdown-wrap', - filterInputField: 'filter', - action, - }; - - if (!this.loaded[action]) { - const filterableList = new GroupFilterableList(opts); - filterableList.initSearch(); - } - } - - cleanFilterState() { - const values = Object.values(this.loaded); - const loadedTabs = values.filter((e) => e === true); - - if (!loadedTabs.length) { - return; - } - - const newState = removeParams(['page'], window.location.search); - - window.history.replaceState( - { - url: newState, - }, - document.title, - newState, - ); - } -} diff --git a/app/assets/javascripts/vue_shared/components/group_select/constants.js b/app/assets/javascripts/vue_shared/components/group_select/constants.js new file mode 100644 index 00000000000..bc70936eb36 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/group_select/constants.js @@ -0,0 +1,6 @@ +import { __ } from '~/locale'; + +export const TOGGLE_TEXT = __('Search for a group'); +export const FETCH_GROUPS_ERROR = __('Unable to fetch groups. Reload the page to try again.'); +export const FETCH_GROUP_ERROR = __('Unable to fetch group. Reload the page to try again.'); +export const QUERY_TOO_SHORT_MESSAGE = __('Enter at least three characters to search.'); diff --git a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue new file mode 100644 index 00000000000..1de6c0121bc --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue @@ -0,0 +1,195 @@ + + + diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index 032aba3d289..c91edb74d6b 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -35,7 +35,7 @@ module SendFileUpload end def cdn_fronted_url(file, redirect_params) - if Feature.enabled?(:use_cdn_with_job_artifacts_ui_downloads) && file.respond_to?(:cdn_enabled_url) + if file.respond_to?(:cdn_enabled_url) result = file.cdn_enabled_url(request.remote_ip, redirect_params[:query]) Gitlab::ApplicationContext.push(artifact_used_cdn: result.used_cdn) result.url diff --git a/app/controllers/concerns/verifies_with_email.rb b/app/controllers/concerns/verifies_with_email.rb index 49475c74590..ac1475597ff 100644 --- a/app/controllers/concerns/verifies_with_email.rb +++ b/app/controllers/concerns/verifies_with_email.rb @@ -8,7 +8,7 @@ module VerifiesWithEmail include ActionView::Helpers::DateHelper included do - prepend_before_action :verify_with_email, only: :create, unless: -> { two_factor_enabled? } + prepend_before_action :verify_with_email, only: :create, unless: -> { skip_verify_with_email? } skip_before_action :required_signup_info, only: :successful_verification end @@ -55,6 +55,10 @@ module VerifiesWithEmail private + def skip_verify_with_email? + two_factor_enabled? || Gitlab::Qa.request?(request.user_agent) + end + def find_verification_user return unless session[:verification_user_id] diff --git a/app/graphql/resolvers/issues/base_resolver.rb b/app/graphql/resolvers/issues/base_resolver.rb index 88579b09482..9a2c4572abb 100644 --- a/app/graphql/resolvers/issues/base_resolver.rb +++ b/app/graphql/resolvers/issues/base_resolver.rb @@ -97,7 +97,7 @@ module Resolvers end def ready?(**args) - if args[:or].present? && ::Feature.disabled?(:or_issuable_queries, resource_parent) + if args[:or].present? && or_issuable_queries_disabled? raise ::Gitlab::Graphql::Errors::ArgumentError, "'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled." end @@ -115,6 +115,14 @@ module Resolvers private + def or_issuable_queries_disabled? + if respond_to?(:resource_parent, true) + ::Feature.disabled?(:or_issuable_queries, resource_parent) + else + ::Feature.disabled?(:or_issuable_queries) + end + end + def prepare_finder_params(args) params = super(args) params[:not] = params[:not].to_h if params[:not] diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb new file mode 100644 index 00000000000..e3102a7d32a --- /dev/null +++ b/app/graphql/resolvers/issues_resolver.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Resolvers + class IssuesResolver < Issues::BaseResolver + prepend ::Issues::LookAheadPreloads + include ::Issues::SortArguments + + argument :state, Types::IssuableStateEnum, + required: false, + description: 'Current state of this issue.' + + # see app/graphql/types/issue_connection.rb + type 'Types::IssueConnection', null: true + + def resolve_with_lookahead(**args) + return unless Feature.enabled?(:root_level_issues_query) + + issues = apply_lookahead( + IssuesFinder.new(current_user, prepare_finder_params(args)).execute + ) + + if non_stable_cursor_sort?(args[:sort]) + # Certain complex sorts are not supported by the stable cursor pagination yet. + # In these cases, we use offset pagination, so we return the correct connection. + offset_pagination(issues) + else + issues + end + end + end +end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 8fe6d364118..21cb3f9e06c 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -82,6 +82,13 @@ module Types field :echo, resolver: Resolvers::EchoResolver + field :issues, + null: true, + alpha: { milestone: '15.6' }, + resolver: Resolvers::IssuesResolver, + description: 'Issues visible by the current user.' \ + ' Returns null if the `root_level_issues_query` feature flag is disabled.' + field :issue, Types::IssueType, null: true, description: 'Find an issue.' do diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index b234669c6d3..e8fc6bc292f 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -148,8 +148,13 @@ module GroupsHelper } end - def subgroups_and_projects_list_app_data(group) + def group_overview_tabs_app_data(group) { + subgroups_and_projects_endpoint: group_children_path(group, format: :json), + shared_projects_endpoint: group_shared_projects_path(group, format: :json), + archived_projects_endpoint: group_children_path(group, format: :json, archived: 'only'), + current_group_visibility: group.visibility, + initial_sort: project_list_sort_by, show_schema_markup: 'true', new_subgroup_path: new_group_path(parent_id: group.id, anchor: 'create-group-pane'), new_project_path: new_project_path(namespace_id: group.id), @@ -162,16 +167,6 @@ module GroupsHelper } end - def group_overview_tabs_app_data(group) - { - subgroups_and_projects_endpoint: group_children_path(group, format: :json), - shared_projects_endpoint: group_shared_projects_path(group, format: :json), - archived_projects_endpoint: group_children_path(group, format: :json, archived: 'only'), - current_group_visibility: group.visibility, - initial_sort: project_list_sort_by - }.merge(subgroups_and_projects_list_app_data(group)) - end - def enabled_git_access_protocol_options_for_group case ::Gitlab::CurrentSettings.enabled_git_access_protocol when nil, "" diff --git a/app/helpers/recaptcha_helper.rb b/app/helpers/recaptcha_helper.rb index 59f0dc8f819..b6b75d03b2e 100644 --- a/app/helpers/recaptcha_helper.rb +++ b/app/helpers/recaptcha_helper.rb @@ -2,27 +2,17 @@ module RecaptchaHelper def recaptcha_enabled? - return false if gitlab_qa? + return false if Gitlab::Qa.request?(request.user_agent) !!Gitlab::Recaptcha.enabled? end alias_method :show_recaptcha_sign_up?, :recaptcha_enabled? def recaptcha_enabled_on_login? - return false if gitlab_qa? + return false if Gitlab::Qa.request?(request.user_agent) Gitlab::Recaptcha.enabled_on_login? end - - private - - def gitlab_qa? - return false unless Gitlab.com? - return false unless request.user_agent.present? - return false unless Gitlab::Environment.qa_user_agent.present? - - ActiveSupport::SecurityUtils.secure_compare(request.user_agent, Gitlab::Environment.qa_user_agent) - end end RecaptchaHelper.prepend_mod diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 387d492b886..735c0df1529 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -246,9 +246,7 @@ class MergeRequest < ApplicationRecord end after_transition any => [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking, :can_be_merged, :cannot_be_merged] do |merge_request, transition| - if Feature.enabled?(:trigger_mr_subscription_on_merge_status_change, merge_request.project) - GraphqlTriggers.merge_request_merge_status_updated(merge_request) - end + GraphqlTriggers.merge_request_merge_status_updated(merge_request) end # rubocop: disable CodeReuse/ServiceClass diff --git a/app/views/groups/_archived_projects.html.haml b/app/views/groups/_archived_projects.html.haml deleted file mode 100644 index 21107cc22a1..00000000000 --- a/app/views/groups/_archived_projects.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -#js-groups-archived-tree - .empty-state.text-center.hidden - %p= _("There are no archived projects yet") - - %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } } - .js-groups-list-holder - = gl_loading_icon(size: 'md', css_class: 'gl-mt-6') diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml deleted file mode 100644 index 6063d160fab..00000000000 --- a/app/views/groups/_shared_projects.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -#js-groups-shared-tree - .empty-state.text-center.hidden - %p= _("There are no projects shared with this group yet") - - %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } } - .js-groups-list-holder{ data: { current_group_visibility: group.visibility } } - = gl_loading_icon diff --git a/app/views/groups/_subgroups_and_projects.html.haml b/app/views/groups/_subgroups_and_projects.html.haml deleted file mode 100644 index dc749af3c0c..00000000000 --- a/app/views/groups/_subgroups_and_projects.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -#js-groups-subgroups_and_projects-tree - %section{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } } - .js-groups-list-holder{ data: subgroups_and_projects_list_app_data(group) } - = gl_loading_icon(size: 'md', css_class: 'gl-mt-6') diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index d535d13ca64..72b7bec1b92 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -29,36 +29,4 @@ = render_if_exists 'groups/group_activity_analytics', group: @group -- if Feature.enabled?(:group_overview_tabs_vue, @group) - #js-group-overview-tabs{ data: group_overview_tabs_app_data(@group) } -- else - .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } } - .top-area.group-nav-container.justify-content-between - .scrolling-tabs-container.inner-page-scroll-tabs - .fade-left= sprite_icon('chevron-lg-left', size: 12) - .fade-right= sprite_icon('chevron-lg-right', size: 12) - -# `item_active` is set to `false` as the active state is set by `app/assets/javascripts/pages/groups/shared/group_details.js` - -# TODO: Replace this approach in https://gitlab.com/gitlab-org/gitlab/-/issues/23466 - = gl_tabs_nav({ class: 'nav-links scrolling-tabs gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do - = gl_tab_link_to group_path, item_active: false, tab_class: 'js-subgroups_and_projects-tab', data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab' } do - = _("Subgroups and projects") - = gl_tab_link_to group_shared_path, item_active: false, tab_class: 'js-shared-tab', data: { target: 'div#shared', action: 'shared', toggle: 'tab' } do - = _("Shared projects") - = gl_tab_link_to group_archived_path, item_active: false, tab_class: 'js-archived-tab', data: { target: 'div#archived', action: 'archived', toggle: 'tab' } do - = _("Archived projects") - - .nav-controls.d-block.d-md-flex - .group-search - = render "shared/groups/search_form" - - = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash - - .tab-content - #subgroups_and_projects.tab-pane - = render "subgroups_and_projects", group: @group - - #shared.tab-pane - = render "shared_projects", group: @group - - #archived.tab-pane - = render "archived_projects", group: @group +#js-group-overview-tabs{ data: group_overview_tabs_app_data(@group) } diff --git a/config/feature_flags/development/use_cdn_with_job_artifacts_ui_downloads.yml b/config/feature_flags/development/root_level_issues_query.yml similarity index 65% rename from config/feature_flags/development/use_cdn_with_job_artifacts_ui_downloads.yml rename to config/feature_flags/development/root_level_issues_query.yml index 25ed76195aa..308f9168167 100644 --- a/config/feature_flags/development/use_cdn_with_job_artifacts_ui_downloads.yml +++ b/config/feature_flags/development/root_level_issues_query.yml @@ -1,8 +1,8 @@ --- -name: use_cdn_with_job_artifacts_ui_downloads -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102839 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/381479 +name: root_level_issues_query +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102348 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382250 milestone: '15.6' type: development -group: group::pipeline insights +group: group::project management default_enabled: false diff --git a/config/feature_flags/development/trigger_mr_subscription_on_merge_status_change.yml b/config/feature_flags/development/trigger_mr_subscription_on_merge_status_change.yml deleted file mode 100644 index 058fde35110..00000000000 --- a/config/feature_flags/development/trigger_mr_subscription_on_merge_status_change.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: trigger_mr_subscription_on_merge_status_change -introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/99213" -rollout_issue_url: "https://gitlab.com/gitlab-org/gitlab/-/issues/375704" -milestone: '15.5' -type: development -group: group::code review -default_enabled: false diff --git a/config/feature_flags/development/group_overview_tabs_vue.yml b/config/feature_flags/development/vue_group_select.yml similarity index 64% rename from config/feature_flags/development/group_overview_tabs_vue.yml rename to config/feature_flags/development/vue_group_select.yml index 4c54ab31b53..d31f57a3eb9 100644 --- a/config/feature_flags/development/group_overview_tabs_vue.yml +++ b/config/feature_flags/development/vue_group_select.yml @@ -1,8 +1,8 @@ --- -name: group_overview_tabs_vue -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95850 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370872 -milestone: '15.4' +name: vue_group_select +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98597 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/381042 +milestone: '15.6' type: development -group: group::workspace +group: group::foundations default_enabled: false diff --git a/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml b/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml index 1ce9be56198..b8362fb9d71 100644 --- a/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml +++ b/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: create + series: 0 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml b/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml index 98ed67d0910..b93fe4f735e 100644 --- a/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml +++ b/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: create + series: 0 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml b/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml index 67177bce5dd..b6b3fbb3c49 100644 --- a/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml +++ b/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: create + series: 1 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml b/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml index c5c057fce25..4c457b33db3 100644 --- a/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml +++ b/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: create + series: 1 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml b/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml index c25864475c4..b0611253f8f 100644 --- a/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml +++ b/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: create + series: 2 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml b/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml index 37ec35d95ba..150e8ba7f3a 100644 --- a/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml +++ b/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: create + series: 2 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml b/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml index 59e3aff567d..d52c4b0066c 100644 --- a/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml +++ b/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: verify + series: 0 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml b/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml index 17ca0252a0c..6deabd1edda 100644 --- a/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml +++ b/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: verify + series: 0 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml b/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml index dfd8bb11919..6d0d53c94d4 100644 --- a/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml +++ b/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: verify + series: 1 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml b/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml index 6c76386456a..dd412d2cb96 100644 --- a/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml +++ b/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: verify + series: 1 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml b/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml index 8cc755cc5bf..f076b40f403 100644 --- a/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml +++ b/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: verify + series: 2 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml b/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml index 61940a9e0cb..1eea75bb65f 100644 --- a/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml +++ b/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: verify + series: 2 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml b/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml index cc2a9c29f1f..216997d9710 100644 --- a/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml +++ b/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: trial + series: 0 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml b/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml index 3b4a1f7a88e..4a8cbd28827 100644 --- a/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml +++ b/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: trial + series: 0 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml b/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml index dd7487b6609..faaf52e4edd 100644 --- a/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml +++ b/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: trial + series: 1 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml b/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml index 0dd150778fe..87b8a1322d5 100644 --- a/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml +++ b/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: trial + series: 1 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml b/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml index 66610ba34f5..0a1c9d7018e 100644 --- a/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml +++ b/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: trial + series: 2 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml b/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml index e630c81f53c..10d03b730d0 100644 --- a/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml +++ b/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: trial + series: 2 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml b/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml index b8febff5fcd..572547424c7 100644 --- a/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml +++ b/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: team + series: 0 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml b/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml index d90a0f9dbde..aeaced8c4f2 100644 --- a/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml +++ b/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: team + series: 0 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml b/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml index 84f7d7d28af..3e8fead4c32 100644 --- a/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml +++ b/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: team + series: 1 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml b/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml index 1abe86f5a89..8445229f781 100644 --- a/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml +++ b/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: team + series: 1 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml b/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml index ba1530445e8..d8f4beb30c1 100644 --- a/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml +++ b/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: team + series: 2 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml b/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml index f9b473aec6c..b6ff4c4ad85 100644 --- a/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml +++ b/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml @@ -13,6 +13,10 @@ milestone: "13.12" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: team + series: 2 distribution: - ce - ee diff --git a/config/metrics/counts_all/20210727095918_in_product_marketing_email_team_short_0_cta_clicked.yml b/config/metrics/counts_all/20210727095918_in_product_marketing_email_team_short_0_cta_clicked.yml index 94b3340d1ca..4c8659e9f95 100644 --- a/config/metrics/counts_all/20210727095918_in_product_marketing_email_team_short_0_cta_clicked.yml +++ b/config/metrics/counts_all/20210727095918_in_product_marketing_email_team_short_0_cta_clicked.yml @@ -12,6 +12,10 @@ milestone: "14.2" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66854 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: team_short + series: 0 data_category: optional distribution: - ce diff --git a/config/metrics/counts_all/20210727095923_in_product_marketing_email_team_short_0_sent.yml b/config/metrics/counts_all/20210727095923_in_product_marketing_email_team_short_0_sent.yml index dbcd4faf26f..9f18e78c443 100644 --- a/config/metrics/counts_all/20210727095923_in_product_marketing_email_team_short_0_sent.yml +++ b/config/metrics/counts_all/20210727095923_in_product_marketing_email_team_short_0_sent.yml @@ -12,6 +12,10 @@ milestone: "14.2" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66854 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: team_short + series: 0 data_category: optional distribution: - ce diff --git a/config/metrics/counts_all/20210727170553_in_product_marketing_email_trial_short_0_cta_clicked.yml b/config/metrics/counts_all/20210727170553_in_product_marketing_email_trial_short_0_cta_clicked.yml index a23c977240a..57f1fcc6694 100644 --- a/config/metrics/counts_all/20210727170553_in_product_marketing_email_trial_short_0_cta_clicked.yml +++ b/config/metrics/counts_all/20210727170553_in_product_marketing_email_trial_short_0_cta_clicked.yml @@ -12,6 +12,10 @@ milestone: "14.2" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66943 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: trial_short + series: 0 data_category: optional distribution: - ce diff --git a/config/metrics/counts_all/20210727170558_in_product_marketing_email_trial_short_0_sent.yml b/config/metrics/counts_all/20210727170558_in_product_marketing_email_trial_short_0_sent.yml index c24e2f4b6b5..dc18b08081f 100644 --- a/config/metrics/counts_all/20210727170558_in_product_marketing_email_trial_short_0_sent.yml +++ b/config/metrics/counts_all/20210727170558_in_product_marketing_email_trial_short_0_sent.yml @@ -12,6 +12,10 @@ milestone: "14.2" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66943 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: trial_short + series: 0 data_category: optional distribution: - ce diff --git a/config/metrics/counts_all/20210729140021_in_product_marketing_email_admin_verify_0_cta_clicked.yml b/config/metrics/counts_all/20210729140021_in_product_marketing_email_admin_verify_0_cta_clicked.yml index 2a62f9a572f..a991d9362d1 100644 --- a/config/metrics/counts_all/20210729140021_in_product_marketing_email_admin_verify_0_cta_clicked.yml +++ b/config/metrics/counts_all/20210729140021_in_product_marketing_email_admin_verify_0_cta_clicked.yml @@ -12,6 +12,10 @@ milestone: "14.2" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67147 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailCtaClickedMetric +options: + track: admin_verify + series: 0 data_category: optional distribution: - ce diff --git a/config/metrics/counts_all/20210729140423_in_product_marketing_email_admin_verify_0_sent.yml b/config/metrics/counts_all/20210729140423_in_product_marketing_email_admin_verify_0_sent.yml index b04c9502a12..0ecf55cc5fc 100644 --- a/config/metrics/counts_all/20210729140423_in_product_marketing_email_admin_verify_0_sent.yml +++ b/config/metrics/counts_all/20210729140423_in_product_marketing_email_admin_verify_0_sent.yml @@ -12,6 +12,10 @@ milestone: "14.2" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67147 time_frame: all data_source: database +instrumentation_class: InProductMarketingEmailSentMetric +options: + track: admin_verify + series: 0 data_category: optional distribution: - ce diff --git a/db/migrate/20221110105857_add_index_for_in_product_marketing_email_metrics.rb b/db/migrate/20221110105857_add_index_for_in_product_marketing_email_metrics.rb new file mode 100644 index 00000000000..f1bc07e4197 --- /dev/null +++ b/db/migrate/20221110105857_add_index_for_in_product_marketing_email_metrics.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddIndexForInProductMarketingEmailMetrics < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + INDEX_NAME = 'index_in_product_marketing_emails_on_track_series_id_clicked' + + def up + add_concurrent_index :in_product_marketing_emails, %i[track series id cta_clicked_at], name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :in_product_marketing_emails, INDEX_NAME + end +end diff --git a/db/post_migrate/20221103150250_migrate_sidekiq_queued_jobs.rb b/db/post_migrate/20221103150250_migrate_sidekiq_queued_jobs.rb index ae2d7a47342..1934711f9ef 100644 --- a/db/post_migrate/20221103150250_migrate_sidekiq_queued_jobs.rb +++ b/db/post_migrate/20221103150250_migrate_sidekiq_queued_jobs.rb @@ -1,74 +1,8 @@ # frozen_string_literal: true class MigrateSidekiqQueuedJobs < Gitlab::Database::Migration[2.0] - class SidekiqMigrateJobs - LOG_FREQUENCY_QUEUES = 10 - - attr_reader :logger, :mappings - - # mappings is a hash of WorkerClassName => target_queue_name - def initialize(mappings, logger: nil) - @mappings = mappings - @logger = logger - end - - # Migrates jobs from queues that are outside the mappings - # rubocop: disable Cop/SidekiqRedisCall - def migrate_queues - routing_rules_queues = mappings.values.uniq - logger&.info("List of queues based on routing rules: #{routing_rules_queues}") - Sidekiq.redis do |conn| - # Redis 6 supports conn.scan_each(match: "queue:*", type: 'list') - conn.scan_each(match: "queue:*") do |key| - # Redis 5 compatibility - next unless conn.type(key) == 'list' - - queue_from = key.split(':', 2).last - next if routing_rules_queues.include?(queue_from) - - logger&.info("Migrating #{queue_from} queue") - - migrated = 0 - while queue_length(queue_from) > 0 - begin - if migrated >= 0 && migrated % LOG_FREQUENCY_QUEUES == 0 - logger&.info("Migrating from #{queue_from}. Total: #{queue_length(queue_from)}. Migrated: #{migrated}.") - end - - job = conn.rpop "queue:#{queue_from}" - job_hash = Sidekiq.load_json job - next unless mappings.has_key?(job_hash['class']) - - destination_queue = mappings[job_hash['class']] - job_hash['queue'] = destination_queue - conn.lpush("queue:#{destination_queue}", Sidekiq.dump_json(job_hash)) - migrated += 1 - rescue JSON::ParserError - logger&.error("Unmarshal JSON payload from SidekiqMigrateJobs failed. Job: #{job}") - next - end - end - logger&.info("Finished migrating #{queue_from} queue") - end - end - end - - private - - def queue_length(queue_name) - Sidekiq.redis do |conn| - conn.llen("queue:#{queue_name}") - end - end - # rubocop: enable Cop/SidekiqRedisCall - end - def up - return if Gitlab.com? - - mappings = Gitlab::SidekiqConfig.worker_queue_mappings - logger = ::Gitlab::BackgroundMigration::Logger.build - SidekiqMigrateJobs.new(mappings, logger: logger).migrate_queues + # no-op because of https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1991 end def down diff --git a/db/schema_migrations/20221110105857 b/db/schema_migrations/20221110105857 new file mode 100644 index 00000000000..f515180dcdf --- /dev/null +++ b/db/schema_migrations/20221110105857 @@ -0,0 +1 @@ +5ef00449d9c5b4a44b99410839d3ba4c4d6d8cb152460822b882c6ac60f771e3 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 852460460ec..6d632b6d0e7 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -29261,6 +29261,8 @@ CREATE INDEX index_imported_projects_on_import_type_creator_id_created_at ON pro CREATE INDEX index_imported_projects_on_import_type_id ON projects USING btree (import_type, id) WHERE (import_type IS NOT NULL); +CREATE INDEX index_in_product_marketing_emails_on_track_series_id_clicked ON in_product_marketing_emails USING btree (track, series, id, cta_clicked_at); + CREATE UNIQUE INDEX index_in_product_marketing_emails_on_user_campaign ON in_product_marketing_emails USING btree (user_id, campaign); CREATE INDEX index_in_product_marketing_emails_on_user_id ON in_product_marketing_emails USING btree (user_id); diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md index c610640c46e..a2463c6ff88 100644 --- a/doc/administration/reference_architectures/10k_users.md +++ b/doc/administration/reference_architectures/10k_users.md @@ -28,7 +28,7 @@ full list of reference architectures, see | Internal load balancing node3 | 1 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Redis/Sentinel - Cache2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | | Redis/Sentinel - Persistent2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | -| Gitaly56 | 3 | 16 vCPU, 60 GB memory | `n1-standard-16` | `m5.4xlarge` | +| Gitaly5 6 | 3 | 16 vCPU, 60 GB memory | `n1-standard-16` | `m5.4xlarge` | | Praefect5 | 3 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Praefect PostgreSQL1 | 1+ | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Sidekiq | 4 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | @@ -2300,6 +2300,10 @@ compute deployments. With this, _stateless_ components can benefit from cloud na workload management benefits while _stateful_ components are deployed in compute VMs with Omnibus to benefit from increased permanence. +Refer to the Helm charts [Advanced configuration](https://docs.gitlab.com/charts/advanced/) +documentation for setup instructions including guidance on what GitLab secrets to sync +between Kubernetes and the backend components. + NOTE: This is an **advanced** setup. Running services in Kubernetes is well known to be complex. **This setup is only recommended** if you have strong working @@ -2342,7 +2346,7 @@ services where applicable): | Internal load balancing node3 | 1 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Redis/Sentinel - Cache2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | | Redis/Sentinel - Persistent2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | -| Gitaly56 | 3 | 16 vCPU, 60 GB memory | `n1-standard-16` | `m5.4xlarge` | +| Gitaly5 6 | 3 | 16 vCPU, 60 GB memory | `n1-standard-16` | `m5.4xlarge` | | Praefect5 | 3 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Praefect PostgreSQL1 | 1+ | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Object storage4 | - | - | - | - | @@ -2467,7 +2471,7 @@ ratio for each additional pod. For further information on resource usage, see the [Sidekiq resources](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/#resources). -### Supporting +#### Supporting The Supporting Node Pool is designed to house all supporting deployments that don't need to be on the Webservice and Sidekiq pools. @@ -2480,6 +2484,12 @@ to deploy these in this pool where possible and not in the Webservice or Sidekiq specifically to accommodate several additional deployments. However, if your deployments don't fit into the pool as given, you can increase the node pool accordingly. +## Secrets + +When setting up a Cloud Native Hybrid environment, it's worth noting that several secrets should be synced from backend VMs from the `/etc/gitlab/gitlab-secrets.json` file into Kubernetes. + +For this setup specifically, the [GitLab Rails](https://docs.gitlab.com/charts/installation/secrets.html#gitlab-rails-secret) and [GitLab Shell](https://docs.gitlab.com/charts/installation/secrets.html#gitlab-rails-secret) secrets should be synced. +
Back to setup components diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md index a071bc94bf0..84eba01fe11 100644 --- a/doc/administration/reference_architectures/25k_users.md +++ b/doc/administration/reference_architectures/25k_users.md @@ -28,7 +28,7 @@ full list of reference architectures, see | Internal load balancing node3 | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | | Redis/Sentinel - Cache2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | | Redis/Sentinel - Persistent2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | -| Gitaly56 | 3 | 32 vCPU, 120 GB memory | `n1-standard-32` | `m5.8xlarge` | +| Gitaly5 6 | 3 | 32 vCPU, 120 GB memory | `n1-standard-32` | `m5.8xlarge` | | Praefect5 | 3 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | | Praefect PostgreSQL1 | 1+ | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Sidekiq | 4 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | @@ -2319,6 +2319,10 @@ compute deployments. With this, _stateless_ components can benefit from cloud na workload management benefits while _stateful_ components are deployed in compute VMs with Omnibus to benefit from increased permanence. +Refer to the Helm charts [Advanced configuration](https://docs.gitlab.com/charts/advanced/) +documentation for setup instructions including guidance on what GitLab secrets to sync +between Kubernetes and the backend components. + NOTE: This is an **advanced** setup. Running services in Kubernetes is well known to be complex. **This setup is only recommended** if you have strong working @@ -2361,7 +2365,7 @@ services where applicable): | Internal load balancing node3 | 1 | 4 vCPU, 3.6GB memory | `n1-highcpu-4` | `c5.xlarge` | | Redis/Sentinel - Cache2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | | Redis/Sentinel - Persistent2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | -| Gitaly56 | 3 | 32 vCPU, 120 GB memory | `n1-standard-32` | `m5.8xlarge` | +| Gitaly5 6 | 3 | 32 vCPU, 120 GB memory | `n1-standard-32` | `m5.8xlarge` | | Praefect5 | 3 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | | Praefect PostgreSQL1 | 1+ | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Object storage4 | - | - | - | - | diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md index 07f857c7f3e..1acae93f764 100644 --- a/doc/administration/reference_architectures/2k_users.md +++ b/doc/administration/reference_architectures/2k_users.md @@ -1023,7 +1023,9 @@ compute deployments. With this, _stateless_ components can benefit from cloud na workload management benefits while _stateful_ components are deployed in compute VMs with Omnibus to benefit from increased permanence. -The 2,000 reference architecture is not a highly-available setup. To achieve HA, you can follow a modified [3K reference architecture](3k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative). +Refer to the Helm charts [Advanced configuration](https://docs.gitlab.com/charts/advanced/) +documentation for setup instructions including guidance on what GitLab secrets to sync +between Kubernetes and the backend components. NOTE: This is an **advanced** setup. Running services in Kubernetes is well known @@ -1031,6 +1033,10 @@ to be complex. **This setup is only recommended** if you have strong working knowledge and experience in Kubernetes. The rest of this section assumes this. +NOTE: +The 2,000 reference architecture is not a highly-available setup. To achieve HA, +you can follow a modified [3K reference architecture](3k_users.md#cloud-native-hybrid-reference-architecture-with-helm-charts-alternative). + NOTE: **Gitaly Cluster is not supported to be run in Kubernetes**. Refer to [epic 6127](https://gitlab.com/groups/gitlab-org/-/epics/6127) for more details. diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md index 23b5f59f674..74074f0803b 100644 --- a/doc/administration/reference_architectures/3k_users.md +++ b/doc/administration/reference_architectures/3k_users.md @@ -37,7 +37,7 @@ For a full list of reference architectures, see | PostgreSQL1 | 3 | 2 vCPU, 7.5 GB memory | `n1-standard-2` | `m5.large` | | PgBouncer1 | 3 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Internal load balancing node3 | 1 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | -| Gitaly56 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | +| Gitaly5 6 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | | Praefect5 | 3 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Praefect PostgreSQL1 | 1+ | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Sidekiq | 4 | 2 vCPU, 7.5 GB memory | `n1-standard-2` | `m5.large` | @@ -2291,6 +2291,10 @@ compute deployments. With this, _stateless_ components can benefit from cloud na workload management benefits while _stateful_ components are deployed in compute VMs with Omnibus to benefit from increased permanence. +Refer to the Helm charts [Advanced configuration](https://docs.gitlab.com/charts/advanced/) +documentation for setup instructions including guidance on what GitLab secrets to sync +between Kubernetes and the backend components. + NOTE: This is an **advanced** setup. Running services in Kubernetes is well known to be complex. **This setup is only recommended** if you have strong working @@ -2332,7 +2336,7 @@ services where applicable): | PostgreSQL1 | 3 | 2 vCPU, 7.5 GB memory | `n1-standard-2` | `m5.large` | | PgBouncer1 | 3 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Internal load balancing node3 | 1 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | -| Gitaly56 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | +| Gitaly5 6 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | | Praefect5 | 3 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Praefect PostgreSQL1 | 1+ | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Object storage4 | - | - | - | - | diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md index 06889644223..ca159d62f1f 100644 --- a/doc/administration/reference_architectures/50k_users.md +++ b/doc/administration/reference_architectures/50k_users.md @@ -28,7 +28,7 @@ full list of reference architectures, see | Internal load balancing node3 | 1 | 8 vCPU, 7.2 GB memory | `n1-highcpu-8` | `c5.2xlarge` | | Redis/Sentinel - Cache2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | | Redis/Sentinel - Persistent2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | -| Gitaly56 | 3 | 64 vCPU, 240 GB memory | `n1-standard-64` | `m5.16xlarge` | +| Gitaly5 6 | 3 | 64 vCPU, 240 GB memory | `n1-standard-64` | `m5.16xlarge` | | Praefect5 | 3 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | | Praefect PostgreSQL1 | 1+ | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Sidekiq | 4 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | @@ -2321,6 +2321,10 @@ compute deployments. With this, _stateless_ components can benefit from cloud na workload management benefits while _stateful_ components are deployed in compute VMs with Omnibus to benefit from increased permanence. +Refer to the Helm charts [Advanced configuration](https://docs.gitlab.com/charts/advanced/) +documentation for setup instructions including guidance on what GitLab secrets to sync +between Kubernetes and the backend components. + NOTE: This is an **advanced** setup. Running services in Kubernetes is well known to be complex. **This setup is only recommended** if you have strong working @@ -2363,7 +2367,7 @@ services where applicable): | Internal load balancing node3 | 1 | 8 vCPU, 7.2 GB memory | `n1-highcpu-8` | `c5.2xlarge` | | Redis/Sentinel - Cache2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | | Redis/Sentinel - Persistent2 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | -| Gitaly56 | 3 | 64 vCPU, 240 GB memory | `n1-standard-64` | `m5.16xlarge` | +| Gitaly5 6 | 3 | 64 vCPU, 240 GB memory | `n1-standard-64` | `m5.16xlarge` | | Praefect5 | 3 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | | Praefect PostgreSQL1 | 1+ | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Object storage4 | - | - | - | - | diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md index b27d8ce4847..a2b92f9c300 100644 --- a/doc/administration/reference_architectures/5k_users.md +++ b/doc/administration/reference_architectures/5k_users.md @@ -34,7 +34,7 @@ costly-to-operate environment by using the | PostgreSQL1 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | | PgBouncer1 | 3 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Internal load balancing node3 | 1 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | -| Gitaly56 | 3 | 8 vCPU, 30 GB memory | `n1-standard-8` | `m5.2xlarge` | +| Gitaly5 6 | 3 | 8 vCPU, 30 GB memory | `n1-standard-8` | `m5.2xlarge` | | Praefect5 | 3 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Praefect PostgreSQL1 | 1+ | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Sidekiq | 4 | 2 vCPU, 7.5 GB memory | `n1-standard-2` | `m5.large` | @@ -2265,6 +2265,10 @@ compute deployments. With this, _stateless_ components can benefit from cloud na workload management benefits while _stateful_ components are deployed in compute VMs with Omnibus to benefit from increased permanence. +Refer to the Helm charts [Advanced configuration](https://docs.gitlab.com/charts/advanced/) +documentation for setup instructions including guidance on what GitLab secrets to sync +between Kubernetes and the backend components. + NOTE: This is an **advanced** setup. Running services in Kubernetes is well known to be complex. **This setup is only recommended** if you have strong working @@ -2306,7 +2310,7 @@ services where applicable): | PostgreSQL1 | 3 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | | PgBouncer1 | 3 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Internal load balancing node3 | 1 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | -| Gitaly56 | 3 | 8 vCPU, 30 GB memory | `n1-standard-8` | `m5.2xlarge` | +| Gitaly5 6 | 3 | 8 vCPU, 30 GB memory | `n1-standard-8` | `m5.2xlarge` | | Praefect5 | 3 | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Praefect PostgreSQL1 | 1+ | 2 vCPU, 1.8 GB memory | `n1-highcpu-2` | `c5.large` | | Object storage4 | - | - | - | - | diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index b230725a728..1c4e6923867 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -214,6 +214,57 @@ Returns [`Issue`](#issue). | ---- | ---- | ----------- | | `id` | [`IssueID!`](#issueid) | Global ID of the issue. | +### `Query.issues` + +Issues visible by the current user. Returns null if the `root_level_issues_query` feature flag is disabled. + +WARNING: +**Introduced** in 15.6. +This feature is in Alpha. It can be changed or removed at any time. + +Returns [`IssueConnection`](#issueconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#connection-pagination-arguments): +`before: String`, `after: String`, `first: Int`, `last: Int`. + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `assigneeId` | [`String`](#string) | ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported. | +| `assigneeUsername` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.11. Use `assigneeUsernames`. | +| `assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. | +| `authorUsername` | [`String`](#string) | Username of the author of the issue. | +| `closedAfter` | [`Time`](#time) | Issues closed after this date. | +| `closedBefore` | [`Time`](#time) | Issues closed before this date. | +| `confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. | +| `createdAfter` | [`Time`](#time) | Issues created after this date. | +| `createdBefore` | [`Time`](#time) | Issues created before this date. | +| `crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. | +| `crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. | +| `epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. | +| `healthStatusFilter` | [`HealthStatusFilter`](#healthstatusfilter) | Health status of the issue, "none" and "any" values are supported. | +| `iid` | [`String`](#string) | IID of the issue. For example, "1". | +| `iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. | +| `in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. | +| `includeSubepics` | [`Boolean`](#boolean) | Whether to include subepics when filtering issues by epicId. | +| `iterationId` | [`[ID]`](#id) | List of iteration Global IDs applied to the issue. | +| `iterationWildcardId` | [`IterationWildcardId`](#iterationwildcardid) | Filter by iteration ID wildcard. | +| `labelName` | [`[String]`](#string) | Labels applied to this issue. | +| `milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. | +| `milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. | +| `myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. | +| `not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. | +| `or` | [`UnionedIssueFilterInput`](#unionedissuefilterinput) | List of arguments with inclusive OR. | +| `search` | [`String`](#string) | Search query for title or description. | +| `sort` | [`IssueSort`](#issuesort) | Sort issues by this criteria. | +| `state` | [`IssuableState`](#issuablestate) | Current state of this issue. | +| `types` | [`[IssueType!]`](#issuetype) | Filter issues by the given issue types. | +| `updatedAfter` | [`Time`](#time) | Issues updated after this date. | +| `updatedBefore` | [`Time`](#time) | Issues updated before this date. | +| `weight` | [`String`](#string) | Weight applied to the issue, "none" and "any" values are supported. | + ### `Query.iteration` Find an iteration. diff --git a/doc/api/openapi/openapi.yaml b/doc/api/openapi/openapi.yaml index 93326fd15e2..9ee2b8119be 100644 --- a/doc/api/openapi/openapi.yaml +++ b/doc/api/openapi/openapi.yaml @@ -78,45 +78,45 @@ paths: metadata: get: - tags: - - metadata - summary: 'Retrieve metadata information for this GitLab instance.' - operationId: 'getMetadata' - responses: - '401': - description: 'unauthorized operation' - '200': - description: 'successful operation' - content: - 'application/json': - schema: - title: 'MetadataResponse' - type: 'object' - properties: - version: - type: 'string' - revision: - type: 'string' - kas: - type: 'object' - properties: - enabled: - type: 'boolean' - externalUrl: - type: 'string' - nullable: true - version: - type: 'string' - nullable: true - examples: - Example: - value: - version: '15.0-pre' - revision: 'c401a659d0c' + tags: + - metadata + summary: 'Retrieve metadata information for this GitLab instance.' + operationId: 'getMetadata' + responses: + '401': + description: 'unauthorized operation' + '200': + description: 'successful operation' + content: + 'application/json': + schema: + title: 'MetadataResponse' + type: 'object' + properties: + version: + type: 'string' + revision: + type: 'string' kas: - enabled: true - externalUrl: 'grpc://gitlab.example.com:8150' - version: '15.0.0' + type: 'object' + properties: + enabled: + type: 'boolean' + externalUrl: + type: 'string' + nullable: true + version: + type: 'string' + nullable: true + examples: + Example: + value: + version: '15.0-pre' + revision: 'c401a659d0c' + kas: + enabled: true + externalUrl: 'grpc://gitlab.example.com:8150' + version: '15.0.0' version: get: diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md index 20ff4aa0da3..a7f3cfb59d3 100644 --- a/doc/ci/pipelines/settings.md +++ b/doc/ci/pipelines/settings.md @@ -373,12 +373,12 @@ https://gitlab.example.com///badges//coverage.svg ``` To get the coverage report from a specific job, add -the `job=coverage_job_name` parameter to the URL. For example, the following -Markdown code embeds the test coverage report badge of the `coverage` job -in your `README.md`: +the `job=coverage_job_name` parameter to the URL. For example, you can use code +similar to the following to add the test coverage report badge of the `coverage` job +to a Markdown file: ```markdown -![coverage](https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=coverage) +![coverage](https://gitlab.example.com///badges//coverage.svg?job=coverage) ``` #### Test coverage report badge colors and limits diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md index 5ab2ca63152..8ffe044c4d8 100644 --- a/doc/development/testing_guide/end_to_end/index.md +++ b/doc/development/testing_guide/end_to_end/index.md @@ -196,7 +196,8 @@ Use these environment variables to configure metrics export: | `QA_INFLUXDB_URL` | `true` | Should be set to `https://influxdb.quality.gitlab.net`. No default value. | | `QA_INFLUXDB_TOKEN` | `true` | InfluxDB write token that can be found under `Influxdb auth tokens` document in `Gitlab-QA` `1Password` vault. No default value. | | `QA_RUN_TYPE` | `false` | Arbitrary name for test execution, like `package-and-test`. Automatically inferred from the project name for live environment test executions. No default value. | -| `QA_EXPORT_TEST_METRICS` | `false` | Flag to enable or disable metrics export. Defaults to `true`. | +| `QA_EXPORT_TEST_METRICS` | `false` | Flag to enable or disable metrics export to InfluxDB. Defaults to `false`. | +| `QA_SAVE_TEST_METRICS` | `false` | Flag to enable or disable saving metrics as JSON file. Defaults to `false`. | ## Test reports diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 177393c55a0..4ed9ceedb4d 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -640,6 +640,7 @@ The following variables are used for configuring specific analyzers (used for a | `GEMNASIUM_DB_REMOTE_URL` | `gemnasium` | `https://gitlab.com/gitlab-org/security-products/gemnasium-db.git` | Repository URL for fetching the Gemnasium database. | | `GEMNASIUM_DB_REF_NAME` | `gemnasium` | `master` | Branch name for remote repository database. `GEMNASIUM_DB_REMOTE_URL` is required. | | `DS_REMEDIATE` | `gemnasium` | `"true"`, `"false"` in FIPS mode | Enable automatic remediation of vulnerable dependencies. Not supported in FIPS mode. | +| `DS_REMEDIATE_TIMEOUT` | `gemnasium` | `5m` | Timeout for auto-remediation. | | `GEMNASIUM_LIBRARY_SCAN_ENABLED` | `gemnasium` | `"true"` | Enable detecting vulnerabilities in vendored JavaScript libraries. For now, `gemnasium` leverages [`Retire.js`](https://github.com/RetireJS/retire.js) to do this job. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/350512) in GitLab 14.8. | | `DS_JAVA_VERSION` | `gemnasium-maven` | `17` | Version of Java. Available versions: `8`, `11`, `13`, `14`, `15`, `16`, `17`. Available versions in FIPS-enabled image: `8`, `11`, `17`. | | `MAVEN_CLI_OPTS` | `gemnasium-maven` | `"-DskipTests --batch-mode"` | List of command line arguments that are passed to `maven` by the analyzer. See an example for [using private repositories](../index.md#using-private-maven-repositories). | diff --git a/lib/gitlab/environment.rb b/lib/gitlab/environment.rb index 3c6ed696b9d..b1a9603d3a5 100644 --- a/lib/gitlab/environment.rb +++ b/lib/gitlab/environment.rb @@ -5,9 +5,5 @@ module Gitlab def self.hostname @hostname ||= ENV['HOSTNAME'] || Socket.gethostname end - - def self.qa_user_agent - ENV['GITLAB_QA_USER_AGENT'] - end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index bf111957a32..a0daa03bbed 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -65,6 +65,7 @@ module Gitlab push_frontend_feature_flag(:new_header_search) push_frontend_feature_flag(:source_editor_toolbar) push_frontend_feature_flag(:integration_slack_app_notifications) + push_frontend_feature_flag(:vue_group_select) end # Exposes the state of a feature flag to the frontend code. diff --git a/lib/gitlab/qa.rb b/lib/gitlab/qa.rb new file mode 100644 index 00000000000..c47a8982901 --- /dev/null +++ b/lib/gitlab/qa.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module Qa + def self.user_agent + ENV['GITLAB_QA_USER_AGENT'] + end + + def self.request?(request_user_agent) + return false unless Gitlab.com? + return false unless request_user_agent.present? + return false unless user_agent.present? + + ActiveSupport::SecurityUtils.secure_compare(request_user_agent, user_agent) + end + end +end diff --git a/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric.rb b/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric.rb new file mode 100644 index 00000000000..b1a2de29fd7 --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class InProductMarketingEmailCtaClickedMetric < DatabaseMetric + operation :count + + def initialize(metric_definition) + super + + unless track.in?(allowed_track) + raise ArgumentError, "track '#{track}' must be one of: #{allowed_track.join(', ')}" + end + + return if series.in?(allowed_series) + + raise ArgumentError, "series '#{series}' must be one of: #{allowed_series.join(', ')}" + end + + relation { Users::InProductMarketingEmail } + + private + + def relation + scope = super.where.not(cta_clicked_at: nil) + scope = scope.where(series: series) + scope.where(track: track) + end + + def track + options[:track] + end + + def series + options[:series] + end + + def allowed_track + Users::InProductMarketingEmail::ACTIVE_TRACKS.keys + end + + def allowed_series + @allowed_series ||= begin + series_amount = Namespaces::InProductMarketingEmailsService.email_count_for_track(track) + 0.upto(series_amount - 1).to_a + end + end + end + end + end + end +end diff --git a/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric.rb b/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric.rb new file mode 100644 index 00000000000..50dec606d9b --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class InProductMarketingEmailSentMetric < DatabaseMetric + operation :count + + def initialize(metric_definition) + super + + unless track.in?(allowed_track) + raise ArgumentError, "track '#{track}' must be one of: #{allowed_track.join(', ')}" + end + + return if series.in?(allowed_series) + + raise ArgumentError, "series '#{series}' must be one of: #{allowed_series.join(', ')}" + end + + relation { Users::InProductMarketingEmail } + + private + + def relation + scope = super + scope = scope.where(series: series) + scope.where(track: track) + end + + def track + options[:track] + end + + def series + options[:series] + end + + def allowed_track + Users::InProductMarketingEmail::ACTIVE_TRACKS.keys + end + + def allowed_series + @allowed_series ||= begin + series_amount = Namespaces::InProductMarketingEmailsService.email_count_for_track(track) + 0.upto(series_amount - 1).to_a + end + end + end + end + end + end +end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index dd22b7ada60..5021dac453f 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -158,8 +158,7 @@ module Gitlab integrations_usage, user_preferences_usage, container_expiration_policies_usage, - service_desk_counts, - email_campaign_counts + service_desk_counts ).tap do |data| data[:snippets] = add(data[:personal_snippets], data[:project_snippets]) end @@ -708,37 +707,6 @@ module Gitlab end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord - def email_campaign_counts - # rubocop:disable UsageData/LargeTable - sent_emails = count(Users::InProductMarketingEmail.group(:track, :series)) - clicked_emails = count(Users::InProductMarketingEmail.where.not(cta_clicked_at: nil).group(:track, :series)) - - Users::InProductMarketingEmail::ACTIVE_TRACKS.keys.each_with_object({}) do |track, result| - series_amount = Namespaces::InProductMarketingEmailsService.email_count_for_track(track) - # rubocop: enable UsageData/LargeTable: - - 0.upto(series_amount - 1).map do |series| - sent_count = sent_in_product_marketing_email_count(sent_emails, track, series) - clicked_count = clicked_in_product_marketing_email_count(clicked_emails, track, series) - - result["in_product_marketing_email_#{track}_#{series}_sent"] = sent_count - result["in_product_marketing_email_#{track}_#{series}_cta_clicked"] = clicked_count unless track == 'experience' - end - end - end - # rubocop: enable CodeReuse/ActiveRecord - - def sent_in_product_marketing_email_count(sent_emails, track, series) - # When there is an error with the query and it's not the Hash we expect, we return what we got from `count`. - sent_emails.is_a?(Hash) ? sent_emails.fetch([track, series], 0) : sent_emails - end - - def clicked_in_product_marketing_email_count(clicked_emails, track, series) - # When there is an error with the query and it's not the Hash we expect, we return what we got from `count`. - clicked_emails.is_a?(Hash) ? clicked_emails.fetch([track, series], 0) : clicked_emails - end - def total_alert_issues # Remove prometheus table queries once they are deprecated # To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/217407. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f719a9833e1..9c6d59edcea 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2954,7 +2954,7 @@ msgstr "" msgid "AdminSettings|Select a CI/CD template" msgstr "" -msgid "AdminSettings|Select a group to use as the source for instance-level project templates." +msgid "AdminSettings|Select a group to use as a source of custom templates for new projects. %{link_start}Learn more%{link_end}." msgstr "" msgid "AdminSettings|Select to disable public access for Pages sites, which requires users to sign in for access to the Pages sites in your instance. %{link_start}Learn more.%{link_end}" @@ -3017,9 +3017,6 @@ msgstr "" msgid "AdminSettings|The latest artifacts for all jobs in the most recent successful pipelines in each project are stored and do not expire." msgstr "" -msgid "AdminSettings|The projects in this group can be selected as templates for new projects created on the instance. %{link_start}Learn more.%{link_end} " -msgstr "" - msgid "AdminSettings|The template for the required pipeline configuration can be one of the GitLab-provided templates, or a custom template added to an instance template repository. %{link_start}How do I create an instance template repository?%{link_end}" msgstr "" @@ -15164,6 +15161,9 @@ msgstr "" msgid "Enter at least three characters to search" msgstr "" +msgid "Enter at least three characters to search." +msgstr "" + msgid "Enter in your Bitbucket Server URL and personal access token below" msgstr "" @@ -19425,7 +19425,7 @@ msgstr "" msgid "GroupSettings|Reporting" msgstr "" -msgid "GroupSettings|Select a subgroup to use as the source for custom project templates for this group." +msgid "GroupSettings|Select a subgroup to use as a source of custom templates for new projects in this group. %{link_start}Learn more%{link_end}." msgstr "" msgid "GroupSettings|Select parent group" @@ -19446,9 +19446,6 @@ msgstr "" msgid "GroupSettings|The Auto DevOps pipeline runs if no alternative CI configuration file is found." msgstr "" -msgid "GroupSettings|The projects in this subgroup can be selected as templates for new projects created in the group. %{link_start}Learn more.%{link_end}" -msgstr "" - msgid "GroupSettings|There was a problem updating Auto DevOps pipeline: %{error_messages}." msgstr "" @@ -27304,6 +27301,9 @@ msgstr "" msgid "No results found" msgstr "" +msgid "No results found." +msgstr "" + msgid "No runner executable" msgstr "" @@ -37033,6 +37033,9 @@ msgstr "" msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes." msgstr "" +msgid "Select a group" +msgstr "" + msgid "Select a label" msgstr "" @@ -41007,9 +41010,6 @@ msgstr "" msgid "There are no abuse reports!" msgstr "" -msgid "There are no archived projects yet" -msgstr "" - msgid "There are no archived requirements" msgstr "" @@ -41067,9 +41067,6 @@ msgstr "" msgid "There are no packages yet" msgstr "" -msgid "There are no projects shared with this group yet" -msgstr "" - msgid "There are no secure files yet." msgstr "" @@ -43212,6 +43209,12 @@ msgstr "" msgid "Unable to fetch branches list, please close the form and try again" msgstr "" +msgid "Unable to fetch group. Reload the page to try again." +msgstr "" + +msgid "Unable to fetch groups. Reload the page to try again." +msgstr "" + msgid "Unable to fetch upstream and downstream pipelines." msgstr "" diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 782ba1cf2fa..7cb7625118e 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -443,7 +443,11 @@ module QA end def export_metrics? - running_in_ci? && enabled?(ENV['QA_EXPORT_TEST_METRICS'], default: true) + enabled?(ENV['QA_EXPORT_TEST_METRICS'], default: false) + end + + def save_metrics_json? + enabled?(ENV['QA_SAVE_TEST_METRICS'], default: false) end def ee_activation_code diff --git a/qa/qa/specs/spec_helper.rb b/qa/qa/specs/spec_helper.rb index e3754c13ee3..8fb0ce0ab2f 100644 --- a/qa/qa/specs/spec_helper.rb +++ b/qa/qa/specs/spec_helper.rb @@ -24,7 +24,7 @@ RSpec.configure do |config| config.add_formatter QA::Support::Formatters::ContextFormatter config.add_formatter QA::Support::Formatters::QuarantineFormatter config.add_formatter QA::Support::Formatters::FeatureFlagFormatter - config.add_formatter QA::Support::Formatters::TestStatsFormatter if QA::Runtime::Env.export_metrics? + config.add_formatter QA::Support::Formatters::TestMetricsFormatter if QA::Runtime::Env.running_in_ci? config.before(:suite) do |suite| QA::Resource::ReusableCollection.register_resource_classes do |collection| diff --git a/qa/qa/support/formatters/test_stats_formatter.rb b/qa/qa/support/formatters/test_metrics_formatter.rb similarity index 73% rename from qa/qa/support/formatters/test_stats_formatter.rb rename to qa/qa/support/formatters/test_metrics_formatter.rb index 54b6625253d..e84373a487d 100644 --- a/qa/qa/support/formatters/test_stats_formatter.rb +++ b/qa/qa/support/formatters/test_metrics_formatter.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true +require "active_support/core_ext/string/conversions" + module QA module Support module Formatters - class TestStatsFormatter < RSpec::Core::Formatters::BaseFormatter + class TestMetricsFormatter < RSpec::Core::Formatters::BaseFormatter include Support::InfluxdbTools RSpec::Core::Formatters.register(self, :stop) @@ -13,29 +15,43 @@ module QA # @param [RSpec::Core::Notifications::ExamplesNotification] notification # @return [void] def stop(notification) - push_test_stats(notification.examples) - push_fabrication_stats + return log(:warn, "Missing run_type, skipping metrics export!") unless run_type + + parse_execution_data(notification.examples) + + if Runtime::Env.export_metrics? + push_test_metrics + push_fabrication_metrics + end + + save_test_metrics if Runtime::Env.save_metrics_json? end private - # Push test execution stats to influxdb + # Save execution data for the run # # @param [Array] examples - # @return [void] - def push_test_stats(examples) - data = examples.map { |example| test_stats(example) }.compact - - write_api.write(data: data) - log(:debug, "Pushed #{data.length} test execution entries to influxdb") - rescue StandardError => e - log(:error, "Failed to push test execution stats to influxdb, error: #{e}") + # @return [Array] + def execution_data(examples = nil) + @execution_metrics ||= examples.map { |example| test_stats(example) }.compact end + alias_method :parse_execution_data, :execution_data - # Push resource fabrication stats to influxdb + # Push test execution metrics to influxdb # # @return [void] - def push_fabrication_stats + def push_test_metrics + write_api.write(data: execution_data) + log(:debug, "Pushed #{execution_data.length} test execution entries to influxdb") + rescue StandardError => e + log(:error, "Failed to push test execution metrics to influxdb, error: #{e}") + end + + # Push resource fabrication metrics to influxdb + # + # @return [void] + def push_fabrication_metrics data = Tools::TestResourceDataProcessor.resources.flat_map do |resource, values| values.map { |v| fabrication_stats(resource: resource, **v) } end @@ -44,7 +60,16 @@ module QA write_api.write(data: data) log(:debug, "Pushed #{data.length} resource fabrication entries to influxdb") rescue StandardError => e - log(:error, "Failed to push fabrication stats to influxdb, error: #{e}") + log(:error, "Failed to push fabrication metrics to influxdb, error: #{e}") + end + + # Save metrics in json file + # + # @return [void] + def save_test_metrics + File.write("tmp/test-metrics-#{env('CI_JOB_NAME_SLUG') || 'local'}.json", execution_data.to_json) + rescue StandardError => e + log(:error, "Failed to save test execution metrics, error: #{e}") end # Transform example to influxdb compatible metrics data @@ -57,6 +82,9 @@ module QA api_fabrication = ((example.metadata[:api_fabrication] || 0) * 1000).round ui_fabrication = ((example.metadata[:browser_ui_fabrication] || 0) * 1000).round + # do not export results for tests that are not compatible with environment + return if incompatible_env?(example) + { name: 'test-stats', time: time, @@ -101,6 +129,7 @@ module QA # @param [Symbol] fabrication_method # @param [Symbol] http_method # @param [Integer] fabrication_time + # @param [String] timestamp # @return [Hash] def fabrication_stats(resource:, info:, fabrication_method:, http_method:, fabrication_time:, timestamp:, **) { @@ -136,7 +165,7 @@ module QA @time ||= begin return Time.now unless env('CI_PIPELINE_CREATED_AT') - DateTime.strptime(env('CI_PIPELINE_CREATED_AT')).to_time + env('CI_PIPELINE_CREATED_AT').to_time end end @@ -172,6 +201,17 @@ module QA retry_attempts(example.metadata) > 0 ? :flaky : :passed end + # Check if test was skipped due to context condition + # + # @param [RSpec::Core::Example] example + # @return [Boolean] + def incompatible_env?(example) + return false unless example.execution_result.status == :pending + return false unless example.metadata[:skip] + + !example.metadata[:skip].to_s.include?("quarantine") # rubocop:disable Rails/NegateInclude + end + # Retry attempts # # @param [Hash] metadata diff --git a/qa/qa/support/influxdb_tools.rb b/qa/qa/support/influxdb_tools.rb index e53b843ca87..e817b096864 100644 --- a/qa/qa/support/influxdb_tools.rb +++ b/qa/qa/support/influxdb_tools.rb @@ -7,7 +7,11 @@ module QA # Common tools for use with influxdb metrics setup # module InfluxdbTools + # @return [String] bucket for storing all test run metrics INFLUX_TEST_METRICS_BUCKET = "e2e-test-stats" + # @return [String] bucket for storing metrics from main runs + INFLUX_MAIN_TEST_METRICS_BUCKET = "e2e-test-stats-main" + # @return [Array] live environment names LIVE_ENVS = %w[staging staging-canary staging-ref canary preprod production].freeze private diff --git a/qa/qa/tools/ci/test_metrics.rb b/qa/qa/tools/ci/test_metrics.rb new file mode 100644 index 00000000000..96df432374a --- /dev/null +++ b/qa/qa/tools/ci/test_metrics.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "active_support/core_ext/string/conversions" + +module QA + module Tools + module Ci + class TestMetrics + include Helpers + include Support::InfluxdbTools + + def initialize(metrics_file_glob) + @metrics_file_glob = metrics_file_glob + end + + def self.export(metrics_file_glob) + new(metrics_file_glob).export + end + + # Export metrics to main bucket + # + # @return [void] + def export + return logger.warn("No files matched pattern '#{metrics_file_glob}'") if metrics_files.empty? + + logger.info("Exporting #{metrics_data.size} entries to influxdb") + influx_client.create_write_api.write(data: metrics_data, bucket: INFLUX_MAIN_TEST_METRICS_BUCKET) + end + + private + + attr_reader :metrics_file_glob + + # Metrics data files + # + # @return [Array] + def metrics_files + @metrics_files ||= Dir.glob(metrics_file_glob) + end + + # Test metrics data + # + # @return [Array] + def metrics_data + @metrics_data ||= metrics_files + .flat_map { |file| JSON.parse(File.read(file), symbolize_names: true) } + .map { |entry| entry.merge(time: entry[:time].to_time) } + end + end + end + end +end diff --git a/qa/spec/support/formatters/test_stats_formatter_spec.rb b/qa/spec/support/formatters/test_metrics_formatter_spec.rb similarity index 90% rename from qa/spec/support/formatters/test_stats_formatter_spec.rb rename to qa/spec/support/formatters/test_metrics_formatter_spec.rb index a8e0ae62280..76bde98cc33 100644 --- a/qa/spec/support/formatters/test_stats_formatter_spec.rb +++ b/qa/spec/support/formatters/test_metrics_formatter_spec.rb @@ -3,7 +3,7 @@ require 'rspec/core/sandbox' require 'active_support/testing/time_helpers' -describe QA::Support::Formatters::TestStatsFormatter do +describe QA::Support::Formatters::TestMetricsFormatter do include QA::Support::Helpers::StubEnv include QA::Specs::Helpers::RSpec include ActiveSupport::Testing::TimeHelpers @@ -58,7 +58,7 @@ describe QA::Support::Formatters::TestStatsFormatter do testcase: testcase }, fields: { - id: './spec/support/formatters/test_stats_formatter_spec.rb[1:1]', + id: './spec/support/formatters/test_metrics_formatter_spec.rb[1:1]', run_time: 0, api_fabrication: api_fabrication * 1000, ui_fabrication: ui_fabrication * 1000, @@ -133,6 +133,7 @@ describe QA::Support::Formatters::TestStatsFormatter do stub_env('CI_MERGE_REQUEST_IID', nil) stub_env('TOP_UPSTREAM_MERGE_REQUEST_IID', nil) stub_env('QA_RUN_TYPE', run_type) + stub_env('QA_EXPORT_TEST_METRICS', "true") end context 'with reliable spec' do @@ -204,6 +205,20 @@ describe QA::Support::Formatters::TestStatsFormatter do end end + context 'with skipped spec' do + it 'skips export' do + run_spec do + it( + 'spec', + skip: 'not compatible', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234' + ) {} + end + + expect(influx_write_api).to have_received(:write).with(data: []) + end + end + context 'with staging full run' do let(:run_type) { 'staging-full' } @@ -301,5 +316,21 @@ describe QA::Support::Formatters::TestStatsFormatter do expect(influx_write_api).to have_received(:write).with(data: [fabrication_data]) end end + + context 'with persisting metrics' do + before do + stub_env('QA_EXPORT_TEST_METRICS', "false") + stub_env('QA_SAVE_TEST_METRICS', "true") + stub_env('CI_JOB_NAME_SLUG', "test-job") + + allow(File).to receive(:write) + end + + it 'saves test metrics as json files' do + run_spec + + expect(File).to have_received(:write).with("tmp/test-metrics-test-job.json", [data].to_json) + end + end end end diff --git a/qa/spec/tools/ci/test_metrics_spec.rb b/qa/spec/tools/ci/test_metrics_spec.rb new file mode 100644 index 00000000000..4c1c4092d15 --- /dev/null +++ b/qa/spec/tools/ci/test_metrics_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.describe QA::Tools::Ci::TestMetrics do + include QA::Support::Helpers::StubEnv + + let(:influx_client) { instance_double("InfluxDB2::Client", create_write_api: influx_write_api) } + let(:influx_write_api) { instance_double("InfluxDB2::WriteApi", write: nil) } + let(:logger) { instance_double("Logger", info: true, warn: true) } + + let(:glob) { "metrics_glob/*.json" } + let(:paths) { ["/metrics_glob/metrics.json"] } + let(:timestamp) { "2022-11-11 07:54:11 +0000" } + let(:metrics_json) { metrics_data.to_json } + + let(:metrics_data) do + [ + { + time: timestamp.to_time, + name: "name", + tags: {}, + fields: {} + } + ] + end + + before do + allow(InfluxDB2::Client).to receive(:new) { influx_client } + allow(Gitlab::QA::TestLogger).to receive(:logger) { logger } + allow(Dir).to receive(:glob).with(glob) { paths } + allow(File).to receive(:read).with(paths.first) { metrics_json } + + stub_env('QA_INFLUXDB_URL', "test") + stub_env('QA_INFLUXDB_TOKEN', "test") + end + + context "with metrics files present" do + it "exports saved metrics to influxdb" do + described_class.export(glob) + + expect(influx_write_api).to have_received(:write).with(data: metrics_data, bucket: "e2e-test-stats-main") + end + end + + context "without metrics files present" do + let(:paths) { [] } + + it "exits without error" do + described_class.export(glob) + + expect(influx_write_api).not_to have_received(:write) + expect(logger).to have_received(:warn).with("No files matched pattern '#{glob}'") + end + end +end diff --git a/qa/tasks/ci.rake b/qa/tasks/ci.rake index 92b837447c7..84a26e3e555 100644 --- a/qa/tasks/ci.rake +++ b/qa/tasks/ci.rake @@ -1,8 +1,8 @@ # frozen_string_literal: true -require_relative "helpers/util" - namespace :ci do + require_relative "helpers/util" + include Task::Helpers::Util desc "Detect changes and populate test variables for selective test execution and feature flag testing" @@ -58,4 +58,11 @@ namespace :ci do task :download_test_results, [:trigger_name, :test_report_job_name, :report_path] do |_, args| QA::Tools::Ci::TestResults.get(args[:trigger_name], args[:test_report_job_name], args[:report_path]) end + + desc "Export test run metrics to influxdb" + task :export_test_metrics, [:glob] do |_, args| + raise("Metrics file glob pattern is required") unless args[:glob] + + QA::Tools::Ci::TestMetrics.export(args[:glob]) + end end diff --git a/scripts/generate-e2e-pipeline b/scripts/generate-e2e-pipeline index 8d3f7a44b27..aef2447e800 100755 --- a/scripts/generate-e2e-pipeline +++ b/scripts/generate-e2e-pipeline @@ -25,6 +25,8 @@ variables: GITLAB_QA_CACHE_KEY: "$qa_cache_key" GITLAB_VERSION: "$(cat VERSION)" COLORIZED_LOGS: "true" + QA_EXPORT_TEST_METRICS: "${QA_EXPORT_TEST_METRICS:-true}" + QA_SAVE_TEST_METRICS: "${QA_SAVE_TEST_METRICS:-false}" QA_RUN_ALL_TESTS: "${QA_RUN_ALL_TESTS:-false}" QA_FRAMEWORK_CHANGES: "${QA_FRAMEWORK_CHANGES:-false}" QA_FEATURE_FLAGS: "${QA_FEATURE_FLAGS}" diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb index 55e4129e533..0b24387483b 100644 --- a/spec/controllers/concerns/send_file_upload_spec.rb +++ b/spec/controllers/concerns/send_file_upload_spec.rb @@ -289,27 +289,13 @@ RSpec.describe SendFileUpload do allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { false } end - context 'when use_cdn_with_job_artifacts_ui_downloads feature is enabled' do - it 'sends a file when CDN URL' do - expect(uploader).to receive(:use_cdn?).and_return(true) - expect(uploader).to receive(:cdn_provider).and_return(cdn_provider) - expect(controller).to receive(:request).and_return(request) - expect(controller).to receive(:redirect_to).with(signed_url) + it 'sends a file when CDN URL' do + expect(uploader).to receive(:use_cdn?).and_return(true) + expect(uploader).to receive(:cdn_provider).and_return(cdn_provider) + expect(controller).to receive(:request).and_return(request) + expect(controller).to receive(:redirect_to).with(signed_url) - subject - end - end - - context 'when use_cdn_with_job_artifacts_ui_downloads is disabled' do - before do - stub_feature_flags(use_cdn_with_job_artifacts_ui_downloads: false) - end - - it 'sends a file' do - expect(controller).to receive(:redirect_to).with(/#{uploader.path}/) - - subject - end + subject end end end diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb index 50d0f5a65c6..00efd7d7b56 100644 --- a/spec/controllers/projects/artifacts_controller_spec.rb +++ b/spec/controllers/projects/artifacts_controller_spec.rb @@ -191,28 +191,13 @@ RSpec.describe Projects::ArtifactsController do request.env['action_dispatch.remote_ip'] = '18.245.0.42' end - context 'with use_cdn_with_job_artifacts_ui_downloads enabled' do - it 'redirects to a Google CDN request' do - expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).and_call_original - expect(Gitlab::ApplicationContext).to receive(:push).with(artifact_used_cdn: true).and_call_original + it 'redirects to a Google CDN request' do + expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).and_call_original + expect(Gitlab::ApplicationContext).to receive(:push).with(artifact_used_cdn: true).and_call_original - download_artifact(file_type: file_type) + download_artifact(file_type: file_type) - expect(response.redirect_url).to start_with("https://cdn.example.org/") - end - end - - context 'with use_cdn_with_job_artifacts_ui_downloads disabled' do - before do - stub_feature_flags(use_cdn_with_job_artifacts_ui_downloads: false) - end - - it 'does not redirect to the CDN' do - download_artifact(file_type: file_type) - - expect(response.redirect_url).to be_present - expect(response.redirect_url).not_to start_with("https://cdn.example.org/") - end + expect(response.redirect_url).to start_with("https://cdn.example.org/") end end end diff --git a/spec/frontend/vue_shared/components/group_select/group_select_spec.js b/spec/frontend/vue_shared/components/group_select/group_select_spec.js new file mode 100644 index 00000000000..f959d2225fa --- /dev/null +++ b/spec/frontend/vue_shared/components/group_select/group_select_spec.js @@ -0,0 +1,202 @@ +import { nextTick } from 'vue'; +import { GlListbox } from '@gitlab/ui'; +import MockAdapter from 'axios-mock-adapter'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import axios from '~/lib/utils/axios_utils'; +import { createAlert } from '~/flash'; +import GroupSelect from '~/vue_shared/components/group_select/group_select.vue'; +import { + TOGGLE_TEXT, + FETCH_GROUPS_ERROR, + FETCH_GROUP_ERROR, + QUERY_TOO_SHORT_MESSAGE, +} from '~/vue_shared/components/group_select/constants'; +import waitForPromises from 'helpers/wait_for_promises'; + +jest.mock('~/flash'); + +describe('GroupSelect', () => { + let wrapper; + let mock; + + // Mocks + const groupMock = { + full_name: 'selectedGroup', + id: '1', + }; + const groupEndpoint = `/api/undefined/groups/${groupMock.id}`; + + // Props + const inputName = 'inputName'; + const inputId = 'inputId'; + + // Finders + const findListbox = () => wrapper.findComponent(GlListbox); + const findInput = () => wrapper.findByTestId('input'); + + // Helpers + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMountExtended(GroupSelect, { + propsData: { + inputName, + inputId, + ...props, + }, + }); + }; + const openListbox = () => findListbox().vm.$emit('shown'); + const search = (searchString) => findListbox().vm.$emit('search', searchString); + const createComponentWithGroups = () => { + mock.onGet('/api/undefined/groups.json').reply(200, [groupMock]); + createComponent(); + openListbox(); + return waitForPromises(); + }; + const selectGroup = () => { + findListbox().vm.$emit('select', groupMock.id); + return nextTick(); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('on mount', () => { + it('fetches groups when the listbox is opened', async () => { + createComponent(); + await waitForPromises(); + + expect(mock.history.get).toHaveLength(0); + + openListbox(); + await waitForPromises(); + + expect(mock.history.get).toHaveLength(1); + }); + + describe('with an initial selection', () => { + it('if the selected group is not part of the fetched list, fetches it individually', async () => { + mock.onGet(groupEndpoint).reply(200, groupMock); + createComponent({ props: { initialSelection: groupMock.id } }); + await waitForPromises(); + + expect(mock.history.get).toHaveLength(1); + expect(findListbox().props('toggleText')).toBe(groupMock.full_name); + }); + + it('show an error if fetching the individual group fails', async () => { + mock + .onGet('/api/undefined/groups.json') + .reply(200, [{ full_name: 'notTheSelectedGroup', id: '2' }]); + mock.onGet(groupEndpoint).reply(500); + createComponent({ props: { initialSelection: groupMock.id } }); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: FETCH_GROUP_ERROR, + error: expect.any(Error), + parent: wrapper.vm.$el, + }); + }); + }); + }); + + it('shows an error when fetching groups fails', async () => { + mock.onGet('/api/undefined/groups.json').reply(500); + createComponent(); + openListbox(); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: FETCH_GROUPS_ERROR, + error: expect.any(Error), + parent: wrapper.vm.$el, + }); + }); + + describe('selection', () => { + it('uses the default toggle text while no group is selected', async () => { + await createComponentWithGroups(); + + expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT); + }); + + describe('once a group is selected', () => { + it(`uses the selected group's name as the toggle text`, async () => { + await createComponentWithGroups(); + await selectGroup(); + + expect(findListbox().props('toggleText')).toBe(groupMock.full_name); + }); + + it(`uses the selected group's ID as the listbox' and input value`, async () => { + await createComponentWithGroups(); + await selectGroup(); + + expect(findListbox().attributes('selected')).toBe(groupMock.id); + expect(findInput().attributes('value')).toBe(groupMock.id); + }); + + it(`on reset, falls back to the default toggle text`, async () => { + await createComponentWithGroups(); + await selectGroup(); + + findListbox().vm.$emit('reset'); + await nextTick(); + + expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT); + }); + }); + }); + + describe('search', () => { + it('sets `searching` to `true` when first opening the dropdown', async () => { + createComponent(); + + expect(findListbox().props('searching')).toBe(false); + + openListbox(); + await nextTick(); + + expect(findListbox().props('searching')).toBe(true); + }); + + it('sets `searching` to `true` while searching', async () => { + await createComponentWithGroups(); + + expect(findListbox().props('searching')).toBe(false); + + search('foo'); + await nextTick(); + + expect(findListbox().props('searching')).toBe(true); + }); + + it('fetches groups matching the search string', async () => { + const searchString = 'searchString'; + await createComponentWithGroups(); + + expect(mock.history.get).toHaveLength(1); + + search(searchString); + await waitForPromises(); + + expect(mock.history.get).toHaveLength(2); + expect(mock.history.get[1].params).toStrictEqual({ search: searchString }); + }); + + it('shows a notice if the search query is too short', async () => { + const searchString = 'a'; + await createComponentWithGroups(); + search(searchString); + await waitForPromises(); + + expect(mock.history.get).toHaveLength(1); + expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE); + }); + }); +}); diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index aebf41d1333..8b4ac6a7cfd 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -469,32 +469,6 @@ RSpec.describe GroupsHelper do end end - describe '#subgroups_and_projects_list_app_data' do - let_it_be(:group) { create(:group) } - let_it_be(:user) { create(:user) } - - before do - allow(helper).to receive(:current_user).and_return(user) - - allow(helper).to receive(:can?).with(user, :create_subgroup, group) { true } - allow(helper).to receive(:can?).with(user, :create_projects, group) { true } - end - - it 'returns expected hash' do - expect(helper.subgroups_and_projects_list_app_data(group)).to match({ - show_schema_markup: 'true', - new_subgroup_path: including("groups/new?parent_id=#{group.id}#create-group-pane"), - new_project_path: including("/projects/new?namespace_id=#{group.id}"), - new_subgroup_illustration: including('illustrations/subgroup-create-new-sm'), - new_project_illustration: including('illustrations/project-create-new-sm'), - empty_subgroup_illustration: including('illustrations/empty-state/empty-subgroup-md'), - render_empty_state: 'true', - can_create_subgroups: 'true', - can_create_projects: 'true' - }) - end - end - describe '#group_overview_tabs_app_data' do let_it_be(:group) { create(:group) } let_it_be(:user) { create(:user) } @@ -515,8 +489,17 @@ RSpec.describe GroupsHelper do shared_projects_endpoint: including("/groups/#{group.path}/-/shared_projects.json"), archived_projects_endpoint: including("/groups/#{group.path}/-/children.json?archived=only"), current_group_visibility: group.visibility, - initial_sort: initial_sort - }.merge(helper.group_overview_tabs_app_data(group)) + initial_sort: initial_sort, + show_schema_markup: 'true', + new_subgroup_path: including("groups/new?parent_id=#{group.id}#create-group-pane"), + new_project_path: including("/projects/new?namespace_id=#{group.id}"), + new_subgroup_illustration: including('illustrations/subgroup-create-new-sm'), + new_project_illustration: including('illustrations/project-create-new-sm'), + empty_subgroup_illustration: including('illustrations/empty-state/empty-subgroup-md'), + render_empty_state: 'true', + can_create_subgroups: 'true', + can_create_projects: 'true' + } ) end end diff --git a/spec/helpers/recaptcha_helper_spec.rb b/spec/helpers/recaptcha_helper_spec.rb index 2c327431437..d97712ce302 100644 --- a/spec/helpers/recaptcha_helper_spec.rb +++ b/spec/helpers/recaptcha_helper_spec.rb @@ -10,29 +10,12 @@ RSpec.describe RecaptchaHelper, type: :helper do end shared_examples 'Gitlab QA bypass' do - context 'when GITLAB_QA_USER_AGENT env var is present' do - using RSpec::Parameterized::TableSyntax - - where(:dot_com, :user_agent, :qa_user_agent, :result) do - false | 'qa_user_agent' | 'qa_user_agent' | true - true | nil | 'qa_user_agent' | true - true | '' | 'qa_user_agent' | true - true | 'qa_user_agent' | '' | true - true | 'qa_user_agent' | nil | true - true | 'qa_user_agent' | 'qa_user_agent' | false + context 'when it is a QA request' do + before do + allow(Gitlab::Qa).to receive(:request?).and_return(true) end - with_them do - before do - allow(Gitlab).to receive(:com?).and_return(dot_com) - stub_env('GITLAB_QA_USER_AGENT', qa_user_agent) - - request_double = instance_double(ActionController::TestRequest, user_agent: user_agent) - allow(helper).to receive(:request).and_return(request_double) - end - - it { is_expected.to eq result } - end + it { is_expected.to eq false } end end diff --git a/spec/lib/gitlab/qa_spec.rb b/spec/lib/gitlab/qa_spec.rb new file mode 100644 index 00000000000..c26f4c89fec --- /dev/null +++ b/spec/lib/gitlab/qa_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Qa do + describe '.request?' do + using RSpec::Parameterized::TableSyntax + + where(:dot_com, :request_user_agent, :qa_user_agent, :result) do + false | 'qa_user_agent' | 'qa_user_agent' | false + true | nil | 'qa_user_agent' | false + true | '' | 'qa_user_agent' | false + true | 'qa_user_agent' | '' | false + true | 'qa_user_agent' | nil | false + true | 'qa_user_agent' | 'qa_user_agent' | true + end + + with_them do + before do + allow(Gitlab).to receive(:com?).and_return(dot_com) + stub_env('GITLAB_QA_USER_AGENT', qa_user_agent) + end + + subject { described_class.request?(request_user_agent) } + + it { is_expected.to eq(result) } + end + end +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric_spec.rb new file mode 100644 index 00000000000..cb94da11d58 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::InProductMarketingEmailCtaClickedMetric do + using RSpec::Parameterized::TableSyntax + + let(:email_attributes) { { cta_clicked_at: Date.yesterday, track: 'verify', series: 0 } } + let(:options) { { track: 'verify', series: 0 } } + let(:expected_value) { 2 } + let(:expected_query) do + 'SELECT COUNT("in_product_marketing_emails"."id") FROM "in_product_marketing_emails"' \ + ' WHERE "in_product_marketing_emails"."cta_clicked_at" IS NOT NULL' \ + ' AND "in_product_marketing_emails"."series" = 0'\ + ' AND "in_product_marketing_emails"."track" = 1' + end + + before do + create_list :in_product_marketing_email, 2, email_attributes + + create :in_product_marketing_email, email_attributes.merge(cta_clicked_at: nil) + create :in_product_marketing_email, email_attributes.merge(track: 'team') + create :in_product_marketing_email, email_attributes.merge(series: 1) + end + + it_behaves_like 'a correct instrumented metric value and query', { + options: { track: 'verify', series: 0 }, + time_frame: 'all' + } + + where(:options_key, :valid_value, :invalid_value) do + :track | 'admin_verify' | 'invite_team' + :series | 1 | 5 + end + + with_them do + it "raises an exception if option is not present" do + expect do + described_class.new(options: options.except(options_key), time_frame: 'all') + end.to raise_error(ArgumentError, %r{#{options_key} .* must be one of}) + end + + it "raises an exception if option has invalid value" do + expect do + options[options_key] = invalid_value + described_class.new(options: options, time_frame: 'all') + end.to raise_error(ArgumentError, %r{#{options_key} .* must be one of}) + end + + it "doesn't raise exceptions if option has valid value" do + options[options_key] = valid_value + described_class.new(options: options, time_frame: 'all') + end + end +end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric_spec.rb new file mode 100644 index 00000000000..0cc82773d56 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::InProductMarketingEmailSentMetric do + using RSpec::Parameterized::TableSyntax + + let(:email_attributes) { { track: 'verify', series: 0 } } + let(:expected_value) { 2 } + let(:expected_query) do + 'SELECT COUNT("in_product_marketing_emails"."id") FROM "in_product_marketing_emails"' \ + ' WHERE "in_product_marketing_emails"."series" = 0'\ + ' AND "in_product_marketing_emails"."track" = 1' + end + + before do + create_list :in_product_marketing_email, 2, email_attributes + + create :in_product_marketing_email, email_attributes.merge(track: 'team') + create :in_product_marketing_email, email_attributes.merge(series: 1) + end + + it_behaves_like 'a correct instrumented metric value and query', { + options: { track: 'verify', series: 0 }, + time_frame: 'all' + } + + where(:options_key, :valid_value, :invalid_value) do + :track | 'admin_verify' | 'invite_team' + :series | 1 | 5 + end + + with_them do + it "raises an exception if option is not present" do + expect do + described_class.new(options: email_attributes.except(options_key), time_frame: 'all') + end.to raise_error(ArgumentError, %r{#{options_key} .* must be one of}) + end + + it "raises an exception if option has invalid value" do + expect do + email_attributes[options_key] = invalid_value + described_class.new(options: email_attributes, time_frame: 'all') + end.to raise_error(ArgumentError, %r{#{options_key} .* must be one of}) + end + + it "doesn't raise exceptions if option has valid value" do + email_attributes[options_key] = valid_value + described_class.new(options: email_attributes, time_frame: 'all') + end + end +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 5fbc807fa6d..d8f50fa27bb 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1178,110 +1178,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end end - describe '.email_campaign_counts' do - subject { described_class.send(:email_campaign_counts) } - - context 'when queries time out' do - before do - allow_any_instance_of(ActiveRecord::Relation).to receive(:count).and_raise(ActiveRecord::StatementInvalid) - allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(should_raise_for_dev) - end - - context 'with should_raise_for_dev? true' do - let(:should_raise_for_dev) { true } - - it 'raises an error' do - expect { subject }.to raise_error(ActiveRecord::StatementInvalid) - end - end - - context 'with should_raise_for_dev? false' do - let(:should_raise_for_dev) { false } - - it 'returns -1 for email campaign data' do - expected_data = { - "in_product_marketing_email_create_0_sent" => -1, - "in_product_marketing_email_create_0_cta_clicked" => -1, - "in_product_marketing_email_create_1_sent" => -1, - "in_product_marketing_email_create_1_cta_clicked" => -1, - "in_product_marketing_email_create_2_sent" => -1, - "in_product_marketing_email_create_2_cta_clicked" => -1, - "in_product_marketing_email_team_short_0_sent" => -1, - "in_product_marketing_email_team_short_0_cta_clicked" => -1, - "in_product_marketing_email_trial_short_0_sent" => -1, - "in_product_marketing_email_trial_short_0_cta_clicked" => -1, - "in_product_marketing_email_admin_verify_0_sent" => -1, - "in_product_marketing_email_admin_verify_0_cta_clicked" => -1, - "in_product_marketing_email_verify_0_sent" => -1, - "in_product_marketing_email_verify_0_cta_clicked" => -1, - "in_product_marketing_email_verify_1_sent" => -1, - "in_product_marketing_email_verify_1_cta_clicked" => -1, - "in_product_marketing_email_verify_2_sent" => -1, - "in_product_marketing_email_verify_2_cta_clicked" => -1, - "in_product_marketing_email_trial_0_sent" => -1, - "in_product_marketing_email_trial_0_cta_clicked" => -1, - "in_product_marketing_email_trial_1_sent" => -1, - "in_product_marketing_email_trial_1_cta_clicked" => -1, - "in_product_marketing_email_trial_2_sent" => -1, - "in_product_marketing_email_trial_2_cta_clicked" => -1, - "in_product_marketing_email_team_0_sent" => -1, - "in_product_marketing_email_team_0_cta_clicked" => -1, - "in_product_marketing_email_team_1_sent" => -1, - "in_product_marketing_email_team_1_cta_clicked" => -1, - "in_product_marketing_email_team_2_sent" => -1, - "in_product_marketing_email_team_2_cta_clicked" => -1 - } - - expect(subject).to eq(expected_data) - end - end - end - - context 'when there are entries' do - before do - create(:in_product_marketing_email, track: :create, series: 0, cta_clicked_at: Time.zone.now) - create(:in_product_marketing_email, track: :verify, series: 0) - end - - it 'gathers email campaign data' do - expected_data = { - "in_product_marketing_email_create_0_sent" => 1, - "in_product_marketing_email_create_0_cta_clicked" => 1, - "in_product_marketing_email_create_1_sent" => 0, - "in_product_marketing_email_create_1_cta_clicked" => 0, - "in_product_marketing_email_create_2_sent" => 0, - "in_product_marketing_email_create_2_cta_clicked" => 0, - "in_product_marketing_email_team_short_0_sent" => 0, - "in_product_marketing_email_team_short_0_cta_clicked" => 0, - "in_product_marketing_email_trial_short_0_sent" => 0, - "in_product_marketing_email_trial_short_0_cta_clicked" => 0, - "in_product_marketing_email_admin_verify_0_sent" => 0, - "in_product_marketing_email_admin_verify_0_cta_clicked" => 0, - "in_product_marketing_email_verify_0_sent" => 1, - "in_product_marketing_email_verify_0_cta_clicked" => 0, - "in_product_marketing_email_verify_1_sent" => 0, - "in_product_marketing_email_verify_1_cta_clicked" => 0, - "in_product_marketing_email_verify_2_sent" => 0, - "in_product_marketing_email_verify_2_cta_clicked" => 0, - "in_product_marketing_email_trial_0_sent" => 0, - "in_product_marketing_email_trial_0_cta_clicked" => 0, - "in_product_marketing_email_trial_1_sent" => 0, - "in_product_marketing_email_trial_1_cta_clicked" => 0, - "in_product_marketing_email_trial_2_sent" => 0, - "in_product_marketing_email_trial_2_cta_clicked" => 0, - "in_product_marketing_email_team_0_sent" => 0, - "in_product_marketing_email_team_0_cta_clicked" => 0, - "in_product_marketing_email_team_1_sent" => 0, - "in_product_marketing_email_team_1_cta_clicked" => 0, - "in_product_marketing_email_team_2_sent" => 0, - "in_product_marketing_email_team_2_cta_clicked" => 0 - } - - expect(subject).to eq(expected_data) - end - end - end - describe ".with_duration" do it 'records duration' do expect(::Gitlab::Usage::ServicePing::LegacyMetricTimingDecorator) diff --git a/spec/migrations/20221103150250_migrate_sidekiq_queued_jobs_spec.rb b/spec/migrations/20221103150250_migrate_sidekiq_queued_jobs_spec.rb deleted file mode 100644 index 46ca200a170..00000000000 --- a/spec/migrations/20221103150250_migrate_sidekiq_queued_jobs_spec.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require_migration! - -RSpec.describe MigrateSidekiqQueuedJobs, :clean_gitlab_redis_queues do - around do |example| - Sidekiq::Testing.disable!(&example) - end - - describe '#up', :aggregate_failures, :silence_stdout do - before do - EmailReceiverWorker.sidekiq_options queue: 'email_receiver' - EmailReceiverWorker.perform_async('foo') - EmailReceiverWorker.perform_async('bar') - end - - after do - EmailReceiverWorker.set_queue - end - - context 'with worker_queue_mappings mocked' do - it 'migrates the jobs to the correct destination queue' do - allow(Gitlab::SidekiqConfig).to receive(:worker_queue_mappings) - .and_return({ "EmailReceiverWorker" => "default" }) - expect(queue_length('email_receiver')).to eq(2) - expect(queue_length('default')).to eq(0) - migrate! - expect(queue_length('email_receiver')).to eq(0) - expect(queue_length('default')).to eq(2) - - jobs = list_jobs('default') - expect(jobs[0]).to include("class" => "EmailReceiverWorker", "args" => ["bar"]) - expect(jobs[1]).to include("class" => "EmailReceiverWorker", "args" => ["foo"]) - end - end - - context 'without worker_queue_mappings mocked' do - it 'migration still runs' do - # Assuming Settings.sidekiq.routing_rules is [] (named queue) - # If the default Settings.sidekiq.routing_rules or Gitlab::SidekiqConfig.worker_queue_mappings changed, - # this spec might be failing. We'll have to adjust the migration or this spec. - expect(queue_length('email_receiver')).to eq(2) - expect(queue_length('default')).to eq(0) - migrate! - expect(queue_length('email_receiver')).to eq(2) - expect(queue_length('default')).to eq(0) - - jobs = list_jobs('email_receiver') - expect(jobs[0]).to include("class" => "EmailReceiverWorker", "args" => ["bar"]) - expect(jobs[1]).to include("class" => "EmailReceiverWorker", "args" => ["foo"]) - end - end - - context 'with illegal JSON payload' do - let(:job) { '{foo: 1}' } - - before do - Sidekiq.redis do |conn| - conn.lpush("queue:email_receiver", job) - end - end - - it 'logs an error' do - allow(Gitlab::SidekiqConfig).to receive(:worker_queue_mappings) - .and_return({ "EmailReceiverWorker" => "default" }) - allow(::Gitlab::BackgroundMigration::Logger).to receive(:build).and_return(Logger.new($stdout)) - migrate! - expect($stdout.string).to include("Unmarshal JSON payload from SidekiqMigrateJobs failed. Job: #{job}") - end - end - - context 'when run in GitLab.com' do - it 'skips the migration' do - allow(Gitlab).to receive(:com?).and_return(true) - expect(described_class::SidekiqMigrateJobs).not_to receive(:new) - migrate! - end - end - - def queue_length(queue_name) - Sidekiq.redis do |conn| - conn.llen("queue:#{queue_name}") - end - end - - def list_jobs(queue_name) - Sidekiq.redis { |conn| conn.lrange("queue:#{queue_name}", 0, -1) } - .map { |item| Sidekiq.load_json item } - end - end -end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 922f4ac8804..cf4f58f558c 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -4229,14 +4229,6 @@ RSpec.describe MergeRequest, factory_default: :keep do transition! end - - context 'when trigger_mr_subscription_on_merge_status_change is disabled' do - before do - stub_feature_flags(trigger_mr_subscription_on_merge_status_change: false) - end - - it_behaves_like 'transition not triggering mergeRequestMergeStatusUpdated GraphQL subscription' - end end shared_examples 'for an invalid state transition' do diff --git a/spec/requests/api/graphql/issues_spec.rb b/spec/requests/api/graphql/issues_spec.rb new file mode 100644 index 00000000000..8838ad78f72 --- /dev/null +++ b/spec/requests/api/graphql/issues_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting an issue list at root level' do + include GraphqlHelpers + + let_it_be(:developer) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:group1) { create(:group).tap { |group| group.add_developer(developer) } } + let_it_be(:group2) { create(:group).tap { |group| group.add_developer(developer) } } + let_it_be(:project_a) { create(:project, :repository, :public, group: group1) } + let_it_be(:project_b) { create(:project, :repository, :private, group: group1) } + let_it_be(:project_c) { create(:project, :repository, :public, group: group2) } + let_it_be(:project_d) { create(:project, :repository, :private, group: group2) } + let_it_be(:early_milestone) { create(:milestone, project: project_d, due_date: 10.days.from_now) } + let_it_be(:late_milestone) { create(:milestone, project: project_c, due_date: 30.days.from_now) } + let_it_be(:priority1) { create(:label, project: project_c, priority: 1) } + let_it_be(:priority2) { create(:label, project: project_d, priority: 5) } + let_it_be(:priority3) { create(:label, project: project_a, priority: 10) } + + let_it_be(:issue_a) { create(:issue, project: project_a, labels: [priority3]) } + let_it_be(:issue_b) { create(:issue, :with_alert, project: project_b, discussion_locked: true) } + let_it_be(:issue_c) do + create( + :issue, + project: project_c, + title: 'title matching issue plus', + labels: [priority1], + milestone: late_milestone + ) + end + + let_it_be(:issue_d) { create(:issue, :with_alert, project: project_d, discussion_locked: true, labels: [priority2]) } + let_it_be(:issue_e) { create(:issue, project: project_d, milestone: early_milestone) } + + let(:issue_filter_params) { {} } + + let(:fields) do + <<~QUERY + nodes { + #{all_graphql_fields_for('issues'.classify)} + } + QUERY + end + + before_all do + group2.add_reporter(reporter) + end + + context 'when the root_level_issues_query feature flag is disabled' do + before do + stub_feature_flags(root_level_issues_query: false) + end + + it 'the field returns null' do + post_graphql(query, current_user: developer) + + expect(graphql_data).to eq('issues' => nil) + end + end + + it_behaves_like 'graphql issue list request spec' do + subject(:post_query) { post_graphql(query, current_user: current_user) } + + let(:current_user) { developer } + let(:another_user) { reporter } + let(:issues_data) { graphql_data['issues']['nodes'] } + let(:issue_ids) { graphql_dig_at(issues_data, :id) } + + # filters + let(:expected_negated_assignee_issues) { [issue_b, issue_c, issue_d, issue_e] } + let(:expected_unioned_assignee_issues) { [issue_a, issue_c] } + let(:voted_issues) { [issue_a, issue_c] } + let(:no_award_issues) { [issue_b, issue_d, issue_e] } + let(:locked_discussion_issues) { [issue_b, issue_d] } + let(:unlocked_discussion_issues) { [issue_a, issue_c, issue_e] } + let(:search_title_term) { 'matching issue' } + let(:title_search_issue) { issue_c } + + # sorting + let(:data_path) { [:issues] } + let(:expected_severity_sorted_asc) { [issue_c, issue_a, issue_b, issue_e, issue_d] } + let(:expected_priority_sorted_asc) { [issue_e, issue_c, issue_d, issue_a, issue_b] } + let(:expected_priority_sorted_desc) { [issue_c, issue_e, issue_a, issue_d, issue_b] } + + before_all do + issue_a.assignee_ids = developer.id + issue_c.assignee_ids = reporter.id + + create(:award_emoji, :upvote, user: developer, awardable: issue_a) + create(:award_emoji, :upvote, user: developer, awardable: issue_c) + + # severity sorting + create(:issuable_severity, issue: issue_a, severity: :unknown) + create(:issuable_severity, issue: issue_b, severity: :low) + create(:issuable_severity, issue: issue_d, severity: :critical) + create(:issuable_severity, issue: issue_e, severity: :high) + end + + def pagination_query(params) + graphql_query_for( + :issues, + params, + "#{page_info} nodes { id }" + ) + end + end + + def query(params = issue_filter_params) + graphql_query_for( + :issues, + params, + fields + ) + end +end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 3d09206fa96..214165cb171 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -8,140 +8,74 @@ RSpec.describe 'getting an issue list for a project' do let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, :public, group: group) } let_it_be(:current_user) { create(:user) } - let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) } - let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) } - let_it_be(:issues, reload: true) { [issue_a, issue_b] } + let_it_be(:another_user) { create(:user).tap { |u| group.add_reporter(u) } } + let_it_be(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) } + let_it_be(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) } + let_it_be(:priority1) { create(:label, project: project, priority: 1) } + let_it_be(:priority2) { create(:label, project: project, priority: 5) } + let_it_be(:priority3) { create(:label, project: project, priority: 10) } + + let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true, labels: [priority3]) } + let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project, title: 'title matching issue i') } + let_it_be(:issue_c) { create(:issue, project: project, labels: [priority1], milestone: late_milestone) } + let_it_be(:issue_d) { create(:issue, project: project, labels: [priority2]) } + let_it_be(:issue_e) { create(:issue, project: project, milestone: early_milestone) } + let_it_be(:issues, reload: true) { [issue_a, issue_b, issue_c, issue_d, issue_e] } let(:issue_a_gid) { issue_a.to_global_id.to_s } let(:issue_b_gid) { issue_b.to_global_id.to_s } - let(:issues_data) { graphql_data['project']['issues']['edges'] } + let(:issues_data) { graphql_data['project']['issues']['nodes'] } let(:issue_filter_params) { {} } let(:fields) do <<~QUERY - edges { - node { - #{all_graphql_fields_for('issues'.classify)} - } + nodes { + #{all_graphql_fields_for('issues'.classify)} } QUERY end - it_behaves_like 'a working graphql query' do - before do - post_graphql(query, current_user: current_user) - end - end + # All new specs should be added to the shared example if the change also + # affects the `issues` query at the root level of the API. + # Shared example also used in spec/requests/api/graphql/issues_spec.rb + it_behaves_like 'graphql issue list request spec' do + subject(:post_query) { post_graphql(query, current_user: current_user) } - it 'includes a web_url' do - post_graphql(query, current_user: current_user) + # filters + let(:expected_negated_assignee_issues) { [issue_b, issue_c, issue_d, issue_e] } + let(:expected_unioned_assignee_issues) { [issue_a, issue_b] } + let(:voted_issues) { [issue_a] } + let(:no_award_issues) { [issue_b, issue_c, issue_d, issue_e] } + let(:locked_discussion_issues) { [issue_a] } + let(:unlocked_discussion_issues) { [issue_b, issue_c, issue_d, issue_e] } + let(:search_title_term) { 'matching issue' } + let(:title_search_issue) { issue_b } - expect(issues_data[0]['node']['webUrl']).to be_present - end + # sorting + let(:data_path) { [:project, :issues] } + let(:expected_severity_sorted_asc) { [issue_c, issue_a, issue_b, issue_e, issue_d] } + let(:expected_priority_sorted_asc) { [issue_e, issue_c, issue_d, issue_a, issue_b] } + let(:expected_priority_sorted_desc) { [issue_c, issue_e, issue_a, issue_d, issue_b] } - it 'includes discussion locked' do - post_graphql(query, current_user: current_user) - - expect(issues_data[0]['node']['discussionLocked']).to eq(false) - expect(issues_data[1]['node']['discussionLocked']).to eq(true) - end - - context 'when both assignee_username filters are provided' do - let(:issue_filter_params) { { assignee_username: current_user.username, assignee_usernames: [current_user.username] } } - - it 'returns a mutually exclusive param error' do - post_graphql(query, current_user: current_user) - - expect_graphql_errors_to_include('only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.') - end - end - - context 'when filtering by a negated argument' do - let(:issue_filter_params) { { not: { assignee_usernames: current_user.username } } } - - it 'returns correctly filtered issues' do - issue_a.assignee_ids = current_user.id - - post_graphql(query, current_user: current_user) - - expect(issues_ids).to contain_exactly(issue_b_gid) - end - - context 'when argument is blank' do - let(:issue_filter_params) { { not: {} } } - - it 'does not raise an error' do - post_graphql(query, current_user: current_user) - - expect_graphql_errors_to_be_empty - end - end - end - - context 'when filtering by a unioned argument' do - let(:another_user) { create(:user) } - let(:issue_filter_params) { { or: { assignee_usernames: [current_user.username, another_user.username] } } } - - it 'returns correctly filtered issues' do + before_all do issue_a.assignee_ids = current_user.id issue_b.assignee_ids = another_user.id - post_graphql(query, current_user: current_user) + create(:award_emoji, :upvote, user: current_user, awardable: issue_a) - expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid) + # severity sorting + create(:issuable_severity, issue: issue_a, severity: :unknown) + create(:issuable_severity, issue: issue_b, severity: :low) + create(:issuable_severity, issue: issue_d, severity: :critical) + create(:issuable_severity, issue: issue_e, severity: :high) end - context 'when argument is blank' do - let(:issue_filter_params) { { or: {} } } - - it 'does not raise an error' do - post_graphql(query, current_user: current_user) - - expect_graphql_errors_to_be_empty - end - end - - context 'when feature flag is disabled' do - it 'returns an error' do - stub_feature_flags(or_issuable_queries: false) - - post_graphql(query, current_user: current_user) - - expect_graphql_errors_to_include("'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled.") - end - end - end - - context 'filtering by my_reaction_emoji' do - using RSpec::Parameterized::TableSyntax - - let_it_be(:upvote_award) { create(:award_emoji, :upvote, user: current_user, awardable: issue_a) } - - where(:value, :gids) do - 'thumbsup' | lazy { [issue_a_gid] } - 'ANY' | lazy { [issue_a_gid] } - 'any' | lazy { [issue_a_gid] } - 'AnY' | lazy { [issue_a_gid] } - 'NONE' | lazy { [issue_b_gid] } - 'thumbsdown' | lazy { [] } - end - - with_them do - let(:issue_filter_params) { { my_reaction_emoji: value } } - - it 'returns correctly filtered issues' do - post_graphql(query, current_user: current_user) - - expect(issues_ids).to eq(gids) - end - end - end - - context 'when filtering by search' do - it_behaves_like 'query with a search term' do - let(:issuable_data) { issues_data } - let(:user) { current_user } - let_it_be(:issuable) { create(:issue, project: project, description: 'bar') } + def pagination_query(params) + graphql_query_for( + :project, + { full_path: project.full_path }, + query_graphql_field(:issues, params, "#{page_info} nodes { id }") + ) end end @@ -211,10 +145,10 @@ RSpec.describe 'getting an issue list for a project' do it 'returns issues without confidential issues' do post_graphql(query, current_user: current_user) - expect(issues_data.size).to eq(2) + expect(issues_data.size).to eq(5) issues_data.each do |issue| - expect(issue.dig('node', 'confidential')).to eq(false) + expect(issue['confidential']).to eq(false) end end @@ -234,7 +168,7 @@ RSpec.describe 'getting an issue list for a project' do it 'returns correctly filtered issues' do post_graphql(query, current_user: current_user) - expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid) + expect(issue_ids).to match_array(issues.map { |i| i.to_gid.to_s }) end end end @@ -247,13 +181,13 @@ RSpec.describe 'getting an issue list for a project' do it 'returns issues with confidential issues' do post_graphql(query, current_user: current_user) - expect(issues_data.size).to eq(3) + expect(issues_data.size).to eq(6) confidentials = issues_data.map do |issue| - issue.dig('node', 'confidential') + issue['confidential'] end - expect(confidentials).to eq([true, false, false]) + expect(confidentials).to contain_exactly(true, false, false, false, false, false) end context 'filtering for confidential issues' do @@ -262,7 +196,7 @@ RSpec.describe 'getting an issue list for a project' do it 'returns correctly filtered issues' do post_graphql(query, current_user: current_user) - expect(issues_ids).to contain_exactly(confidential_issue_gid) + expect(issue_ids).to contain_exactly(confidential_issue_gid) end end @@ -272,7 +206,7 @@ RSpec.describe 'getting an issue list for a project' do it 'returns correctly filtered issues' do post_graphql(query, current_user: current_user) - expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid) + expect(issue_ids).to match_array([issue_a, issue_b, issue_c, issue_d, issue_e].map { |i| i.to_gid.to_s }) end end end @@ -294,37 +228,7 @@ RSpec.describe 'getting an issue list for a project' do data.map { |issue| issue['iid'].to_i } end - context 'when sorting by severity' do - let_it_be(:severty_issue1) { create(:issue, project: sort_project) } - let_it_be(:severty_issue2) { create(:issue, project: sort_project) } - let_it_be(:severty_issue3) { create(:issue, project: sort_project) } - let_it_be(:severty_issue4) { create(:issue, project: sort_project) } - let_it_be(:severty_issue5) { create(:issue, project: sort_project) } - - before(:all) do - create(:issuable_severity, issue: severty_issue1, severity: :unknown) - create(:issuable_severity, issue: severty_issue2, severity: :low) - create(:issuable_severity, issue: severty_issue4, severity: :critical) - create(:issuable_severity, issue: severty_issue5, severity: :high) - end - - context 'when ascending' do - it_behaves_like 'sorted paginated query' do - let(:sort_param) { :SEVERITY_ASC } - let(:first_param) { 2 } - let(:all_records) { [severty_issue3.iid, severty_issue1.iid, severty_issue2.iid, severty_issue5.iid, severty_issue4.iid] } - end - end - - context 'when descending' do - it_behaves_like 'sorted paginated query' do - let(:sort_param) { :SEVERITY_DESC } - let(:first_param) { 2 } - let(:all_records) { [severty_issue4.iid, severty_issue5.iid, severty_issue2.iid, severty_issue1.iid, severty_issue3.iid] } - end - end - end - + # rubocop:disable RSpec/MultipleMemoizedHelpers context 'when sorting by due date' do let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) } let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) } @@ -370,41 +274,6 @@ RSpec.describe 'getting an issue list for a project' do end end - context 'when sorting by priority' do - let_it_be(:on_project) { { project: sort_project } } - let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) } - let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) } - let_it_be(:priority_1) { create(:label, **on_project, priority: 1) } - let_it_be(:priority_2) { create(:label, **on_project, priority: 5) } - let_it_be(:priority_issue1) { create(:issue, **on_project, labels: [priority_1], milestone: late_milestone) } - let_it_be(:priority_issue2) { create(:issue, **on_project, labels: [priority_2]) } - let_it_be(:priority_issue3) { create(:issue, **on_project, milestone: early_milestone) } - let_it_be(:priority_issue4) { create(:issue, **on_project) } - - context 'when ascending' do - it_behaves_like 'sorted paginated query' do - let(:sort_param) { :PRIORITY_ASC } - let(:first_param) { 2 } - let(:all_records) do - [ - priority_issue3.iid, priority_issue1.iid, - priority_issue2.iid, priority_issue4.iid - ] - end - end - end - - context 'when descending' do - it_behaves_like 'sorted paginated query' do - let(:sort_param) { :PRIORITY_DESC } - let(:first_param) { 2 } - let(:all_records) do - [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] - end - end - end - end - context 'when sorting by label priority' do let_it_be(:label1) { create(:label, project: sort_project, priority: 1) } let_it_be(:label2) { create(:label, project: sort_project, priority: 5) } @@ -430,6 +299,7 @@ RSpec.describe 'getting an issue list for a project' do end end end + # rubocop:enable RSpec/MultipleMemoizedHelpers context 'when sorting by milestone due date' do let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) } @@ -459,8 +329,7 @@ RSpec.describe 'getting an issue list for a project' do context 'when fetching alert management alert' do let(:fields) do <<~QUERY - edges { - node { + nodes { iid alertManagementAlert { title @@ -471,7 +340,6 @@ RSpec.describe 'getting an issue list for a project' do } } } - } QUERY end @@ -491,7 +359,7 @@ RSpec.describe 'getting an issue list for a project' do it 'returns the alert data' do post_graphql(query, current_user: current_user) - alert_titles = issues_data.map { |issue| issue.dig('node', 'alertManagementAlert', 'title') } + alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlert', 'title') } expected_titles = issues.map { |issue| issue.alert_management_alert&.title } expect(alert_titles).to contain_exactly(*expected_titles) @@ -500,7 +368,7 @@ RSpec.describe 'getting an issue list for a project' do it 'returns the alerts data' do post_graphql(query, current_user: current_user) - alert_titles = issues_data.map { |issue| issue.dig('node', 'alertManagementAlerts', 'nodes') } + alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlerts', 'nodes') } expected_titles = issues.map do |issue| issue.alert_management_alerts.map { |alert| { 'title' => alert.title } } end @@ -541,13 +409,11 @@ RSpec.describe 'getting an issue list for a project' do context 'when fetching labels' do let(:fields) do <<~QUERY - edges { - node { - id - labels { - nodes { - id - } + nodes { + id + labels { + nodes { + id } } } @@ -563,8 +429,8 @@ RSpec.describe 'getting an issue list for a project' do end def response_label_ids(response_data) - response_data.map do |edge| - edge['node']['labels']['nodes'].map { |u| u['id'] } + response_data.map do |node| + node['labels']['nodes'].map { |u| u['id'] } end.flatten end @@ -574,7 +440,7 @@ RSpec.describe 'getting an issue list for a project' do it 'avoids N+1 queries', :aggregate_failures do control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) } - expect(issues_data.count).to eq(2) + expect(issues_data.count).to eq(5) expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(issues)) new_issues = issues + [create(:issue, project: project, labels: [create(:label, project: project)])] @@ -582,8 +448,8 @@ RSpec.describe 'getting an issue list for a project' do expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control) # graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb) # so we have to parse the body ourselves the second time - issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['edges'] - expect(issues_data.count).to eq(3) + issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['nodes'] + expect(issues_data.count).to eq(6) expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(new_issues)) end end @@ -591,13 +457,11 @@ RSpec.describe 'getting an issue list for a project' do context 'when fetching assignees' do let(:fields) do <<~QUERY - edges { - node { - id - assignees { - nodes { - id - } + nodes { + id + assignees { + nodes { + id } } } @@ -613,8 +477,8 @@ RSpec.describe 'getting an issue list for a project' do end def response_assignee_ids(response_data) - response_data.map do |edge| - edge['node']['assignees']['nodes'].map { |node| node['id'] } + response_data.map do |node| + node['assignees']['nodes'].map { |node| node['id'] } end.flatten end @@ -624,7 +488,7 @@ RSpec.describe 'getting an issue list for a project' do it 'avoids N+1 queries', :aggregate_failures do control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) } - expect(issues_data.count).to eq(2) + expect(issues_data.count).to eq(5) expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(issues)) new_issues = issues + [create(:issue, project: project, assignees: [create(:user)])] @@ -632,8 +496,8 @@ RSpec.describe 'getting an issue list for a project' do expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control) # graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb) # so we have to parse the body ourselves the second time - issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['edges'] - expect(issues_data.count).to eq(3) + issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['nodes'] + expect(issues_data.count).to eq(6) expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues)) end end @@ -644,11 +508,9 @@ RSpec.describe 'getting an issue list for a project' do let(:statuses) { issue_data.to_h { |issue| [issue['iid'], issue['escalationStatus']] } } let(:fields) do <<~QUERY - edges { - node { - id - escalationStatus - } + nodes { + id + escalationStatus } QUERY end @@ -660,9 +522,9 @@ RSpec.describe 'getting an issue list for a project' do it 'returns the escalation status values' do post_graphql(query, current_user: current_user) - statuses = issues_data.map { |issue| issue.dig('node', 'escalationStatus') } + statuses = issues_data.map { |issue| issue['escalationStatus'] } - expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil) + expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil, nil, nil, nil) end it 'avoids N+1 queries', :aggregate_failures do @@ -798,8 +660,8 @@ RSpec.describe 'getting an issue list for a project' do end end - def issues_ids - graphql_dig_at(issues_data, :node, :id) + def issue_ids + graphql_dig_at(issues_data, :id) end def query(params = issue_filter_params) diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb index e8d3e94bd0e..34fda1cce4d 100644 --- a/spec/requests/verifies_with_email_spec.rb +++ b/spec/requests/verifies_with_email_spec.rb @@ -30,6 +30,97 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_ end end + shared_examples_for 'rate limited' do + it 'redirects to the login form and shows an alert message' do + expect(response).to redirect_to(new_user_session_path) + expect(flash[:alert]) + .to eq(s_('IdentityVerification|Maximum login attempts exceeded. Wait 10 minutes and try again.')) + end + end + + shared_examples_for 'two factor prompt or successful login' do + it 'shows the 2FA prompt when enabled or redirects to the root path' do + if user.two_factor_enabled? + expect(response.body).to include('Two-factor authentication code') + else + expect(response).to redirect_to(root_path) + end + end + end + + shared_examples_for 'verifying with email' do + context 'when rate limited' do + before do + allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true) + sign_in + end + + it_behaves_like 'rate limited' + end + + context 'when the user already has an unlock_token set' do + before do + user.update!(unlock_token: 'token') + sign_in + end + + it_behaves_like 'prompt for email verification' + end + + context 'when the user is already locked' do + before do + user.update!(locked_at: Time.current) + perform_enqueued_jobs { sign_in } + end + + it_behaves_like 'send verification instructions' + it_behaves_like 'prompt for email verification' + end + + context 'when the user is signing in from an unknown ip address' do + before do + allow(AuthenticationEvent) + .to receive(:initial_login_or_known_ip_address?) + .and_return(false) + perform_enqueued_jobs { sign_in } + end + + it_behaves_like 'send verification instructions' + it_behaves_like 'prompt for email verification' + end + end + + shared_examples_for 'not verifying with email' do + context 'when rate limited' do + before do + allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true) + sign_in + end + + it_behaves_like 'two factor prompt or successful login' + end + + context 'when the user already has an unlock_token set' do + before do + user.update!(unlock_token: 'token') + sign_in + end + + it_behaves_like 'two factor prompt or successful login' + end + + context 'when the user is signing in from an unknown ip address' do + before do + allow(AuthenticationEvent) + .to receive(:initial_login_or_known_ip_address?) + .and_return(false) + sign_in + end + + it_behaves_like 'two factor prompt or successful login' + end + end + describe 'verify_with_email' do context 'when user is locked and a verification_user_id session variable exists' do before do @@ -99,69 +190,34 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_ end context 'when signing in with a valid password' do - let(:sign_in) { post(user_session_path(user: { login: user.username, password: user.password })) } + let(:headers) { {} } + let(:sign_in) do + post user_session_path, params: { user: { login: user.username, password: user.password } }, headers: headers + end + + it_behaves_like 'not verifying with email' context 'when the feature flag is toggled on' do before do stub_feature_flags(require_email_verification: user) end - context 'when rate limited' do + it_behaves_like 'verifying with email' + + context 'when 2FA is enabled' do before do - allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true) - sign_in + user.update!(otp_required_for_login: true) end - it 'redirects to the login form and shows an alert message' do - expect(response).to redirect_to(new_user_session_path) - expect(flash[:alert]) - .to eq(s_('IdentityVerification|Maximum login attempts exceeded. Wait 10 minutes and try again.')) - end + it_behaves_like 'not verifying with email' end - context 'when the user already has an unlock_token set' do + context 'when request is not from a QA user' do before do - user.update!(unlock_token: 'token') - sign_in + allow(Gitlab::Qa).to receive(:request?).and_return(false) end - it_behaves_like 'prompt for email verification' - end - - context 'when the user is already locked' do - before do - user.update!(locked_at: Time.current) - perform_enqueued_jobs { sign_in } - end - - it_behaves_like 'send verification instructions' - it_behaves_like 'prompt for email verification' - end - - context 'when the user is signing in from an unknown ip address' do - before do - allow(AuthenticationEvent) - .to receive(:initial_login_or_known_ip_address?) - .and_return(false) - - perform_enqueued_jobs { sign_in } - end - - it_behaves_like 'send verification instructions' - it_behaves_like 'prompt for email verification' - end - end - - context 'when the feature flag is toggled off' do - let(:another_user) { build(:user) } - - before do - stub_feature_flags(require_email_verification: another_user) - sign_in - end - - it 'redirects to the root path' do - expect(response).to redirect_to(root_path) + it_behaves_like 'verifying with email' end end end diff --git a/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb index 22805cf7aed..bb492425fd7 100644 --- a/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb @@ -1,13 +1,15 @@ # frozen_string_literal: true # Requires `query(params)` , `user`, `issuable_data` and `issuable` bindings -RSpec.shared_examples 'query with a search term' do +RSpec.shared_examples 'query with a search term' do |fields = [:DESCRIPTION]| + let(:search_term) { 'bar' } + let(:ids) { graphql_dig_at(issuable_data, :node, :id) } + it 'returns only matching issuables' do - filter_params = { search: 'bar', in: [:DESCRIPTION] } + filter_params = { search: search_term, in: fields } graphql_query = query(filter_params) post_graphql(graphql_query, current_user: user) - ids = graphql_dig_at(issuable_data, :node, :id) expect(ids).to contain_exactly(issuable.to_global_id.to_s) end diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb new file mode 100644 index 00000000000..5469fd80a4f --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'graphql issue list request spec' do + it_behaves_like 'a working graphql query' do + before do + post_query + end + end + + describe 'filters' do + context 'when filtering by assignees' do + context 'when both assignee_username filters are provided' do + let(:issue_filter_params) do + { assignee_username: current_user.username, assignee_usernames: [current_user.username] } + end + + it 'returns a mutually exclusive param error' do + post_query + + expect_graphql_errors_to_include( + 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.' + ) + end + end + + context 'when filtering by a negated argument' do + let(:issue_filter_params) { { not: { assignee_usernames: [current_user.username] } } } + + it 'returns correctly filtered issues' do + post_query + + expect(issue_ids).to match_array(expected_negated_assignee_issues.map { |i| i.to_gid.to_s }) + end + end + end + + context 'when filtering by unioned arguments' do + let(:issue_filter_params) { { or: { assignee_usernames: [current_user.username, another_user.username] } } } + + it 'returns correctly filtered issues' do + post_query + + expect(issue_ids).to match_array(expected_unioned_assignee_issues.map { |i| i.to_gid.to_s }) + end + + context 'when argument is blank' do + let(:issue_filter_params) { { or: {} } } + + it 'does not raise an error' do + post_query + + expect_graphql_errors_to_be_empty + end + end + + context 'when feature flag is disabled' do + it 'returns an error' do + stub_feature_flags(or_issuable_queries: false) + + post_query + + expect_graphql_errors_to_include( + "'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled." + ) + end + end + end + + context 'when filtering by a blank negated argument' do + let(:issue_filter_params) { { not: {} } } + + it 'does not raise an error' do + post_query + + expect_graphql_errors_to_be_empty + end + end + + context 'when filtering by reaction emoji' do + using RSpec::Parameterized::TableSyntax + + where(:value, :issue_list) do + 'thumbsup' | lazy { voted_issues } + 'ANY' | lazy { voted_issues } + 'any' | lazy { voted_issues } + 'AnY' | lazy { voted_issues } + 'NONE' | lazy { no_award_issues } + 'thumbsdown' | lazy { [] } + end + + with_them do + let(:issue_filter_params) { { my_reaction_emoji: value } } + let(:gids) { to_gid_list(issue_list) } + + it 'returns correctly filtered issues' do + post_query + + expect(issue_ids).to match_array(gids) + end + end + end + + context 'when filtering by search' do + it_behaves_like 'query with a search term', [:TITLE] do + let(:search_term) { search_title_term } + let(:issuable_data) { issues_data } + let(:user) { current_user } + let(:issuable) { title_search_issue } + let(:ids) { issue_ids } + end + end + end + + describe 'sorting and pagination' do + context 'when sorting by severity' do + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :SEVERITY_ASC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_severity_sorted_asc) } + end + end + + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :SEVERITY_DESC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_severity_sorted_asc.reverse) } + end + end + end + + context 'when sorting by priority' do + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :PRIORITY_ASC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_priority_sorted_asc) } + end + end + + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :PRIORITY_DESC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_priority_sorted_desc) } + end + end + end + end + + it 'includes a web_url' do + post_query + + expect(issues_data[0]['webUrl']).to be_present + end + + it 'includes discussion locked' do + post_query + + expect(issues_data).to contain_exactly( + *locked_discussion_issues.map { |i| hash_including('id' => i.to_gid.to_s, 'discussionLocked' => true) }, + *unlocked_discussion_issues.map { |i| hash_including('id' => i.to_gid.to_s, 'discussionLocked' => false) } + ) + end + + def to_gid_list(instance_list) + instance_list.map { |instance| instance.to_gid.to_s } + end +end