diff options
46 files changed, 1077 insertions, 717 deletions
diff --git a/.gitignore b/.gitignore index 9d9f2cdb475..abccf99229f 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,4 @@ webpack-dev-server.json /.nvimrc .solargraph.yml apollo.config.js +/tmp/matching_foss_tests.txt diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index b9c19b21ef3..fa468634c33 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -324,3 +324,25 @@ db:rollback geo: - bundle exec rake geo:db:migrate # EE: default refs (MRs, master, schedules) jobs # ################################################## + +################################################## +# EE: Canonical MR pipelines +rspec foss-impact: + extends: + - .rspec-base + - .as-if-foss + - .rails:rules:ee-mr-only + - .use-pg11 + script: + - install_gitlab_gem + - run_timed_command "scripts/gitaly-test-build" + - run_timed_command "scripts/gitaly-test-spawn" + - source scripts/rspec_helpers.sh + - tooling/bin/find_foss_tests tmp/matching_foss_tests.txt + - rspec_simple_job "--tag ~quarantine --tag ~geo --tag ~level:migration $(cat tmp/matching_foss_tests.txt)" + artifacts: + expire_in: 7d + paths: + - tmp/matching_foss_tests.txt +# EE: Merge Request pipelines +################################################## diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index cd131d3f66a..db5e9f8acf4 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -446,6 +446,15 @@ - <<: *if-master-refs changes: *code-backstage-patterns +.rails:rules:ee-mr-only: + rules: + - <<: *if-not-ee + when: never + - <<: *if-security-merge-request + changes: *code-backstage-patterns + - <<: *if-dot-com-gitlab-org-merge-request + changes: *code-backstage-patterns + .rails:rules:downtime_check: rules: - <<: *if-merge-request diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 9abb305dd99..a35ebc6eaa7 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.33.0 +8.34.0 diff --git a/app/assets/javascripts/design_management/components/upload/button.vue b/app/assets/javascripts/design_management/components/upload/button.vue index e3c5e369170..68555104a3c 100644 --- a/app/assets/javascripts/design_management/components/upload/button.vue +++ b/app/assets/javascripts/design_management/components/upload/button.vue @@ -41,7 +41,7 @@ export default { variant="success" @click="openFileUpload" > - {{ s__('DesignManagement|Add designs') }} + {{ s__('DesignManagement|Upload designs') }} <gl-loading-icon v-if="isSaving" inline class="ml-1" /> </gl-deprecated-button> diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/global_search_input.js index 05e0b9e7089..a7c121259d4 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/global_search_input.js @@ -1,10 +1,8 @@ /* eslint-disable no-return-assign, consistent-return, class-methods-use-this */ import $ from 'jquery'; -import { escape, throttle } from 'lodash'; +import { throttle } from 'lodash'; import { s__, __, sprintf } from '~/locale'; -import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper'; -import axios from './lib/utils/axios_utils'; import { isInGroupsPage, isInProjectPage, @@ -67,15 +65,11 @@ function setSearchOptions() { } } -export class SearchAutocomplete { - constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) { +export class GlobalSearchInput { + constructor({ wrap } = {}) { setSearchOptions(); this.bindEventContext(); this.wrap = wrap || $('.search'); - this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts'); - this.autocompletePath = autocompletePath || this.optsEl.data('autocompletePath'); - this.projectId = projectId || (this.optsEl.data('autocompleteProjectId') || ''); - this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || ''); this.dropdown = this.wrap.find('.dropdown'); this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle'); this.dropdownMenu = this.dropdown.find('.dropdown-menu'); @@ -92,7 +86,7 @@ export class SearchAutocomplete { // Only when user is logged in if (gon.current_user_id) { - this.createAutocomplete(); + this.createGlobalSearchInput(); } this.bindEvents(); @@ -117,7 +111,7 @@ export class SearchAutocomplete { return (this.originalState = this.serializeState()); } - createAutocomplete() { + createGlobalSearchInput() { return this.searchInput.glDropdown({ filterInputBlur: false, filterable: true, @@ -149,116 +143,17 @@ export class SearchAutocomplete { if (glDropdownInstance) { glDropdownInstance.filter.options.callback(contents); } - this.enableAutocomplete(); + this.enableDropdown(); } return; } - // Prevent multiple ajax calls - if (this.loadingSuggestions) { - return; - } - - this.loadingSuggestions = true; - - return axios - .get(this.autocompletePath, { - params: { - project_id: this.projectId, - project_ref: this.projectRef, - term, - }, - }) - .then(response => { - const options = this.scopedSearchOptions(term); - - // List results - let lastCategory = null; - for (let i = 0, len = response.data.length; i < len; i += 1) { - const suggestion = response.data[i]; - // Add group header before list each group - if (lastCategory !== suggestion.category) { - options.push({ type: 'separator' }); - options.push({ - type: 'header', - content: suggestion.category, - }); - lastCategory = suggestion.category; - } - - // Add the suggestion - options.push({ - id: `${suggestion.category.toLowerCase()}-${suggestion.id}`, - icon: this.getAvatar(suggestion), - category: suggestion.category, - text: suggestion.label, - url: suggestion.url, - }); - } - - callback(options); - - this.loadingSuggestions = false; - this.highlightFirstRow(); - this.setScrollFade(); - }) - .catch(() => { - this.loadingSuggestions = false; - }); - } - - getCategoryContents() { - const userName = gon.current_username; - const { projectOptions, groupOptions, dashboardOptions } = gl; - - // Get options - let options; - if (isInProjectPage() && projectOptions) { - options = projectOptions[getProjectSlug()]; - } else if (isInGroupsPage() && groupOptions) { - options = groupOptions[getGroupSlug()]; - } else if (dashboardOptions) { - options = dashboardOptions; - } - - const { issuesPath, mrPath, name, issuesDisabled } = options; - const baseItems = []; - - if (name) { - baseItems.push({ - type: 'header', - content: `${name}`, - }); - } + const options = this.scopedSearchOptions(term); - const issueItems = [ - { - text: s__('SearchAutocomplete|Issues assigned to me'), - url: `${issuesPath}/?assignee_username=${userName}`, - }, - { - text: s__("SearchAutocomplete|Issues I've created"), - url: `${issuesPath}/?author_username=${userName}`, - }, - ]; - const mergeRequestItems = [ - { - text: s__('SearchAutocomplete|Merge requests assigned to me'), - url: `${mrPath}/?assignee_username=${userName}`, - }, - { - text: s__("SearchAutocomplete|Merge requests I've created"), - url: `${mrPath}/?author_username=${userName}`, - }, - ]; + callback(options); - let items; - if (issuesDisabled) { - items = baseItems.concat(mergeRequestItems); - } else { - items = baseItems.concat(...issueItems, ...mergeRequestItems); - } - return items; + this.highlightFirstRow(); + this.setScrollFade(); } // Add option to proceed with the search for each @@ -343,7 +238,7 @@ export class SearchAutocomplete { }); } - enableAutocomplete() { + enableDropdown() { this.setScrollFade(); // No need to enable anything if user is not logged in @@ -360,7 +255,7 @@ export class SearchAutocomplete { } onSearchInputChange() { - this.enableAutocomplete(); + this.enableDropdown(); } onSearchInputKeyUp(e) { @@ -369,7 +264,7 @@ export class SearchAutocomplete { this.restoreOriginalState(); break; case KEYCODE.ENTER: - this.disableAutocomplete(); + this.disableDropdown(); break; default: } @@ -422,7 +317,7 @@ export class SearchAutocomplete { return results; } - disableAutocomplete() { + disableDropdown() { if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) { this.searchInput.addClass('js-autocomplete-disabled'); this.dropdownToggle.dropdown('toggle'); @@ -438,16 +333,8 @@ export class SearchAutocomplete { onClick(item, $el, e) { if (window.location.pathname.indexOf(item.url) !== -1) { if (!e.metaKey) e.preventDefault(); - /* eslint-disable-next-line @gitlab/require-i18n-strings */ - if (item.category === 'Projects') { - this.projectInputEl.val(item.id); - } - // eslint-disable-next-line @gitlab/require-i18n-strings - if (item.category === 'Groups') { - this.groupInputEl.val(item.id); - } $el.removeClass('is-active'); - this.disableAutocomplete(); + this.disableDropdown(); return this.searchInput.val('').focus(); } } @@ -456,20 +343,58 @@ export class SearchAutocomplete { this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0); } - getAvatar(item) { - if (!Object.hasOwnProperty.call(item, 'avatar_url')) { - return false; + getCategoryContents() { + const userName = gon.current_username; + const { projectOptions, groupOptions, dashboardOptions } = gl; + + // Get options + let options; + if (isInProjectPage() && projectOptions) { + options = projectOptions[getProjectSlug()]; + } else if (isInGroupsPage() && groupOptions) { + options = groupOptions[getGroupSlug()]; + } else if (dashboardOptions) { + options = dashboardOptions; } - const { label, id } = item; - const avatarUrl = item.avatar_url; - const avatar = avatarUrl - ? `<img class="search-item-avatar" src="${avatarUrl}" />` - : `<div class="s16 avatar identicon ${getIdenticonBackgroundClass(id)}">${getIdenticonTitle( - escape(label), - )}</div>`; + const { issuesPath, mrPath, name, issuesDisabled } = options; + const baseItems = []; + + if (name) { + baseItems.push({ + type: 'header', + content: `${name}`, + }); + } - return avatar; + const issueItems = [ + { + text: s__('SearchAutocomplete|Issues assigned to me'), + url: `${issuesPath}/?assignee_username=${userName}`, + }, + { + text: s__("SearchAutocomplete|Issues I've created"), + url: `${issuesPath}/?author_username=${userName}`, + }, + ]; + const mergeRequestItems = [ + { + text: s__('SearchAutocomplete|Merge requests assigned to me'), + url: `${mrPath}/?assignee_username=${userName}`, + }, + { + text: s__("SearchAutocomplete|Merge requests I've created"), + url: `${mrPath}/?author_username=${userName}`, + }, + ]; + + let items; + if (issuesDisabled) { + items = baseItems.concat(mergeRequestItems); + } else { + items = baseItems.concat(...issueItems, ...mergeRequestItems); + } + return items; } isScrolledUp() { @@ -495,6 +420,6 @@ export class SearchAutocomplete { } } -export default function initSearchAutocomplete(opts) { - return new SearchAutocomplete(opts); +export default function initGlobalSearchInput(opts) { + return new GlobalSearchInput(opts); } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 6e3066f55d5..5f5fd790f67 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -32,7 +32,7 @@ import initFrequentItemDropdowns from './frequent_items'; import initBreadcrumbs from './breadcrumb'; import initUsagePingConsent from './usage_ping_consent'; import initPerformanceBar from './performance_bar'; -import initSearchAutocomplete from './search_autocomplete'; +import initGlobalSearchInput from './global_search_input'; import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; import initBroadcastNotifications from './broadcast_notification'; @@ -110,7 +110,7 @@ function deferredInitialisation() { initFrequentItemDropdowns(); initPersistentUserCallouts(); - if (document.querySelector('.search')) initSearchAutocomplete(); + if (document.querySelector('.search')) initGlobalSearchInput(); addSelectOnFocusBehaviour('.js-select-on-focus'); diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index ff6d9350a5c..217f08dd648 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -51,21 +51,6 @@ class SearchController < ApplicationController render json: { count: count } end - # rubocop: disable CodeReuse/ActiveRecord - def autocomplete - term = params[:term] - - if params[:project_id].present? - @project = Project.find_by(id: params[:project_id]) - @project = nil unless can?(current_user, :read_project, @project) - end - - @ref = params[:project_ref] if params[:project_ref].present? - - render json: search_autocomplete_opts(term).to_json - end - # rubocop: enable CodeReuse/ActiveRecord - private def preload_method diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 5ad65c59a2e..4e3b6aad8cc 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -3,28 +3,6 @@ module SearchHelper SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze - def search_autocomplete_opts(term) - return unless current_user - - resources_results = [ - groups_autocomplete(term), - projects_autocomplete(term) - ].flatten - - search_pattern = Regexp.new(Regexp.escape(term), "i") - - generic_results = project_autocomplete + default_autocomplete + help_autocomplete - generic_results.concat(default_autocomplete_admin) if current_user.admin? - generic_results.select! { |result| result[:label] =~ search_pattern } - - [ - resources_results, - generic_results - ].flatten.uniq do |item| - item[:label] - end - end - def search_entries_info(collection, scope, term) return if collection.to_a.empty? @@ -95,91 +73,6 @@ module SearchHelper private - # Autocomplete results for various settings pages - def default_autocomplete - [ - { category: "Settings", label: _("User settings"), url: profile_path }, - { category: "Settings", label: _("SSH Keys"), url: profile_keys_path }, - { category: "Settings", label: _("Dashboard"), url: root_path } - ] - end - - # Autocomplete results for settings pages, for admins - def default_autocomplete_admin - [ - { category: "Settings", label: _("Admin Section"), url: admin_root_path } - ] - end - - # Autocomplete results for internal help pages - def help_autocomplete - [ - { category: "Help", label: _("API Help"), url: help_page_path("api/README") }, - { category: "Help", label: _("Markdown Help"), url: help_page_path("user/markdown") }, - { category: "Help", label: _("Permissions Help"), url: help_page_path("user/permissions") }, - { category: "Help", label: _("Public Access Help"), url: help_page_path("public_access/public_access") }, - { category: "Help", label: _("Rake Tasks Help"), url: help_page_path("raketasks/README") }, - { category: "Help", label: _("SSH Keys Help"), url: help_page_path("ssh/README") }, - { category: "Help", label: _("System Hooks Help"), url: help_page_path("system_hooks/system_hooks") }, - { category: "Help", label: _("Webhooks Help"), url: help_page_path("user/project/integrations/webhooks") }, - { category: "Help", label: _("Workflow Help"), url: help_page_path("workflow/README") } - ] - end - - # Autocomplete results for the current project, if it's defined - def project_autocomplete - if @project && @project.repository.root_ref - ref = @ref || @project.repository.root_ref - - [ - { category: "In this project", label: _("Files"), url: project_tree_path(@project, ref) }, - { category: "In this project", label: _("Commits"), url: project_commits_path(@project, ref) }, - { category: "In this project", label: _("Network"), url: project_network_path(@project, ref) }, - { category: "In this project", label: _("Graph"), url: project_graph_path(@project, ref) }, - { category: "In this project", label: _("Issues"), url: project_issues_path(@project) }, - { category: "In this project", label: _("Merge Requests"), url: project_merge_requests_path(@project) }, - { category: "In this project", label: _("Milestones"), url: project_milestones_path(@project) }, - { category: "In this project", label: _("Snippets"), url: project_snippets_path(@project) }, - { category: "In this project", label: _("Members"), url: project_project_members_path(@project) }, - { category: "In this project", label: _("Wiki"), url: project_wikis_path(@project) } - ] - else - [] - end - end - - # Autocomplete results for the current user's groups - # rubocop: disable CodeReuse/ActiveRecord - def groups_autocomplete(term, limit = 5) - current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group| - { - category: "Groups", - id: group.id, - label: "#{search_result_sanitize(group.full_name)}", - url: group_path(group), - avatar_url: group.avatar_url || '' - } - end - end - # rubocop: enable CodeReuse/ActiveRecord - - # Autocomplete results for the current user's projects - # rubocop: disable CodeReuse/ActiveRecord - def projects_autocomplete(term, limit = 5) - current_user.authorized_projects.order_id_desc.search_by_title(term) - .sorted_by_stars_desc.non_archived.limit(limit).map do |p| - { - category: "Projects", - id: p.id, - value: "#{search_result_sanitize(p.name)}", - label: "#{search_result_sanitize(p.full_name)}", - url: project_path(p), - avatar_url: p.avatar_url || '' - } - end - end - # rubocop: enable CodeReuse/ActiveRecord - def search_result_sanitize(str) Sanitize.clean(str) end diff --git a/app/models/event.rb b/app/models/event.rb index 926c3fe6a25..e404ddc22eb 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -149,7 +149,9 @@ class Event < ApplicationRecord def visible_to_user?(user = nil) return false unless capability.present? - Ability.allowed?(user, capability, permission_object) + capability.all? do |rule| + Ability.allowed?(user, rule, permission_object) + end end def resource_parent @@ -361,34 +363,30 @@ class Event < ApplicationRecord protected - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - # - # TODO Refactor this method so we no longer need to disable the above cops - # https://gitlab.com/gitlab-org/gitlab/-/issues/216879. def capability @capability ||= begin - if push_action? || commit_note? - :download_code - elsif membership_changed? || created_project_action? - :read_project - elsif issue? || issue_note? - :read_issue - elsif merge_request? || merge_request_note? - :read_merge_request - elsif personal_snippet_note? || project_snippet_note? - :read_snippet - elsif milestone? - :read_milestone - elsif wiki_page? - :read_wiki - elsif design_note? || design? - :read_design - end - end - end - # rubocop:enable Metrics/CyclomaticComplexity - # rubocop:enable Metrics/PerceivedComplexity + capabilities.flat_map do |ability, syms| + if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend + [ability] + else + [] + end + end + end + end + + def capabilities + { + download_code: %i[push_action? commit_note?], + read_project: %i[membership_changed? created_project_action?], + read_issue: %i[issue? issue_note?], + read_merge_request: %i[merge_request? merge_request_note?], + read_snippet: %i[personal_snippet_note? project_snippet_note?], + read_milestone: %i[milestone?], + read_wiki: %i[wiki_page?], + read_design: %i[design_note? design?] + } + end private diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb index 1de2f31f87c..c4109765a1c 100644 --- a/app/services/auto_merge/base_service.rb +++ b/app/services/auto_merge/base_service.rb @@ -6,19 +6,18 @@ module AutoMerge include MergeRequests::AssignsMergeParams def execute(merge_request) - assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy)) - - merge_request.auto_merge_enabled = true - merge_request.merge_user = current_user - - return :failed unless merge_request.save - - yield if block_given? + ActiveRecord::Base.transaction do + register_auto_merge_parameters!(merge_request) + yield if block_given? + end # Notify the event that auto merge is enabled or merge param is updated AutoMergeProcessWorker.perform_async(merge_request.id) strategy.to_sym + rescue => e + track_exception(e, merge_request) + :failed end def update(merge_request) @@ -30,23 +29,27 @@ module AutoMerge end def cancel(merge_request) - if clear_auto_merge_parameters(merge_request) + ActiveRecord::Base.transaction do + clear_auto_merge_parameters!(merge_request) yield if block_given? - - success - else - error("Can't cancel the automatic merge", 406) end + + success + rescue => e + track_exception(e, merge_request) + error("Can't cancel the automatic merge", 406) end def abort(merge_request, reason) - if clear_auto_merge_parameters(merge_request) + ActiveRecord::Base.transaction do + clear_auto_merge_parameters!(merge_request) yield if block_given? - - success - else - error("Can't abort the automatic merge", 406) end + + success + rescue => e + track_exception(e, merge_request) + error("Can't abort the automatic merge", 406) end def available_for?(merge_request) @@ -65,7 +68,14 @@ module AutoMerge end end - def clear_auto_merge_parameters(merge_request) + def register_auto_merge_parameters!(merge_request) + assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy)) + merge_request.auto_merge_enabled = true + merge_request.merge_user = current_user + merge_request.save! + end + + def clear_auto_merge_parameters!(merge_request) merge_request.auto_merge_enabled = false merge_request.merge_user = nil @@ -76,7 +86,11 @@ module AutoMerge 'auto_merge_strategy' ) - merge_request.save + merge_request.save! + end + + def track_exception(error, merge_request) + Gitlab::ErrorTracking.track_exception(error, merge_request_id: merge_request&.id) end end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 8b3ecd32ca3..bffd443c49f 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -150,7 +150,7 @@ module Projects if @project.save unless @project.gitlab_project_import? - create_services_from_active_templates(@project) + create_services_from_active_instances_or_templates(@project) @project.create_labels end @@ -175,15 +175,6 @@ module Projects @project end - # rubocop: disable CodeReuse/ActiveRecord - def create_services_from_active_templates(project) - Service.where(template: true, active: true).each do |template| - service = Service.build_from_integration(project.id, template) - service.save! - end - end - # rubocop: enable CodeReuse/ActiveRecord - def create_prometheus_service service = @project.find_or_initialize_service(::PrometheusService.to_param) @@ -225,6 +216,15 @@ module Projects private + # rubocop: disable CodeReuse/ActiveRecord + def create_services_from_active_instances_or_templates(project) + Service.active.where(instance: true).or(Service.active.where(template: true)).group_by(&:type).each do |type, records| + service = records.find(&:instance?) || records.find(&:template?) + Service.build_from_integration(project.id, service).save! + end + end + # rubocop: enable CodeReuse/ActiveRecord + def project_namespace @project_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace end diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 81fe0798bd1..97d00bce11b 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -2,7 +2,7 @@ = form_tag search_path, method: :get, class: 'form-inline' do |f| .search-input-container .search-input-wrap - .dropdown{ data: { url: search_autocomplete_path } } + .dropdown = search_field_tag 'search', nil, placeholder: _('Search or jump to…'), class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, @@ -37,6 +37,3 @@ -# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb - if ENV['RAILS_ENV'] == 'test' %noscript= button_tag 'Search' - .search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path, - :'data-autocomplete-project-id' => search_context.project.try(:id), - :'data-autocomplete-project-ref' => search_context.ref } diff --git a/changelogs/unreleased/211828-placement-of-add-designs-button-could-be-confusing-with-the-additi.yml b/changelogs/unreleased/211828-placement-of-add-designs-button-could-be-confusing-with-the-additi.yml new file mode 100644 index 00000000000..cb172caf997 --- /dev/null +++ b/changelogs/unreleased/211828-placement-of-add-designs-button-could-be-confusing-with-the-additi.yml @@ -0,0 +1,5 @@ +--- +title: Rename Add Designs button +merge_request: 33491 +author: +type: changed diff --git a/changelogs/unreleased/213699-remove-search-results-autocomplete.yml b/changelogs/unreleased/213699-remove-search-results-autocomplete.yml new file mode 100644 index 00000000000..539d4695658 --- /dev/null +++ b/changelogs/unreleased/213699-remove-search-results-autocomplete.yml @@ -0,0 +1,5 @@ +--- +title: Remove all search autocomplete for groups/projects/other +merge_request: 31187 +author: +type: removed diff --git a/changelogs/unreleased/curd-auto-merge-in-transaction.yml b/changelogs/unreleased/curd-auto-merge-in-transaction.yml new file mode 100644 index 00000000000..9a9cfbea1c7 --- /dev/null +++ b/changelogs/unreleased/curd-auto-merge-in-transaction.yml @@ -0,0 +1,5 @@ +--- +title: Wrap auto merge parameters update in database transaction +merge_request: 33471 +author: +type: fixed diff --git a/changelogs/unreleased/sh-update-workhorse-8-34-0.yml b/changelogs/unreleased/sh-update-workhorse-8-34-0.yml new file mode 100644 index 00000000000..5ecb3840463 --- /dev/null +++ b/changelogs/unreleased/sh-update-workhorse-8-34-0.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Workhorse to v8.34.0 +merge_request: 33543 +author: +type: fixed diff --git a/config/feature_categories.yml b/config/feature_categories.yml index 010d3d14fcb..7cbc90497a4 100644 --- a/config/feature_categories.yml +++ b/config/feature_categories.yml @@ -43,6 +43,7 @@ - digital_experience_management - disaster_recovery - dynamic_application_security_testing +- editor_extension - epics - error_tracking - feature_flags @@ -52,6 +53,7 @@ - geo_replication - git_lfs - gitaly +- gitlab_docs - gitlab_handbook - gitter - global_search @@ -82,6 +84,7 @@ - pages - pki_management - planning_analytics +- product_analytics - quality_management - release_evidence - release_orchestration @@ -100,7 +103,6 @@ - source_code_management - static_application_security_testing - static_site_editor -- status_page - subgroups - templates - time_tracking diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb index 9636440926b..7e675e478cf 100644 --- a/config/initializers/zz_metrics.rb +++ b/config/initializers/zz_metrics.rb @@ -148,6 +148,7 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d config.middleware.use(Gitlab::Metrics::RackMiddleware) config.middleware.use(Gitlab::Middleware::RailsQueueDuration) config.middleware.use(Gitlab::Metrics::RedisRackMiddleware) + config.middleware.use(Gitlab::Metrics::ElasticsearchRackMiddleware) end Sidekiq.configure_server do |config| diff --git a/config/routes.rb b/config/routes.rb index bfe8172a88e..72d9c531017 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,7 +58,6 @@ Rails.application.routes.draw do # Search get 'search' => 'search#show' - get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete get 'search/count' => 'search#count', as: :search_count # JSON Web Token diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index fdd64c20c70..695de61f9a7 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -94,6 +94,8 @@ The following metrics are available: | `http_request_duration_seconds` | Histogram | 9.4 | HTTP response time from rack middleware | `method`, `status` | | `http_redis_requests_duration_seconds` | Histogram | 13.1 | Redis requests duration during web transactions | `controller`, `action` | | `http_redis_requests_total` | Counter | 13.1 | Redis requests count during web transactions | `controller`, `action` | +| `http_elasticsearch_requests_duration_seconds` **(STARTER)** | Histogram | 13.1 | Elasticsearch requests duration during web transactions | `controller`, `action` | +| `http_elasticsearch_requests_total` **(STARTER)** | Counter | 13.1 | Elasticsearch requests count during web transactions | `controller`, `action` | | `pipelines_created_total` | Counter | 9.4 | Counter of pipelines created | | | `rack_uncaught_errors_total` | Counter | 9.4 | Rack connections handling uncaught errors count | | | `user_session_logins_total` | Counter | 9.4 | Counter of how many users have logged in | | diff --git a/doc/ci/README.md b/doc/ci/README.md index 61f00d2e0fd..806d4dba3cd 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -60,8 +60,10 @@ the following documents: - [GitLab CI/CD basic workflow](introduction/index.md#basic-cicd-workflow). - [Step-by-step guide for writing `.gitlab-ci.yml` for the first time](../user/project/pages/getting_started_part_four.md). -If you're coming over from Jenkins, you can also check out our handy [reference](jenkins/index.md) -for converting your pipelines. +If you're migrating from another CI/CD tool, check out our handy references: + +- [Migrating from CircleCI](migration/circleci.md) +- [Migrating from Jenkins](jenkins/index.md) You can also get started by using one of the [`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates) diff --git a/doc/ci/migration/circleci.md b/doc/ci/migration/circleci.md new file mode 100644 index 00000000000..f6868abc334 --- /dev/null +++ b/doc/ci/migration/circleci.md @@ -0,0 +1,332 @@ +--- +comments: false +type: index, howto +--- + +# Migrating from CircleCI + +If you are currently using CircleCI, you can migrate your CI/CD pipelines to [GitLab CI/CD](../introduction/index.md), +and start making use of all its powerful features. Check out our +[CircleCI vs GitLab](https://about.gitlab.com/devops-tools/circle-ci-vs-gitlab.html) +comparison to see what's different. + +We have collected several resources that you may find useful before starting to migrate. + +The [Quick Start Guide](../quick_start/README.md) is a good overview of how GitLab CI/CD works. You may also be interested in [Auto DevOps](../../topics/autodevops/index.md) which can be used to build, test, and deploy your applications with little to no configuration needed at all. + +For advanced CI/CD teams, [custom project templates](../../user/admin_area/custom_project_templates.md) can enable the reuse of pipeline configurations. + +If you have questions that are not answered here, the [GitLab community forum](https://forum.gitlab.com/) can be a great resource. + +## `config.yml` vs `gitlab-ci.yml` + +CircleCI's `config.yml` configuration file defines scripts, jobs, and workflows (known as "stages" in GitLab). In GitLab, a similar approach is used with a `.gitlab-ci.yml` file in the root directory of your repository. + +### Jobs + +In CircleCI, jobs are a collection of steps to perform a specific task. In GitLab, [jobs](../yaml/README.md#introduction) are also a fundamental element in the configuration file. The `checkout` parameter is not necessary in GitLab CI/CD as the repository is automatically fetched. + +CircleCI example job definition: + +```yaml +jobs: + job1: + steps: + - checkout + - run: "execute-script-for-job1" +``` + +Example of the same job definition in GitLab CI/CD: + +``` yaml +job1: + script: "execute-script-for-job1" +``` + +### Docker image definition + +CircleCI defines images at the job level, which is also supported by GitLab CI/CD. Additionally, GitLab CI/CD supports setting this globally to be used by all jobs that don't have `image` defined. + +CircleCI example image definition: + +```yaml +jobs: + job1: + docker: + - image: ruby:2.6 +``` + +Example of the same image definition in GitLab CI/CD: + +```yaml +job1: + image: ruby:2.6 +``` + +### Workflows + +CircleCI determines the run order for jobs with `workflows`. This is also used to determine concurrent, sequential, scheduled, or manual runs. The equivalent function in GitLab CI/CD is called [stages](../yaml/README.md#stages). Jobs on the same stage run in parallel, and only run after previous stages complete. Execution of the next stage is skipped when a job fails by default, but this can be allowed to continue even [after a failed job](../yaml/README.md#allow_failure). + +See [the Pipeline Architecture Overview](../pipelines/pipeline_architectures.md) for guidance on different types of pipelines that you can use. Pipelines can be tailored to meet your needs, such as for a large complex project or a monorepo with independent defined components. + +#### Parallel and sequential job execution + +The following examples show how jobs can run in parallel, or sequentially: + +1. `job1` and `job2` run in parallel (in the `build` stage for GitLab CI/CD). +1. `job3` runs only after `job1` and `job2` complete successfully (in the `test` stage). +1. `job4` runs only after `job3` completes successfully (in the `deploy` stage). + +CircleCI example with `workflows`: + +```yaml +version: 2 +jobs: + job1: + steps: + - checkout + - run: make build dependencies + job2: + steps: + - run: make build artifacts + job3: + steps: + - run: make test + job4: + steps: + - run: make deploy + +workflows: + version: 2 + jobs: + - job1 + - job2 + - job3: + requires: + - job1 + - job2 + - job4: + requires: + - job3 +``` + +Example of the same workflow as `stages` in GitLab CI/CD: + +```yaml +stages: + - build + - test + - deploy + +job 1: + stage: build + script: make build dependencies + +job 2: + stage: build + script: make build artifacts + +job3: + stage: test + script: make test + +job4: + stage: deploy + script: make deploy +``` + +#### Scheduled run + +GitLab CI/CD has an easy to use UI to [schedule pipelines](../pipelines/schedules.md). Also, [rules](../yaml/README.md#rules) can be used to determine if jobs should be included or excluded from a scheduled pipeline. + +CircleCI example of a scheduled workflow: + +```yaml +commit-workflow: + jobs: + - build +scheduled-workflow: + triggers: + - schedule: + cron: "0 1 * * *" + filters: + branches: + only: try-schedule-workflow + jobs: + - build +``` + +Example of the same scheduled pipeline using [`rules`](../yaml/README.md#rules) in GitLab CI/CD: + +```yaml +job1: + script: + - make build + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule" && $CI_COMMIT_REF_NAME == "try-schedule-workflow"' +``` + +After the pipeline configuration is saved, you configure the cron schedule in the [GitLab UI](../pipelines/schedules.md#configuring-pipeline-schedules), and can enable or disable schedules in the UI as well. + +#### Manual run + +CircleCI example of a manual workflow: + +```yaml +release-branch-workflow: + jobs: + - build + - testing: + requires: + - build + - deploy: + type: approval + requires: + - testing +``` + +Example of the same workflow using [`when: manual`](../yaml/README.md#whenmanual) in GitLab CI/CD: + +```yaml +deploy_prod: + stage: deploy + script: + - echo "Deploy to production server" + when: manual +``` + +### Filter job by branch + +[Rules](../yaml/README.md#rules) are a mechanism to determine if the job will or will not run for a specific branch. + +CircleCI example of a job filtered by branch: + +```yaml +jobs: + deploy: + branches: + only: + - master + - /rc-.*/ +``` + +Example of the same workflow using `rules` in GitLab CI/CD: + +```yaml +deploy_prod: + stage: deploy + script: + - echo "Deploy to production server" + rules: + - if: '$CI_COMMIT_BRANCH == "master"' +``` + +### Caching + +GitLab provides a caching mechanism to speed up build times for your jobs by reusing previously downloaded dependencies. It's important to know the different between [cache and artifacts](../caching/index.md#cache-vs-artifacts) to make the best use of these features. + +CircleCI example of a job using a cache: + +```yaml +jobs: + job1: + steps: + - restore_cache: + key: source-v1-< .Revision > + - checkout + - run: npm install + - save_cache: + key: source-v1-< .Revision > + paths: + - "node_modules" +``` + +Example of the same pipeline using `cache` in GitLab CI/CD: + +```yaml +image: node:latest + +# Cache modules in between jobs +cache: + key: $CI_COMMIT_REF_SLUG + paths: + - .npm/ + +before_script: + - npm ci --cache .npm --prefer-offline + +test_async: + script: + - node ./specs/start.js ./specs/async.spec.js +``` + +## Contexts and variables + +CircleCI provides [Contexts](https://circleci.com/docs/2.0/contexts/) to securely pass environment variables across project pipelines. In GitLab, a [Group](../../user/group/index.md) can be created to assemble related projects together. At the group level, [variables](../variables/README.md#group-level-environment-variables) can be stored outside the individual projects, and securely passed into pipelines across multiple projects. + +## Orbs + +There are two GitLab issues open addressing CircleCI Orbs and how GitLab can achieve similar functionality. + +- <https://gitlab.com/gitlab-com/Product/-/issues/1151> +- <https://gitlab.com/gitlab-org/gitlab/-/issues/195173> + +## Build environments + +CircleCI offers `executors` as the underlying technology to run a specific job. In GitLab, this is done by [Runners](https://docs.gitlab.com/runner/). + +The following environments are supported: + +Self-Managed Runners: + +- Linux +- Windows +- macOS + +GitLab.com Shared Runners: + +- Linux +- Windows +- [Planned: macOS](https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/5720) + +### Machine and specific build environments + +[Tags](../yaml/README.md#tags) can be used to run jobs on different platforms, by telling GitLab which Runners should run the jobs. + +CircleCI example of a job running on a specific environment: + +```yaml +jobs: + ubuntuJob: + machine: + image: ubuntu-1604:201903-01 + steps: + - checkout + - run: echo "Hello, $USER!" + osxJob: + macos: + xcode: 11.3.0 + steps: + - checkout + - run: echo "Hello, $USER!" +``` + +Example of the same job using `tags` in GitLab CI/CD: + +```yaml +windows job: + stage: + - build + tags: + - windows + script: + - echo Hello, %USERNAME%! + +osx job: + stage: + - build + tags: + - osx + script: + - echo "Hello, $USER!" +``` diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md index 9ebcf258ee9..8dc019d3fe1 100644 --- a/doc/gitlab-basics/start-using-git.md +++ b/doc/gitlab-basics/start-using-git.md @@ -22,6 +22,11 @@ This guide will help you get started with Git through the command line and can b for Git commands in the future. If you're only looking for a quick reference of Git commands, you can download GitLab's [Git Cheat Sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf). +> For more information about the advantages of working with Git and GitLab: +> +> - Watch the [GitLab Source Code Management Walkthrough](https://www.youtube.com/watch?v=wTQ3aXJswtM) video. +> - Learn how GitLab became the backbone of [Worldline](https://about.gitlab.com/customers/worldline/)’s development environment. + TIP: **Tip:** To help you visualize what you're doing locally, there are [Git GUI apps](https://git-scm.com/download/gui/) you can install. diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md index 8597325db7b..2b67edc25a3 100644 --- a/doc/topics/git/numerous_undo_possibilities_in_git/index.md +++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md @@ -26,6 +26,11 @@ This means that until Git automatically cleans detached commits (which cannot be accessed by branch or tag) it will be possible to view them with `git reflog` command and access them with direct commit ID. Read more about _[redoing the undo](#redoing-the-undo)_ in the section below. +> For more information about working with Git and GitLab: +> +> - Learn why [North Western Mutual chose GitLab](https://youtu.be/kPNMyxKRRoM) for their Enterprise source code management. +> - Learn how to [get started with Git](https://about.gitlab.com/resources/whitepaper-moving-to-git/). + ## Introduction This guide is organized depending on the [stage of development](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) diff --git a/lib/gitlab/metrics/elasticsearch_rack_middleware.rb b/lib/gitlab/metrics/elasticsearch_rack_middleware.rb new file mode 100644 index 00000000000..6830eed68d5 --- /dev/null +++ b/lib/gitlab/metrics/elasticsearch_rack_middleware.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Gitlab + module Metrics + # Rack middleware for tracking Elasticsearch metrics from Grape and Web requests. + class ElasticsearchRackMiddleware + HISTOGRAM_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60].freeze + + def initialize(app) + @app = app + + @requests_total_counter = Gitlab::Metrics.counter(:http_elasticsearch_requests_total, + 'Amount of calls to Elasticsearch servers during web requests', + Gitlab::Metrics::Transaction::BASE_LABELS) + @requests_duration_histogram = Gitlab::Metrics.histogram(:http_elasticsearch_requests_duration_seconds, + 'Query time for Elasticsearch servers during web requests', + Gitlab::Metrics::Transaction::BASE_LABELS, + HISTOGRAM_BUCKETS) + end + + def call(env) + transaction = Gitlab::Metrics.current_transaction + + @app.call(env) + ensure + record_metrics(transaction) + end + + private + + def record_metrics(transaction) + labels = transaction.labels + query_time = ::Gitlab::Instrumentation::ElasticsearchTransport.query_time + request_count = ::Gitlab::Instrumentation::ElasticsearchTransport.get_request_count + + @requests_total_counter.increment(labels, request_count) + @requests_duration_histogram.observe(labels, query_time) + end + end + end +end diff --git a/lib/gitlab/metrics/redis_rack_middleware.rb b/lib/gitlab/metrics/redis_rack_middleware.rb index 0ed5e786fa3..f0f99c5f45d 100644 --- a/lib/gitlab/metrics/redis_rack_middleware.rb +++ b/lib/gitlab/metrics/redis_rack_middleware.rb @@ -6,6 +6,14 @@ module Gitlab class RedisRackMiddleware def initialize(app) @app = app + + @requests_total_counter = Gitlab::Metrics.counter(:http_redis_requests_total, + 'Amount of calls to Redis servers during web requests', + Gitlab::Metrics::Transaction::BASE_LABELS) + @requests_duration_histogram = Gitlab::Metrics.histogram(:http_redis_requests_duration_seconds, + 'Query time for Redis servers during web requests', + Gitlab::Metrics::Transaction::BASE_LABELS, + Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS) end def call(env) @@ -13,7 +21,7 @@ module Gitlab @app.call(env) ensure - record_metrics(transaction) if transaction + record_metrics(transaction) end private @@ -23,14 +31,8 @@ module Gitlab query_time = Gitlab::Instrumentation::Redis.query_time request_count = Gitlab::Instrumentation::Redis.get_request_count - Gitlab::Metrics.counter(:http_redis_requests_total, - 'Amount of calls to Redis servers during web requests', - Gitlab::Metrics::Transaction::BASE_LABELS).increment(labels, request_count) - - Gitlab::Metrics.histogram(:http_redis_requests_duration_seconds, - 'Query time for Redis servers during web requests', - Gitlab::Metrics::Transaction::BASE_LABELS, - Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS).observe(labels, query_time) + @requests_total_counter.increment(labels, request_count) + @requests_duration_histogram.observe(labels, query_time) end end end diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb index 7e41adc67bd..64a30fbe16c 100644 --- a/lib/gitlab/setup_helper.rb +++ b/lib/gitlab/setup_helper.rb @@ -79,6 +79,7 @@ module Gitlab config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path } config[:bin_dir] = Gitlab.config.gitaly.client_path + config[:gitlab] = { url: Gitlab.config.gitlab.url } TomlRB.dump(config) end @@ -97,7 +98,8 @@ module Gitlab def configuration_toml(gitaly_dir, storage_paths) nodes = [{ storage: 'default', address: "unix:#{gitaly_dir}/gitaly.socket", primary: true, token: 'secret' }] storages = [{ name: 'default', node: nodes }] - config = { socket_path: "#{gitaly_dir}/praefect.socket", memory_queue_enabled: true, virtual_storage: storages } + failover = { enabled: false } + config = { socket_path: "#{gitaly_dir}/praefect.socket", memory_queue_enabled: true, virtual_storage: storages, failover: failover } config[:token] = 'secret' if Rails.env.test? TomlRB.dump(config) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b93d56bd8b2..60e6b5bcc53 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -983,9 +983,6 @@ msgstr "" msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'" msgstr "" -msgid "API Help" -msgstr "" - msgid "API Token" msgstr "" @@ -1441,9 +1438,6 @@ msgstr "" msgid "Admin Overview" msgstr "" -msgid "Admin Section" -msgstr "" - msgid "Admin mode already enabled" msgstr "" @@ -7487,9 +7481,6 @@ msgstr "" msgid "DesignManagement|%{filename} did not change." msgstr "" -msgid "DesignManagement|Add designs" -msgstr "" - msgid "DesignManagement|Adding a design with the same filename replaces the file in a new version." msgstr "" @@ -7574,6 +7565,9 @@ msgstr "" msgid "DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance." msgstr "" +msgid "DesignManagement|Upload designs" +msgstr "" + msgid "DesignManagement|Upload skipped." msgstr "" @@ -13336,9 +13330,6 @@ msgstr "" msgid "Markdown" msgstr "" -msgid "Markdown Help" -msgstr "" - msgid "Markdown enabled" msgstr "" @@ -15654,9 +15645,6 @@ msgstr "" msgid "Permissions" msgstr "" -msgid "Permissions Help" -msgstr "" - msgid "Permissions, LFS, 2FA" msgstr "" @@ -17685,9 +17673,6 @@ msgstr "" msgid "Public - The project can be accessed without any authentication." msgstr "" -msgid "Public Access Help" -msgstr "" - msgid "Public deploy keys (%{deploy_keys_count})" msgstr "" @@ -17817,9 +17802,6 @@ msgstr "" msgid "README" msgstr "" -msgid "Rake Tasks Help" -msgstr "" - msgid "Raw blob request rate limit per minute" msgstr "" @@ -18861,9 +18843,6 @@ msgstr "" msgid "SSH Keys" msgstr "" -msgid "SSH Keys Help" -msgstr "" - msgid "SSH host key fingerprints" msgstr "" @@ -21343,9 +21322,6 @@ msgstr "" msgid "System Hooks" msgstr "" -msgid "System Hooks Help" -msgstr "" - msgid "System Info" msgstr "" @@ -23975,9 +23951,6 @@ msgstr "" msgid "User restrictions" msgstr "" -msgid "User settings" -msgstr "" - msgid "User was successfully created." msgstr "" @@ -24789,9 +24762,6 @@ msgstr "" msgid "Webhooks" msgstr "" -msgid "Webhooks Help" -msgstr "" - msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group." msgstr "" @@ -25058,9 +25028,6 @@ msgstr "" msgid "Work in progress Limit" msgstr "" -msgid "Workflow Help" -msgstr "" - msgid "Write" msgstr "" diff --git a/rubocop/cop/rspec/empty_line_after_shared_example.rb b/rubocop/cop/rspec/empty_line_after_shared_example.rb deleted file mode 100644 index 5d09565bd5a..00000000000 --- a/rubocop/cop/rspec/empty_line_after_shared_example.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -require 'rubocop/rspec/final_end_location' -require 'rubocop/rspec/blank_line_separation' -require 'rubocop/rspec/language' - -module RuboCop - module Cop - module RSpec - # Checks if there is an empty line after shared example blocks. - # - # @example - # # bad - # RSpec.describe Foo do - # it_behaves_like 'do this first' - # it_behaves_like 'does this' do - # end - # it_behaves_like 'does that' do - # end - # it_behaves_like 'do some more' - # end - # - # # good - # RSpec.describe Foo do - # it_behaves_like 'do this first' - # it_behaves_like 'does this' do - # end - # - # it_behaves_like 'does that' do - # end - # - # it_behaves_like 'do some more' - # end - # - # # fair - it's ok to have non-separated without blocks - # RSpec.describe Foo do - # it_behaves_like 'do this first' - # it_behaves_like 'does this' - # end - # - class EmptyLineAfterSharedExample < RuboCop::Cop::Cop - include RuboCop::RSpec::BlankLineSeparation - include RuboCop::RSpec::Language - - MSG = 'Add an empty line after `%<example>s` block.' - - def_node_matcher :shared_examples, - (SharedGroups::ALL + Includes::ALL).block_pattern - - def on_block(node) - shared_examples(node) do - break if last_child?(node) - - missing_separating_line(node) do |location| - add_offense(node, - location: location, - message: format(MSG, example: node.method_name)) - end - end - end - end - end - end -end diff --git a/scripts/gitaly-test-build b/scripts/gitaly-test-build index fcf0049162b..5254d957afd 100755 --- a/scripts/gitaly-test-build +++ b/scripts/gitaly-test-build @@ -14,6 +14,7 @@ class GitalyTestBuild def run abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir) + ensure_gitlab_shell_secret! check_gitaly_config! # Starting gitaly further validates its configuration diff --git a/scripts/gitaly_test.rb b/scripts/gitaly_test.rb index e6f2c9885d9..c69c4ea747b 100644 --- a/scripts/gitaly_test.rb +++ b/scripts/gitaly_test.rb @@ -4,6 +4,7 @@ # Please be careful when modifying this file. Your changes must work # both for local development rspec runs, and in CI. +require 'securerandom' require 'socket' module GitalyTest @@ -11,10 +12,22 @@ module GitalyTest File.expand_path('../tmp/tests/gitaly', __dir__) end + def tmp_tests_gitlab_shell_dir + File.expand_path('../tmp/tests/gitlab-shell', __dir__) + end + + def rails_gitlab_shell_secret + File.expand_path('../.gitlab_shell_secret', __dir__) + end + def gemfile File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile') end + def gitlab_shell_secret_file + File.join(tmp_tests_gitlab_shell_dir, '.gitlab_shell_secret') + end + def env env_hash = { 'HOME' => File.expand_path('tmp/tests'), @@ -70,6 +83,20 @@ module GitalyTest pid end + # Taken from Gitlab::Shell.generate_and_link_secret_token + def ensure_gitlab_shell_secret! + secret_file = rails_gitlab_shell_secret + shell_link = gitlab_shell_secret_file + + unless File.size?(secret_file) + File.write(secret_file, SecureRandom.hex(16)) + end + + unless File.exist?(shell_link) + FileUtils.ln_s(secret_file, shell_link) + end + end + def check_gitaly_config! puts "Checking gitaly-ruby Gemfile..." diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 79ffa297da3..19eeabf99ee 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -211,9 +211,4 @@ describe SearchController do end.to raise_error(ActionController::ParameterMissing) end end - - describe 'GET #autocomplete' do - it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' } - it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' } - end end diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb index 1a8af335244..8ab2c7ffd64 100644 --- a/spec/fast_spec_helper.rb +++ b/spec/fast_spec_helper.rb @@ -13,4 +13,6 @@ require 'active_support/all' ActiveSupport::Dependencies.autoload_paths << 'lib' ActiveSupport::Dependencies.autoload_paths << 'ee/lib' +ActiveSupport::Dependencies.autoload_paths << 'tooling/lib' + ActiveSupport::XmlMini.backend = 'Nokogiri' diff --git a/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap index 185bf4a48f7..27c0ba589e6 100644 --- a/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap +++ b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap @@ -10,7 +10,7 @@ exports[`Design management upload button component renders inverted upload desig variant="success" > - Add designs + Upload designs <!----> </gl-deprecated-button-stub> @@ -34,7 +34,7 @@ exports[`Design management upload button component renders loading icon 1`] = ` variant="success" > - Add designs + Upload designs <gl-loading-icon-stub class="ml-1" @@ -63,7 +63,7 @@ exports[`Design management upload button component renders upload design button variant="success" > - Add designs + Upload designs <!----> </gl-deprecated-button-stub> diff --git a/spec/frontend/fixtures/static/search_autocomplete.html b/spec/frontend/fixtures/static/global_search_input.html index 29db9020424..29db9020424 100644 --- a/spec/frontend/fixtures/static/search_autocomplete.html +++ b/spec/frontend/fixtures/static/global_search_input.html diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 6a06b012c6c..b209ed869bf 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -8,99 +8,6 @@ describe SearchHelper do str end - describe 'search_autocomplete_opts' do - context "with no current user" do - before do - allow(self).to receive(:current_user).and_return(nil) - end - - it "returns nil" do - expect(search_autocomplete_opts("q")).to be_nil - end - end - - context "with a standard user" do - let(:user) { create(:user) } - - before do - allow(self).to receive(:current_user).and_return(user) - end - - it "includes Help sections" do - expect(search_autocomplete_opts("hel").size).to eq(9) - end - - it "includes default sections" do - expect(search_autocomplete_opts("dash").size).to eq(1) - end - - it "does not include admin sections" do - expect(search_autocomplete_opts("admin").size).to eq(0) - end - - it "does not allow regular expression in search term" do - expect(search_autocomplete_opts("(webhooks|api)").size).to eq(0) - end - - it "includes the user's groups" do - create(:group).add_owner(user) - expect(search_autocomplete_opts("gro").size).to eq(1) - end - - it "includes nested group" do - create(:group, :nested, name: 'foo').add_owner(user) - expect(search_autocomplete_opts('foo').size).to eq(1) - end - - it "includes the user's projects" do - project = create(:project, namespace: create(:namespace, owner: user)) - expect(search_autocomplete_opts(project.name).size).to eq(1) - end - - it "includes the required project attrs" do - project = create(:project, namespace: create(:namespace, owner: user)) - result = search_autocomplete_opts(project.name).first - - expect(result.keys).to match_array(%i[category id value label url avatar_url]) - end - - it "includes the required group attrs" do - create(:group).add_owner(user) - result = search_autocomplete_opts("gro").first - - expect(result.keys).to match_array(%i[category id label url avatar_url]) - end - - it "does not include the public group" do - group = create(:group) - expect(search_autocomplete_opts(group.name).size).to eq(0) - end - - context "with a current project" do - before do - @project = create(:project, :repository) - end - - it "includes project-specific sections" do - expect(search_autocomplete_opts("Files").size).to eq(1) - expect(search_autocomplete_opts("Commits").size).to eq(1) - end - end - end - - context 'with an admin user' do - let(:admin) { create(:admin) } - - before do - allow(self).to receive(:current_user).and_return(admin) - end - - it "includes admin sections" do - expect(search_autocomplete_opts("admin").size).to eq(1) - end - end - end - describe 'search_entries_info' do using RSpec::Parameterized::TableSyntax diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/global_search_input_spec.js index 4f42d4880e8..00ae8a8f2ea 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/global_search_input_spec.js @@ -2,10 +2,10 @@ import $ from 'jquery'; import '~/gl_dropdown'; -import initSearchAutocomplete from '~/search_autocomplete'; +import initGlobalSearchInput from '~/global_search_input'; import '~/lib/utils/common_utils'; -describe('Search autocomplete dropdown', () => { +describe('Global search input dropdown', () => { let widget = null; const userName = 'root'; @@ -112,15 +112,15 @@ describe('Search autocomplete dropdown', () => { expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created"); }; - preloadFixtures('static/search_autocomplete.html'); + preloadFixtures('static/global_search_input.html'); beforeEach(function() { - loadFixtures('static/search_autocomplete.html'); + loadFixtures('static/global_search_input.html'); window.gon = {}; window.gon.current_user_id = userId; window.gon.current_username = userName; - return (widget = initSearchAutocomplete()); + return (widget = initGlobalSearchInput()); }); afterEach(function() { @@ -189,25 +189,25 @@ describe('Search autocomplete dropdown', () => { expect(submitSpy).not.toHaveBeenTriggered(); }); - describe('disableAutocomplete', function() { + describe('disableDropdown', function() { beforeEach(function() { - widget.enableAutocomplete(); + widget.enableDropdown(); }); it('should close the Dropdown', function() { const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown'); widget.dropdown.addClass('show'); - widget.disableAutocomplete(); + widget.disableDropdown(); expect(toggleSpy).toHaveBeenCalledWith('toggle'); }); }); - describe('enableAutocomplete', function() { + describe('enableDropdown', function() { it('should open the Dropdown', function() { const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown'); - widget.enableAutocomplete(); + widget.enableDropdown(); expect(toggleSpy).toHaveBeenCalledWith('toggle'); }); diff --git a/spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb new file mode 100644 index 00000000000..305768ef060 --- /dev/null +++ b/spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Metrics::ElasticsearchRackMiddleware do + let(:app) { double(:app, call: 'app call result') } + let(:middleware) { described_class.new(app) } + let(:env) { {} } + let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) } + + describe '#call' do + let(:counter) { instance_double(Prometheus::Client::Counter, increment: nil) } + let(:histogram) { instance_double(Prometheus::Client::Histogram, observe: nil) } + let(:elasticsearch_query_time) { 0.1 } + let(:elasticsearch_requests_count) { 2 } + + before do + allow(Gitlab::Instrumentation::ElasticsearchTransport).to receive(:query_time) { elasticsearch_query_time } + allow(Gitlab::Instrumentation::ElasticsearchTransport).to receive(:get_request_count) { elasticsearch_requests_count } + + allow(Gitlab::Metrics).to receive(:counter) + .with(:http_elasticsearch_requests_total, + an_instance_of(String), + Gitlab::Metrics::Transaction::BASE_LABELS) + .and_return(counter) + + allow(Gitlab::Metrics).to receive(:histogram) + .with(:http_elasticsearch_requests_duration_seconds, + an_instance_of(String), + Gitlab::Metrics::Transaction::BASE_LABELS, + described_class::HISTOGRAM_BUCKETS) + .and_return(histogram) + + allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) + end + + it 'calls the app' do + expect(middleware.call(env)).to eq('app call result') + end + + it 'records elasticsearch metrics' do + expect(counter).to receive(:increment).with(transaction.labels, elasticsearch_requests_count) + expect(histogram).to receive(:observe).with(transaction.labels, elasticsearch_query_time) + + middleware.call(env) + end + + it 'records elasticsearch metrics if an error is raised' do + expect(counter).to receive(:increment).with(transaction.labels, elasticsearch_requests_count) + expect(histogram).to receive(:observe).with(transaction.labels, elasticsearch_query_time) + + allow(app).to receive(:call).with(env).and_raise(StandardError) + + expect { middleware.call(env) }.to raise_error(StandardError) + end + end +end diff --git a/spec/lib/gitlab/metrics/redis_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/redis_rack_middleware_spec.rb index 025b40b7013..f2f36ccad20 100644 --- a/spec/lib/gitlab/metrics/redis_rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/redis_rack_middleware_spec.rb @@ -13,68 +13,49 @@ describe Gitlab::Metrics::RedisRackMiddleware do end describe '#call' do - context 'when metrics are disabled' do - before do - allow(Gitlab::Metrics).to receive(:current_transaction).and_return(nil) - end - - it 'calls the app' do - expect(middleware.call(env)).to eq('wub wub') - end - - it 'does not record metrics' do - expect(Gitlab::Metrics).not_to receive(:counter) - expect(Gitlab::Metrics).not_to receive(:histogram) - - middleware.call(env) - end + let(:counter) { double(Prometheus::Client::Counter, increment: nil) } + let(:histogram) { double(Prometheus::Client::Histogram, observe: nil) } + let(:redis_query_time) { 0.1 } + let(:redis_requests_count) { 2 } + + before do + allow(Gitlab::Instrumentation::Redis).to receive(:query_time) { redis_query_time } + allow(Gitlab::Instrumentation::Redis).to receive(:get_request_count) { redis_requests_count } + + allow(Gitlab::Metrics).to receive(:counter) + .with(:http_redis_requests_total, + an_instance_of(String), + Gitlab::Metrics::Transaction::BASE_LABELS) + .and_return(counter) + + allow(Gitlab::Metrics).to receive(:histogram) + .with(:http_redis_requests_duration_seconds, + an_instance_of(String), + Gitlab::Metrics::Transaction::BASE_LABELS, + Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS) + .and_return(histogram) + + allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) end - context 'when metrics are enabled' do - let(:counter) { double(Prometheus::Client::Counter, increment: nil) } - let(:histogram) { double(Prometheus::Client::Histogram, observe: nil) } - let(:redis_query_time) { 0.1 } - let(:redis_requests_count) { 2 } - - before do - allow(Gitlab::Instrumentation::Redis).to receive(:query_time) { redis_query_time } - allow(Gitlab::Instrumentation::Redis).to receive(:get_request_count) { redis_requests_count } - - allow(Gitlab::Metrics).to receive(:counter) - .with(:http_redis_requests_total, - an_instance_of(String), - Gitlab::Metrics::Transaction::BASE_LABELS) - .and_return(counter) - - allow(Gitlab::Metrics).to receive(:histogram) - .with(:http_redis_requests_duration_seconds, - an_instance_of(String), - Gitlab::Metrics::Transaction::BASE_LABELS, - Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS) - .and_return(histogram) - - allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) - end - - it 'calls the app' do - expect(middleware.call(env)).to eq('wub wub') - end + it 'calls the app' do + expect(middleware.call(env)).to eq('wub wub') + end - it 'records redis metrics' do - expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count) - expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time) + it 'records redis metrics' do + expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count) + expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time) - middleware.call(env) - end + middleware.call(env) + end - it 'records redis metrics if an error is raised' do - expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count) - expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time) + it 'records redis metrics if an error is raised' do + expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count) + expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time) - allow(app).to receive(:call).with(env).and_raise(StandardError) + allow(app).to receive(:call).with(env).and_raise(StandardError) - expect { middleware.call(env) }.to raise_error(StandardError) - end + expect { middleware.call(env) }.to raise_error(StandardError) end end end diff --git a/spec/rubocop/cop/rspec/empty_line_after_shared_example_spec.rb b/spec/rubocop/cop/rspec/empty_line_after_shared_example_spec.rb deleted file mode 100644 index cee593fe535..00000000000 --- a/spec/rubocop/cop/rspec/empty_line_after_shared_example_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -require_relative '../../../../rubocop/cop/rspec/empty_line_after_shared_example' - -describe RuboCop::Cop::RSpec::EmptyLineAfterSharedExample do - subject(:cop) { described_class.new } - - it 'flags a missing empty line after `it_behaves_like` block' do - expect_offense(<<-RUBY) - RSpec.describe Foo do - it_behaves_like 'does this' do - end - ^^^ Add an empty line after `it_behaves_like` block. - it_behaves_like 'does that' do - end - end - RUBY - - expect_correction(<<-RUBY) - RSpec.describe Foo do - it_behaves_like 'does this' do - end - - it_behaves_like 'does that' do - end - end - RUBY - end - - it 'ignores one-line shared examples before shared example blocks' do - expect_no_offenses(<<-RUBY) - RSpec.describe Foo do - it_behaves_like 'does this' - it_behaves_like 'does that' do - end - end - RUBY - end - - it 'flags a missing empty line after `shared_examples`' do - expect_offense(<<-RUBY) - RSpec.context 'foo' do - shared_examples do - end - ^^^ Add an empty line after `shared_examples` block. - shared_examples 'something gets done' do - end - end - RUBY - - expect_correction(<<-RUBY) - RSpec.context 'foo' do - shared_examples do - end - - shared_examples 'something gets done' do - end - end - RUBY - end - - it 'ignores consecutive one-liners' do - expect_no_offenses(<<-RUBY) - RSpec.describe Foo do - it_behaves_like 'do this' - it_behaves_like 'do that' - end - RUBY - end - - it 'flags mixed one-line and multi-line shared examples' do - expect_offense(<<-RUBY) - RSpec.context 'foo' do - it_behaves_like 'do this' - it_behaves_like 'do that' - it_behaves_like 'does this' do - end - ^^^ Add an empty line after `it_behaves_like` block. - it_behaves_like 'do this' - it_behaves_like 'do that' - end - RUBY - end -end diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb index 0a6bcb1badc..e08e1d670bf 100644 --- a/spec/services/auto_merge/base_service_spec.rb +++ b/spec/services/auto_merge/base_service_spec.rb @@ -82,9 +82,9 @@ describe AutoMerge::BaseService do end end - context 'when failed to save' do + context 'when failed to save merge request' do before do - allow(merge_request).to receive(:save) { false } + allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new } end it 'does not yield block' do @@ -94,6 +94,39 @@ describe AutoMerge::BaseService do it 'returns failed' do is_expected.to eq(:failed) end + + it 'tracks the exception' do + expect(Gitlab::ErrorTracking) + .to receive(:track_exception).with(kind_of(ActiveRecord::RecordInvalid), + merge_request_id: merge_request.id) + + subject + end + end + + context 'when exception happens in yield block' do + def execute_with_error_in_yield + service.execute(merge_request) { raise 'Something went wrong' } + end + + it 'returns failed status' do + expect(execute_with_error_in_yield).to eq(:failed) + end + + it 'rollback the transaction' do + execute_with_error_in_yield + + merge_request.reload + expect(merge_request).not_to be_auto_merge_enabled + end + + it 'tracks the exception' do + expect(Gitlab::ErrorTracking) + .to receive(:track_exception).with(kind_of(RuntimeError), + merge_request_id: merge_request.id) + + execute_with_error_in_yield + end end end @@ -162,7 +195,7 @@ describe AutoMerge::BaseService do context 'when failed to save' do before do - allow(merge_request).to receive(:save) { false } + allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new } end it 'does not yield block' do @@ -178,9 +211,9 @@ describe AutoMerge::BaseService do it_behaves_like 'Canceled or Dropped' - context 'when failed to save' do + context 'when failed to save merge request' do before do - allow(merge_request).to receive(:save) { false } + allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new } end it 'returns error status' do @@ -188,6 +221,33 @@ describe AutoMerge::BaseService do expect(subject[:message]).to eq("Can't cancel the automatic merge") end end + + context 'when exception happens in yield block' do + def cancel_with_error_in_yield + service.cancel(merge_request) { raise 'Something went wrong' } + end + + it 'returns error' do + result = cancel_with_error_in_yield + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq("Can't cancel the automatic merge") + end + + it 'rollback the transaction' do + cancel_with_error_in_yield + + merge_request.reload + expect(merge_request).to be_auto_merge_enabled + end + + it 'tracks the exception' do + expect(Gitlab::ErrorTracking) + .to receive(:track_exception).with(kind_of(RuntimeError), + merge_request_id: merge_request.id) + + cancel_with_error_in_yield + end + end end describe '#abort' do @@ -200,7 +260,7 @@ describe AutoMerge::BaseService do context 'when failed to save' do before do - allow(merge_request).to receive(:save) { false } + allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new } end it 'returns error status' do @@ -208,5 +268,32 @@ describe AutoMerge::BaseService do expect(subject[:message]).to eq("Can't abort the automatic merge") end end + + context 'when exception happens in yield block' do + def abort_with_error_in_yield + service.abort(merge_request, reason) { raise 'Something went wrong' } + end + + it 'returns error' do + result = abort_with_error_in_yield + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq("Can't abort the automatic merge") + end + + it 'rollback the transaction' do + abort_with_error_in_yield + + merge_request.reload + expect(merge_request).to be_auto_merge_enabled + end + + it 'tracks the exception' do + expect(Gitlab::ErrorTracking) + .to receive(:track_exception).with(kind_of(RuntimeError), + merge_request_id: merge_request.id) + + abort_with_error_in_yield + end + end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index e542f1e9108..27813bedfd6 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -339,29 +339,40 @@ describe Projects::CreateService, '#execute' do end end - context 'when there is an active service template' do - before do - create(:prometheus_service, project: nil, template: true, active: true) - end + describe 'create service for the project' do + subject(:project) { create_project(user, opts) } - it 'creates a service from this template' do - project = create_project(user, opts) + context 'when there is an active instance-level and an active template integration' do + before do + create(:prometheus_service, :instance, api_url: 'https://prometheus.instance.com/') + create(:prometheus_service, :template, api_url: 'https://prometheus.template.com/') + end - expect(project.services.count).to eq 1 - expect(project.errors).to be_empty + it 'creates a service from the instance-level integration' do + expect(project.services.count).to eq(1) + expect(project.services.first.api_url).to eq('https://prometheus.instance.com/') + end end - end - context 'when a bad service template is created' do - it 'sets service to be inactive' do - opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-foss' - create(:service, type: 'DroneCiService', project: nil, template: true, active: true) + context 'when there is an active service template' do + before do + create(:prometheus_service, :template, active: true) + end - project = create_project(user, opts) - service = project.services.first + it 'creates a service from the template' do + expect(project.services.count).to eq(1) + end + end - expect(project).to be_persisted - expect(service.active).to be false + context 'when there is an invalid integration' do + before do + create(:service, :template, type: 'DroneCiService', active: true) + end + + it 'creates an inactive service' do + expect(project).to be_persisted + expect(project.services.first.active).to be false + end end end diff --git a/spec/tooling/lib/tooling/test_file_finder_spec.rb b/spec/tooling/lib/tooling/test_file_finder_spec.rb new file mode 100644 index 00000000000..7a5d39a014a --- /dev/null +++ b/spec/tooling/lib/tooling/test_file_finder_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Tooling::TestFileFinder do + subject { Tooling::TestFileFinder.new(file) } + + describe '#test_files' do + context 'when given non .rb files' do + let(:file) { 'app/assets/images/emoji.png' } + + it 'does not return a test file' do + expect(subject.test_files).to be_empty + end + end + + context 'when given file in app/' do + let(:file) { 'app/finders/admin/projects_finder.rb' } + + it 'returns the matching app spec file' do + expect(subject.test_files).to contain_exactly('spec/finders/admin/projects_finder_spec.rb') + end + end + + context 'when given file in lib/' do + let(:file) { 'lib/banzai/color_parser.rb' } + + it 'returns the matching app spec file' do + expect(subject.test_files).to contain_exactly('spec/lib/banzai/color_parser_spec.rb') + end + end + + context 'when given a file in tooling/' do + let(:file) { 'tooling/lib/quality/test_file_finder.rb' } + + it 'returns the matching tooling test' do + expect(subject.test_files).to contain_exactly('spec/tooling/lib/quality/test_file_finder_spec.rb') + end + end + + context 'when given a test file' do + let(:file) { 'spec/lib/banzai/color_parser_spec.rb' } + + it 'returns the matching test file itself' do + expect(subject.test_files).to contain_exactly('spec/lib/banzai/color_parser_spec.rb') + end + end + + context 'when given an app file in ee/' do + let(:file) { 'ee/app/models/analytics/cycle_analytics/group_level.rb' } + + it 'returns the matching ee/ test file' do + expect(subject.test_files).to contain_exactly('ee/spec/models/analytics/cycle_analytics/group_level_spec.rb') + end + end + + context 'when given a module file in ee/' do + let(:file) { 'ee/app/models/ee/user.rb' } + + it 'returns the matching ee/ module test file and the ee/ model test file' do + test_files = ['ee/spec/models/ee/user_spec.rb', 'spec/app/models/user_spec.rb'] + expect(subject.test_files).to contain_exactly(*test_files) + end + end + + context 'when given a lib file in ee/' do + let(:file) { 'ee/lib/flipper_session.rb' } + + it 'returns the matching ee/ lib test file' do + expect(subject.test_files).to contain_exactly('ee/spec/lib/flipper_session_spec.rb') + end + end + + context 'when given a test file in ee/' do + let(:file) { 'ee/spec/models/container_registry/event_spec.rb' } + + it 'returns the test file itself' do + expect(subject.test_files).to contain_exactly('ee/spec/models/container_registry/event_spec.rb') + end + end + + context 'when given a module test file in ee/' do + let(:file) { 'ee/spec/models/ee/appearance_spec.rb' } + + it 'returns the matching module test file itself and the corresponding spec model test file' do + test_files = ['ee/spec/models/ee/appearance_spec.rb', 'spec/models/appearance_spec.rb'] + expect(subject.test_files).to contain_exactly(*test_files) + end + end + + context 'with foss_test_only: true' do + subject { Tooling::TestFileFinder.new(file, foss_test_only: true) } + + context 'when given a module file in ee/' do + let(:file) { 'ee/app/models/ee/user.rb' } + + it 'returns only the corresponding spec model test file in foss' do + expect(subject.test_files).to contain_exactly('spec/app/models/user_spec.rb') + end + end + + context 'when given an app file in ee/' do + let(:file) { 'ee/app/models/approval.rb' } + + it 'returns no test file in foss' do + expect(subject.test_files).to be_empty + end + end + end + end +end diff --git a/tooling/bin/find_foss_tests b/tooling/bin/find_foss_tests new file mode 100755 index 00000000000..c694210ad40 --- /dev/null +++ b/tooling/bin/find_foss_tests @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative '../../lib/gitlab/popen' +require_relative '../lib/tooling/test_file_finder' + +require 'gitlab' + +gitlab_token = ENV.fetch('DANGER_GITLAB_API_TOKEN', '') + +Gitlab.configure do |config| + config.endpoint = 'https://gitlab.com/api/v4' + config.private_token = gitlab_token +end + +output_file = ARGV.shift + +mr_project_path = ENV.fetch('CI_MERGE_REQUEST_PROJECT_PATH') +mr_iid = ENV.fetch('CI_MERGE_REQUEST_IID') + +mr_changes = Gitlab.merge_request_changes(mr_project_path, mr_iid) +changed_files = mr_changes.changes.map { |change| change['new_path'] } + +tests_to_run = changed_files.flat_map do |file| + test_files = Tooling::TestFileFinder.new(file, foss_test_only: true).test_files + test_files.select { |f| File.exist?(f) } +end + +File.write(output_file, tests_to_run.uniq.join(' ')) diff --git a/tooling/lib/tooling/test_file_finder.rb b/tooling/lib/tooling/test_file_finder.rb new file mode 100644 index 00000000000..d4a972f759a --- /dev/null +++ b/tooling/lib/tooling/test_file_finder.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'ostruct' +require 'set' + +module Tooling + class TestFileFinder + RUBY_EXTENSION = '.rb' + EE_PREFIX = 'ee/' + + def initialize(file, foss_test_only: false) + @file = file + @foss_test_only = foss_test_only + @result = Set.new + end + + def test_files + contexts = [ee_context, foss_context] + contexts.flat_map do |context| + match_test_files_for(context) + end + + result.to_a + end + + private + + attr_reader :file, :foss_test_only, :result + + def ee_context + OpenStruct.new.tap do |ee| + ee.app = %r{^#{EE_PREFIX}app/(.+)\.rb$} unless foss_test_only + ee.lib = %r{^#{EE_PREFIX}lib/(.+)\.rb$} unless foss_test_only + ee.spec = %r{^#{EE_PREFIX}spec/(.+)_spec.rb$} unless foss_test_only + ee.spec_dir = "#{EE_PREFIX}spec" unless foss_test_only + ee.ee_modules = %r{^#{EE_PREFIX}(?!spec)(.*\/)ee/(.+)\.rb$} + ee.ee_module_spec = %r{^#{EE_PREFIX}spec/(.*\/)ee/(.+)\.rb$} + ee.foss_spec_dir = 'spec' + end + end + + def foss_context + OpenStruct.new.tap do |foss| + foss.app = %r{^app/(.+)\.rb$} + foss.lib = %r{^lib/(.+)\.rb$} + foss.tooling = %r{^(tooling/lib/.+)\.rb$} + foss.spec = %r{^spec/(.+)_spec.rb$} + foss.spec_dir = 'spec' + end + end + + def match_test_files_for(context) + if (match = context.app&.match(file)) + result << "#{context.spec_dir}/#{match[1]}_spec.rb" + end + + if (match = context.lib&.match(file)) + result << "#{context.spec_dir}/lib/#{match[1]}_spec.rb" + end + + if (match = context.tooling&.match(file)) + result << "#{context.spec_dir}/#{match[1]}_spec.rb" + end + + if context.spec&.match(file) + result << file + end + + if (match = context.ee_modules&.match(file)) + result << "#{context.foss_spec_dir}/#{match[1]}#{match[2]}_spec.rb" + end + + if (match = context.ee_module_spec&.match(file)) + result << "#{context.foss_spec_dir}/#{match[1]}#{match[2]}.rb" + end + end + end +end |