diff options
153 files changed, 1910 insertions, 1204 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index a76e2a6c2ce..41ebc6a4c16 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -487,6 +487,12 @@ RSpec/FactoryBot/AvoidCreate: - 'ee/spec/presenters/**/*.rb' - 'ee/spec/serializers/**/*.rb' +RSpec/FactoryBot/StrategyInCallback: + Enabled: true + Include: + - 'spec/factories/**/*.rb' + - 'ee/spec/factories/**/*.rb' + Cop/IncludeSidekiqWorker: Enabled: true Exclude: diff --git a/.rubocop_todo/rspec/factory_bot/strategy_in_callback.yml b/.rubocop_todo/rspec/factory_bot/strategy_in_callback.yml new file mode 100644 index 00000000000..456eca57804 --- /dev/null +++ b/.rubocop_todo/rspec/factory_bot/strategy_in_callback.yml @@ -0,0 +1,54 @@ +--- +RSpec/FactoryBot/StrategyInCallback: + Details: grace period + Exclude: + - 'ee/spec/factories/ci/builds.rb' + - 'ee/spec/factories/ci/pipelines.rb' + - 'ee/spec/factories/dast_scanner_profiles.rb' + - 'ee/spec/factories/description_version.rb' + - 'ee/spec/factories/elastic/reindexing_tasks.rb' + - 'ee/spec/factories/epic_issues.rb' + - 'ee/spec/factories/geo/design_registry.rb' + - 'ee/spec/factories/geo_nodes.rb' + - 'ee/spec/factories/groups.rb' + - 'ee/spec/factories/merge_requests.rb' + - 'ee/spec/factories/namespaces.rb' + - 'ee/spec/factories/projects.rb' + - 'ee/spec/factories/protected_environments.rb' + - 'ee/spec/factories/users.rb' + - 'ee/spec/factories/vulnerabilities.rb' + - 'ee/spec/factories/vulnerabilities/external_issue_links.rb' + - 'ee/spec/factories/vulnerabilities/findings.rb' + - 'ee/spec/factories/vulnerabilities/issue_links.rb' + - 'ee/spec/factories/work_items.rb' + - 'spec/factories/alert_management/alerts.rb' + - 'spec/factories/ci/builds.rb' + - 'spec/factories/ci/pipelines.rb' + - 'spec/factories/ci/processable.rb' + - 'spec/factories/ci/runner_namespaces.rb' + - 'spec/factories/ci/runner_projects.rb' + - 'spec/factories/ci/runners.rb' + - 'spec/factories/clusters/clusters.rb' + - 'spec/factories/commits.rb' + - 'spec/factories/container_expiration_policies.rb' + - 'spec/factories/design_management/designs.rb' + - 'spec/factories/design_management/versions.rb' + - 'spec/factories/environments.rb' + - 'spec/factories/group_members.rb' + - 'spec/factories/groups.rb' + - 'spec/factories/integrations.rb' + - 'spec/factories/issues.rb' + - 'spec/factories/merge_requests.rb' + - 'spec/factories/ml/candidates.rb' + - 'spec/factories/namespaces.rb' + - 'spec/factories/packages/dependency_links.rb' + - 'spec/factories/packages/package_files.rb' + - 'spec/factories/packages/packages.rb' + - 'spec/factories/pool_repositories.rb' + - 'spec/factories/project_members.rb' + - 'spec/factories/projects.rb' + - 'spec/factories/releases.rb' + - 'spec/factories/resource_label_events.rb' + - 'spec/factories/terraform/state.rb' + - 'spec/factories/users.rb' + - 'spec/factories/work_items/parent_links.rb' @@ -362,7 +362,7 @@ gem 'prometheus-client-mmap', '~> 0.16', require: 'prometheus/client' gem 'warning', '~> 1.3.0' group :development do - gem 'lefthook', '~> 1.1.4', require: false + gem 'lefthook', '~> 1.2.0', require: false gem 'rubocop' gem 'solargraph', '~> 0.47.2', require: false diff --git a/Gemfile.checksum b/Gemfile.checksum index 29bacd317fa..b82b37b06bf 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -313,7 +313,7 @@ {"name":"kramdown-parser-gfm","version":"1.1.0","platform":"ruby","checksum":"fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729"}, {"name":"kubeclient","version":"4.9.3","platform":"ruby","checksum":"d5d38e719fbac44f396851aa57cd1b9f4f7dab4410ab680ccd21c9b741230046"}, {"name":"launchy","version":"2.5.0","platform":"ruby","checksum":"954243c4255920982ce682f89a42e76372dba94770bf09c23a523e204bdebef5"}, -{"name":"lefthook","version":"1.1.4","platform":"ruby","checksum":"251fbc6681a7d0f05e594b5091206998fd21060285f9752ac40b92441d5eb93c"}, +{"name":"lefthook","version":"1.2.0","platform":"ruby","checksum":"189e8c2c91eac4ed115ab67e4d9a3f6b7f280967c45c4ea5fdca7612088c73ab"}, {"name":"letter_opener","version":"1.7.0","platform":"ruby","checksum":"095bc0d58e006e5b43ea7d219e64ecf2de8d1f7d9dafc432040a845cf59b4725"}, {"name":"letter_opener_web","version":"2.0.0","platform":"ruby","checksum":"33860ad41e1785d75456500e8ca8bba8ed71ee6eaf08a98d06bbab67c5577b6f"}, {"name":"libyajl2","version":"1.2.0","platform":"ruby","checksum":"1117cd1e48db013b626e36269bbf1cef210538ca6d2e62d3fa3db9ded005b258"}, @@ -384,7 +384,7 @@ {"name":"octokit","version":"4.25.1","platform":"ruby","checksum":"c02092ee82dcdfe84db0e0ea630a70d32becc54245a4f0bacfd21c010df09b96"}, {"name":"ohai","version":"16.10.6","platform":"ruby","checksum":"b835806e585faea4ac8346b68c722fb5fc29a29f73fd7e3a022f9073132dec22"}, {"name":"oj","version":"3.13.23","platform":"ruby","checksum":"206dfdc4020ad9974705037f269cfba211d61b7662a58c717cce771829ccef51"}, -{"name":"oj-introspect","version":"0.7.0","platform":"ruby","checksum":"dacd2504fedf67ed26733efa753e3b4da1888ad2a9752e81948acb8a196b8420"}, +{"name":"oj-introspect","version":"0.7.1","platform":"ruby","checksum":"ea584e78495a62d5356aece7242bb55455d623ad3deb3cd778e623dd19e050c0"}, {"name":"omniauth","version":"2.1.0","platform":"ruby","checksum":"bff7234f5ec9323622b217c7f26d52f850de0b0e2b8c807c3358fc79fe572300"}, {"name":"omniauth-alicloud","version":"2.0.0","platform":"ruby","checksum":"8ecf369d51cd5317c1e7c6b80276891f76cff210a534ec654326af5c62265de3"}, {"name":"omniauth-atlassian-oauth2","version":"0.2.0","platform":"ruby","checksum":"eb07574a188ab8a03376ce288bce86bc2dd4a1382ffa5781cb5e2b7bc15d76c9"}, diff --git a/Gemfile.lock b/Gemfile.lock index f1585eb75ff..e2ec1d23a57 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -835,7 +835,7 @@ GEM rest-client (~> 2.0) launchy (2.5.0) addressable (~> 2.7) - lefthook (1.1.4) + lefthook (1.2.0) letter_opener (1.7.0) launchy (~> 2.2) letter_opener_web (2.0.0) @@ -970,7 +970,7 @@ GEM train-core wmi-lite (~> 1.0) oj (3.13.23) - oj-introspect (0.7.0) + oj-introspect (0.7.1) oj (>= 3.13.23) omniauth (2.1.0) hashie (>= 3.4.6) @@ -1715,7 +1715,7 @@ DEPENDENCIES knapsack (~> 1.21.1) kramdown (~> 2.3.1) kubeclient (~> 4.9.3) - lefthook (~> 1.1.4) + lefthook (~> 1.2.0) letter_opener_web (~> 2.0.0) license_finder (~> 7.0) licensee (~> 9.15) diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js index 22ac37656ea..a785790169d 100644 --- a/app/assets/javascripts/issues/index.js +++ b/app/assets/javascripts/issues/index.js @@ -18,9 +18,9 @@ import { } from '~/issues/show'; import { parseIssuableData } from '~/issues/show/utils/parse_data'; import LabelsSelect from '~/labels/labels_select'; -import MilestoneSelect from '~/milestones/milestone_select'; import initNotesApp from '~/notes'; import { store } from '~/notes/stores'; +import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar'; import ZenMode from '~/zen_mode'; import initAwardsApp from '~/emoji/awards_app'; import initLinkedResources from '~/linked_resources'; @@ -41,11 +41,11 @@ export function initForm() { new IssuableForm($('.issue-form')); // eslint-disable-line no-new new IssuableTemplateSelectors({ warnTemplateOverride: true }); // eslint-disable-line no-new new LabelsSelect(); // eslint-disable-line no-new - new MilestoneSelect(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new initTitleSuggestions(); initTypePopover(); + mountMilestoneDropdown(); } export function initShow() { diff --git a/app/assets/javascripts/milestones/milestone_select.js b/app/assets/javascripts/milestones/milestone_select.js deleted file mode 100644 index d4876c3dbe8..00000000000 --- a/app/assets/javascripts/milestones/milestone_select.js +++ /dev/null @@ -1,273 +0,0 @@ -/* eslint-disable one-var, no-self-compare, consistent-return, no-param-reassign, no-shadow */ -/* global Issuable */ - -import $ from 'jquery'; -import { template, escape } from 'lodash'; -import Api from '~/api'; -import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; -import { __, sprintf } from '~/locale'; -import { sortMilestonesByDueDate } from '~/milestones/utils'; -import axios from '~/lib/utils/axios_utils'; -import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility'; - -export default class MilestoneSelect { - constructor(currentProject, els, options = {}) { - if (currentProject !== null) { - this.currentProject = - typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; - } - - MilestoneSelect.init(els, options); - } - - static init(els, options) { - let $els = $(els); - - if (!els) { - $els = $('.js-milestone-select'); - } - - $els.each((i, dropdown) => { - let milestoneLinkNoneTemplate, - milestoneLinkTemplate, - milestoneExpiredLinkTemplate, - selectedMilestone, - selectedMilestoneDefault; - const $dropdown = $(dropdown); - const issueUpdateURL = $dropdown.data('issueUpdate'); - const showNo = $dropdown.data('showNo'); - const showAny = $dropdown.data('showAny'); - const showMenuAbove = $dropdown.data('showMenuAbove'); - const showUpcoming = $dropdown.data('showUpcoming'); - const showStarted = $dropdown.data('showStarted'); - const useId = $dropdown.data('useId'); - const defaultLabel = $dropdown.data('defaultLabel'); - const defaultNo = $dropdown.data('defaultNo'); - const abilityName = $dropdown.data('abilityName'); - const $selectBox = $dropdown.closest('.selectbox'); - const $block = $selectBox.closest('.block'); - const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); - const $value = $block.find('.value'); - const $loading = $block.find('.block-loading').addClass('gl-display-none'); - selectedMilestoneDefault = showAny ? '' : null; - selectedMilestoneDefault = - showNo && defaultNo ? __('No milestone') : selectedMilestoneDefault; - selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault; - - if (issueUpdateURL) { - milestoneLinkTemplate = template( - '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', - ); - milestoneExpiredLinkTemplate = template( - '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %> (Past due)</a>', - ); - milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`; - } - return initDeprecatedJQueryDropdown($dropdown, { - showMenuAbove, - data: (term, callback) => { - let contextId = parseInt($dropdown.get(0).dataset.projectId, 10); - let getMilestones = Api.projectMilestones.bind(Api); - const reqParams = { state: 'active', include_parent_milestones: true }; - - if (term) { - reqParams.search = term.trim(); - } - - if (!contextId) { - contextId = $dropdown.get(0).dataset.groupId; - delete reqParams.include_parent_milestones; - getMilestones = Api.groupMilestones.bind(Api); - } - - // We don't use $.data() as it caches initial value and never updates! - return getMilestones(contextId, reqParams) - .then(({ data }) => - data - .map((m) => ({ - ...m, - // Public API includes `title` instead of `name`. - name: m.title, - })) - .sort(sortMilestonesByDueDate), - ) - .then((data) => { - const extraOptions = []; - if (showAny) { - extraOptions.push({ - id: null, - name: null, - title: __('Any milestone'), - }); - } - if (showNo && term.trim() === '') { - extraOptions.push({ - id: -1, - name: __('No milestone'), - title: __('No milestone'), - }); - } - if (showUpcoming) { - extraOptions.push({ - id: -2, - name: '#upcoming', - title: __('Upcoming'), - }); - } - if (showStarted) { - extraOptions.push({ - id: -3, - name: '#started', - title: __('Started'), - }); - } - if (extraOptions.length && data.length) { - extraOptions.push({ type: 'divider' }); - } - - callback(extraOptions.concat(data)); - if (showMenuAbove) { - $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove(); - } - $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); - }); - }, - renderRow: (milestone) => { - const milestoneName = milestone.title || milestone.name; - let milestoneDisplayName = escape(milestoneName); - - if (milestone.expired) { - milestoneDisplayName = sprintf(__('%{milestone} (expired)'), { - milestone: milestoneDisplayName, - }); - } - - return ` - <li data-milestone-id="${escape(milestoneName)}"> - <a href='#' class='dropdown-menu-milestone-link'> - ${milestoneDisplayName} - </a> - </li> - `; - }, - filterable: true, - filterRemote: true, - search: { - fields: ['title'], - }, - selectable: true, - toggleLabel: (selected, el) => { - if (selected && 'id' in selected && $(el).hasClass('is-active')) { - return selected.title; - } - return defaultLabel; - }, - defaultLabel, - fieldName: $dropdown.data('fieldName'), - text: (milestone) => escape(milestone.title), - id: (milestone) => { - if (milestone !== undefined) { - if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { - return milestone.name; - } - - return milestone.id; - } - }, - hidden: () => { - $selectBox.hide(); - // display:block overrides the hide-collapse rule - return $value.css('display', ''); - }, - opened: (e) => { - const $el = $(e.currentTarget); - if (options.handleClick) { - selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; - } - $('a.is-active', $el).removeClass('is-active'); - $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); - }, - vue: false, - clicked: (clickEvent) => { - const { e } = clickEvent; - let selected = clickEvent.selectedObj; - - if (!selected) return; - - if (options.handleClick) { - e.preventDefault(); - options.handleClick(selected); - return; - } - - const page = $('body').attr('data-page'); - const isIssueIndex = page === 'projects:issues:index'; - const isMRIndex = page === page && page === 'projects:merge_requests:index'; - const isSelecting = selected.name !== selectedMilestone; - selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault; - - if ( - $dropdown.hasClass('js-filter-bulk-update') || - $dropdown.hasClass('js-issuable-form-dropdown') - ) { - e.preventDefault(); - return; - } - - if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { - return Issuable.filterResults($dropdown.closest('form')); - } else if ($dropdown.hasClass('js-filter-submit')) { - return $dropdown.closest('form').submit(); - } - - selected = $selectBox.find('input[type="hidden"]').val(); - - const data = {}; - data[abilityName] = {}; - data[abilityName].milestone_id = selected != null ? selected : null; - $loading.removeClass('gl-display-none'); - $dropdown.trigger('loading.gl.dropdown'); - return axios - .put(issueUpdateURL, data) - .then(({ data }) => { - $dropdown.trigger('loaded.gl.dropdown'); - $loading.addClass('gl-display-none'); - $selectBox.hide(); - $value.css('display', ''); - if (data.milestone != null) { - data.milestone.remaining = timeFor(data.milestone.due_date); - data.milestone.name = data.milestone.title; - $value.html( - data.milestone.expired - ? milestoneExpiredLinkTemplate({ - ...data.milestone, - remaining: sprintf(__('%{due_date} (Past due)'), { - due_date: dateInWords(parsePikadayDate(data.milestone.due_date)), - }), - }) - : milestoneLinkTemplate(data.milestone), - ); - return $sidebarCollapsedValue - .attr( - 'data-original-title', - `${data.milestone.name}<br />${data.milestone.remaining}`, - ) - .find('span') - .text(data.milestone.title); - } - $value.html(milestoneLinkNoneTemplate); - return $sidebarCollapsedValue - .attr('data-original-title', __('Milestone')) - .find('span') - .text(__('None')); - }) - .catch(() => { - $loading.addClass('gl-display-none'); - }); - }, - }); - }); - } -} - -window.MilestoneSelect = MilestoneSelect; diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js index ebf7c266482..42fa306d226 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js @@ -6,8 +6,8 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import Diff from '~/diff'; import GLForm from '~/gl_form'; import LabelsSelect from '~/labels/labels_select'; -import MilestoneSelect from '~/milestones/milestone_select'; import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors'; +import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar'; export default () => { new Diff(); @@ -15,8 +15,8 @@ export default () => { new GLForm($('.merge-request-form')); new IssuableForm($('.merge-request-form')); new LabelsSelect(); - new MilestoneSelect(); new IssuableTemplateSelectors({ warnTemplateOverride: true, }); + mountMilestoneDropdown('[name="merge_request[milestone_id]"]'); }; diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js index 2580cbcb944..139da5dabbd 100644 --- a/app/assets/javascripts/persistent_user_callouts.js +++ b/app/assets/javascripts/persistent_user_callouts.js @@ -23,9 +23,9 @@ const PERSISTENT_USER_CALLOUTS = [ ]; const initCallouts = () => { - PERSISTENT_USER_CALLOUTS.forEach((calloutContainer) => - PersistentUserCallout.factory(document.querySelector(calloutContainer)), - ); + document + .querySelectorAll(PERSISTENT_USER_CALLOUTS) + .forEach((calloutContainer) => PersistentUserCallout.factory(calloutContainer)); }; export default initCallouts; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue index 39d41415456..fe2ef2c2d71 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -115,6 +115,9 @@ export default { commitTitle() { return this.pipeline?.commit?.title; }, + pipelineName() { + return this.pipeline?.name; + }, }, methods: { trackClick(action) { @@ -125,7 +128,18 @@ export default { </script> <template> <div class="pipeline-tags" data-testid="pipeline-url-table-cell"> - <div class="commit-title gl-mb-2" data-testid="commit-title-container"> + <div v-if="pipelineName" class="gl-mb-2" data-testid="pipeline-name-container"> + <span class="gl-display-flex"> + <tooltip-on-truncate + :title="pipelineName" + class="gl-flex-grow-1 gl-text-truncate gl-text-gray-900" + > + {{ pipelineName }} + </tooltip-on-truncate> + </span> + </div> + + <div v-if="!pipelineName" class="commit-title gl-mb-2" data-testid="commit-title-container"> <span v-if="commitTitle" class="gl-display-flex"> <tooltip-on-truncate :title="commitTitle" class="gl-flex-grow-1 gl-text-truncate"> <gl-link diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue index 387438fb726..cd44c998074 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue @@ -1,12 +1,14 @@ <script> import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; -import { formatDate, getTimeago, durationTimeFormatted } from '~/lib/utils/datetime_utility'; +import { durationTimeFormatted } from '~/lib/utils/datetime_utility'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; export default { directives: { GlTooltip: GlTooltipDirective, }, components: { GlIcon }, + mixins: [timeagoMixin], props: { pipeline: { type: Object, @@ -35,12 +37,6 @@ export default { showSkipped() { return !this.duration && !this.finishedTime && this.skipped; }, - timeFormatted() { - return getTimeago().format(this.finishedTime); - }, - tooltipTitle() { - return formatDate(this.finishedTime); - }, }, }; </script> @@ -73,12 +69,12 @@ export default { <time v-gl-tooltip - :title="tooltipTitle" + :title="tooltipTitle(finishedTime)" :datetime="finishedTime" data-placement="top" data-container="body" > - {{ timeFormatted }} + {{ timeFormatted(finishedTime) }} </time> </p> </div> diff --git a/app/assets/javascripts/sidebar/components/milestone/bulk_update_milestone_dropdown.vue b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue index 075ae1de6e5..1fff089eab4 100644 --- a/app/assets/javascripts/sidebar/components/milestone/bulk_update_milestone_dropdown.vue +++ b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue @@ -1,5 +1,7 @@ <script> -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { GlDropdownItem } from '@gitlab/ui'; +import { TYPE_MILESTONE } from '~/graphql_shared/constants'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { IssuableType, WorkspaceType } from '~/issues/constants'; import { __ } from '~/locale'; import { IssuableAttributeType } from '../../constants'; @@ -18,6 +20,7 @@ const placeholderMilestone = { export default { issuableAttribute: IssuableAttributeType.Milestone, components: { + GlDropdownItem, SidebarDropdown, }, props: { @@ -25,6 +28,11 @@ export default { required: true, type: String, }, + canAdminMilestone: { + type: Boolean, + required: false, + default: false, + }, issuableType: { type: String, required: true, @@ -32,6 +40,26 @@ export default { return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); }, }, + inputName: { + type: String, + required: false, + default: 'update[milestone_id]', + }, + milestoneId: { + type: String, + required: false, + default: '', + }, + milestoneTitle: { + type: String, + required: false, + default: '', + }, + projectMilestonesPath: { + type: String, + required: false, + default: '', + }, workspaceType: { type: String, required: true, @@ -42,10 +70,15 @@ export default { }, data() { return { - milestone: placeholderMilestone, + milestone: this.milestoneId + ? { id: convertToGraphQLId(TYPE_MILESTONE, this.milestoneId), title: this.milestoneTitle } + : placeholderMilestone, }; }, computed: { + footerItemText() { + return this.canAdminMilestone ? __('Manage milestones') : __('View milestones'); + }, value() { return this.milestone.id === placeholderMilestone.id ? undefined @@ -62,14 +95,21 @@ export default { <template> <div> - <input type="hidden" name="update[milestone_id]" :value="value" /> + <input type="hidden" :name="inputName" :value="value" /> <sidebar-dropdown :attr-workspace-path="attrWorkspacePath" :current-attribute="milestone" :issuable-attribute="$options.issuableAttribute" :issuable-type="issuableType" :workspace-type="workspaceType" + data-qa-selector="issuable_milestone_dropdown" @change="handleChange" - /> + > + <template #footer> + <gl-dropdown-item v-if="projectMilestonesPath" :href="projectMilestonesPath"> + {{ footerItemText }} + </gl-dropdown-item> + </template> + </sidebar-dropdown> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue index 77bda5f9473..26e2bc96f54 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue @@ -201,6 +201,7 @@ export default { :header-text="i18n.assignAttribute" lazy :text="dropdownText" + toggle-class="gl-m-0" @show="handleShow" @shown="setFocus" > @@ -244,5 +245,8 @@ export default { </gl-dropdown-item> </slot> </template> + <template #footer> + <slot name="footer"></slot> + </template> </gl-dropdown> </template> diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 5f36486a22e..d078511151b 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -18,7 +18,7 @@ import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assi import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; -import BulkUpdateMilestoneDropdown from '~/sidebar/components/milestone/bulk_update_milestone_dropdown.vue'; +import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue'; import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue'; import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue'; import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue'; @@ -299,16 +299,31 @@ export function mountMilestoneDropdown() { Vue.use(VueApollo); + const { + canAdminMilestone, + fullPath, + inputName, + milestoneId, + milestoneTitle, + projectMilestonesPath, + workspaceType, + } = el.dataset; + return new Vue({ el, - name: 'BulkUpdateMilestoneDropdownRoot', + name: 'MilestoneDropdownRoot', apolloProvider, render(createElement) { - return createElement(BulkUpdateMilestoneDropdown, { + return createElement(MilestoneDropdown, { props: { - attrWorkspacePath: el.dataset.fullPath, + attrWorkspacePath: fullPath, + canAdminMilestone, + inputName, issuableType: isInIssuePage() ? IssuableType.Issue : IssuableType.MergeRequest, - workspaceType: el.dataset.workspaceType, + milestoneId, + milestoneTitle, + projectMilestonesPath, + workspaceType, }, }); }, diff --git a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js index c12ffaac40a..79946ebaecd 100644 --- a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js +++ b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js @@ -1,12 +1,14 @@ export default (Vue) => { Vue.mixin({ - provide: { - glFeatures: - { - ...window.gon?.features, - // TODO: extract into glLicensedFeatures https://gitlab.com/gitlab-org/gitlab/-/issues/322460 - ...window.gon?.licensed_features, - } || {}, + provide() { + return { + glFeatures: + { + ...window.gon?.features, + // TODO: extract into glLicensedFeatures https://gitlab.com/gitlab-org/gitlab/-/issues/322460 + ...window.gon?.licensed_features, + } || {}, + }; }, }); }; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 504fae466bb..99284ea0a64 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -88,14 +88,6 @@ $white-normal: #f0f0f0 !default; $white-dark: #eaeaea !default; $white-transparent: rgba($white, 0.8) !default; -$gray-lightest: #fdfdfd !default; -$gray-light: #fafafa !default; -$gray-lighter: #f9f9f9 !default; -$gray-normal: #f5f5f5 !default; -$gray-dark: darken($gray-light, $darken-dark-factor) !default; -$gray-darker: #eee !default; -$gray-darkest: #c4c4c4 !default; - $purple: #6d49cb !default; $purple-light: #ede8fb !default; @@ -103,11 +95,6 @@ $black: #000 !default; $black-transparent: rgba(0, 0, 0, 0.3) !default; $almost-black: #242424 !default; -$t-gray-a-02: rgba($black, 0.02) !default; -$t-gray-a-04: rgba($black, 0.04) !default; -$t-gray-a-06: rgba($black, 0.06) !default; -$t-gray-a-08: rgba($black, 0.08) !default; - $green-50: #ecf4ee !default; $green-100: #c3e6cd !default; $green-200: #91d4a8 !default; @@ -168,18 +155,33 @@ $purple-800: #453894 !default; $purple-900: #2f2a6b !default; $purple-950: #232150 !default; -$gray-10: #f5f5f5 !default; -$gray-50: #f0f0f0 !default; -$gray-100: #dbdbdb !default; -$gray-200: #bfbfbf !default; -$gray-300: #999 !default; -$gray-400: #868686 !default; -$gray-500: #666 !default; -$gray-600: #5e5e5e !default; -$gray-700: #525252 !default; -$gray-800: #404040 !default; -$gray-900: #303030 !default; -$gray-950: #1f1f1f !default; +$gray-10: #fbfafd !default; +$gray-50: #ececef !default; +$gray-100: #dcdcde !default; +$gray-200: #bfbfc3 !default; +$gray-300: #a4a3a8 !default; +$gray-400: #89888d !default; +$gray-500: #737278 !default; +$gray-600: #626168 !default; +$gray-700: #535158 !default; +$gray-800: #434248 !default; +$gray-900: #333238 !default; +$gray-950: #1f1e24 !default; + +$gray-lightest: lighten($gray-10, 1) !default; +$gray-light: $gray-10 !default; +$gray-lighter: lighten($gray-50, 4) !default; +$gray-normal: lighten($gray-50, 2) !default; +$gray-dark: darken($gray-light, $darken-dark-factor) !default; +$gray-darker: $gray-50 !default; +$gray-darkest: $gray-200 !default; + +$t-gray-a-02: rgba($gray-950, 0.02) !default; +$t-gray-a-04: rgba($gray-950, 0.04) !default; +$t-gray-a-06: rgba($gray-950, 0.06) !default; +$t-gray-a-08: rgba($gray-950, 0.08) !default; +$t-gray-a-16: rgba($gray-950, 0.16) !default; +$t-gray-a-24: rgba($gray-950, 0.24) !default; $greens: ( '50': $green-50, @@ -370,7 +372,7 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor); /* * UI elements */ -$contextual-sidebar-bg-color: #f5f5f5; +$contextual-sidebar-bg-color: $gray-10; $contextual-sidebar-border-color: #e9e9e9; $border-color: $gray-100; $shadow-color: $t-gray-a-08; diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 3a81eec34e3..7b8f9e117fe 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -6,15 +6,15 @@ color-scheme: dark; } body.gl-dark { - --gray-10: #1f1f1f; - --gray-50: #303030; - --gray-100: #404040; - --gray-200: #525252; - --gray-700: #dbdbdb; - --gray-900: #fafafa; + --gray-10: #1f1e24; + --gray-50: #333238; + --gray-100: #434248; + --gray-200: #535158; + --gray-700: #bfbfc3; + --gray-900: #ececef; --green-100: #0d532a; --green-700: #91d4a8; - --gl-text-color: #fafafa; + --gl-text-color: #ececef; --border-color: #4f4f4f; --black: #fff; } @@ -42,9 +42,9 @@ body { font-size: 1rem; font-weight: 400; line-height: 1.5; - color: #fafafa; + color: #ececef; text-align: left; - background-color: #1f1f1f; + background-color: #1f1e24; } ul { margin-top: 0; @@ -118,7 +118,7 @@ kbd { padding: 0.2rem 0.4rem; font-size: 90%; color: #333; - background-color: #fafafa; + background-color: #ececef; border-radius: 0.2rem; } kbd kbd { @@ -141,24 +141,24 @@ kbd kbd { font-size: 0.875rem; font-weight: 400; line-height: 1.5; - color: #fafafa; + color: #ececef; background-color: #333; background-clip: padding-box; - border: 1px solid #868686; + border: 1px solid #737278; border-radius: 0.25rem; } @media (prefers-reduced-motion: reduce) { } .form-control:-moz-focusring { color: transparent; - text-shadow: 0 0 0 #fafafa; + text-shadow: 0 0 0 #ececef; } .form-control::placeholder { - color: #bfbfbf; + color: #a4a3a8; opacity: 1; } .form-control:disabled { - background-color: #303030; + background-color: #333238; opacity: 1; } .form-inline { @@ -176,7 +176,7 @@ kbd kbd { .btn { display: inline-block; font-weight: 400; - color: #fafafa; + color: #ececef; text-align: center; vertical-align: middle; user-select: none; @@ -212,7 +212,7 @@ kbd kbd { padding: 0.5rem 0; margin: 0.125rem 0 0; font-size: 1rem; - color: #fafafa; + color: #ececef; text-align: left; list-style: none; background-color: #333; @@ -319,15 +319,15 @@ kbd kbd { border-radius: 10rem; } .badge-success { - color: #fff; + color: #fbfafd; background-color: #2da160; } .badge-info { - color: #fff; + color: #fbfafd; background-color: #428fdc; } .badge-warning { - color: #fff; + color: #fbfafd; background-color: #c17d10; } .rounded-circle { @@ -455,8 +455,8 @@ a.gl-badge.badge-warning:active { padding-left: 0.75rem; padding-right: 0.75rem; height: auto; - color: #fafafa; - box-shadow: inset 0 0 0 1px #868686; + color: #ececef; + box-shadow: inset 0 0 0 1px #737278; border-style: none; appearance: none; -moz-appearance: none; @@ -465,17 +465,17 @@ a.gl-badge.badge-warning:active { .gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only, .gl-form-input.form-control:disabled, .gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only { - background-color: #1f1f1f; - box-shadow: inset 0 0 0 1px #404040; + background-color: #1f1e24; + box-shadow: inset 0 0 0 1px #434248; } .gl-form-input:disabled, .gl-form-input.form-control:disabled { cursor: not-allowed; - color: #999; + color: #89888d; } .gl-form-input::placeholder, .gl-form-input.form-control::placeholder { - color: #868686; + color: #737278; } .gl-icon { fill: currentColor; @@ -518,9 +518,9 @@ a.gl-badge.badge-warning:active { padding-right: 0.75rem; background-color: transparent; line-height: 1rem; - color: #fafafa; + color: #ececef; fill: currentColor; - box-shadow: inset 0 0 0 1px #525252; + box-shadow: inset 0 0 0 1px #535158; justify-content: center; align-items: center; font-size: 0.875rem; @@ -531,20 +531,20 @@ a.gl-badge.badge-warning:active { } .gl-button.gl-button.btn-default:active, .gl-button.gl-button.btn-default.active { - box-shadow: inset 0 0 0 1px #bfbfbf, 0 0 0 1px #333, 0 0 0 3px #1f75cb; + box-shadow: inset 0 0 0 1px #a4a3a8, 0 0 0 1px #333, 0 0 0 3px #1f75cb; outline: none; - background-color: #404040; + background-color: #434248; } .gl-button.gl-button.btn-default:active .gl-icon, .gl-button.gl-button.btn-default.active .gl-icon { - color: #fafafa; + color: #ececef; } .gl-button.gl-button.btn-default .gl-icon { - color: #999; + color: #89888d; } .gl-search-box-by-type-search-icon { margin: 0.5rem; - color: #999; + color: #89888d; width: 1rem; position: absolute; } @@ -594,11 +594,11 @@ svg { height: 0; margin: 4px 0; overflow: hidden; - border-top: 1px solid #404040; + border-top: 1px solid #434248; } .toggle-sidebar-button .collapse-text, .toggle-sidebar-button .icon-chevron-double-lg-left { - color: #999; + color: #89888d; } html { overflow-y: scroll; @@ -614,20 +614,20 @@ html { font-weight: 400; padding: 6px 10px; background-color: #333; - border-color: #404040; - color: #fafafa; - color: #fafafa; + border-color: #434248; + color: #ececef; + color: #ececef; white-space: nowrap; } .btn:active { - background-color: #303030; + background-color: #333238; box-shadow: none; } .btn:active, .btn.active { background-color: #444; border-color: #4f4f4f; - color: #fafafa; + color: #ececef; } .btn svg { height: 15px; @@ -639,7 +639,7 @@ html { .badge.badge-pill:not(.gl-badge) { font-weight: 400; background-color: rgba(255, 255, 255, 0.07); - color: #dbdbdb; + color: #bfbfc3; vertical-align: baseline; } .gl-font-sm { @@ -658,10 +658,10 @@ html { .dropdown-menu-toggle { padding: 6px 8px 6px 10px; background-color: #333; - color: #fafafa; + color: #ececef; font-size: 14px; text-align: left; - border: 1px solid #404040; + border: 1px solid #434248; border-radius: 0.25rem; white-space: nowrap; } @@ -690,7 +690,7 @@ html { font-weight: 400; padding: 8px 0; background-color: #333; - border: 1px solid #404040; + border: 1px solid #434248; border-radius: 0.25rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } @@ -713,7 +713,7 @@ html { font-weight: 400; position: relative; padding: 8px 12px; - color: #fafafa; + color: #ececef; line-height: 16px; white-space: normal; overflow: hidden; @@ -723,7 +723,7 @@ html { .dropdown-menu li > a:active, .dropdown-menu li button:active { background-color: #4f4f4f; - color: #fafafa; + color: #ececef; outline: 0; text-decoration: none; } @@ -737,7 +737,7 @@ html { height: 1px; margin: 0.25rem 0; padding: 0; - background-color: #404040; + background-color: #434248; } .dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) { margin-right: 40px; @@ -764,7 +764,7 @@ html { } input { border-radius: 0.25rem; - color: #fafafa; + color: #ececef; background-color: #333; } .form-control { @@ -772,23 +772,23 @@ input { padding: 6px 10px; } .form-control::placeholder { - color: #868686; + color: #737278; } kbd { display: inline-block; padding: 3px 5px; font-size: 0.6875rem; line-height: 10px; - color: var(--gray-700, #dbdbdb); + color: var(--gray-700, #bfbfc3); vertical-align: middle; - background-color: var(--gray-10, #1f1f1f); + background-color: var(--gray-10, #1f1e24); border-width: 1px; border-style: solid; - border-color: var(--gray-100, #404040) var(--gray-100, #404040) - var(--gray-200, #525252); + border-color: var(--gray-100, #434248) var(--gray-100, #434248) + var(--gray-200, #535158); border-image: none; border-radius: 3px; - box-shadow: 0 -1px 0 var(--gray-200, #525252) inset; + box-shadow: 0 -1px 0 var(--gray-200, #535158) inset; } .navbar-gitlab { padding: 0 16px; @@ -1042,7 +1042,7 @@ kbd { width: 100%; align-items: center; padding: 10px 16px 10px 10px; - color: #fafafa; + color: #ececef; background-color: transparent; border: 0; text-align: left; @@ -1054,7 +1054,7 @@ kbd { .context-header .sidebar-context-title { overflow: hidden; text-overflow: ellipsis; - color: #fafafa; + color: #ececef; } @media (min-width: 768px) { .page-with-contextual-sidebar { @@ -1078,7 +1078,7 @@ kbd { z-index: 600; width: 256px; top: var(--header-height, 48px); - background-color: #f5f5f5; + background-color: #1f1e24; border-right: 1px solid #e9e9e9; transform: translate3d(0, 0, 0); } @@ -1115,7 +1115,7 @@ kbd { } .nav-sidebar a { text-decoration: none; - color: #fafafa; + color: #ececef; } .nav-sidebar li { white-space: nowrap; @@ -1400,7 +1400,7 @@ kbd { display: block; } .sidebar-top-level-items li > a.gl-link { - color: #fafafa; + color: #ececef; } .sidebar-top-level-items li > a.gl-link:active { text-decoration: none; @@ -1417,12 +1417,12 @@ kbd { .close-nav-button { height: 48px; padding: 0 16px; - background-color: #303030; + background-color: #333238; border: 0; - color: #999; + color: #89888d; display: flex; align-items: center; - background-color: #f5f5f5; + background-color: #1f1e24; position: fixed; bottom: 0; width: 255px; @@ -1493,14 +1493,14 @@ kbd { } } input::-moz-placeholder { - color: #868686; + color: #737278; opacity: 1; } input::-ms-input-placeholder { - color: #868686; + color: #737278; } input:-ms-input-placeholder { - color: #868686; + color: #737278; } svg { fill: currentColor; @@ -1629,7 +1629,7 @@ svg.s16 { padding: 0; background: #222; overflow: hidden; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); + box-shadow: inset 0 0 0 1px rgba(251, 250, 253, 0.1); } .avatar.avatar-tile { border-radius: 0; @@ -1638,8 +1638,8 @@ svg.s16 { .identicon { text-align: center; vertical-align: top; - color: #fafafa; - background-color: #303030; + color: #ececef; + background-color: #333238; } .identicon.s16 { font-size: 10px; @@ -1668,7 +1668,7 @@ svg.s16 { background-color: #5c2900; } .identicon.bg7 { - background-color: #303030; + background-color: #333238; } .avatar-container { overflow: hidden; @@ -1707,18 +1707,18 @@ svg.s16 { color-scheme: dark; } body.gl-dark { - --gray-10: #1f1f1f; - --gray-50: #303030; - --gray-100: #404040; - --gray-200: #525252; - --gray-300: #5e5e5e; - --gray-400: #868686; - --gray-500: #999; - --gray-600: #bfbfbf; - --gray-700: #dbdbdb; - --gray-800: #f0f0f0; - --gray-900: #fafafa; - --gray-950: #fff; + --gray-10: #1f1e24; + --gray-50: #333238; + --gray-100: #434248; + --gray-200: #535158; + --gray-300: #626168; + --gray-400: #737278; + --gray-500: #89888d; + --gray-600: #a4a3a8; + --gray-700: #bfbfc3; + --gray-800: #dcdcde; + --gray-900: #ececef; + --gray-950: #fbfafd; --green-50: #0a4020; --green-100: #0d532a; --green-200: #24663b; @@ -1790,7 +1790,7 @@ body.gl-dark { --dark-icon-color-purple-3: #9a79f7; --dark-icon-color-orange-1: #665349; --dark-icon-color-orange-2: #b37a5d; - --gl-text-color: #fafafa; + --gl-text-color: #ececef; --border-color: #4f4f4f; --white: #333; --black: #fff; @@ -1800,49 +1800,49 @@ body.gl-dark { .nav-sidebar, .toggle-sidebar-button, .close-nav-button { - background-color: #262626; - border-right: 1px solid #303030; + background-color: #29282d; + border-right: 1px solid #333238; } .gl-avatar:not(.gl-avatar-identicon), .avatar-container, .avatar { - background: rgba(255, 255, 255, 0.04); + background: rgba(251, 250, 253, 0.04); } .gl-avatar { border-style: none; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); + box-shadow: inset 0 0 0 1px rgba(251, 250, 253, 0.1); } body.gl-dark { - --gl-theme-accent: #868686; + --gl-theme-accent: #737278; } body.gl-dark .navbar-gitlab { - background-color: #fafafa; + background-color: #ececef; } body.gl-dark .navbar-gitlab .navbar-collapse { - color: #fafafa; + color: #ececef; } body.gl-dark .navbar-gitlab .container-fluid .navbar-toggler { - border-left: 1px solid #b3b3b3; - color: #fafafa; + border-left: 1px solid #a3a2a6; + color: #ececef; } body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > a, body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > button, body.gl-dark .navbar-gitlab .navbar-nav > li.active > a, body.gl-dark .navbar-gitlab .navbar-nav > li.active > button { - color: #fafafa; + color: #ececef; background-color: #333; } body.gl-dark .navbar-gitlab .navbar-sub-nav { - color: #fafafa; + color: #ececef; } body.gl-dark .navbar-gitlab .nav > li { - color: #fafafa; + color: #ececef; } body.gl-dark .navbar-gitlab .nav > li.header-search-new { - color: #fafafa; + color: #ececef; } body.gl-dark .navbar-gitlab .nav > li > a .notification-dot { - border: 2px solid #fafafa; + border: 2px solid #ececef; } body.gl-dark .navbar-gitlab @@ -1850,7 +1850,7 @@ body.gl-dark > li > a.header-help-dropdown-toggle .notification-dot { - background-color: #fafafa; + background-color: #ececef; } body.gl-dark .navbar-gitlab @@ -1858,10 +1858,10 @@ body.gl-dark > li > a.header-user-dropdown-toggle .header-user-avatar { - border-color: #fafafa; + border-color: #ececef; } body.gl-dark .navbar-gitlab .nav > li.active > a { - color: #fafafa; + color: #ececef; background-color: #333; } body.gl-dark .navbar-gitlab .nav > li.active > a .notification-dot { @@ -1873,48 +1873,48 @@ body.gl-dark > li.active > a.header-help-dropdown-toggle .notification-dot { - background-color: #fafafa; + background-color: #ececef; } body.gl-dark .header-search { - background-color: rgba(250, 250, 250, 0.2) !important; + background-color: rgba(236, 236, 239, 0.2) !important; border-radius: 4px; } body.gl-dark .header-search svg.gl-search-box-by-type-search-icon { - color: rgba(250, 250, 250, 0.8); + color: rgba(236, 236, 239, 0.8); } body.gl-dark .header-search input { background-color: transparent; - color: rgba(250, 250, 250, 0.8); - box-shadow: inset 0 0 0 1px rgba(250, 250, 250, 0.4); + color: rgba(236, 236, 239, 0.8); + box-shadow: inset 0 0 0 1px rgba(236, 236, 239, 0.4); } body.gl-dark .header-search input::placeholder { - color: rgba(250, 250, 250, 0.8); + color: rgba(236, 236, 239, 0.8); } body.gl-dark .header-search input:active::placeholder { - color: #868686; + color: #737278; } body.gl-dark .header-search .keyboard-shortcut-helper { - color: #fafafa; - background-color: rgba(250, 250, 250, 0.2); + color: #ececef; + background-color: rgba(236, 236, 239, 0.2); } body.gl-dark .search form { - background-color: rgba(250, 250, 250, 0.2); + background-color: rgba(236, 236, 239, 0.2); } body.gl-dark .search .search-input::placeholder { - color: rgba(250, 250, 250, 0.8); + color: rgba(236, 236, 239, 0.8); } body.gl-dark .search .search-input-wrap .search-icon, body.gl-dark .search .search-input-wrap .clear-icon { - fill: rgba(250, 250, 250, 0.8); + fill: rgba(236, 236, 239, 0.8); } body.gl-dark .nav-sidebar li.active > a { - color: #fafafa; + color: #ececef; } body.gl-dark .nav-sidebar .fly-out-top-item a, body.gl-dark .nav-sidebar .fly-out-top-item.active a, body.gl-dark .nav-sidebar .fly-out-top-item .fly-out-top-item-container { - background-color: var(--gray-100, #303030); - color: var(--gray-900, #fafafa); + background-color: var(--gray-100, #333238); + color: var(--gray-900, #ececef); } body.gl-dark .navbar-gitlab { background-color: var(--gray-50); @@ -1951,18 +1951,18 @@ body.gl-dark .navbar-gitlab .search form .search-input { color-scheme: dark; } body.gl-dark { - --gray-10: #1f1f1f; - --gray-50: #303030; - --gray-100: #404040; - --gray-200: #525252; - --gray-300: #5e5e5e; - --gray-400: #868686; - --gray-500: #999; - --gray-600: #bfbfbf; - --gray-700: #dbdbdb; - --gray-800: #f0f0f0; - --gray-900: #fafafa; - --gray-950: #fff; + --gray-10: #1f1e24; + --gray-50: #333238; + --gray-100: #434248; + --gray-200: #535158; + --gray-300: #626168; + --gray-400: #737278; + --gray-500: #89888d; + --gray-600: #a4a3a8; + --gray-700: #bfbfc3; + --gray-800: #dcdcde; + --gray-900: #ececef; + --gray-950: #fbfafd; --green-50: #0a4020; --green-100: #0d532a; --green-200: #24663b; @@ -2034,7 +2034,7 @@ body.gl-dark { --dark-icon-color-purple-3: #9a79f7; --dark-icon-color-orange-1: #665349; --dark-icon-color-orange-2: #b37a5d; - --gl-text-color: #fafafa; + --gl-text-color: #ececef; --border-color: #4f4f4f; --white: #333; --black: #fff; diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index 802262ba346..90927bd0086 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -23,7 +23,7 @@ body { font-size: 1rem; font-weight: 400; line-height: 1.5; - color: #303030; + color: #333238; text-align: left; background-color: #fff; } @@ -99,7 +99,7 @@ kbd { padding: 0.2rem 0.4rem; font-size: 90%; color: #fff; - background-color: #303030; + background-color: #333238; border-radius: 0.2rem; } kbd kbd { @@ -122,24 +122,24 @@ kbd kbd { font-size: 0.875rem; font-weight: 400; line-height: 1.5; - color: #303030; + color: #333238; background-color: #fff; background-clip: padding-box; - border: 1px solid #868686; + border: 1px solid #89888d; border-radius: 0.25rem; } @media (prefers-reduced-motion: reduce) { } .form-control:-moz-focusring { color: transparent; - text-shadow: 0 0 0 #303030; + text-shadow: 0 0 0 #333238; } .form-control::placeholder { - color: #5e5e5e; + color: #626168; opacity: 1; } .form-control:disabled { - background-color: #fafafa; + background-color: #fbfafd; opacity: 1; } .form-inline { @@ -157,7 +157,7 @@ kbd kbd { .btn { display: inline-block; font-weight: 400; - color: #303030; + color: #333238; text-align: center; vertical-align: middle; user-select: none; @@ -193,7 +193,7 @@ kbd kbd { padding: 0.5rem 0; margin: 0.125rem 0 0; font-size: 1rem; - color: #303030; + color: #333238; text-align: left; list-style: none; background-color: #fff; @@ -436,8 +436,8 @@ a.gl-badge.badge-warning:active { padding-left: 0.75rem; padding-right: 0.75rem; height: auto; - color: #303030; - box-shadow: inset 0 0 0 1px #868686; + color: #333238; + box-shadow: inset 0 0 0 1px #89888d; border-style: none; appearance: none; -moz-appearance: none; @@ -446,17 +446,17 @@ a.gl-badge.badge-warning:active { .gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only, .gl-form-input.form-control:disabled, .gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only { - background-color: #f5f5f5; - box-shadow: inset 0 0 0 1px #dbdbdb; + background-color: #fbfafd; + box-shadow: inset 0 0 0 1px #dcdcde; } .gl-form-input:disabled, .gl-form-input.form-control:disabled { cursor: not-allowed; - color: #666; + color: #737278; } .gl-form-input::placeholder, .gl-form-input.form-control::placeholder { - color: #868686; + color: #89888d; } .gl-icon { fill: currentColor; @@ -499,9 +499,9 @@ a.gl-badge.badge-warning:active { padding-right: 0.75rem; background-color: transparent; line-height: 1rem; - color: #303030; + color: #333238; fill: currentColor; - box-shadow: inset 0 0 0 1px #bfbfbf; + box-shadow: inset 0 0 0 1px #bfbfc3; justify-content: center; align-items: center; font-size: 0.875rem; @@ -512,20 +512,20 @@ a.gl-badge.badge-warning:active { } .gl-button.gl-button.btn-default:active, .gl-button.gl-button.btn-default.active { - box-shadow: inset 0 0 0 1px #5e5e5e, 0 0 0 1px #fff, 0 0 0 3px #428fdc; + box-shadow: inset 0 0 0 1px #626168, 0 0 0 1px #fff, 0 0 0 3px #428fdc; outline: none; - background-color: #dbdbdb; + background-color: #dcdcde; } .gl-button.gl-button.btn-default:active .gl-icon, .gl-button.gl-button.btn-default.active .gl-icon { - color: #303030; + color: #333238; } .gl-button.gl-button.btn-default .gl-icon { - color: #666; + color: #737278; } .gl-search-box-by-type-search-icon { margin: 0.5rem; - color: #666; + color: #737278; width: 1rem; position: absolute; } @@ -575,11 +575,11 @@ svg { height: 0; margin: 4px 0; overflow: hidden; - border-top: 1px solid #dbdbdb; + border-top: 1px solid #dcdcde; } .toggle-sidebar-button .collapse-text, .toggle-sidebar-button .icon-chevron-double-lg-left { - color: #666; + color: #737278; } html { overflow-y: scroll; @@ -595,20 +595,20 @@ html { font-weight: 400; padding: 6px 10px; background-color: #fff; - border-color: #dbdbdb; - color: #303030; - color: #303030; + border-color: #dcdcde; + color: #333238; + color: #333238; white-space: nowrap; } .btn:active { - background-color: #f0f0f0; + background-color: #ececef; box-shadow: none; } .btn:active, .btn.active { background-color: #eaeaea; border-color: #e3e3e3; - color: #303030; + color: #333238; } .btn svg { height: 15px; @@ -620,7 +620,7 @@ html { .badge.badge-pill:not(.gl-badge) { font-weight: 400; background-color: rgba(0, 0, 0, 0.07); - color: #525252; + color: #535158; vertical-align: baseline; } .gl-font-sm { @@ -639,10 +639,10 @@ html { .dropdown-menu-toggle { padding: 6px 8px 6px 10px; background-color: #fff; - color: #303030; + color: #333238; font-size: 14px; text-align: left; - border: 1px solid #dbdbdb; + border: 1px solid #dcdcde; border-radius: 0.25rem; white-space: nowrap; } @@ -671,7 +671,7 @@ html { font-weight: 400; padding: 8px 0; background-color: #fff; - border: 1px solid #dbdbdb; + border: 1px solid #dcdcde; border-radius: 0.25rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } @@ -694,7 +694,7 @@ html { font-weight: 400; position: relative; padding: 8px 12px; - color: #303030; + color: #333238; line-height: 16px; white-space: normal; overflow: hidden; @@ -703,8 +703,8 @@ html { } .dropdown-menu li > a:active, .dropdown-menu li button:active { - background-color: #eee; - color: #303030; + background-color: #ececef; + color: #333238; outline: 0; text-decoration: none; } @@ -718,7 +718,7 @@ html { height: 1px; margin: 0.25rem 0; padding: 0; - background-color: #dbdbdb; + background-color: #dcdcde; } .dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) { margin-right: 40px; @@ -745,7 +745,7 @@ html { } input { border-radius: 0.25rem; - color: #303030; + color: #333238; background-color: #fff; } .form-control { @@ -753,23 +753,23 @@ input { padding: 6px 10px; } .form-control::placeholder { - color: #868686; + color: #89888d; } kbd { display: inline-block; padding: 3px 5px; font-size: 0.6875rem; line-height: 10px; - color: var(--gray-700, #525252); + color: var(--gray-700, #535158); vertical-align: middle; - background-color: var(--gray-10, #f5f5f5); + background-color: var(--gray-10, #fbfafd); border-width: 1px; border-style: solid; - border-color: var(--gray-100, #dbdbdb) var(--gray-100, #dbdbdb) - var(--gray-200, #bfbfbf); + border-color: var(--gray-100, #dcdcde) var(--gray-100, #dcdcde) + var(--gray-200, #bfbfc3); border-image: none; border-radius: 3px; - box-shadow: 0 -1px 0 var(--gray-200, #bfbfbf) inset; + box-shadow: 0 -1px 0 var(--gray-200, #bfbfc3) inset; } .navbar-gitlab { padding: 0 16px; @@ -991,7 +991,7 @@ kbd { float: left; margin-right: 5px; border-radius: 50%; - border: 1px solid #f5f5f5; + border: 1px solid #f2f2f4; } .notification-dot { background-color: #d99530; @@ -1023,7 +1023,7 @@ kbd { width: 100%; align-items: center; padding: 10px 16px 10px 10px; - color: #303030; + color: #333238; background-color: transparent; border: 0; text-align: left; @@ -1035,7 +1035,7 @@ kbd { .context-header .sidebar-context-title { overflow: hidden; text-overflow: ellipsis; - color: #303030; + color: #333238; } @media (min-width: 768px) { .page-with-contextual-sidebar { @@ -1059,7 +1059,7 @@ kbd { z-index: 600; width: 256px; top: var(--header-height, 48px); - background-color: #f5f5f5; + background-color: #fbfafd; border-right: 1px solid #e9e9e9; transform: translate3d(0, 0, 0); } @@ -1096,7 +1096,7 @@ kbd { } .nav-sidebar a { text-decoration: none; - color: #303030; + color: #333238; } .nav-sidebar li { white-space: nowrap; @@ -1381,7 +1381,7 @@ kbd { display: block; } .sidebar-top-level-items li > a.gl-link { - color: #303030; + color: #333238; } .sidebar-top-level-items li > a.gl-link:active { text-decoration: none; @@ -1398,12 +1398,12 @@ kbd { .close-nav-button { height: 48px; padding: 0 16px; - background-color: #fafafa; + background-color: #fbfafd; border: 0; - color: #666; + color: #737278; display: flex; align-items: center; - background-color: #f5f5f5; + background-color: #fbfafd; position: fixed; bottom: 0; width: 255px; @@ -1474,14 +1474,14 @@ kbd { } } input::-moz-placeholder { - color: #868686; + color: #89888d; opacity: 1; } input::-ms-input-placeholder { - color: #868686; + color: #89888d; } input:-ms-input-placeholder { - color: #868686; + color: #89888d; } svg { fill: currentColor; @@ -1608,9 +1608,9 @@ svg.s16 { width: 40px; height: 40px; padding: 0; - background: #fdfdfd; + background: #fefefe; overflow: hidden; - box-shadow: inset 0 0 0 1px rgba(31, 31, 31, 0.1); + box-shadow: inset 0 0 0 1px rgba(31, 30, 36, 0.1); } .avatar.avatar-tile { border-radius: 0; @@ -1619,8 +1619,8 @@ svg.s16 { .identicon { text-align: center; vertical-align: top; - color: #303030; - background-color: #f0f0f0; + color: #333238; + background-color: #ececef; } .identicon.s16 { font-size: 10px; @@ -1649,7 +1649,7 @@ svg.s16 { background-color: #fdf1dd; } .identicon.bg7 { - background-color: #f0f0f0; + background-color: #ececef; } .avatar-container { overflow: hidden; diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss index 33e10b9bd62..7ae158b3930 100644 --- a/app/assets/stylesheets/startup/startup-signin.scss +++ b/app/assets/stylesheets/startup/startup-signin.scss @@ -22,7 +22,7 @@ body { font-size: 1rem; font-weight: 400; line-height: 1.5; - color: #303030; + color: #333238; text-align: left; background-color: #fff; } @@ -110,7 +110,7 @@ h3 { margin-bottom: 0.25rem; font-weight: 600; line-height: 1.2; - color: #303030; + color: #333238; } h1 { font-size: 2.1875rem; @@ -196,24 +196,24 @@ hr { font-size: 0.875rem; font-weight: 400; line-height: 1.5; - color: #303030; + color: #333238; background-color: #fff; background-clip: padding-box; - border: 1px solid #868686; + border: 1px solid #89888d; border-radius: 0.25rem; } @media (prefers-reduced-motion: reduce) { } .form-control:-moz-focusring { color: transparent; - text-shadow: 0 0 0 #303030; + text-shadow: 0 0 0 #333238; } .form-control::placeholder { - color: #5e5e5e; + color: #626168; opacity: 1; } .form-control:disabled { - background-color: #fafafa; + background-color: #fbfafd; opacity: 1; } .form-group { @@ -222,7 +222,7 @@ hr { .btn { display: inline-block; font-weight: 400; - color: #303030; + color: #333238; text-align: center; vertical-align: middle; user-select: none; @@ -282,10 +282,10 @@ input.btn-block[type="button"] { border-color: #b3d7ff; } .custom-control-input:disabled ~ .custom-control-label { - color: #5e5e5e; + color: #626168; } .custom-control-input:disabled ~ .custom-control-label::before { - background-color: #fafafa; + background-color: #fbfafd; } .custom-control-label { position: relative; @@ -302,7 +302,7 @@ input.btn-block[type="button"] { pointer-events: none; content: ""; background-color: #fff; - border: #666 solid 1px; + border: #737278 solid 1px; } .custom-control-label::after { position: absolute; @@ -400,8 +400,8 @@ input.btn-block[type="button"] { padding-left: 0.75rem; padding-right: 0.75rem; height: auto; - color: #303030; - box-shadow: inset 0 0 0 1px #868686; + color: #333238; + box-shadow: inset 0 0 0 1px #89888d; border-style: none; appearance: none; -moz-appearance: none; @@ -410,27 +410,27 @@ input.btn-block[type="button"] { .gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only, .gl-form-input.form-control:disabled, .gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only { - background-color: #f5f5f5; - box-shadow: inset 0 0 0 1px #dbdbdb; + background-color: #fbfafd; + box-shadow: inset 0 0 0 1px #dcdcde; } .gl-form-input:disabled, .gl-form-input.form-control:disabled { cursor: not-allowed; - color: #666; + color: #737278; } .gl-form-input::placeholder, .gl-form-input.form-control::placeholder { - color: #868686; + color: #89888d; } .gl-form-checkbox { font-size: 0.875rem; line-height: 1rem; - color: #303030; + color: #333238; } .gl-form-checkbox .custom-control-input:disabled, .gl-form-checkbox .custom-control-input:disabled ~ .custom-control-label { cursor: not-allowed; - color: #868686; + color: #89888d; } .gl-form-checkbox.custom-control .custom-control-input ~ .custom-control-label { cursor: pointer; @@ -447,7 +447,7 @@ input.btn-block[type="button"] { .custom-control-input ~ .custom-control-label::before { background-color: #fff; - border-color: #868686; + border-color: #89888d; } .gl-form-checkbox.custom-control .custom-control-input:checked @@ -490,8 +490,8 @@ input.btn-block[type="button"] { .gl-form-checkbox.custom-control .custom-control-input:disabled ~ .custom-control-label::before { - background-color: #f0f0f0; - border-color: #dbdbdb; + background-color: #ececef; + border-color: #dcdcde; pointer-events: auto; } .gl-form-checkbox.custom-control @@ -500,8 +500,8 @@ input.btn-block[type="button"] { .gl-form-checkbox.custom-control .custom-control-input[type="checkbox"]:indeterminate:disabled ~ .custom-control-label::before { - background-color: #dbdbdb; - border-color: #dbdbdb; + background-color: #dcdcde; + border-color: #dcdcde; } .gl-form-checkbox.custom-control .custom-control-input:checked:disabled @@ -509,7 +509,7 @@ input.btn-block[type="button"] { .gl-form-checkbox.custom-control .custom-control-input[type="checkbox"]:indeterminate:disabled ~ .custom-control-label::after { - background-color: #5e5e5e; + background-color: #626168; } .gl-button { display: inline-flex; @@ -526,9 +526,9 @@ input.btn-block[type="button"] { padding-right: 0.75rem; background-color: transparent; line-height: 1rem; - color: #303030; + color: #333238; fill: currentColor; - box-shadow: inset 0 0 0 1px #bfbfbf; + box-shadow: inset 0 0 0 1px #bfbfc3; justify-content: center; align-items: center; font-size: 0.875rem; @@ -560,9 +560,9 @@ input.btn-block[type="button"] { .gl-button.gl-button.btn-default.active, .gl-button.gl-button.btn-block.btn-default:active, .gl-button.gl-button.btn-block.btn-default.active { - box-shadow: inset 0 0 0 1px #5e5e5e, 0 0 0 1px #fff, 0 0 0 3px #428fdc; + box-shadow: inset 0 0 0 1px #626168, 0 0 0 1px #fff, 0 0 0 3px #428fdc; outline: none; - background-color: #dbdbdb; + background-color: #dcdcde; } .gl-button.gl-button.btn-confirm, .gl-button.gl-button.btn-block.btn-confirm { @@ -636,20 +636,20 @@ body.navless { font-weight: 400; padding: 6px 10px; background-color: #fff; - border-color: #dbdbdb; - color: #303030; - color: #303030; + border-color: #dcdcde; + color: #333238; + color: #333238; white-space: nowrap; } .btn:active { - background-color: #f0f0f0; + background-color: #ececef; box-shadow: none; } .btn:active, .btn.active { background-color: #eaeaea; border-color: #e3e3e3; - color: #303030; + color: #333238; } .btn svg { height: 15px; @@ -676,7 +676,7 @@ body.navless { } hr { margin: 1.5rem 0; - border-top: 1px solid #eee; + border-top: 1px solid #ececef; } .footer-links { margin-bottom: 20px; @@ -704,7 +704,7 @@ hr { } input { border-radius: 0.25rem; - color: #303030; + color: #333238; background-color: #fff; } label { @@ -721,7 +721,7 @@ label.label-bold { padding: 6px 10px; } .form-control::placeholder { - color: #868686; + color: #89888d; } .gl-show-field-errors .form-control:not(textarea) { height: 34px; @@ -730,7 +730,7 @@ label.label-bold { justify-content: center; height: var(--header-height, 48px); background: #fff; - border-bottom: 1px solid #dbdbdb; + border-bottom: 1px solid #dcdcde; } .navbar-empty .tanuki-logo, .navbar-empty .brand-header-logo { @@ -747,14 +747,14 @@ label.label-bold { fill: #fca326; } input::-moz-placeholder { - color: #868686; + color: #89888d; opacity: 1; } input::-ms-input-placeholder { - color: #868686; + color: #89888d; } input:-ms-input-placeholder { - color: #868686; + color: #89888d; } svg { fill: currentColor; @@ -805,7 +805,7 @@ svg { } .login-page .login-box, .login-page .omniauth-container { - box-shadow: 0 0 0 1px #dbdbdb; + box-shadow: 0 0 0 1px #dcdcde; border-radius: 0.25rem; } .login-page .login-box .login-heading h3, @@ -863,7 +863,7 @@ svg { } .login-page .new-session-tabs { display: flex; - box-shadow: 0 0 0 1px #dbdbdb; + box-shadow: 0 0 0 1px #dcdcde; border-top-right-radius: 4px; border-top-left-radius: 4px; } @@ -874,7 +874,7 @@ svg { .login-page .new-session-tabs.nav-links-unboxed .nav-item { border-left: 0; border-right: 0; - border-bottom: 1px solid #dbdbdb; + border-bottom: 1px solid #dcdcde; background-color: transparent; } .login-page .new-session-tabs.custom-provider-tabs { @@ -885,7 +885,7 @@ svg { flex-basis: auto; } .login-page .new-session-tabs.custom-provider-tabs li:nth-child(n + 5) { - border-top: 1px solid #dbdbdb; + border-top: 1px solid #dcdcde; } .login-page .new-session-tabs.custom-provider-tabs a { font-size: 16px; @@ -893,7 +893,7 @@ svg { .login-page .new-session-tabs li { flex: 1; text-align: center; - border-left: 1px solid #dbdbdb; + border-left: 1px solid #dcdcde; } .login-page .new-session-tabs li:first-of-type { border-left: 0; @@ -903,7 +903,7 @@ svg { border-top-right-radius: 4px; } .login-page .new-session-tabs li:not(.active) { - background-color: #fafafa; + background-color: #fbfafd; } .login-page .new-session-tabs li a { width: 100%; diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss index 7126c99988c..a3474d2ed50 100644 --- a/app/assets/stylesheets/themes/_dark.scss +++ b/app/assets/stylesheets/themes/_dark.scss @@ -1,15 +1,15 @@ -$gray-10: #1f1f1f; -$gray-50: #303030; -$gray-100: #404040; -$gray-200: #525252; -$gray-300: #5e5e5e; -$gray-400: #868686; -$gray-500: #999; -$gray-600: #bfbfbf; -$gray-700: #dbdbdb; -$gray-800: #f0f0f0; -$gray-900: #fafafa; -$gray-950: #fff; +$gray-10: #1f1e24; +$gray-50: #333238; +$gray-100: #434248; +$gray-200: #535158; +$gray-300: #626168; +$gray-400: #737278; +$gray-500: #89888d; +$gray-600: #a4a3a8; +$gray-700: #bfbfc3; +$gray-800: #dcdcde; +$gray-900: #ececef; +$gray-950: #fbfafd; $green-50: #0a4020; $green-100: #0d532a; diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index e9e6241a6a7..fd181109a94 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -135,17 +135,6 @@ module IssuablesHelper end # rubocop: enable CodeReuse/ActiveRecord - def milestone_dropdown_label(milestone_title, default_label = _('Milestone')) - title = - case milestone_title - when Milestone::Upcoming.name then Milestone::Upcoming.title - when Milestone::Started.name then Milestone::Started.title - else milestone_title.presence - end - - h(title || default_label) - end - def issuable_meta_author_status(author) return "" unless author&.status&.customized? && status = user_status(author) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 13279b19a3a..76f92310043 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -81,7 +81,7 @@ module Ci # Since Gitlab 12.9, we started persisting the expanded environment name to # avoid repeated variables expansion in `action: stop` builds as well. def persisted_environment - return unless has_environment? + return unless has_environment_keyword? strong_memoize(:persisted_environment) do # This code path has caused N+1s in the past, since environments are only indirectly @@ -492,7 +492,7 @@ module Ci end def expanded_environment_name - return unless has_environment? + return unless has_environment_keyword? strong_memoize(:expanded_environment_name) do # We're using a persisted expanded environment name in order to avoid @@ -506,7 +506,7 @@ module Ci end def expanded_kubernetes_namespace - return unless has_environment? + return unless has_environment_keyword? namespace = options.dig(:environment, :kubernetes, :namespace) @@ -517,16 +517,16 @@ module Ci end end - def has_environment? + def has_environment_keyword? environment.present? end def starts_environment? - has_environment? && self.environment_action == 'start' + has_environment_keyword? && self.environment_action == 'start' end def stops_environment? - has_environment? && self.environment_action == 'stop' + has_environment_keyword? && self.environment_action == 'stop' end def environment_action @@ -968,7 +968,7 @@ module Ci def collect_codequality_reports!(codequality_report) each_report(Ci::JobArtifact.file_types_for_report(:codequality)) do |file_type, blob| - Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, codequality_report) + Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, codequality_report, { project: project, commit_sha: pipeline.sha }) end codequality_report @@ -1183,7 +1183,7 @@ module Ci def environment_status strong_memoize(:environment_status) do - if has_environment? && merge_request + if has_environment_keyword? && merge_request EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha) end end diff --git a/app/models/concerns/ci/track_environment_usage.rb b/app/models/concerns/ci/track_environment_usage.rb index 45d9cdeeb59..fe548c77590 100644 --- a/app/models/concerns/ci/track_environment_usage.rb +++ b/app/models/concerns/ci/track_environment_usage.rb @@ -17,7 +17,7 @@ module Ci end def verifies_environment? - has_environment? && environment_action == 'verify' + has_environment_keyword? && environment_action == 'verify' end def count_user_deployment? diff --git a/app/serializers/ci/pipeline_entity.rb b/app/serializers/ci/pipeline_entity.rb index 8edb07e4f59..143017c5159 100644 --- a/app/serializers/ci/pipeline_entity.rb +++ b/app/serializers/ci/pipeline_entity.rb @@ -10,6 +10,7 @@ class Ci::PipelineEntity < Grape::Entity expose :iid expose :user, using: UserEntity expose :active?, as: :active + expose :name, if: -> (pipeline, _) { Feature.enabled?(:pipeline_name, pipeline.project) } # Coverage isn't always necessary (e.g. when displaying project pipelines in # the UI). Instead of creating an entirely different entity we just allow the diff --git a/app/serializers/codequality_degradation_entity.rb b/app/serializers/codequality_degradation_entity.rb index 6289260465b..52945a753dc 100644 --- a/app/serializers/codequality_degradation_entity.rb +++ b/app/serializers/codequality_degradation_entity.rb @@ -13,4 +13,6 @@ class CodequalityDegradationEntity < Grape::Entity expose :line do |degradation| degradation.dig(:location, :lines, :begin) || degradation.dig(:location, :positions, :begin, :line) end + + expose :web_url end diff --git a/app/serializers/merge_requests/pipeline_entity.rb b/app/serializers/merge_requests/pipeline_entity.rb index 9c2545a2834..76e75a8ca6d 100644 --- a/app/serializers/merge_requests/pipeline_entity.rb +++ b/app/serializers/merge_requests/pipeline_entity.rb @@ -5,6 +5,7 @@ class MergeRequests::PipelineEntity < Grape::Entity expose :id expose :active?, as: :active + expose :name, if: -> (pipeline, _) { Feature.enabled?(:pipeline_name, pipeline.project) } expose :path do |pipeline| project_pipeline_path(pipeline.project, pipeline) diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index 9cfc81e8705..b738438a78f 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -40,6 +40,7 @@ class PipelineSerializer < BaseSerializer def preloaded_relations [ + :pipeline_metadata, :cancelable_statuses, :retryable_builds, :stages, diff --git a/app/services/loose_foreign_keys/process_deleted_records_service.rb b/app/services/loose_foreign_keys/process_deleted_records_service.rb index 54f54d99afb..8700276c982 100644 --- a/app/services/loose_foreign_keys/process_deleted_records_service.rb +++ b/app/services/loose_foreign_keys/process_deleted_records_service.rb @@ -9,6 +9,7 @@ module LooseForeignKeys end def execute + raised_error = false modification_tracker = ModificationTracker.new tracked_tables.cycle do |table| records = load_batch_for_table(table) @@ -35,13 +36,30 @@ module LooseForeignKeys break if modification_tracker.over_limit? end + ::Gitlab::Metrics::LooseForeignKeysSlis.record_apdex( + success: !modification_tracker.over_limit?, + db_config_name: db_config_name + ) + modification_tracker.stats + rescue StandardError + raised_error = true + raise + ensure + ::Gitlab::Metrics::LooseForeignKeysSlis.record_error_rate( + error: raised_error, + db_config_name: db_config_name + ) end private attr_reader :connection + def db_config_name + ::Gitlab::Database.db_config_name(connection) + end + def load_batch_for_table(table) fully_qualified_table_name = "#{current_schema}.#{table}" LooseForeignKeys::DeletedRecord.load_batch_for_table(fully_qualified_table_name, BATCH_SIZE) diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb index 8a7b98ab944..e332b51ae94 100644 --- a/app/services/tags/create_service.rb +++ b/app/services/tags/create_service.rb @@ -3,6 +3,8 @@ module Tags class CreateService < BaseService def execute(tag_name, target, message) + return error('Target is empty', 400) if target.blank? + valid_tag = Gitlab::GitRefValidator.validate(tag_name) return error('Tag name invalid', 400) unless valid_tag diff --git a/app/views/admin/application_settings/appearances/_form.html.haml b/app/views/admin/application_settings/appearances/_form.html.haml index a3bd8b52148..5f51e91436c 100644 --- a/app/views/admin/application_settings/appearances/_form.html.haml +++ b/app/views/admin/application_settings/appearances/_form.html.haml @@ -101,7 +101,7 @@ = parsed_with_gfm .gl-mt-3.gl-mb-3 - = f.submit _('Update appearance settings'), class: 'btn gl-button btn-confirm' + = f.submit _('Update appearance settings'), pajamas_button: true - if @appearance.persisted? || @appearance.updated_at .mt-4 - if @appearance.persisted? diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 47a761e608f..6809f147ef8 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -75,8 +75,8 @@ %div - if @user.new_record? - = f.submit _('Create user'), class: "btn gl-button btn-confirm" + = f.submit _('Create user'), pajamas_button: true = link_to _('Cancel'), admin_users_path, class: "gl-button btn btn-default btn-cancel" - else - = f.submit _('Save changes'), class: "btn gl-button btn-confirm" + = f.submit _('Save changes'), pajamas_button: true = link_to _('Cancel'), admin_user_path(@user), class: "gl-button btn btn-default btn-cancel" diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index d9fef8940eb..4da70c8bf5d 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,8 +1,6 @@ - add_page_specific_style 'page_bundles/members' - page_title _('Group members') -= render_if_exists 'shared/free_user_cap_alert', source: @group - .row.gl-mt-3 .col-lg-12 .gl-display-flex.gl-flex-wrap diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 012a31c1ecf..d535d13ca64 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -4,7 +4,6 @@ - add_page_specific_style 'page_bundles/group' = render_if_exists 'shared/qrtly_reconciliation_alert', group: @group -= render_if_exists 'shared/free_user_cap_alert', source: @group - if show_invite_banner?(@group) = content_for :group_invite_members_banner do diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index d668399b408..4bb1def1ade 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -21,6 +21,7 @@ = dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert" = yield :page_level_alert = yield :free_user_cap_alert + = yield :usage_quotas_free_user_cap_alert = yield :group_invite_members_banner - unless @hide_breadcrumbs = render "layouts/nav/breadcrumbs" diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 97c2b8bb7e3..95934f43a51 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -16,4 +16,6 @@ :plain window.uploads_path = "#{group_uploads_path(@group)}"; += dispensable_render_if_exists "shared/free_user_cap_alert", source: @group + = render template: base_layout || "layouts/application" diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 75d5e40011c..6ad6696b313 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -19,5 +19,6 @@ window.uploads_path = "#{project_uploads_path(project)}"; = dispensable_render_if_exists "shared/web_hooks/web_hook_disabled_alert" += dispensable_render_if_exists "projects/free_user_cap_alert", project: @project = render template: "layouts/application" diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 03158412d5e..43159a759f4 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -3,8 +3,6 @@ - escaped_default_branch_name = default_branch_name.shellescape - @skip_current_level_breadcrumb = true -= render_if_exists 'projects/free_user_cap_alert', project: @project - = render partial: 'flash_messages', locals: { project: @project } = render 'clusters_deprecation_alert' diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index fb7c1130f5c..cdb8a63bca9 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -25,8 +25,8 @@ .form-actions - if @milestone.new_record? - = f.submit _('Create milestone'), class: 'gl-button btn-confirm btn', data: { qa_selector: 'create_milestone_button' } + = f.submit _('Create milestone'), data: { qa_selector: 'create_milestone_button' }, pajamas_button: true = link_to _('Cancel'), project_milestones_path(@project), class: 'gl-button btn btn-default btn-cancel' - else - = f.submit _('Save changes'), class: 'gl-button btn-confirm btn' + = f.submit _('Save changes'), pajamas_button: true = link_to _('Cancel'), project_milestone_path(@project, @milestone), class: 'gl-button btn btn-default btn-cancel' diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml index a8a30d73000..e3f46d601a3 100644 --- a/app/views/projects/no_repo.html.haml +++ b/app/views/projects/no_repo.html.haml @@ -1,8 +1,6 @@ - page_title _('No repository') - @skip_current_level_breadcrumb = true -= render_if_exists 'projects/free_user_cap_alert', project: @project - %h2.gl-display-flex .gl-display-flex.gl-align-items-center.gl-justify-content-center = sprite_icon('warning-solid', size: 24, css_class: 'gl-mr-2') diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 7c4b0a5fdb9..30cc7f94311 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,9 +1,13 @@ -.commit-box - %h3.commit-title - = markdown(commit.title, pipeline: :single_line) - - if commit.description.present? - %pre.commit-description< - = preserve(markdown(commit.description, pipeline: :single_line)) +- if Feature.enabled?(:pipeline_name, @pipeline.project) && @pipeline.name + %h3 + = @pipeline.name +- else + .commit-box + %h3.commit-title + = markdown(commit.title, pipeline: :single_line) + - if commit.description.present? + %pre.commit-description< + = preserve(markdown(commit.description, pipeline: :single_line)) .info-well .well-segment.pipeline-info{ class: "gl-align-items-baseline!" } @@ -45,11 +49,16 @@ - if @pipeline.stuck? = gl_badge_tag s_('Pipelines|stuck'), { variant: :warning, size: :sm }, { class: 'js-pipeline-url-stuck has-tooltip' } - .well-segment + .well-segment{ 'data-testid': 'commit-row' } .icon-container.commit-icon = sprite_icon('commit', css_class: 'gl-top-0!') - = link_to commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha" - = clipboard_button(text: @pipeline.sha, title: _("Copy commit SHA")) + - if Feature.enabled?(:pipeline_name, @pipeline.project) && @pipeline.name + = markdown(commit.title, pipeline: :single_line) + = clipboard_button(text: @pipeline.sha, title: _("Copy commit SHA")) + = link_to commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha" + - else + = link_to commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha" + = clipboard_button(text: @pipeline.sha, title: _("Copy commit SHA")) .well-segment.related-merge-request-info .icon-container diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 34305d15eff..c7818602f52 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,7 +1,6 @@ - add_page_specific_style 'page_bundles/members' - page_title _("Members") -= render_if_exists 'projects/free_user_cap_alert', project: @project = render_if_exists 'shared/ultimate_feature_removal_banner', project: @project .row.gl-mt-3 diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index c7ac28fa194..77c44b792ab 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -7,7 +7,6 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity") -= render_if_exists 'projects/free_user_cap_alert', project: @project = render_if_exists 'shared/ultimate_feature_removal_banner', project: @project = render partial: 'flash_messages', locals: { project: @project } = render 'clusters_deprecation_alert' diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml index 9043b8e60fc..b6b24a0c26a 100644 --- a/app/views/projects/triggers/_form.html.haml +++ b/app/views/projects/triggers/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @trigger], html: { class: 'gl-show-field-errors' } do |f| += gitlab_ui_form_for [@project, @trigger], html: { class: 'gl-show-field-errors' } do |f| = form_errors(@trigger) - if @trigger.token @@ -8,4 +8,4 @@ .form-group = f.label :key, "Description", class: "label-bold" = f.text_field :description, class: 'form-control gl-form-input', required: true, title: 'Trigger description is required.', placeholder: "Trigger description" - = f.submit btn_text, class: "gl-button btn btn-confirm" + = f.submit btn_text, pajamas_button: true diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 58108ceeb76..6984709e735 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,19 +1,11 @@ +- name = local_assigns.fetch(:name, nil) - project = @target_project || @project -- extra_class = extra_class || '' -- show_menu_above = show_menu_above || false - selected = local_assigns.fetch(:selected, nil) -- selected_text = selected.try(:title) || params[:milestone_title] -- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by milestone')) -- if selected.present? || params[:milestone_title].present? - = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) -= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", dropdown_qa_selector: "issuable_milestone_dropdown_content", - placeholder: _('Search milestones'), footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), default_label: _('Milestone'), qa_selector: "issuable_milestone_dropdown", testid: "issuable-milestone-dropdown" } }) do - - if project - %ul.dropdown-footer-list - %li - = link_to project_milestones_path(project) do - - if can? current_user, :admin_milestone, project - = _('Manage milestones') - - else - = _('View milestones') +.js-milestone-dropdown{ data: { can_admin_milestone: can?(current_user, :admin_milestone, project), + full_path: project.full_path, + input_name: name, + milestone_id: selected.try(:id), + milestone_title: selected.try(:title), + project_milestones_path: project_milestones_path(project), + workspace_type: Namespaces::ProjectNamespace.sti_name.downcase } } diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index ae730629b02..9603178f7de 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -35,7 +35,7 @@ = form.label :milestone_id, _('Milestone'), class: "col-12" .col-12 .issuable-form-select-holder - = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: _('Select milestone') + = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]" .form-group.row = form.label :label_ids, _('Labels'), class: "col-12" diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 914e17da355..c991c8de489 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -814,7 +814,7 @@ end # Settings['sidekiq'] ||= Settingslogic.new({}) Settings['sidekiq']['log_format'] ||= 'default' -Settings['sidekiq']['routing_rules'] = Settings.__send__(:build_sidekiq_routing_rules, Settings['sidekiq']['routing_rules']) +Settings['sidekiq']['routing_rules'] ||= [] # # GitLab Shell diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb index 940d8eed61f..ff3ae9a2467 100644 --- a/config/initializers/zz_metrics.rb +++ b/config/initializers/zz_metrics.rb @@ -40,8 +40,9 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d if Gitlab::Runtime.puma? Gitlab::Metrics::RequestsRackMiddleware.initialize_metrics Gitlab::Metrics::GlobalSearchSlis.initialize_slis! - elsif Gitlab.ee? && Gitlab::Runtime.sidekiq? - Gitlab::Metrics::GlobalSearchIndexingSlis.initialize_slis! + elsif Gitlab::Runtime.sidekiq? + Gitlab::Metrics::GlobalSearchIndexingSlis.initialize_slis! if Gitlab.ee? + Gitlab::Metrics::LooseForeignKeysSlis.initialize_slis! end GC::Profiler.enable diff --git a/config/open_api.yml b/config/open_api.yml index d6d2549d980..03dbff99907 100644 --- a/config/open_api.yml +++ b/config/open_api.yml @@ -46,19 +46,21 @@ metadata: - name: ci_lint description: Operations related to linting a CI config file - name: group_export - description: Operations related to export groups + description: Operations related to exporting groups - name: merge_requests description: Operations related to merge requests - name: metadata description: Operations related to metadata of the GitLab instance - name: project_export - description: Operations related to export projects + description: Operations related to exporting projects - name: project_hooks description: Operations related to project hooks + - name: project_import + description: Operations related to importing projects - name: project_import_bitbucket - description: Operations related to import BitBucket projects + description: Operations related to importing BitBucket projects - name: project_import_github - description: Operations related to import GitHub projects + description: Operations related to importing GitHub projects - name: protected environments description: Operations related to protected environments - name: release_links diff --git a/config/settings.rb b/config/settings.rb index b242e970cc6..51d54817646 100644 --- a/config/settings.rb +++ b/config/settings.rb @@ -204,11 +204,5 @@ class Settings < Settingslogic "#{minute} #{hour} * * #{day_of_week}" end - - # Route all jobs to 'default' queue. This setting is meant for self-managed instances use to keep things simple. - # See https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1491 - def build_sidekiq_routing_rules(rules) - rules.nil? || rules&.empty? ? [['*', 'default']] : rules - end end end diff --git a/db/migrate/20221102202130_extend_x509_subject_limit.rb b/db/migrate/20221102202130_extend_x509_subject_limit.rb new file mode 100644 index 00000000000..3e6bfc7691c --- /dev/null +++ b/db/migrate/20221102202130_extend_x509_subject_limit.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ExtendX509SubjectLimit < Gitlab::Database::Migration[2.0] + def up + change_column :x509_certificates, :subject, :string, limit: 512 + end + + def down + change_column :x509_certificates, :subject, :string, limit: 255 + end +end diff --git a/db/schema_migrations/20221102202130 b/db/schema_migrations/20221102202130 new file mode 100644 index 00000000000..82ee1088544 --- /dev/null +++ b/db/schema_migrations/20221102202130 @@ -0,0 +1 @@ +76c2fe9422491d0bd457584580b383924b895574cec7e90cdfa5de9ed56a3639
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 95b6fa6776c..df56391c710 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -23291,7 +23291,7 @@ CREATE TABLE x509_certificates ( created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, subject_key_identifier character varying(255) NOT NULL, - subject character varying(255) NOT NULL, + subject character varying(512) NOT NULL, email character varying(255) NOT NULL, serial_number bytea NOT NULL, certificate_status smallint DEFAULT 0 NOT NULL, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 2750ad6a2cd..834e044110c 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -11061,6 +11061,7 @@ Represents a code quality degradation on the pipeline. | <a id="codequalitydegradationline"></a>`line` | [`Int!`](#int) | Line on which the code quality degradation occurred. | | <a id="codequalitydegradationpath"></a>`path` | [`String!`](#string) | Relative path to the file containing the code quality degradation. | | <a id="codequalitydegradationseverity"></a>`severity` | [`CodeQualityDegradationSeverity!`](#codequalitydegradationseverity) | Status of the degradation (BLOCKER, CRITICAL, MAJOR, MINOR, INFO, UNKNOWN). | +| <a id="codequalitydegradationweburl"></a>`webUrl` | [`String`](#string) | URL to the file along with line number. | ### `Commit` diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 162b313b531..14df73b8779 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -135,10 +135,17 @@ version of the product: 1. Enable **Allow use of licensed EE features** to make licensed EE features available to projects only if the project namespace's plan includes the feature. - 1. Visit **Admin > Settings > General**. - 1. Expand **Account and limit**. - 1. Select the **Allow use of licensed EE features** checkbox. - 1. Click **Save changes**. + 1. Visit **Admin > Settings > General**. + 1. Expand **Account and limit**. + 1. Select the **Allow use of licensed EE features** checkbox. + 1. Click **Save changes**. + +1. Ensure that the group for which you want to test the EE feature, is actually using an EE plan: + 1. On the top bar, select **Main menu > Admin**. + 1. On the left sidebar, select **Overview > Groups**. + 1. Identify the group you want to modify, and select **Edit**. + 1. Scroll to **Permissions and group features**. For **Plan**, select `Ultimate`. + 1. Select **Save changes**. ### Run CI pipelines in a FOSS context diff --git a/doc/development/import_export.md b/doc/development/import_export.md index f864dd3b678..9aab7f38dbb 100644 --- a/doc/development/import_export.md +++ b/doc/development/import_export.md @@ -6,116 +6,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Import/Export development documentation -Troubleshooting and general development guidelines and tips for the [Import/Export feature](../user/project/settings/import_export.md). +General development guidelines and tips for the [Import/Export feature](../user/project/settings/import_export.md). <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> This document is originally based on the [Import/Export 201 presentation available on YouTube](https://www.youtube.com/watch?v=V3i1OfExotE). -## Troubleshooting commands - -Finds information about the status of the import and further logs using the JID: - -```ruby -# Rails console -Project.find_by_full_path('group/project').import_state.slice(:jid, :status, :last_error) -> {"jid"=>"414dec93f941a593ea1a6894", "status"=>"finished", "last_error"=>nil} -``` - -```shell -# Logs -grep JID /var/log/gitlab/sidekiq/current -grep "Import/Export error" /var/log/gitlab/sidekiq/current -grep "Import/Export backtrace" /var/log/gitlab/sidekiq/current -tail /var/log/gitlab/gitlab-rails/importer.log -``` - -## Troubleshooting performance issues - -Read through the current performance problems using the Import/Export below. - -### OOM errors - -Out of memory (OOM) errors are normally caused by the [Sidekiq Memory Killer](../administration/operations/sidekiq_memory_killer.md): - -```shell -SIDEKIQ_MEMORY_KILLER_MAX_RSS = 2000000 -SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS = 3000000 -SIDEKIQ_MEMORY_KILLER_GRACE_TIME = 900 -``` - -An import status `started`, and the following Sidekiq logs signal a memory issue: - -```shell -WARN: Work still in progress <struct with JID> -``` - -### Timeouts - -Timeout errors occur due to the `Gitlab::Import::StuckProjectImportJobsWorker` marking the process as failed: - -```ruby -module Gitlab - module Import - class StuckProjectImportJobsWorker - include Gitlab::Import::StuckImportJob - # ... - end - end -end - -module Gitlab - module Import - module StuckImportJob - # ... - IMPORT_JOBS_EXPIRATION = 15.hours.to_i - # ... - def perform - stuck_imports_without_jid_count = mark_imports_without_jid_as_failed! - stuck_imports_with_jid_count = mark_imports_with_jid_as_failed! - - track_metrics(stuck_imports_with_jid_count, stuck_imports_without_jid_count) - end - # ... - end - end -end -``` - -```shell -Marked stuck import jobs as failed. JIDs: xyz -``` - -```plaintext - +-----------+ +-----------------------------------+ - |Export Job |--->| Calls ActiveRecord `as_json` and | - +-----------+ | `to_json` on all project models | - +-----------------------------------+ - - +-----------+ +-----------------------------------+ - |Import Job |--->| Loads all JSON in memory, then | - +-----------+ | inserts into the DB in batches | - +-----------------------------------+ -``` - -### Problems and solutions - -| Problem | Possible solutions | -| -------- | -------- | -| [Slow JSON](https://gitlab.com/gitlab-org/gitlab/-/issues/25251) loading/dumping models from the database | [split the worker](https://gitlab.com/gitlab-org/gitlab/-/issues/25252) | -| | Batch export -| | Optimize SQL -| | Move away from `ActiveRecord` callbacks (difficult) -| High memory usage (see also some [analysis](https://gitlab.com/gitlab-org/gitlab/-/issues/18857) | DB Commit sweet spot that uses less memory | -| | [Netflix Fast JSON API](https://github.com/Netflix/fast_jsonapi) may help | -| | Batch reading/writing to disk and any SQL - -### Temporary solutions - -While the performance problems are not tackled, there is a process to workaround -importing big projects, using a foreground import: - -[Foreground import](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/5384) of big projects for customers. -(Using the import template in the [infrastructure tracker](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues)) - ## Security The Import/Export feature is constantly updated (adding new things to export), however diff --git a/doc/development/performance.md b/doc/development/performance.md index e1918bf2e22..3881fad0528 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -21,7 +21,7 @@ consistent performance of GitLab. Refer to the [Index](#performance-documentatio - [Query performance guidelines](database/query_performance.md) - [Pagination performance guidelines](../development/database/pagination_performance_guidelines.md) - [Keyset pagination performance](../development/database/keyset_pagination.md#performance) - - [Troubleshooting import/export performance issues](../development/import_export.md#troubleshooting-performance-issues) + - [Troubleshooting import/export performance issues](../user/project/settings/import_export_troubleshooting.md#troubleshooting-performance-issues) - [Pipelines performance in the `gitlab` project](pipelines/performance.md) - Frontend: - [Performance guidelines and monitoring](../development/fe_guide/performance.md) diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 61f60567418..58f6b02c1f1 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -1440,9 +1440,10 @@ GitLab uses [factory_bot](https://github.com/thoughtbot/factory_bot) as a test f resulting record to pass validation. - When instantiating from a factory, don't supply attributes that aren't required by the test. -- Prefer [implicit](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#implicit-definition) - or [explicit](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#explicit-definition) - association definitions instead of using `create` / `build` for association setup. +- Prefer [implicit](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#implicit-definition), + [explicit](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#explicit-definition), or + [inline](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#inline-definition) associations + over `create` / `build` for association setup in callbacks. See [issue #262624](https://gitlab.com/gitlab-org/gitlab/-/issues/262624) for further context. - Factories don't have to be limited to `ActiveRecord` objects. [See example](https://gitlab.com/gitlab-org/gitlab-foss/commit/0b8cefd3b2385a21cfed779bd659978c0402766d). diff --git a/doc/user/group/import/index.md b/doc/user/group/import/index.md index ee50cfcf182..df0c12c1569 100644 --- a/doc/user/group/import/index.md +++ b/doc/user/group/import/index.md @@ -4,7 +4,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- -# Migrating groups **(FREE)** +# Migrating groups with GitLab Migration **(FREE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/249160) in GitLab 13.7 for group resources [with a flag](../../feature_flags.md) named `bulk_import`. Disabled by default. > - Group items [enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/338985) in GitLab 14.3. @@ -18,15 +18,15 @@ this feature, ask an administrator to [enable the feature flag](../../../adminis `bulk_import_projects`. On GitLab.com, migrating group resources is available but migrating project resources is not available. -Users with the Owner role on a top-level group can migrate it to: +Users with the Owner role on a top-level group can use GitLab Migration to migrate the group to: - Another top-level group. - The subgroup of any existing top-level group. - Another GitLab instance, including GitLab.com. -Migrating groups using the method documented here is not the same as [migrating groups using file exports](../settings/import_export.md). +Migrating groups using GitLab Migration is not the same as [migrating groups using file exports](../settings/import_export.md). Importing and exporting groups using file exports requires you to export a group to a file and then import that file in -another GitLab instance. Migrating groups using the method documented here automates this step. +another GitLab instance. Migrating groups using GitLab Migration automates this step. ## Import your groups into GitLab diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 86416f439e7..3c12bb9b80f 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -254,170 +254,3 @@ and the exports between them are compatible. - [Project import/export administration Rake tasks](../../../administration/raketasks/project_import_export.md) - [Group import/export](../../group/settings/import_export.md) - [Group import/export API](../../../api/group_import_export.md) - -## Troubleshooting - -### Project fails to import due to mismatch - -If the [shared runners enablement](../../../ci/runners/runners_scope.md#enable-shared-runners-for-a-project) -does not match between the exported project, and the project import, the project fails to import. -Review [issue 276930](https://gitlab.com/gitlab-org/gitlab/-/issues/276930), and either: - -- Ensure shared runners are enabled in both the source and destination projects. -- Disable shared runners on the parent group when you import the project. - -### Import workarounds for large repositories - -[Maximum import size limitations](#import-a-project-and-its-data) -can prevent an import from being successful. If changing the import limits is not possible, you can -try one of the workarounds listed here. - -#### Workaround option 1 - -The following local workflow can be used to temporarily -reduce the repository size for another import attempt: - -1. Create a temporary working directory from the export: - - ```shell - EXPORT=<filename-without-extension> - - mkdir "$EXPORT" - tar -xf "$EXPORT".tar.gz --directory="$EXPORT"/ - cd "$EXPORT"/ - git clone project.bundle - - # Prevent interference with recreating an importable file later - mv project.bundle ../"$EXPORT"-original.bundle - mv ../"$EXPORT".tar.gz ../"$EXPORT"-original.tar.gz - - git switch --create smaller-tmp-main - ``` - -1. To reduce the repository size, work on this `smaller-tmp-main` branch: - [identify and remove large files](../repository/reducing_the_repo_size_using_git.md) - or [interactively rebase and fixup](../../../topics/git/git_rebase.md#interactive-rebase) - to reduce the number of commits. - - ```shell - # Reduce the .git/objects/pack/ file size - cd project - git reflog expire --expire=now --all - git gc --prune=now --aggressive - - # Prepare recreating an importable file - git bundle create ../project.bundle <default-branch-name> - cd .. - mv project/ ../"$EXPORT"-project - cd .. - - # Recreate an importable file - tar -czf "$EXPORT"-smaller.tar.gz --directory="$EXPORT"/ . - ``` - -1. Import this new, smaller file into GitLab. -1. In a full clone of the original repository, - use `git remote set-url origin <new-url> && git push --force --all` - to complete the import. -1. Update the imported repository's - [branch protection rules](../protected_branches.md) and - its [default branch](../repository/branches/default.md), and - delete the temporary, `smaller-tmp-main` branch, and - the local, temporary data. - -#### Workaround option 2 - -NOTE: -This workaround does not account for LFS objects. - -Rather than attempting to push all changes at once, this workaround: - -- Separates the project import from the Git Repository import -- Incrementally pushes the repository to GitLab - -1. Make a local clone of the repository to migrate. In a later step, you push this clone outside of - the project export. -1. Download the export and remove the `project.bundle` (which contains the Git repository): - - ```shell - tar -czvf new_export.tar.gz --exclude='project.bundle' @old_export.tar.gz - ``` - -1. Import the export without a Git repository. It asks you to confirm to import without a - repository. -1. Save this bash script as a file and run it after adding the appropriate origin. - - ```shell - #!/bin/sh - - # ASSUMPTIONS: - # - The GitLab location is "origin" - # - The default branch is "main" - # - This will attempt to push in chunks of 500MB (dividing the total size by 500MB). - # Decrease this size to push in smaller chunks if you still receive timeouts. - - git gc - SIZE=$(git count-objects -v 2> /dev/null | grep size-pack | awk '{print $2}') - - # Be conservative... and try to push 2GB at a time - # (given this assumes each commit is the same size - which is wrong) - BATCHES=$(($SIZE / 500000)) - TOTAL_COMMITS=$(git rev-list --count HEAD) - if (( BATCHES > TOTAL_COMMITS )); then - BATCHES=$TOTAL_COMMITS - fi - - INCREMENTS=$(( ($TOTAL_COMMITS / $BATCHES) - 1 )) - - for (( BATCH=BATCHES; BATCH>=1; BATCH-- )) - do - COMMIT_NUM=$(( $BATCH - $INCREMENTS )) - COMMIT_SHA=$(git log -n $COMMIT_NUM --format=format:%H | tail -1) - git push -u origin ${COMMIT_SHA}:refs/heads/main - done - git push -u origin main - git push -u origin --all - git push -u origin --tags - ``` - -### Manually execute export steps - -You usually export a project through [the web interface](#export-a-project-and-its-data) or through [the API](../../../api/project_import_export.md). Exporting using these -methods can sometimes fail without giving enough information to troubleshoot. In these cases, -[open a rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session) -Execute each line individually, rather than pasting the entire block at once, so you can see any -errors each command returns. - -```shell -# User needs to have permission to export -u = User.find_by_username('someuser') -p = Project.find_by_full_path('some/project') -e = Projects::ImportExport::ExportService.new(p,u) - -e.send(:version_saver).send(:save) -e.send(:repo_saver).send(:save) -## continue using `e.send(:exporter_name).send(:save)` going through the list of exporters - -# The following line should show you the export_path similar to /var/opt/gitlab/gitlab-rails/shared/tmp/gitlab_exports/@hashed/49/94/4994.... -s = Gitlab::ImportExport::Saver.new(exportable: p, shared:p.import_export_shared) - -# To try and upload use: -s.send(:compress_and_save) -s.send(:save_upload) -``` - -After the project is successfully uploaded, the exported project is located in a `.tar.gz` file in `/var/opt/gitlab/gitlab-rails/uploads/-/system/import_export_upload/export_file/`. - -### Import using the REST API fails when using a group access token - -[Group access tokens](../../group/settings/group_access_tokens.md) -don't work for project or group import operations. When a group access token initiates an import, -the import fails with this message: - -```plaintext -Error adding importer user to Project members. -Validation failed: User project bots cannot be added to other groups / projects -``` - -To use [Import REST API](../../../api/project_import_export.md), -pass regular user account credentials such as [personal access tokens](../../profile/personal_access_tokens.md). diff --git a/doc/user/project/settings/import_export_troubleshooting.md b/doc/user/project/settings/import_export_troubleshooting.md new file mode 100644 index 00000000000..81ceba7139b --- /dev/null +++ b/doc/user/project/settings/import_export_troubleshooting.md @@ -0,0 +1,280 @@ +--- +stage: Manage +group: Import +info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments" +--- + +# Troubleshooting file export project migrations + +If you have problems with [migrating projects using file exports](import_export.md), see the possible solutions below. + +## Troubleshooting commands + +Finds information about the status of the import and further logs using the JID: + +```ruby +# Rails console +Project.find_by_full_path('group/project').import_state.slice(:jid, :status, :last_error) +> {"jid"=>"414dec93f941a593ea1a6894", "status"=>"finished", "last_error"=>nil} +``` + +```shell +# Logs +grep JID /var/log/gitlab/sidekiq/current +grep "Import/Export error" /var/log/gitlab/sidekiq/current +grep "Import/Export backtrace" /var/log/gitlab/sidekiq/current +tail /var/log/gitlab/gitlab-rails/importer.log +``` + +## Project fails to import due to mismatch + +If the [shared runners enablement](../../../ci/runners/runners_scope.md#enable-shared-runners-for-a-project) +does not match between the exported project, and the project import, the project fails to import. +Review [issue 276930](https://gitlab.com/gitlab-org/gitlab/-/issues/276930), and either: + +- Ensure shared runners are enabled in both the source and destination projects. +- Disable shared runners on the parent group when you import the project. + +## Import workarounds for large repositories + +[Maximum import size limitations](import_export.md#import-a-project-and-its-data) +can prevent an import from being successful. If changing the import limits is not possible, you can +try one of the workarounds listed here. + +### Workaround option 1 + +The following local workflow can be used to temporarily +reduce the repository size for another import attempt: + +1. Create a temporary working directory from the export: + + ```shell + EXPORT=<filename-without-extension> + + mkdir "$EXPORT" + tar -xf "$EXPORT".tar.gz --directory="$EXPORT"/ + cd "$EXPORT"/ + git clone project.bundle + + # Prevent interference with recreating an importable file later + mv project.bundle ../"$EXPORT"-original.bundle + mv ../"$EXPORT".tar.gz ../"$EXPORT"-original.tar.gz + + git switch --create smaller-tmp-main + ``` + +1. To reduce the repository size, work on this `smaller-tmp-main` branch: + [identify and remove large files](../repository/reducing_the_repo_size_using_git.md) + or [interactively rebase and fixup](../../../topics/git/git_rebase.md#interactive-rebase) + to reduce the number of commits. + + ```shell + # Reduce the .git/objects/pack/ file size + cd project + git reflog expire --expire=now --all + git gc --prune=now --aggressive + + # Prepare recreating an importable file + git bundle create ../project.bundle <default-branch-name> + cd .. + mv project/ ../"$EXPORT"-project + cd .. + + # Recreate an importable file + tar -czf "$EXPORT"-smaller.tar.gz --directory="$EXPORT"/ . + ``` + +1. Import this new, smaller file into GitLab. +1. In a full clone of the original repository, + use `git remote set-url origin <new-url> && git push --force --all` + to complete the import. +1. Update the imported repository's + [branch protection rules](../protected_branches.md) and + its [default branch](../repository/branches/default.md), and + delete the temporary, `smaller-tmp-main` branch, and + the local, temporary data. + +### Workaround option 2 + +NOTE: +This workaround does not account for LFS objects. + +Rather than attempting to push all changes at once, this workaround: + +- Separates the project import from the Git Repository import +- Incrementally pushes the repository to GitLab + +1. Make a local clone of the repository to migrate. In a later step, you push this clone outside of + the project export. +1. Download the export and remove the `project.bundle` (which contains the Git repository): + + ```shell + tar -czvf new_export.tar.gz --exclude='project.bundle' @old_export.tar.gz + ``` + +1. Import the export without a Git repository. It asks you to confirm to import without a + repository. +1. Save this bash script as a file and run it after adding the appropriate origin. + + ```shell + #!/bin/sh + + # ASSUMPTIONS: + # - The GitLab location is "origin" + # - The default branch is "main" + # - This will attempt to push in chunks of 500MB (dividing the total size by 500MB). + # Decrease this size to push in smaller chunks if you still receive timeouts. + + git gc + SIZE=$(git count-objects -v 2> /dev/null | grep size-pack | awk '{print $2}') + + # Be conservative... and try to push 2GB at a time + # (given this assumes each commit is the same size - which is wrong) + BATCHES=$(($SIZE / 500000)) + TOTAL_COMMITS=$(git rev-list --count HEAD) + if (( BATCHES > TOTAL_COMMITS )); then + BATCHES=$TOTAL_COMMITS + fi + + INCREMENTS=$(( ($TOTAL_COMMITS / $BATCHES) - 1 )) + + for (( BATCH=BATCHES; BATCH>=1; BATCH-- )) + do + COMMIT_NUM=$(( $BATCH - $INCREMENTS )) + COMMIT_SHA=$(git log -n $COMMIT_NUM --format=format:%H | tail -1) + git push -u origin ${COMMIT_SHA}:refs/heads/main + done + git push -u origin main + git push -u origin --all + git push -u origin --tags + ``` + +## Manually execute export steps + +You usually export a project through [the web interface](import_export.md#export-a-project-and-its-data) or through [the API](../../../api/project_import_export.md). Exporting using these +methods can sometimes fail without giving enough information to troubleshoot. In these cases, +[open a rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session) +Execute each line individually, rather than pasting the entire block at once, so you can see any +errors each command returns. + +```shell +# User needs to have permission to export +u = User.find_by_username('someuser') +p = Project.find_by_full_path('some/project') +e = Projects::ImportExport::ExportService.new(p,u) + +e.send(:version_saver).send(:save) +e.send(:repo_saver).send(:save) +## continue using `e.send(:exporter_name).send(:save)` going through the list of exporters + +# The following line should show you the export_path similar to /var/opt/gitlab/gitlab-rails/shared/tmp/gitlab_exports/@hashed/49/94/4994.... +s = Gitlab::ImportExport::Saver.new(exportable: p, shared:p.import_export_shared) + +# To try and upload use: +s.send(:compress_and_save) +s.send(:save_upload) +``` + +After the project is successfully uploaded, the exported project is located in a `.tar.gz` file in `/var/opt/gitlab/gitlab-rails/uploads/-/system/import_export_upload/export_file/`. + +## Import using the REST API fails when using a group access token + +[Group access tokens](../../group/settings/group_access_tokens.md) +don't work for project or group import operations. When a group access token initiates an import, +the import fails with this message: + +```plaintext +Error adding importer user to Project members. +Validation failed: User project bots cannot be added to other groups / projects +``` + +To use [Import REST API](../../../api/project_import_export.md), +pass regular user account credentials such as [personal access tokens](../../profile/personal_access_tokens.md). + +## Troubleshooting performance issues + +Read through the current performance problems using the Import/Export below. + +### OOM errors + +Out of memory (OOM) errors are normally caused by the [Sidekiq Memory Killer](../../../administration/sidekiq/sidekiq_memory_killer.md): + +```shell +SIDEKIQ_MEMORY_KILLER_MAX_RSS = 2000000 +SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS = 3000000 +SIDEKIQ_MEMORY_KILLER_GRACE_TIME = 900 +``` + +An import status `started`, and the following Sidekiq logs signal a memory issue: + +```shell +WARN: Work still in progress <struct with JID> +``` + +### Timeouts + +Timeout errors occur due to the `Gitlab::Import::StuckProjectImportJobsWorker` marking the process as failed: + +```ruby +module Gitlab + module Import + class StuckProjectImportJobsWorker + include Gitlab::Import::StuckImportJob + # ... + end + end +end + +module Gitlab + module Import + module StuckImportJob + # ... + IMPORT_JOBS_EXPIRATION = 15.hours.to_i + # ... + def perform + stuck_imports_without_jid_count = mark_imports_without_jid_as_failed! + stuck_imports_with_jid_count = mark_imports_with_jid_as_failed! + + track_metrics(stuck_imports_with_jid_count, stuck_imports_without_jid_count) + end + # ... + end + end +end +``` + +```shell +Marked stuck import jobs as failed. JIDs: xyz +``` + +```plaintext + +-----------+ +-----------------------------------+ + |Export Job |--->| Calls ActiveRecord `as_json` and | + +-----------+ | `to_json` on all project models | + +-----------------------------------+ + + +-----------+ +-----------------------------------+ + |Import Job |--->| Loads all JSON in memory, then | + +-----------+ | inserts into the DB in batches | + +-----------------------------------+ +``` + +### Problems and solutions + +| Problem | Possible solutions | +| -------- | -------- | +| [Slow JSON](https://gitlab.com/gitlab-org/gitlab/-/issues/25251) loading/dumping models from the database | [split the worker](https://gitlab.com/gitlab-org/gitlab/-/issues/25252) | +| | Batch export +| | Optimize SQL +| | Move away from `ActiveRecord` callbacks (difficult) +| High memory usage (see also some [analysis](https://gitlab.com/gitlab-org/gitlab/-/issues/18857) | DB Commit sweet spot that uses less memory | +| | [Netflix Fast JSON API](https://github.com/Netflix/fast_jsonapi) may help | +| | Batch reading/writing to disk and any SQL + +### Temporary solutions + +While the performance problems are not tackled, there is a process to workaround +importing big projects, using a foreground import: + +[Foreground import](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/5384) of big projects for customers. +(Using the import template in the [infrastructure tracker](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues)) diff --git a/lib/api/api.rb b/lib/api/api.rb index b0d76cd3eb4..04114d38b33 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -177,6 +177,7 @@ module API mount ::API::Applications mount ::API::BroadcastMessages mount ::API::BulkImports + mount ::API::Ci::Jobs mount ::API::Ci::ResourceGroups mount ::API::Ci::Runner mount ::API::Ci::Runners @@ -198,6 +199,7 @@ module API mount ::API::GroupVariables mount ::API::ImportBitbucketServer mount ::API::ImportGithub + mount ::API::Invitations mount ::API::Keys mount ::API::Lint mount ::API::MergeRequestDiffs @@ -208,6 +210,7 @@ module API mount ::API::ProjectEvents mount ::API::ProjectExport mount ::API::ProjectHooks + mount ::API::ProjectImport mount ::API::ProjectRepositoryStorageMoves mount ::API::ProjectSnippets mount ::API::ProjectSnapshots @@ -243,7 +246,6 @@ module API mount ::API::Boards mount ::API::Branches mount ::API::Ci::JobArtifacts - mount ::API::Ci::Jobs mount ::API::Ci::PipelineSchedules mount ::API::Ci::Pipelines mount ::API::Ci::SecureFiles @@ -278,7 +280,6 @@ module API mount ::API::HelmPackages mount ::API::Integrations mount ::API::Integrations::JiraConnect::Subscriptions - mount ::API::Invitations mount ::API::IssueLinks mount ::API::Issues mount ::API::Labels @@ -301,7 +302,7 @@ module API mount ::API::PagesDomains mount ::API::ProjectContainerRepositories mount ::API::ProjectDebianDistributions - mount ::API::ProjectImport + mount ::API::ProjectEvents mount ::API::ProjectMilestones mount ::API::ProjectPackages mount ::API::ProjectStatistics diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb index 2fe270bcb8a..1836cfd8217 100644 --- a/lib/api/ci/jobs.rb +++ b/lib/api/ci/jobs.rb @@ -29,12 +29,19 @@ module API else ['unknown'] end - } + }, + documentation: { example: %w[pending running] } end end desc 'Get a projects jobs' do - success Entities::Ci::Job + success code: 200, model: Entities::Ci::Job + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' } + ] + is_array true end params do use :optional_scope @@ -53,10 +60,15 @@ module API # rubocop: enable CodeReuse/ActiveRecord desc 'Get a specific job of a project' do - success Entities::Ci::Job + success code: 200, model: Entities::Ci::Job + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' } + ] end params do - requires :job_id, type: Integer, desc: 'The ID of a job' + requires :job_id, type: Integer, desc: 'The ID of a job', documentation: { example: 88 } end get ':id/jobs/:job_id', urgency: :low, feature_category: :continuous_integration do authorize_read_builds! @@ -69,9 +81,16 @@ module API # TODO: We should use `present_disk_file!` and leave this implementation for backward compatibility (when build trace # is saved in the DB instead of file). But before that, we need to consider how to replace the value of # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. - desc 'Get a trace of a specific job of a project' + desc 'Get a trace of a specific job of a project' do + success code: 200, model: Entities::Ci::Job + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' } + ] + end params do - requires :job_id, type: Integer, desc: 'The ID of a job' + requires :job_id, type: Integer, desc: 'The ID of a job', documentation: { example: 88 } end get ':id/jobs/:job_id/trace', urgency: :low, feature_category: :continuous_integration do authorize_read_builds! @@ -90,10 +109,15 @@ module API end desc 'Cancel a specific job of a project' do - success Entities::Ci::Job + success code: 201, model: Entities::Ci::Job + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' } + ] end params do - requires :job_id, type: Integer, desc: 'The ID of a job' + requires :job_id, type: Integer, desc: 'The ID of a job', documentation: { example: 88 } end post ':id/jobs/:job_id/cancel', urgency: :low, feature_category: :continuous_integration do authorize_update_builds! @@ -107,10 +131,15 @@ module API end desc 'Retry a specific build of a project' do - success Entities::Ci::Job + success code: 201, model: Entities::Ci::Job + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' } + ] end params do - requires :job_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a build', documentation: { example: 88 } end post ':id/jobs/:job_id/retry', urgency: :low, feature_category: :continuous_integration do authorize_update_builds! @@ -128,10 +157,16 @@ module API end desc 'Erase job (remove artifacts and the trace)' do - success Entities::Ci::Job + success code: 201, model: Entities::Ci::Job + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 409, message: 'Conflict' } + ] end params do - requires :job_id, type: Integer, desc: 'The ID of a build' + requires :job_id, type: Integer, desc: 'The ID of a build', documentation: { example: 88 } end post ':id/jobs/:job_id/erase', urgency: :low, feature_category: :continuous_integration do authorize_update_builds! @@ -148,15 +183,21 @@ module API end desc 'Trigger an actionable job (manual, delayed, etc)' do - success Entities::Ci::JobBasic detail 'This feature was added in GitLab 8.11' + success code: 200, model: Entities::Ci::JobBasic + failure [ + { code: 400, message: 'Bad request' }, + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' } + ] end params do - requires :job_id, type: Integer, desc: 'The ID of a Job' + requires :job_id, type: Integer, desc: 'The ID of a Job', documentation: { example: 88 } optional :job_variables_attributes, type: Array, desc: 'User defined variables that will be included when running the job' do - requires :key, type: String, desc: 'The name of the variable' - requires :value, type: String, desc: 'The value of the variable' + requires :key, type: String, desc: 'The name of the variable', documentation: { example: 'foo' } + requires :value, type: String, desc: 'The value of the variable', documentation: { example: 'bar' } end end @@ -183,7 +224,12 @@ module API resource :job do desc 'Get current job using job token' do - success Entities::Ci::Job + success code: 200, model: Entities::Ci::Job + failure [ + { code: 400, message: 'Bad request' }, + { code: 401, message: 'Unauthorized' }, + { code: 404, message: 'Not found' } + ] end route_setting :authentication, job_token_allowed: true get '', feature_category: :continuous_integration, urgency: :low do @@ -194,6 +240,12 @@ module API desc 'Get current agents' do detail 'Retrieves a list of agents for the given job token' + success code: 200, model: Entities::Ci::Job + failure [ + { code: 400, message: 'Bad request' }, + { code: 401, message: 'Unauthorized' }, + { code: 404, message: 'Not found' } + ] end route_setting :authentication, job_token_allowed: true get '/allowed_agents', urgency: :low, feature_category: :kubernetes_management do diff --git a/lib/api/entities/ci/job.rb b/lib/api/entities/ci/job.rb index cf87684ce55..d9e6b7eed75 100644 --- a/lib/api/entities/ci/job.rb +++ b/lib/api/entities/ci/job.rb @@ -6,10 +6,17 @@ module API class Job < JobBasic # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5) expose :artifacts_file, using: ::API::Entities::Ci::JobArtifactFile, if: -> (job, opts) { job.artifacts? } - expose :job_artifacts, as: :artifacts, using: ::API::Entities::Ci::JobArtifact + expose :job_artifacts, as: :artifacts, + using: ::API::Entities::Ci::JobArtifact, + documentation: { is_array: true } expose :runner, with: ::API::Entities::Ci::Runner - expose :artifacts_expire_at - expose :tag_list do |job| + expose :artifacts_expire_at, + documentation: { type: 'dateTime', example: '2016-01-19T09:05:50.355Z' } + + expose( + :tag_list, + documentation: { type: 'string', is_array: true, example: ['ubuntu18', 'docker runner'] } + ) do |job| job.tags.map(&:name).sort end end diff --git a/lib/api/entities/ci/job_artifact.rb b/lib/api/entities/ci/job_artifact.rb index 9e504aee383..8276c0f4073 100644 --- a/lib/api/entities/ci/job_artifact.rb +++ b/lib/api/entities/ci/job_artifact.rb @@ -4,7 +4,12 @@ module API module Entities module Ci class JobArtifact < Grape::Entity - expose :file_type, :size, :filename, :file_format + expose :file_type, + documentation: { type: 'string', values: ::Ci::JobArtifact.file_types.keys, example: 'archive' } + expose :size, documentation: { type: 'integer', example: 1000 } + expose :filename, documentation: { type: 'string', example: 'artifacts.zip' } + expose :file_format, + documentation: { type: 'string', values: ::Ci::JobArtifact.file_formats.keys, example: 'zip' } end end end diff --git a/lib/api/entities/ci/job_artifact_file.rb b/lib/api/entities/ci/job_artifact_file.rb index 418eb408ab6..0266f99cd6d 100644 --- a/lib/api/entities/ci/job_artifact_file.rb +++ b/lib/api/entities/ci/job_artifact_file.rb @@ -4,8 +4,8 @@ module API module Entities module Ci class JobArtifactFile < Grape::Entity - expose :filename - expose :cached_size, as: :size + expose :filename, documentation: { type: 'string', example: 'artifacts.zip' } + expose :cached_size, as: :size, documentation: { type: 'integer', example: 1000 } end end end diff --git a/lib/api/entities/ci/job_basic.rb b/lib/api/entities/ci/job_basic.rb index e5b04b86075..3cbb8aad313 100644 --- a/lib/api/entities/ci/job_basic.rb +++ b/lib/api/entities/ci/job_basic.rb @@ -10,7 +10,7 @@ module API expose :name, documentation: { type: 'string', example: 'deploy_to_production' } expose :ref, documentation: { type: 'string', example: 'main' } expose :tag, documentation: { type: 'boolean' } - expose :coverage, documentation: { type: 'number', format: 'float', example: 0.90 } + expose :coverage, documentation: { type: 'number', format: 'float', example: 98.29 } expose :allow_failure, documentation: { type: 'boolean' } expose :created_at, documentation: { type: 'dateTime', example: '2015-12-24T15:51:21.880Z' } expose :started_at, documentation: { type: 'dateTime', example: '2015-12-24T17:54:30.733Z' } @@ -25,12 +25,15 @@ module API expose :failure_reason, documentation: { type: 'string', example: 'script_failure' }, if: -> (job) { job.failed? } - expose :web_url do |job, _options| + expose( + :web_url, + documentation: { type: 'string', example: 'https://example.com/foo/bar/-/jobs/1' } + ) do |job, _options| Gitlab::Routing.url_helpers.project_job_url(job.project, job) end expose :project do - expose :ci_job_token_scope_enabled do |job| + expose :ci_job_token_scope_enabled, documentation: { type: 'string', example: false } do |job| job.project.ci_outbound_job_token_scope_enabled? end end diff --git a/lib/api/entities/ci/runner.rb b/lib/api/entities/ci/runner.rb index f034eb5c94c..9361709b6ed 100644 --- a/lib/api/entities/ci/runner.rb +++ b/lib/api/entities/ci/runner.rb @@ -4,20 +4,22 @@ module API module Entities module Ci class Runner < Grape::Entity - expose :id - expose :description - expose :ip_address - expose :active # TODO Remove in v5 in favor of `paused` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/375709 - expose :paused do |runner| + expose :id, documentation: { type: 'integer', example: 8 } + expose :description, documentation: { type: 'string', example: 'test-1-20150125' } + expose :ip_address, documentation: { type: 'string', example: '127.0.0.1' } + # TODO Remove in v5 in favor of `paused` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/375709 + expose :active, documentation: { type: 'boolean', example: true } + expose :paused, documentation: { type: 'boolean', example: false } do |runner| !runner.active end - expose :instance_type?, as: :is_shared - expose :runner_type - expose :name - expose :online?, as: :online + expose :instance_type?, as: :is_shared, documentation: { type: 'boolean', example: true } + expose :runner_type, + documentation: { type: 'string', values: ::Ci::Runner.runner_types.keys, example: 'instance_type' } + expose :name, documentation: { type: 'string', example: 'test' } + expose :online?, as: :online, documentation: { type: 'boolean', example: true } # DEPRECATED # TODO Remove in v5 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/375709 - expose :deprecated_rest_status, as: :status + expose :deprecated_rest_status, as: :status, documentation: { type: 'string', example: 'online' } end end end diff --git a/lib/api/entities/project_import_failed_relation.rb b/lib/api/entities/project_import_failed_relation.rb index 26cfae7260c..543cc62f364 100644 --- a/lib/api/entities/project_import_failed_relation.rb +++ b/lib/api/entities/project_import_failed_relation.rb @@ -3,14 +3,17 @@ module API module Entities class ProjectImportFailedRelation < Grape::Entity - expose :id, :created_at, :exception_class, :source + expose :id, documentation: { type: 'string', example: 1 } + expose :created_at, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' } + expose :exception_class, documentation: { type: 'string', example: 'StandardError' } + expose :source, documentation: { type: 'string', example: 'ImportRepositoryWorker' } - expose :exception_message do |_| + expose :exception_message, documentation: { type: 'string' } do |_| nil end - expose :relation_key, as: :relation_name - expose :relation_index, as: :line_number + expose :relation_key, as: :relation_name, documentation: { type: 'string', example: 'issues' } + expose :relation_index, as: :line_number, documentation: { type: 'integer', example: 1 } end end end diff --git a/lib/api/entities/project_import_status.rb b/lib/api/entities/project_import_status.rb index 5daae4a70f2..59388aacafd 100644 --- a/lib/api/entities/project_import_status.rb +++ b/lib/api/entities/project_import_status.rb @@ -3,21 +3,25 @@ module API module Entities class ProjectImportStatus < ProjectIdentity - expose :import_status - expose :import_type - expose :correlation_id do |project, _options| + expose :import_status, documentation: { type: 'string', example: 'scheduled' } + expose :import_type, documentation: { type: 'string', example: 'gitlab_project' } + expose :correlation_id, documentation: { + type: 'string', example: 'dfcf583058ed4508e4c7c617bd7f0edd' + } do |project, _options| project.import_state&.correlation_id end - expose :failed_relations, using: Entities::ProjectImportFailedRelation do |project, _options| + expose :failed_relations, using: Entities::ProjectImportFailedRelation, documentation: { + is_array: true + } do |project, _options| project.import_state&.relation_hard_failures(limit: 100) || [] end - expose :import_error do |project, _options| + expose :import_error, documentation: { type: 'string', example: 'Error message' } do |project, _options| project.import_state&.last_error end - expose :stats do |project, _options| + expose :stats, documentation: { type: 'object' } do |project, _options| if project.github_import? ::Gitlab::GithubImport::ObjectCounter.summary(project) end diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb index 6dd4d55d38a..6aefdf146cf 100644 --- a/lib/api/invitations.rb +++ b/lib/api/invitations.rb @@ -18,6 +18,7 @@ module API desc 'Invite non-members by email address to a group or project.' do detail 'This feature was introduced in GitLab 13.6' success Entities::Invitation + tags %w[invitations] end params do requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)' @@ -44,8 +45,12 @@ module API desc 'Get a list of group or project invitations viewable by the authenticated user' do detail 'This feature was introduced in GitLab 13.6' success Entities::Invitation + is_array true + tags %w[invitations] end params do + optional :page, type: Integer, desc: 'Page to retrieve' + optional :per_page, type: Integer, desc: 'Number of member invitations to return per page' optional :query, type: String, desc: 'A query string to search for members' use :pagination end @@ -62,6 +67,7 @@ module API desc 'Updates a group or project invitation.' do success Entities::Member + tags %w[invitations] end params do requires :email, type: String, desc: 'The email address of the invitation' @@ -93,7 +99,15 @@ module API end end - desc 'Removes an invitation from a group or project.' + desc 'Removes an invitation from a group or project.' do + success code: 204 + failure [ + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 409, message: 'Could not delete invitation' } + ] + tags %w[invitations] + end params do requires :email, type: String, desc: 'The email address of the invitation' end diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 268a238421e..02f0d9a2a70 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -40,6 +40,7 @@ module API resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Workhorse authorize the project import upload' do detail 'This feature was introduced in GitLab 12.9' + tags ['project_import'] end post 'import/authorize' do require_gitlab_workhorse! @@ -77,7 +78,16 @@ module API end desc 'Create a new project import' do detail 'This feature was introduced in GitLab 10.6.' - success Entities::ProjectImportStatus + success code: 201, model: Entities::ProjectImportStatus + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 400, message: 'Bad request' }, + { code: 404, message: 'Not found' }, + { code: 503, message: 'Service unavailable' } + ] + tags ['project_import'] + consumes ['multipart/form-data'] end post 'import' do require_gitlab_workhorse! @@ -112,7 +122,15 @@ module API end desc 'Get a project import status' do detail 'This feature was introduced in GitLab 10.6.' - success Entities::ProjectImportStatus + success code: 200, model: Entities::ProjectImportStatus + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 400, message: 'Bad request' }, + { code: 404, message: 'Not found' }, + { code: 503, message: 'Service unavailable' } + ] + tags ['project_import'] end route_setting :skip_authentication, true get ':id/import' do @@ -133,7 +151,17 @@ module API end desc 'Create a new project import using a remote object storage path' do detail 'This feature was introduced in GitLab 13.2.' - success Entities::ProjectImportStatus + consumes ['multipart/form-data'] + tags ['project_import'] + success code: 201, model: Entities::ProjectImportStatus + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 400, message: 'Bad request' }, + { code: 404, message: 'Not found' }, + { code: 429, message: 'Too many requests' }, + { code: 503, message: 'Service unavailable' } + ] end post 'remote-import' do check_rate_limit! :project_import, scope: [current_user, :project_import] @@ -176,7 +204,17 @@ module API end desc 'Create a new project import using a file from AWS S3' do detail 'This feature was introduced in GitLab 14.9.' - success Entities::ProjectImportStatus + consumes ['multipart/form-data'] + tags ['project_import'] + success code: 201, model: Entities::ProjectImportStatus + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 400, message: 'Bad request' }, + { code: 404, message: 'Not found' }, + { code: 429, message: 'Too many requests' }, + { code: 503, message: 'Service unavailable' } + ] end post 'remote-import-s3' do not_found! unless ::Feature.enabled?(:import_project_from_remote_file_s3) diff --git a/lib/gitlab/ci/parsers/codequality/code_climate.rb b/lib/gitlab/ci/parsers/codequality/code_climate.rb index 628d50b84cb..14ea907edd8 100644 --- a/lib/gitlab/ci/parsers/codequality/code_climate.rb +++ b/lib/gitlab/ci/parsers/codequality/code_climate.rb @@ -5,23 +5,36 @@ module Gitlab module Parsers module Codequality class CodeClimate - def parse!(json_data, codequality_report) + def parse!(json_data, codequality_report, metadata = {}) root = Gitlab::Json.parse(json_data) - parse_all(root, codequality_report) + parse_all(root, codequality_report, metadata) rescue JSON::ParserError => e codequality_report.set_error_message("JSON parsing failed: #{e}") end private - def parse_all(root, codequality_report) + def parse_all(root, codequality_report, metadata) return unless root.present? root.each do |degradation| - break unless codequality_report.add_degradation(degradation) + break unless codequality_report.valid_degradation?(degradation) + + degradation['web_url'] = web_url(degradation, metadata) + codequality_report.add_degradation(degradation) end end + + def web_url(degradation, metadata) + return unless metadata[:project].present? && metadata[:commit_sha].present? + + path = degradation.dig('location', 'path') + line = degradation.dig('location', 'lines', 'begin') || + degradation.dig('location', 'positions', 'begin', 'line') + "#{Routing.url_helpers.project_blob_url( + metadata[:project], File.join(metadata[:commit_sha], path))}#L#{line}" + end end end end diff --git a/lib/gitlab/ci/pipeline/chain/ensure_environments.rb b/lib/gitlab/ci/pipeline/chain/ensure_environments.rb index 3dd9b85d9b2..cd71c305552 100644 --- a/lib/gitlab/ci/pipeline/chain/ensure_environments.rb +++ b/lib/gitlab/ci/pipeline/chain/ensure_environments.rb @@ -16,7 +16,7 @@ module Gitlab private def ensure_environment(build) - return unless build.instance_of?(::Ci::Build) && build.has_environment? + return unless build.instance_of?(::Ci::Build) && build.has_environment_keyword? environment = ::Gitlab::Ci::Pipeline::Seed::Environment .new(build, merge_request: @command.merge_request) diff --git a/lib/gitlab/ci/reports/codequality_reports.rb b/lib/gitlab/ci/reports/codequality_reports.rb index 353d359fde8..3196bf3fc6d 100644 --- a/lib/gitlab/ci/reports/codequality_reports.rb +++ b/lib/gitlab/ci/reports/codequality_reports.rb @@ -37,8 +37,6 @@ module Gitlab end.to_h end - private - def valid_degradation?(degradation) JSONSchemer.schema(Pathname.new(CODECLIMATE_SCHEMA_PATH)).valid?(degradation) rescue StandardError => _ diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb index 4640f85bb0a..f701f84291e 100644 --- a/lib/gitlab/data_builder/build.rb +++ b/lib/gitlab/data_builder/build.rb @@ -91,7 +91,7 @@ module Gitlab end def build_environment(build) - return unless build.has_environment? + return unless build.has_environment_keyword? { name: build.expanded_environment_name, diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index 7753974b621..939eaa377aa 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -147,7 +147,7 @@ module Gitlab end def environment_hook_attrs(build) - return unless build.has_environment? + return unless build.has_environment_keyword? { name: build.expanded_environment_name, diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb index 1330b06bf9c..f4d4cebc096 100644 --- a/lib/gitlab/git_ref_validator.rb +++ b/lib/gitlab/git_ref_validator.rb @@ -13,6 +13,7 @@ module Gitlab # # Returns true for a valid reference name, false otherwise def validate(ref_name) + return false if ref_name.to_s.empty? # #blank? raises an ArgumentError for invalid encodings return false if ref_name.start_with?(*(EXPANDED_PREFIXES + DISALLOWED_PREFIXES)) return false if ref_name == 'HEAD' @@ -24,6 +25,7 @@ module Gitlab end def validate_merge_request_branch(ref_name) + return false if ref_name.to_s.empty? return false if ref_name.start_with?(*DISALLOWED_PREFIXES) expanded_name = if ref_name.start_with?(*EXPANDED_PREFIXES) diff --git a/lib/gitlab/metrics/loose_foreign_keys_slis.rb b/lib/gitlab/metrics/loose_foreign_keys_slis.rb new file mode 100644 index 00000000000..5d8245aa609 --- /dev/null +++ b/lib/gitlab/metrics/loose_foreign_keys_slis.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Metrics + module LooseForeignKeysSlis + class << self + def initialize_slis! + Gitlab::Metrics::Sli::Apdex.initialize_sli(:loose_foreign_key_clean_ups, possible_labels) + Gitlab::Metrics::Sli::ErrorRate.initialize_sli(:loose_foreign_key_clean_ups, possible_labels) + end + + def record_apdex(success:, db_config_name:) + Gitlab::Metrics::Sli::Apdex[:loose_foreign_key_clean_ups].increment( + labels: labels(db_config_name), + success: success + ) + end + + def record_error_rate(error:, db_config_name:) + Gitlab::Metrics::Sli::ErrorRate[:loose_foreign_key_clean_ups].increment( + labels: labels(db_config_name), + error: error + ) + end + + private + + def possible_labels + ::Gitlab::Database.db_config_names.map do |db_config_name| + { + db_config_name: db_config_name, + feature_category: :database + } + end + end + + def labels(db_config_name) + { + db_config_name: db_config_name, + feature_category: :database + } + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f48b2e87994..ddbd3220077 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -649,9 +649,6 @@ msgstr "" msgid "%{docs_link_start}What is two-factor authentication?%{docs_link_end}" msgstr "" -msgid "%{due_date} (Past due)" -msgstr "" - msgid "%{duration}ms" msgstr "" @@ -17051,9 +17048,6 @@ msgstr "" msgid "Filter by merge requests that are currently merged." msgstr "" -msgid "Filter by milestone" -msgstr "" - msgid "Filter by milestone name" msgstr "" @@ -24686,9 +24680,6 @@ msgstr "" msgid "Logs" msgstr "" -msgid "Looks like you've reached your %{free_limit} member limit for %{strong_start}%{namespace_name}%{strong_end}" -msgstr "" - msgid "Low vulnerabilities present" msgstr "" @@ -42422,6 +42413,9 @@ msgstr "" msgid "To reactivate your account, sign in to GitLab at %{gitlab_url}." msgstr "" +msgid "To remove the %{link_start}read-only%{link_end} state and regain write access, you can reduce the number of users in your namespace to %{free_limit} users or less. You can also upgrade to a paid tier, which do not have user limits. If you need additional time, you can start a free 30-day trial which includes unlimited users." +msgstr "" + msgid "To resolve this, try to:" msgstr "" @@ -46636,9 +46630,6 @@ msgstr "" msgid "You can view the source or %{linkStart}%{cloneIcon} clone the repository%{linkEnd}" msgstr "" -msgid "You can't add any more, but you can manage your existing members, for example, by removing inactive members and replacing them with new members. To get more members an owner of the group can start a trial or upgrade to a paid tier." -msgstr "" - msgid "You can't follow more than %{limit} users. To follow more users, unfollow some others." msgstr "" @@ -47275,6 +47266,9 @@ msgstr "" msgid "Your name" msgstr "" +msgid "Your namespace %{namespace_name} is over the %{free_limit} user limit and has been placed in a read-only state." +msgstr "" + msgid "Your new %{accessTokenType}" msgstr "" diff --git a/package.json b/package.json index 84da16079e6..98379a91baf 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@gitlab/at.js": "1.5.7", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/svgs": "3.7.0", - "@gitlab/ui": "49.3.0", + "@gitlab/ui": "49.5.1", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20220815034418", "@rails/actioncable": "6.1.4-7", diff --git a/qa/qa/page/issuable/new.rb b/qa/qa/page/issuable/new.rb index f3e6a84ef54..b74dc705888 100644 --- a/qa/qa/page/issuable/new.rb +++ b/qa/qa/page/issuable/new.rb @@ -12,9 +12,8 @@ module QA element :issuable_form_description_field end - view 'app/views/shared/issuable/_milestone_dropdown.html.haml' do + view 'app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue' do element :issuable_milestone_dropdown - element :issuable_milestone_dropdown_content end view 'app/views/shared/issuable/_label_dropdown.html.haml' do @@ -38,9 +37,9 @@ module QA end def choose_milestone(milestone) - click_element :issuable_milestone_dropdown - within_element(:issuable_milestone_dropdown_content) do - click_on milestone.title + within_element(:issuable_milestone_dropdown) do + click_button 'Select milestone' + click_button milestone.title end end diff --git a/rubocop/cop/rspec/factory_bot/strategy_in_callback.rb b/rubocop/cop/rspec/factory_bot/strategy_in_callback.rb new file mode 100644 index 00000000000..3153d54887e --- /dev/null +++ b/rubocop/cop/rspec/factory_bot/strategy_in_callback.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'rubocop-rspec' + +module RuboCop + module Cop + module RSpec + module FactoryBot + class StrategyInCallback < RuboCop::Cop::Base + include RuboCop::RSpec::FactoryBot::Language + + MSG = 'Prefer inline `association` over `%{type}`. ' \ + 'See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories' + + FORBIDDEN_METHODS = %i[build build_list build_stubbed build_stubbed_list create create_list].freeze + + def_node_matcher :forbidden_factory_usage, <<~PATTERN + (block + (send nil? { :after :before } (sym _strategy)) + _args + ` # in all descandents + (send + { nil? #factory_bot? } + ${ #{FORBIDDEN_METHODS.map(&:inspect).join(' ')} } + (sym _factory_name) + ... + ) + ) + PATTERN + + RESTRICT_ON_SEND = FORBIDDEN_METHODS + + def on_send(node) + parent = node.each_ancestor(:block).first + + strategy = forbidden_factory_usage(parent) + return unless strategy + + add_offense(node, message: format(MSG, type: strategy)) + end + end + end + end + end +end diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb index 9b721d8cfca..fe2081fa5de 100644 --- a/spec/config/settings_spec.rb +++ b/spec/config/settings_spec.rb @@ -151,20 +151,6 @@ RSpec.describe Settings do end end - describe '.build_sidekiq_routing_rules' do - [ - [nil, [['*', 'default']]], - [[], [['*', 'default']]], - [[['name=foobar', 'foobar']], [['name=foobar', 'foobar']]] - ].each do |input_rules, output_rules| - context "Given input routing_rules #{input_rules}" do - it "returns output routing_rules #{output_rules}" do - expect(described_class.send(:build_sidekiq_routing_rules, input_rules)).to eq(output_rules) - end - end - end - end - describe '.microsoft_graph_mailer' do it 'defaults' do expect(described_class.microsoft_graph_mailer.enabled).to be false diff --git a/spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json b/spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json index 2a01d85c0ee..2ebfd1bfdf2 100644 --- a/spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json +++ b/spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json @@ -198,6 +198,9 @@ "match": "regex", "regex": "^(push|web|trigger|schedule|api|external|pipeline|chat|webide|merge_request_event|external_pull_request_event|parent_pipeline|ondemand_dast_scan|ondemand_dast_validation)$" }, + "$.body.pipelines[*].name": { + "match": "type" + }, "$.body.pipelines[*].created_at": { "match": "regex", "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$" diff --git a/spec/factories/ci/reports/codequality_degradations.rb b/spec/factories/ci/reports/codequality_degradations.rb index 8b53f2bf46e..632f5a3ecaa 100644 --- a/spec/factories/ci/reports/codequality_degradations.rb +++ b/spec/factories/ci/reports/codequality_degradations.rb @@ -26,7 +26,8 @@ FactoryBot.define do "remediation_points": 900000, "severity": "major", "type": "issue", - "engine_name": "structure" + "engine_name": "structure", + "web_url": "http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_a.rb#L10" }.with_indifferent_access end end @@ -56,7 +57,8 @@ FactoryBot.define do "remediation_points": 900000, "severity": "major", "type": "issue", - "engine_name": "structure" + "engine_name": "structure", + "web_url": "http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_a.rb#L10" }.with_indifferent_access end end @@ -91,7 +93,8 @@ FactoryBot.define do }, "engine_name": "rubocop", "fingerprint": "ab5f8b935886b942d621399f5a2ca16e", - "severity": "minor" + "severity": "minor", + "web_url": "http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10" }.with_indifferent_access end end diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb index bf8e64fa1e2..9892d6fec1e 100644 --- a/spec/features/groups/members/sort_members_spec.rb +++ b/spec/features/groups/members/sort_members_spec.rb @@ -18,7 +18,7 @@ RSpec.describe 'Groups > Members > Sort members', :js do def expect_sort_by(text, sort_direction) within('[data-testid="members-sort-dropdown"]') do - expect(page).to have_css('button[aria-haspopup="true"]', text: text) + expect(page).to have_css('button[aria-haspopup="menu"]', text: text) expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}") end end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index e749c555dcf..fe591d7fe3a 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -140,14 +140,10 @@ RSpec.describe 'New/edit issue', :js do end expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible - click_button 'Milestone' - page.within '.issue-milestone' do - click_link milestone.title - end + click_button 'Select milestone' + click_button milestone.title expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title - end + expect(page).to have_button milestone.title click_button 'Labels' page.within '.dropdown-menu-labels' do @@ -307,14 +303,11 @@ RSpec.describe 'New/edit issue', :js do end it 'escapes milestone' do - click_button 'Milestone' + click_button 'Select milestone' + click_button milestone.title page.within '.issue-milestone' do - click_link milestone.title - end - - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title + expect(page).to have_button milestone.title expect(page).not_to have_selector 'img' end end @@ -444,9 +437,7 @@ RSpec.describe 'New/edit issue', :js do expect(page).to have_content user.name end - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title - end + expect(page).to have_button milestone.title click_button 'Labels' page.within '.dropdown-menu-labels' do diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb index b96490bd7e7..1d023a15159 100644 --- a/spec/features/issues/user_creates_issue_spec.rb +++ b/spec/features/issues/user_creates_issue_spec.rb @@ -188,7 +188,7 @@ RSpec.describe "User creates issue" do end it 'does not hide the milestone select' do - expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]') + expect(page).to have_button 'Select milestone' end end @@ -204,7 +204,7 @@ RSpec.describe "User creates issue" do end it 'shows the milestone select' do - expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]') + expect(page).to have_button 'Select milestone' end it 'hides the incident help text' do @@ -265,7 +265,7 @@ RSpec.describe "User creates issue" do end it 'shows the milestone select' do - expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]') + expect(page).to have_button 'Select milestone' end it 'hides the weight input' do diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb index 8aadd6302d0..3fc5a1258e4 100644 --- a/spec/features/projects/members/sorting_spec.rb +++ b/spec/features/projects/members/sorting_spec.rb @@ -148,7 +148,7 @@ RSpec.describe 'Projects > Members > Sorting', :js do def expect_sort_by(text, sort_direction) within('[data-testid="members-sort-dropdown"]') do - expect(page).to have_css('button[aria-haspopup="true"]', text: text) + expect(page).to have_css('button[aria-haspopup="menu"]', text: text) expect(page).to have_button("Sorting Direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}") end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 66916aea8d7..2d729af513a 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -64,7 +64,9 @@ RSpec.describe 'Pipeline', :js do let_it_be(:group) { create(:group) } let_it_be(:project, reload: true) { create(:project, :repository, group: group) } - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) } + let(:pipeline) do + create(:ci_pipeline, name: 'Build pipeline', project: project, ref: 'master', sha: project.commit.id, user: user) + end subject(:visit_pipeline) { visit project_pipeline_path(project, pipeline) } @@ -96,6 +98,45 @@ RSpec.describe 'Pipeline', :js do end end + context 'with pipeline_name feature flag enabled' do + before do + stub_feature_flags(pipeline_name: true) + end + + it 'displays pipeline name instead of commit title' do + visit_pipeline + + within 'h3' do + expect(page).to have_content(pipeline.name) + end + + within '.well-segment[data-testid="commit-row"]' do + expect(page).to have_content(project.commit.title) + expect(page).to have_content(project.commit.short_id) + end + end + end + + context 'with pipeline_name feature flag disabled' do + before do + stub_feature_flags(pipeline_name: false) + end + + it 'displays commit title' do + visit_pipeline + + within 'h3' do + expect(page).not_to have_content(pipeline.name) + expect(page).to have_content(project.commit.title) + end + + within '.well-segment[data-testid="commit-row"]' do + expect(page).not_to have_content(project.commit.title) + expect(page).to have_content(project.commit.short_id) + end + end + end + describe 'related merge requests' do context 'when there are no related merge requests' do it 'shows a "no related merge requests" message' do diff --git a/spec/fixtures/api/schemas/entities/codequality_degradation.json b/spec/fixtures/api/schemas/entities/codequality_degradation.json index 6cf20ee8b9e..863b9f0c77e 100644 --- a/spec/fixtures/api/schemas/entities/codequality_degradation.json +++ b/spec/fixtures/api/schemas/entities/codequality_degradation.json @@ -18,7 +18,10 @@ }, "line": { "type": "integer" + }, + "web_url": { + "type": "string" } }, "additionalProperties": false -} +}
\ No newline at end of file diff --git a/spec/fixtures/api/schemas/entities/codequality_reports_comparer.json b/spec/fixtures/api/schemas/entities/codequality_reports_comparer.json index afe82f5e632..05ae036d986 100644 --- a/spec/fixtures/api/schemas/entities/codequality_reports_comparer.json +++ b/spec/fixtures/api/schemas/entities/codequality_reports_comparer.json @@ -1,6 +1,12 @@ { "type": "object", - "required": ["status", "summary", "new_errors", "resolved_errors", "existing_errors"], + "required": [ + "status", + "summary", + "new_errors", + "resolved_errors", + "existing_errors" + ], "properties": { "status": { "type": "string" @@ -18,7 +24,11 @@ "type": "integer" } }, - "required": ["total", "resolved", "errored"] + "required": [ + "total", + "resolved", + "errored" + ] }, "new_errors": { "type": "array", @@ -40,4 +50,4 @@ } }, "additionalProperties": false -} +}
\ No newline at end of file diff --git a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap index 2bd2b17a12d..42818c14029 100644 --- a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap +++ b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap @@ -21,6 +21,7 @@ exports[`~/access_tokens/components/expires_at_field should render datepicker wi mindate="Mon Jul 06 2020 00:00:00 GMT+0000 (Greenwich Mean Time)" placeholder="YYYY-MM-DD" showclearbutton="true" + size="medium" theme="" /> </gl-form-group-stub> diff --git a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap index 656e72baf77..21ffda8578a 100644 --- a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap +++ b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap @@ -3,7 +3,7 @@ exports[`NewCluster renders the cluster component correctly 1`] = ` "<div class=\\"gl-pt-4\\"> <h4>Enter your Kubernetes cluster certificate details</h4> - <p>Enter details about your cluster. <b-link-stub href=\\"/help/user/project/clusters/add_existing_cluster\\" event=\\"click\\" routertag=\\"a\\" class=\\"gl-link\\">How do I use a certificate to connect to my cluster?</b-link-stub> + <p>Enter details about your cluster. <b-link-stub href=\\"/help/user/project/clusters/add_existing_cluster\\" class=\\"gl-link\\">How do I use a certificate to connect to my cluster?</b-link-stub> </p> </div>" `; diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap index a63cca006da..b8e6bcbc3c4 100644 --- a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap +++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`content_editor/components/toolbar_button displays tertiary, medium button with a provided label and icon 1`] = ` -"<b-button-stub size=\\"md\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\"> +"<b-button-stub size=\\"md\\" tag=\\"button\\" type=\\"button\\" variant=\\"default\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\"> <!----> <gl-icon-stub name=\\"bold\\" size=\\"16\\" class=\\"gl-button-icon\\"></gl-icon-stub> <!----> diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js index e1563a7bb3a..e540e8856ba 100644 --- a/spec/frontend/invite_members/components/group_select_spec.js +++ b/spec/frontend/invite_members/components/group_select_spec.js @@ -33,7 +33,7 @@ describe('GroupSelect', () => { const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="true"]'); + const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="menu"]'); const findAvatarByLabel = (text) => wrapper .findAllComponents(GlAvatarLabeled) diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap index a72528ae36b..b2b3a93a11d 100644 --- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap +++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap @@ -2,7 +2,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] = ` <table - aria-busy="false" + aria-busy="" aria-colcount="3" class="table b-table gl-table b-table-fixed" role="table" @@ -92,7 +92,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] = <!----> <button aria-expanded="false" - aria-haspopup="true" + aria-haspopup="menu" class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle" type="button" > @@ -223,7 +223,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] = <!----> <button aria-expanded="false" - aria-haspopup="true" + aria-haspopup="menu" class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle" type="button" > diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js index ef3c8bde3cf..526f839ece8 100644 --- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js +++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js @@ -46,7 +46,7 @@ describe('SortDropdown', () => { const findSortingComponent = () => wrapper.findComponent(GlSorting); const findSortDirectionToggle = () => findSortingComponent().find('button[title^="Sort direction"]'); - const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]'); + const findDropdownToggle = () => wrapper.find('button[aria-haspopup="menu"]'); const findDropdownItemByText = (text) => wrapper .findAllComponents(GlSortingItem) diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js index b254cce4d72..3835d3fd33d 100644 --- a/spec/frontend/members/components/table/role_dropdown_spec.js +++ b/spec/frontend/members/components/table/role_dropdown_spec.js @@ -60,7 +60,7 @@ describe('RoleDropdown', () => { .findAllComponents(GlDropdownItem) .wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('isChecked')); - const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]'); + const findDropdownToggle = () => wrapper.find('button[aria-haspopup="menu"]'); const findDropdown = () => wrapper.findComponent(GlDropdown); afterEach(() => { diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap index a33528d2d91..9bd435badc4 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap @@ -48,9 +48,7 @@ exports[`packages_list_app renders 1`] = ` Learn how to <b-link-stub class="gl-link" - event="click" href="helpUrl" - routertag="a" target="_blank" > publish and share your packages diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap index 92c2cd90568..84b37824390 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap @@ -20,7 +20,7 @@ exports[`PypiInstallation renders all the messages 1`] = ` <!----> <button aria-expanded="false" - aria-haspopup="true" + aria-haspopup="menu" class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle" id="__BVID__27__BV_toggle_" type="button" @@ -59,7 +59,6 @@ exports[`PypiInstallation renders all the messages 1`] = ` </div> <fieldset - aria-describedby="installation-pip-command-group__BV_description_" class="form-group gl-form-group" id="installation-pip-command-group" > @@ -75,12 +74,7 @@ exports[`PypiInstallation renders all the messages 1`] = ` <!----> </legend> - <div - aria-labelledby="installation-pip-command-group__BV_label_" - class="bv-no-focus-ring" - role="group" - tabindex="-1" - > + <div> <div data-testid="pip-command" id="installation-pip-command" diff --git a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap index 7759c366796..b279a7d0936 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap @@ -94,9 +94,7 @@ exports[`PackagesListApp renders 1`] = ` Learn how to <b-link-stub class="gl-link" - event="click" href="/help/user/packages/package_registry/index" - routertag="a" target="_blank" > publish and share your packages diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js index 26e04903163..36bce65dd56 100644 --- a/spec/frontend/pipelines/mock_data.js +++ b/spec/frontend/pipelines/mock_data.js @@ -730,6 +730,7 @@ export const mockPipelineTag = () => { }, active: false, source: 'push', + name: 'Build pipeline', created_at: '2022-02-02T15:39:04.012Z', updated_at: '2022-02-02T15:40:59.573Z', path: '/root/mr-widgets/-/pipelines/311', @@ -955,6 +956,7 @@ export const mockPipelineBranch = () => { }, active: false, source: 'push', + name: 'Build pipeline', created_at: '2022-01-14T17:40:27.866Z', updated_at: '2022-01-14T18:02:35.850Z', path: '/root/mr-widgets/-/pipelines/268', diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js index 1d66607e72b..c62898f0c83 100644 --- a/spec/frontend/pipelines/pipeline_url_spec.js +++ b/spec/frontend/pipelines/pipeline_url_spec.js @@ -1,3 +1,4 @@ +import { merge } from 'lodash'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue'; @@ -20,6 +21,7 @@ describe('Pipeline Url Component', () => { const findCommitRefName = () => wrapper.findByTestId('commit-ref-name'); const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container'); + const findPipelineNameContainer = () => wrapper.findByTestId('pipeline-name-container'); const findCommitTitle = (commitWrapper) => commitWrapper.find('[data-testid="commit-title"]'); const defaultProps = mockPipeline(projectPath); @@ -51,7 +53,16 @@ describe('Pipeline Url Component', () => { expect(findPipelineUrlLink().text()).toBe('#1'); }); - it('should render the commit title, commit reference and commit-short-sha', () => { + it('should render the pipeline name instead of commit title', () => { + createComponent(merge(mockPipeline(projectPath), { pipeline: { name: 'Build pipeline' } })); + + expect(findCommitTitleContainer().exists()).toBe(false); + expect(findPipelineNameContainer().exists()).toBe(true); + expect(findRefName().exists()).toBe(true); + expect(findCommitShortSha().exists()).toBe(true); + }); + + it('should render the commit title when pipeline has no name', () => { createComponent(); const commitWrapper = findCommitTitleContainer(); @@ -59,6 +70,7 @@ describe('Pipeline Url Component', () => { expect(findCommitTitle(commitWrapper).exists()).toBe(true); expect(findRefName().exists()).toBe(true); expect(findCommitShortSha().exists()).toBe(true); + expect(findPipelineNameContainer().exists()).toBe(false); }); describe('commit user avatar', () => { @@ -142,7 +154,7 @@ describe('Pipeline Url Component', () => { }); it('tracks commit title click', () => { - createComponent(mockPipelineBranch()); + createComponent(merge(mockPipelineBranch(), { pipeline: { name: null } })); findCommitTitle(findCommitTitleContainer()).vm.$emit('click'); diff --git a/spec/frontend/sidebar/components/milestone/bulk_update_milestone_dropdown_spec.js b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js index 03b2a3ca728..843ac1da4bb 100644 --- a/spec/frontend/sidebar/components/milestone/bulk_update_milestone_dropdown_spec.js +++ b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js @@ -3,10 +3,10 @@ import { nextTick } from 'vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { IssuableType, WorkspaceType } from '~/issues/constants'; import { __ } from '~/locale'; -import BulkUpdateMilestoneDropdown from '~/sidebar/components/milestone/bulk_update_milestone_dropdown.vue'; +import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue'; import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue'; -describe('BulkUpdateMilestoneDropdown component', () => { +describe('MilestoneDropdown component', () => { let wrapper; const propsData = { @@ -18,24 +18,24 @@ describe('BulkUpdateMilestoneDropdown component', () => { const findHiddenInput = () => wrapper.find('input'); const findSidebarDropdown = () => wrapper.findComponent(SidebarDropdown); - const createComponent = () => { - wrapper = shallowMount(BulkUpdateMilestoneDropdown, { propsData }); + const createComponent = (props = {}) => { + wrapper = shallowMount(MilestoneDropdown, { propsData: { ...propsData, ...props } }); }; - beforeEach(() => { + it('renders SidebarDropdown', () => { createComponent(); - }); - it('renders SidebarDropdown', () => { expect(findSidebarDropdown().props()).toMatchObject({ attrWorkspacePath: propsData.attrWorkspacePath, - issuableAttribute: BulkUpdateMilestoneDropdown.issuableAttribute, + issuableAttribute: MilestoneDropdown.issuableAttribute, issuableType: propsData.issuableType, workspaceType: propsData.workspaceType, }); }); it('renders hidden input', () => { + createComponent(); + expect(findHiddenInput().attributes()).toEqual({ type: 'hidden', name: 'update[milestone_id]', @@ -43,7 +43,26 @@ describe('BulkUpdateMilestoneDropdown component', () => { }); }); + describe('when milestone ID and title is provided', () => { + it('is used in the dropdown and hidden input', () => { + const milestone = { + id: 'gid://gitlab/Milestone/52', + title: __('Milestone 52'), + }; + createComponent({ milestoneId: milestone.id, milestoneTitle: milestone.title }); + + expect(findSidebarDropdown().props('currentAttribute')).toEqual(milestone); + expect(findHiddenInput().attributes('value')).toBe( + getIdFromGraphQLId(milestone.id).toString(), + ); + }); + }); + describe('when SidebarDropdown emits `change` event', () => { + beforeEach(() => { + createComponent(); + }); + describe('when valid milestone is emitted', () => { it('updates the hidden input value', async () => { const milestone = { diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap index 8c3a4978bb8..0ba327df297 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap +++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap @@ -84,7 +84,7 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have <!----> <button aria-expanded="false" - aria-haspopup="true" + aria-haspopup="menu" class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" type="button" > @@ -261,7 +261,7 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c <!----> <button aria-expanded="false" - aria-haspopup="true" + aria-haspopup="menu" class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" type="button" > diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap index bdf5ea23812..aaf0466f0ac 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap @@ -234,7 +234,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = ` <!----> <button aria-expanded="false" - aria-haspopup="true" + aria-haspopup="menu" class="btn dropdown-toggle btn-default btn-md add-reaction-button btn-icon gl-relative! gl-button gl-dropdown-toggle btn-default-secondary" id="__BVID__13__BV_toggle_" type="button" diff --git a/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb index 6a08e8f0b7f..1ef341ff863 100644 --- a/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb +++ b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do describe '#parse!' do - subject(:parse) { described_class.new.parse!(code_climate, codequality_report) } + subject(:parse) { described_class.new.parse!(code_climate, codequality_report, metadata) } let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new } let(:code_climate) do @@ -35,6 +35,15 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do ].to_json end + let_it_be(:group) { create(:group, name: 'test-group') } + let_it_be(:project) { create(:project, path: 'test-project', group: group) } + let(:metadata) do + { + project: project, + commit_sha: 'f0cc5229e2aa5e9429f1b17a3b3b102f21d7fe31' + } + end + context "when data is code_climate style JSON" do context "when there are no degradations" do let(:code_climate) { [].to_json } @@ -133,5 +142,56 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do expect(codequality_report.degradations_count).to eq(0) end end + + context 'for web_url' do + let(:code_climate) do + [ + { + "categories": [ + "Complexity" + ], + "check_name": "argument_count", + "content": { + "body": "" + }, + "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", + "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547", + "location": { + "path": "foo.rb", + "lines": { + "begin": 10, + "end": 10 + } + }, + "other_locations": [], + "remediation_points": 900000, + "severity": "major", + "type": "issue", + "engine_name": "structure" + } + ].to_json + end + + context 'when metadata has project and commit_sha' do + it 'adds a non nil url' do + want = 'http://localhost/test-group/test-project/-/blob/f0cc5229e2aa5e9429f1b17a3b3b102f21d7fe31/foo.rb#L10' + expect { parse }.not_to raise_error + + expect(codequality_report.degradations_count).to eq(1) + expect(codequality_report.all_degradations[0]['web_url']).to eq(want) + end + end + + context 'when metadata does not have project and commit_sha' do + let(:metadata) { {} } + + it 'adds a nil url' do + expect { parse }.not_to raise_error + + expect(codequality_report.degradations_count).to eq(1) + expect(codequality_report.all_degradations[0]['web_url']).to be_nil + end + end + end end end diff --git a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb index 8204b104832..43deb465025 100644 --- a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb @@ -12,7 +12,7 @@ RSpec.describe '5-Minute-Production-App.gitlab-ci.yml' do let(:default_branch) { 'master' } let(:pipeline_branch) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb index 65fd2b016ac..f2bff5ff3e0 100644 --- a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb @@ -11,7 +11,7 @@ RSpec.describe 'Deploy-ECS.gitlab-ci.yml' do let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } let(:user) { project.first_owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } let(:platform_target) { 'ECS' } diff --git a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb index cf27d185103..07cfa939623 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'Jobs/Build.gitlab-ci.yml' do let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb index 5b9e1a0d18d..acb296082b8 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb @@ -46,7 +46,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/MATLAB_spec.rb b/spec/lib/gitlab/ci/templates/MATLAB_spec.rb index 432040c4a14..3889d1fc8c9 100644 --- a/spec/lib/gitlab/ci/templates/MATLAB_spec.rb +++ b/spec/lib/gitlab/ci/templates/MATLAB_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'MATLAB.gitlab-ci.yml' do let(:default_branch) { 'master' } let(:pipeline_branch) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb index eca79f37779..42df924f8fd 100644 --- a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb @@ -11,7 +11,7 @@ RSpec.describe 'Terraform/Base.gitlab-ci.yml' do let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } let(:user) { project.first_owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb index 0ab81f97f20..332708ffa13 100644 --- a/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb @@ -11,7 +11,7 @@ RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } let(:user) { project.first_owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb index d6c7cd32f79..0f0192ad38f 100644 --- a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb @@ -25,7 +25,7 @@ RSpec.describe 'Verify/Load-Performance-Testing.gitlab-ci.yml' do let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb index 1a909f52ec3..b2ca906e172 100644 --- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb @@ -17,7 +17,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } let(:user) { project.first_owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb index de94eec09fe..afb7773ad7a 100644 --- a/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'Flutter.gitlab-ci.yml' do let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } let(:user) { project.first_owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb index ebf52e6d65a..62e4188f59b 100644 --- a/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'Kaniko.gitlab-ci.yml' do let(:project) { create(:project, :custom_repo, files: { 'Dockerfile' => 'FROM alpine:latest' }) } let(:user) { project.first_owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb index 5a62324da74..a44833b0c01 100644 --- a/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb @@ -27,7 +27,7 @@ RSpec.describe 'Katalon.gitlab-ci.yml' do let(:user) { project.first_owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: 'master' ) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb index 9795d6d6848..aa7d0249066 100644 --- a/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'Terraform.gitlab-ci.yml' do let_it_be(:project) { create(:project, :repository, create_branch: 'patch-1') } let(:user) { project.first_owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb index ecc6feedf14..6ae51f9783b 100644 --- a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do let_it_be(:project) { create(:project, :repository, create_branch: 'patch-1') } let_it_be(:user) { project.first_owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb index 6938ad51189..03dd4e7b89b 100644 --- a/spec/lib/gitlab/git_ref_validator_spec.rb +++ b/spec/lib/gitlab/git_ref_validator_spec.rb @@ -35,6 +35,8 @@ RSpec.describe Gitlab::GitRefValidator do it { expect(described_class.validate('.tag')).to be false } it { expect(described_class.validate('my branch')).to be false } it { expect(described_class.validate("\xA0\u0000\xB0")).to be false } + it { expect(described_class.validate("")).to be false } + it { expect(described_class.validate(nil)).to be false } end describe '.validate_merge_request_branch' do @@ -67,5 +69,7 @@ RSpec.describe Gitlab::GitRefValidator do it { expect(described_class.validate_merge_request_branch('.tag')).to be false } it { expect(described_class.validate_merge_request_branch('my branch')).to be false } it { expect(described_class.validate_merge_request_branch("\xA0\u0000\xB0")).to be false } + it { expect(described_class.validate_merge_request_branch("")).to be false } + it { expect(described_class.validate_merge_request_branch(nil)).to be false } end end diff --git a/spec/lib/gitlab/metrics/loose_foreign_keys_slis_spec.rb b/spec/lib/gitlab/metrics/loose_foreign_keys_slis_spec.rb new file mode 100644 index 00000000000..58740278425 --- /dev/null +++ b/spec/lib/gitlab/metrics/loose_foreign_keys_slis_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::Metrics::LooseForeignKeysSlis do + # This needs to be dynamic because db_config_names depends on + # config/database.yml and the specs need to work for all configurations. That + # means this assertion is a copy of the implementation. + let(:possible_labels) do + ::Gitlab::Database.db_config_names.map do |db_config_name| + { + db_config_name: db_config_name, + feature_category: :database + } + end + end + + describe '#initialize_slis!' do + it 'initializes Apdex and ErrorRate SLIs for loose_foreign_key_clean_ups' do + expect(::Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with( + :loose_foreign_key_clean_ups, + possible_labels + ) + + expect(::Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with( + :loose_foreign_key_clean_ups, + possible_labels + ) + + described_class.initialize_slis! + end + end + + describe '#record_apdex' do + context 'with success: true' do + it 'increments the loose_foreign_key_clean_ups Apdex as a success' do + expect(Gitlab::Metrics::Sli::Apdex[:loose_foreign_key_clean_ups]).to receive(:increment).with( + labels: { feature_category: :database, db_config_name: 'main' }, + success: true + ) + + described_class.record_apdex(success: true, db_config_name: 'main') + end + end + + context 'with success: false' do + it 'increments the loose_foreign_key_clean_ups Apdex as not a success' do + expect(Gitlab::Metrics::Sli::Apdex[:loose_foreign_key_clean_ups]).to receive(:increment).with( + labels: { feature_category: :database, db_config_name: 'main' }, + success: false + ) + + described_class.record_apdex(success: false, db_config_name: 'main') + end + end + end + + describe '#record_error_rate' do + context 'with error: true' do + it 'increments the loose_foreign_key_clean_ups ErrorRate as an error' do + expect(Gitlab::Metrics::Sli::ErrorRate[:loose_foreign_key_clean_ups]).to receive(:increment).with( + labels: { feature_category: :database, db_config_name: 'main' }, + error: true + ) + + described_class.record_error_rate(error: true, db_config_name: 'main') + end + end + + context 'with error: false' do + it 'increments the loose_foreign_key_clean_ups ErrorRate as not an error' do + expect(Gitlab::Metrics::Sli::ErrorRate[:loose_foreign_key_clean_ups]).to receive(:increment).with( + labels: { feature_category: :database, db_config_name: 'main' }, + error: false + ) + + described_class.record_error_rate(error: false, db_config_name: 'main') + end + end + end +end diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb index cc730e203f6..09548d21106 100644 --- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb @@ -41,10 +41,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Server, :clean_gitlab_r describe '#call' do it 'removes the stored job from redis before execution' do bare_job = { 'class' => 'TestDeduplicationWorker', 'args' => ['hello'] } - job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'default') + job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'test_deduplication') expect(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob) - .to receive(:new).with(a_hash_including(bare_job), 'default') + .to receive(:new).with(a_hash_including(bare_job), 'test_deduplication') .and_return(job_definition).twice # once in client middleware expect(job_definition).to receive(:delete!).ordered.and_call_original @@ -60,10 +60,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Server, :clean_gitlab_r it 'removes the stored job from redis after execution' do bare_job = { 'class' => 'TestDeduplicationWorker', 'args' => ['hello'] } - job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'default') + job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'test_deduplication') expect(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob) - .to receive(:new).with(a_hash_including(bare_job), 'default') + .to receive(:new).with(a_hash_including(bare_job), 'test_deduplication') .and_return(job_definition).twice # once in client middleware expect(TestDeduplicationWorker).to receive(:work).ordered.and_call_original diff --git a/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb b/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb index c66e36c5621..9ed2a0642fc 100644 --- a/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb +++ b/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb @@ -54,7 +54,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do expect(migrator.migrate_set(set_name)).to eq(scanned: 3, migrated: 0) expect(set_after.length).to eq(3) - expect(set_after.map(&:first)).to all(include('queue' => 'default', + expect(set_after.map(&:first)).to all(include('queue' => 'authorized_projects', 'class' => 'AuthorizedProjectsWorker')) end end @@ -73,7 +73,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do if item['class'] == 'AuthorizedProjectsWorker' expect(item).to include('queue' => 'new_queue', 'args' => [i]) else - expect(item).to include('queue' => 'default', 'args' => [i]) + expect(item).to include('queue' => 'post_receive', 'args' => [i]) end expect(score).to be_within(schedule_jitter).of(i.succ.hours.from_now.to_i) @@ -134,7 +134,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do expect(migrator.migrate_set(set_name)).to eq(scanned: 4, migrated: 0) expect(set_after.length).to eq(3) - expect(set_after.map(&:first)).to all(include('queue' => 'default')) + expect(set_after.map(&:first)).to all(include('queue' => 'authorized_projects')) end end @@ -157,7 +157,7 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do expect(migrator.migrate_set(set_name)).to eq(scanned: 4, migrated: 1) expect(set_after.group_by { |job| job.first['queue'] }.transform_values(&:count)) - .to eq('default' => 6, 'new_queue' => 1) + .to eq('authorized_projects' => 6, 'new_queue' => 1) end it 'iterates through the entire set of jobs' do diff --git a/spec/lib/gitlab/sidekiq_queue_spec.rb b/spec/lib/gitlab/sidekiq_queue_spec.rb index 93632848788..5e91282612e 100644 --- a/spec/lib/gitlab/sidekiq_queue_spec.rb +++ b/spec/lib/gitlab/sidekiq_queue_spec.rb @@ -4,15 +4,15 @@ require 'spec_helper' RSpec.describe Gitlab::SidekiqQueue, :clean_gitlab_redis_queues do around do |example| - Sidekiq::Queue.new('foobar').clear + Sidekiq::Queue.new('default').clear Sidekiq::Testing.disable!(&example) - Sidekiq::Queue.new('foobar').clear + Sidekiq::Queue.new('default').clear end def add_job(args, user:, klass: 'AuthorizedProjectsWorker') Sidekiq::Client.push( 'class' => klass, - 'queue' => 'foobar', + 'queue' => 'default', 'args' => args, 'meta.user' => user.username ) @@ -20,7 +20,7 @@ RSpec.describe Gitlab::SidekiqQueue, :clean_gitlab_redis_queues do describe '#drop_jobs!' do shared_examples 'queue processing' do - let(:sidekiq_queue) { described_class.new('foobar') } + let(:sidekiq_queue) { described_class.new('default') } let_it_be(:sidekiq_queue_user) { create(:user) } before do @@ -80,7 +80,7 @@ RSpec.describe Gitlab::SidekiqQueue, :clean_gitlab_redis_queues do it 'raises NoMetadataError' do add_job([1], user: create(:user)) - expect { described_class.new('foobar').drop_jobs!({ username: 'sidekiq_queue_user' }, timeout: 1) } + expect { described_class.new('default').drop_jobs!({ username: 'sidekiq_queue_user' }, timeout: 1) } .to raise_error(described_class::NoMetadataError) end end diff --git a/spec/migrations/20221103150250_migrate_sidekiq_queued_jobs_spec.rb b/spec/migrations/20221103150250_migrate_sidekiq_queued_jobs_spec.rb index 1c8bedadc0a..46ca200a170 100644 --- a/spec/migrations/20221103150250_migrate_sidekiq_queued_jobs_spec.rb +++ b/spec/migrations/20221103150250_migrate_sidekiq_queued_jobs_spec.rb @@ -36,17 +36,17 @@ RSpec.describe MigrateSidekiqQueuedJobs, :clean_gitlab_redis_queues do end context 'without worker_queue_mappings mocked' do - it 'migration still works' do - # Assuming Settings.sidekiq.routing_rules is [['*', 'default']] - # If routing_rules or Gitlab::SidekiqConfig.worker_queue_mappings changed, + 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(0) - expect(queue_length('default')).to eq(2) + expect(queue_length('email_receiver')).to eq(2) + expect(queue_length('default')).to eq(0) - jobs = list_jobs('default') + jobs = list_jobs('email_receiver') expect(jobs[0]).to include("class" => "EmailReceiverWorker", "args" => ["bar"]) expect(jobs[1]).to include("class" => "EmailReceiverWorker", "args" => ["foo"]) end @@ -62,6 +62,8 @@ RSpec.describe MigrateSidekiqQueuedJobs, :clean_gitlab_redis_queues do 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}") diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 6e8312b6785..e1bb1195813 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1620,8 +1620,8 @@ RSpec.describe Ci::Build do end describe 'environment' do - describe '#has_environment?' do - subject { build.has_environment? } + describe '#has_environment_keyword?' do + subject { build.has_environment_keyword? } context 'when environment is defined' do before do diff --git a/spec/rubocop/cop/rspec/factory_bot/strategy_in_callback_spec.rb b/spec/rubocop/cop/rspec/factory_bot/strategy_in_callback_spec.rb new file mode 100644 index 00000000000..9dcfebc7c1d --- /dev/null +++ b/spec/rubocop/cop/rspec/factory_bot/strategy_in_callback_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'rubocop_spec_helper' + +require_relative '../../../../../rubocop/cop/rspec/factory_bot/strategy_in_callback' + +RSpec.describe RuboCop::Cop::RSpec::FactoryBot::StrategyInCallback do + shared_examples 'an offensive factory call' do |namespace| + described_class::FORBIDDEN_METHODS.each do |forbidden_method| + namespaced_forbidden_method = "#{namespace}#{forbidden_method}(:ci_job_artifact, :archive)" + + it "registers an offence for multiple #{namespaced_forbidden_method} calls" do + expect_offense(<<-RUBY) + FactoryBot.define do + factory :ci_build, class: 'Ci::Build', parent: :ci_processable do + trait :artifacts do + before(:create) do + #{namespaced_forbidden_method} + #{'^' * namespaced_forbidden_method.size} Prefer inline `association` over `#{forbidden_method}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories + end + + after(:create) do |build| + #{namespaced_forbidden_method} + #{'^' * namespaced_forbidden_method.size} Prefer inline `association` over `#{forbidden_method}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories + #{namespaced_forbidden_method} + #{'^' * namespaced_forbidden_method.size} Prefer inline `association` over `#{forbidden_method}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories + end + end + end + end + RUBY + end + + it "registers an offense for #{namespaced_forbidden_method} when is a send node" do + expect_offense(<<-RUBY) + FactoryBot.define do + factory :ci_build, class: 'Ci::Build', parent: :ci_processable do + trait :artifacts do + after(:create) do |build| + #{namespaced_forbidden_method} + #{'^' * namespaced_forbidden_method.size} Prefer inline `association` over `#{forbidden_method}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories + end + end + end + end + RUBY + end + + it "registers an offense for #{namespaced_forbidden_method} when is assigned" do + expect_offense(<<-RUBY) + FactoryBot.define do + factory :ci_build, class: 'Ci::Build', parent: :ci_processable do + trait :artifacts do + after(:create) do |build| + ci_build = #{namespaced_forbidden_method} + #{'^' * namespaced_forbidden_method.size} Prefer inline `association` over `#{forbidden_method}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories + + ci_build + end + end + end + end + RUBY + end + end + end + + it_behaves_like 'an offensive factory call', '' + it_behaves_like 'an offensive factory call', 'FactoryBot.' + it_behaves_like 'an offensive factory call', '::FactoryBot.' +end diff --git a/spec/serializers/ci/pipeline_entity_spec.rb b/spec/serializers/ci/pipeline_entity_spec.rb index 7889ac03f8f..ff364918b4f 100644 --- a/spec/serializers/ci/pipeline_entity_spec.rb +++ b/spec/serializers/ci/pipeline_entity_spec.rb @@ -15,12 +15,13 @@ RSpec.describe Ci::PipelineEntity do subject { entity.as_json } context 'when pipeline is empty' do - let(:pipeline) { create(:ci_empty_pipeline) } + let(:pipeline) { create(:ci_empty_pipeline, name: 'Build pipeline') } it 'contains required fields' do expect(subject).to include :id, :iid, :user, :path, :coverage, :source expect(subject).to include :ref, :commit expect(subject).to include :updated_at, :created_at + expect(subject[:name]).to eq('Build pipeline') end it 'excludes coverage data when disabled' do @@ -48,6 +49,16 @@ RSpec.describe Ci::PipelineEntity do .to include :stuck, :auto_devops, :yaml_errors, :retryable, :cancelable, :merge_request end + + context 'when pipeline_name feature flag is disabled' do + before do + stub_feature_flags(pipeline_name: false) + end + + it 'does not return name' do + is_expected.not_to include(:name) + end + end end context 'when default branch not protected' do diff --git a/spec/serializers/codequality_degradation_entity_spec.rb b/spec/serializers/codequality_degradation_entity_spec.rb index f56420bfdbd..0390e232fd5 100644 --- a/spec/serializers/codequality_degradation_entity_spec.rb +++ b/spec/serializers/codequality_degradation_entity_spec.rb @@ -17,6 +17,7 @@ RSpec.describe CodequalityDegradationEntity do expect(subject[:severity]).to eq("major") expect(subject[:file_path]).to eq("file_a.rb") expect(subject[:line]).to eq(10) + expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_a.rb#L10") end end @@ -28,6 +29,7 @@ RSpec.describe CodequalityDegradationEntity do expect(subject[:severity]).to eq("minor") expect(subject[:file_path]).to eq("file_b.rb") expect(subject[:line]).to eq(10) + expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10") end end @@ -43,6 +45,7 @@ RSpec.describe CodequalityDegradationEntity do expect(subject[:severity]).to eq("minor") expect(subject[:file_path]).to eq("file_b.rb") expect(subject[:line]).to eq(10) + expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10") end end end diff --git a/spec/serializers/merge_requests/pipeline_entity_spec.rb b/spec/serializers/merge_requests/pipeline_entity_spec.rb index 31ebe738743..a8f4fc44f10 100644 --- a/spec/serializers/merge_requests/pipeline_entity_spec.rb +++ b/spec/serializers/merge_requests/pipeline_entity_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe MergeRequests::PipelineEntity do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } - let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, name: 'Build pipeline') } let(:request) { double('request') } @@ -29,7 +29,7 @@ RSpec.describe MergeRequests::PipelineEntity do is_expected.to include( :id, :path, :active, :coverage, :ref, :commit, :details, - :flags, :triggered, :triggered_by + :flags, :triggered, :triggered_by, :name ) expect(subject[:commit]).to include(:short_id, :commit_path) expect(subject[:ref]).to include(:branch) @@ -51,5 +51,15 @@ RSpec.describe MergeRequests::PipelineEntity do expect(entity.as_json).not_to include(:coverage) end + + context 'when pipeline_name feature flag is disabled' do + before do + stub_feature_flags(pipeline_name: false) + end + + it 'does not return name' do + is_expected.not_to include(:name) + end + end end end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index cd6b8c77291..9caaeb3450b 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -238,6 +238,7 @@ RSpec.describe PipelineSerializer do create(:ci_empty_pipeline, project: project, status: status, + name: 'Build pipeline', ref: 'feature').tap do |pipeline| Ci::Build::AVAILABLE_STATUSES.each do |build_status| create_build(pipeline, status, build_status) diff --git a/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb b/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb new file mode 100644 index 00000000000..1824f822ba8 --- /dev/null +++ b/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe LooseForeignKeys::ProcessDeletedRecordsService do + include MigrationsHelpers + + def create_table_structure + migration = ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers) + + migration.create_table :_test_loose_fk_parent_table_1 + migration.create_table :_test_loose_fk_parent_table_2 + + migration.create_table :_test_loose_fk_child_table_1_1 do |t| + t.bigint :parent_id + end + + migration.create_table :_test_loose_fk_child_table_1_2 do |t| + t.bigint :parent_id_with_different_column + end + + migration.create_table :_test_loose_fk_child_table_2_1 do |t| + t.bigint :parent_id + end + + migration.track_record_deletions(:_test_loose_fk_parent_table_1) + migration.track_record_deletions(:_test_loose_fk_parent_table_2) + end + + let(:all_loose_foreign_key_definitions) do + { + '_test_loose_fk_parent_table_1' => [ + ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new( + '_test_loose_fk_child_table_1_1', + '_test_loose_fk_parent_table_1', + { + column: 'parent_id', + on_delete: :async_delete, + gitlab_schema: :gitlab_main + } + ), + ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new( + '_test_loose_fk_child_table_1_2', + '_test_loose_fk_parent_table_1', + { + column: 'parent_id_with_different_column', + on_delete: :async_nullify, + gitlab_schema: :gitlab_main + } + ) + ], + '_test_loose_fk_parent_table_2' => [ + ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new( + '_test_loose_fk_child_table_2_1', + '_test_loose_fk_parent_table_2', + { + column: 'parent_id', + on_delete: :async_delete, + gitlab_schema: :gitlab_main + } + ) + ] + } + end + + let(:connection) { ::ApplicationRecord.connection } + + let(:loose_fk_parent_table_1) { table(:_test_loose_fk_parent_table_1) } + let(:loose_fk_parent_table_2) { table(:_test_loose_fk_parent_table_2) } + let(:loose_fk_child_table_1_1) { table(:_test_loose_fk_child_table_1_1) } + let(:loose_fk_child_table_1_2) { table(:_test_loose_fk_child_table_1_2) } + let(:loose_fk_child_table_2_1) { table(:_test_loose_fk_child_table_2_1) } + + before(:all) do + create_table_structure + end + + after(:all) do + migration = ActiveRecord::Migration.new + + migration.drop_table :_test_loose_fk_parent_table_1 + migration.drop_table :_test_loose_fk_parent_table_2 + migration.drop_table :_test_loose_fk_child_table_1_1 + migration.drop_table :_test_loose_fk_child_table_1_2 + migration.drop_table :_test_loose_fk_child_table_2_1 + end + + before do + allow(Gitlab::Database::LooseForeignKeys).to receive(:definitions_by_table) + .and_return(all_loose_foreign_key_definitions) + + parent_record_1 = loose_fk_parent_table_1.create! + loose_fk_child_table_1_1.create!(parent_id: parent_record_1.id) + loose_fk_child_table_1_2.create!(parent_id_with_different_column: parent_record_1.id) + + parent_record_2 = loose_fk_parent_table_1.create! + 2.times { loose_fk_child_table_1_1.create!(parent_id: parent_record_2.id) } + 3.times { loose_fk_child_table_1_2.create!(parent_id_with_different_column: parent_record_2.id) } + + parent_record_3 = loose_fk_parent_table_2.create! + 5.times { loose_fk_child_table_2_1.create!(parent_id: parent_record_3.id) } + + loose_fk_parent_table_1.delete_all + loose_fk_parent_table_2.delete_all + end + + describe '#execute' do + def execute + ::Gitlab::Database::SharedModel.using_connection(connection) do + described_class.new(connection: connection).execute + end + end + + it 'cleans up all rows' do + execute + + expect(loose_fk_child_table_1_1.count).to eq(0) + expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4) + expect(loose_fk_child_table_2_1.count).to eq(0) + end + + it 'returns stats for records cleaned up' do + stats = execute + + expect(stats[:delete_count]).to eq(8) + expect(stats[:update_count]).to eq(4) + end + + it 'records the Apdex as success: true' do + expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_apdex) + .with(success: true, db_config_name: 'main') + + execute + end + + it 'records the error rate as error: false' do + expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_error_rate) + .with(error: false, db_config_name: 'main') + + execute + end + + context 'when the amount of records to clean up exceeds BATCH_SIZE' do + before do + stub_const('LooseForeignKeys::CleanupWorker::BATCH_SIZE', 2) + end + + it 'cleans up everything over multiple batches' do + expect(LooseForeignKeys::BatchCleanerService).to receive(:new).exactly(:twice).and_call_original + + execute + + expect(loose_fk_child_table_1_1.count).to eq(0) + expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4) + expect(loose_fk_child_table_2_1.count).to eq(0) + end + end + + context 'when the amount of records to clean up exceeds the total MAX_DELETES' do + def count_deletable_rows + loose_fk_child_table_1_1.count + loose_fk_child_table_2_1.count + end + + before do + stub_const('LooseForeignKeys::ModificationTracker::MAX_DELETES', 2) + stub_const('LooseForeignKeys::CleanerService::DELETE_LIMIT', 1) + end + + it 'cleans up MAX_DELETES and leaves the rest for the next run' do + expect { execute }.to change { count_deletable_rows }.by(-2) + expect(count_deletable_rows).to be > 0 + end + + it 'records the Apdex as success: false' do + expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_apdex) + .with(success: false, db_config_name: 'main') + + execute + end + end + + context 'when cleanup raises an error' do + before do + expect_next_instance_of(::LooseForeignKeys::BatchCleanerService) do |service| + allow(service).to receive(:execute).and_raise("Something broke") + end + end + + it 'records the error rate as error: true and does not increment apdex' do + expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_error_rate) + .with(error: true, db_config_name: 'main') + expect(::Gitlab::Metrics::LooseForeignKeysSlis).not_to receive(:record_apdex) + + expect { execute }.to raise_error("Something broke") + end + end + end +end diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb index b1c6623308e..bbf6fe62959 100644 --- a/spec/services/tags/create_service_spec.rb +++ b/spec/services/tags/create_service_spec.rb @@ -27,6 +27,26 @@ RSpec.describe Tags::CreateService do end end + context 'when tag_name is empty' do + it 'returns an error' do + response = service.execute('', 'foo', 'Foo') + + expect(response[:status]).to eq(:error) + expect(response[:http_status]).to eq(400) + expect(response[:message]).to eq('Tag name invalid') + end + end + + context 'when target is empty' do + it 'returns an error' do + response = service.execute('v1.1.0', '', 'Foo') + + expect(response[:status]).to eq(:error) + expect(response[:http_status]).to eq(400) + expect(response[:message]).to eq('Target is empty') + end + end + context 'when tag already exists' do it 'returns an error' do expect(repository).to receive(:add_tag) diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 7ab519032fa..8022d4127b9 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -3441,6 +3441,7 @@ - './ee/spec/workers/concerns/elastic/indexing_control_spec.rb' - './ee/spec/workers/concerns/elastic/migration_obsolete_spec.rb' - './ee/spec/workers/concerns/elastic/migration_options_spec.rb' +- './ee/spec/workers/concerns/geo_queue_spec.rb' - './ee/spec/workers/concerns/update_orchestration_policy_configuration_spec.rb' - './ee/spec/workers/create_github_webhook_worker_spec.rb' - './ee/spec/workers/deployments/auto_rollback_worker_spec.rb' @@ -10858,14 +10859,18 @@ - './spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb' - './spec/workers/concerns/application_worker_spec.rb' - './spec/workers/concerns/cluster_agent_queue_spec.rb' +- './spec/workers/concerns/cluster_queue_spec.rb' - './spec/workers/concerns/cronjob_queue_spec.rb' - './spec/workers/concerns/gitlab/github_import/object_importer_spec.rb' +- './spec/workers/concerns/gitlab/github_import/queue_spec.rb' - './spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb' - './spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb' - './spec/workers/concerns/gitlab/notify_upon_death_spec.rb' - './spec/workers/concerns/limited_capacity/job_tracker_spec.rb' - './spec/workers/concerns/limited_capacity/worker_spec.rb' - './spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb' +- './spec/workers/concerns/pipeline_background_queue_spec.rb' +- './spec/workers/concerns/pipeline_queue_spec.rb' - './spec/workers/concerns/project_import_options_spec.rb' - './spec/workers/concerns/reenqueuer_spec.rb' - './spec/workers/concerns/repository_check_queue_spec.rb' diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb index 456175e7113..2cfe353d5d7 100644 --- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb @@ -21,15 +21,10 @@ RSpec.shared_examples 'a creatable merge request' do expect(page).to have_content user.name end - click_button 'Milestone' - page.within '.issue-milestone' do - click_link milestone.title - end - + click_button 'Select milestone' + click_button milestone.title expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title - end + expect(page).to have_button milestone.title click_button 'Labels' page.within '.dropdown-menu-labels' do diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index 2fff4137934..ea6d1655694 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -20,14 +20,10 @@ RSpec.shared_examples 'an editable merge request' do expect(page).to have_content user.name end - click_button 'Milestone' - page.within '.issue-milestone' do - click_link milestone.title - end + click_button 'Select milestone' + click_button milestone.title expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title - end + expect(page).to have_button milestone.title click_button 'Labels' page.within '.dropdown-menu-labels' do diff --git a/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb index 4b4a7f4ce9d..a2a4c57d62e 100644 --- a/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb @@ -52,7 +52,7 @@ RSpec.shared_examples 'acts as branch pipeline' do |jobs| context 'when branch pipeline' do let(:pipeline_branch) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } it 'includes a job' do expect(pipeline.builds.pluck(:name)).to match_array(jobs) diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb index 215d404e395..05ac037c9cb 100644 --- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -45,7 +45,7 @@ RSpec.describe 'projects/merge_requests/edit.html.haml' do expect(rendered).to have_field('merge_request[title]') expect(rendered).to have_field('merge_request[description]') expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false) - expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) + expect(rendered).to have_selector('.js-milestone-dropdown') expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false) end end @@ -57,7 +57,7 @@ RSpec.describe 'projects/merge_requests/edit.html.haml' do expect(rendered).to have_field('merge_request[title]') expect(rendered).to have_field('merge_request[description]') expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false) - expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) + expect(rendered).to have_selector('.js-milestone-dropdown') expect(rendered).to have_selector('#merge_request_target_branch', visible: false) end end diff --git a/spec/workers/concerns/cluster_agent_queue_spec.rb b/spec/workers/concerns/cluster_agent_queue_spec.rb index 4f67102a0be..b5189cbd8c8 100644 --- a/spec/workers/concerns/cluster_agent_queue_spec.rb +++ b/spec/workers/concerns/cluster_agent_queue_spec.rb @@ -14,5 +14,6 @@ RSpec.describe ClusterAgentQueue do end end + it { expect(worker.queue).to eq('cluster_agent:example') } it { expect(worker.get_feature_category).to eq(:kubernetes_management) } end diff --git a/spec/workers/concerns/cluster_queue_spec.rb b/spec/workers/concerns/cluster_queue_spec.rb new file mode 100644 index 00000000000..c03ca9cea48 --- /dev/null +++ b/spec/workers/concerns/cluster_queue_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ClusterQueue do + let(:worker) do + Class.new do + def self.name + 'DummyWorker' + end + + include ApplicationWorker + include ClusterQueue + end + end + + it 'sets a default pipelines queue automatically' do + expect(worker.sidekiq_options['queue']) + .to eq 'gcp_cluster:dummy' + end +end diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb index 7dd016fc78a..0244535051f 100644 --- a/spec/workers/concerns/cronjob_queue_spec.rb +++ b/spec/workers/concerns/cronjob_queue_spec.rb @@ -40,6 +40,10 @@ RSpec.describe CronjobQueue do stub_const("AnotherWorker", another_worker) end + it 'sets the queue name of a worker' do + expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob:dummy') + end + it 'disables retrying of failed jobs' do expect(worker.sidekiq_options['retry']).to eq(false) end diff --git a/spec/workers/concerns/gitlab/github_import/queue_spec.rb b/spec/workers/concerns/gitlab/github_import/queue_spec.rb new file mode 100644 index 00000000000..beca221b593 --- /dev/null +++ b/spec/workers/concerns/gitlab/github_import/queue_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Queue do + it 'sets the Sidekiq options for the worker' do + worker = Class.new do + def self.name + 'DummyWorker' + end + + include ApplicationWorker + include Gitlab::GithubImport::Queue + end + + expect(worker.sidekiq_options['queue']).to eq('github_importer:dummy') + end +end diff --git a/spec/workers/concerns/pipeline_background_queue_spec.rb b/spec/workers/concerns/pipeline_background_queue_spec.rb new file mode 100644 index 00000000000..77c7e7440c5 --- /dev/null +++ b/spec/workers/concerns/pipeline_background_queue_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe PipelineBackgroundQueue do + let(:worker) do + Class.new do + def self.name + 'DummyWorker' + end + + include ApplicationWorker + include PipelineBackgroundQueue + end + end + + it 'sets a default object storage queue automatically' do + expect(worker.sidekiq_options['queue']) + .to eq 'pipeline_background:dummy' + end +end diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb new file mode 100644 index 00000000000..6c1ac2052e4 --- /dev/null +++ b/spec/workers/concerns/pipeline_queue_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe PipelineQueue do + let(:worker) do + Class.new do + def self.name + 'DummyWorker' + end + + include ApplicationWorker + include PipelineQueue + end + end + + it 'sets a default pipelines queue automatically' do + expect(worker.sidekiq_options['queue']) + .to eq 'pipeline_default:dummy' + end +end diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb index 08ac73aac7b..ae377c09b37 100644 --- a/spec/workers/concerns/repository_check_queue_spec.rb +++ b/spec/workers/concerns/repository_check_queue_spec.rb @@ -14,6 +14,10 @@ RSpec.describe RepositoryCheckQueue do end end + it 'sets the queue name of a worker' do + expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check:dummy') + end + it 'disables retrying of failed jobs' do expect(worker.sidekiq_options['retry']).to eq(false) end diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb index 2df5b60deaf..bf156c3b8cb 100644 --- a/spec/workers/concerns/waitable_worker_spec.rb +++ b/spec/workers/concerns/waitable_worker_spec.rb @@ -49,7 +49,8 @@ RSpec.describe WaitableWorker do expect(Gitlab::AppJsonLogger).to( receive(:info).with(a_hash_including('message' => 'running inline', 'class' => 'Gitlab::Foo::Bar::DummyWorker', - 'job_status' => 'running')) + 'job_status' => 'running', + 'queue' => 'foo_bar_dummy')) .once) worker.bulk_perform_and_wait(args_list) diff --git a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb index e01a391e277..09d58a1189e 100644 --- a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb +++ b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb @@ -125,37 +125,6 @@ RSpec.describe LooseForeignKeys::CleanupWorker do expect(loose_fk_child_table_2_1.count).to eq(0) end - context 'when deleting in batches' do - before do - stub_const('LooseForeignKeys::CleanupWorker::BATCH_SIZE', 2) - end - - it 'cleans up all rows' do - expect(LooseForeignKeys::BatchCleanerService).to receive(:new).exactly(:twice).and_call_original - - perform_for(db: :main) - - expect(loose_fk_child_table_1_1.count).to eq(0) - expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4) - expect(loose_fk_child_table_2_1.count).to eq(0) - end - end - - context 'when the deleted rows count limit have been reached' do - def count_deletable_rows - loose_fk_child_table_1_1.count + loose_fk_child_table_2_1.count - end - - before do - stub_const('LooseForeignKeys::ModificationTracker::MAX_DELETES', 2) - stub_const('LooseForeignKeys::CleanerService::DELETE_LIMIT', 1) - end - - it 'cleans up 2 rows' do - expect { perform_for(db: :main) }.to change { count_deletable_rows }.by(-2) - end - end - describe 'multi-database support' do where(:current_minute, :configured_base_models, :expected_connection_model) do 2 | { main: 'ActiveRecord::Base', ci: 'Ci::ApplicationRecord' } | 'ActiveRecord::Base' diff --git a/spec/workers/ssh_keys/expired_notification_worker_spec.rb b/spec/workers/ssh_keys/expired_notification_worker_spec.rb index f93d02e86c0..26d9460d73e 100644 --- a/spec/workers/ssh_keys/expired_notification_worker_spec.rb +++ b/spec/workers/ssh_keys/expired_notification_worker_spec.rb @@ -7,6 +7,7 @@ RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do it 'uses a cronjob queue' do expect(worker.sidekiq_options_hash).to include( + 'queue' => 'cronjob:ssh_keys_expired_notification', 'queue_namespace' => :cronjob ) end diff --git a/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb b/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb index ed6701532a5..e907d035020 100644 --- a/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb +++ b/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb @@ -7,6 +7,7 @@ RSpec.describe SshKeys::ExpiringSoonNotificationWorker, type: :worker do it 'uses a cronjob queue' do expect(worker.sidekiq_options_hash).to include( + 'queue' => 'cronjob:ssh_keys_expiring_soon_notification', 'queue_namespace' => :cronjob ) end diff --git a/yarn.lock b/yarn.lock index 406fde7c772..6385a61f0f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1113,13 +1113,13 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.7.0.tgz#1257b69fb9898ea5614f992aa6b6dc3619c3c38c" integrity sha512-6vTqWZzY63ZUTUqk0dmMDcfU27qtkAu0WmlK4e3FMWmISvTxNhAk2j11c/YlLauf6okE4W2T2fnhvXp1mzcPgA== -"@gitlab/ui@49.3.0": - version "49.3.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-49.3.0.tgz#63e6a375d66c6f6ae568f0d1a08fe0e9bd4e355b" - integrity sha512-c8GSajEdW2Q1ME7lYuQgImR493WaELKJOq/T+1zVs3i82cc1YDWbGEJyKZh6srJ6xNSLuIbn6d7oSqfM/jeSAQ== +"@gitlab/ui@49.5.1": + version "49.5.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-49.5.1.tgz#aae75d633059110395caa452967c8d439c6e204f" + integrity sha512-2/m9VZw42kY4Bdrsfs7QbjXFL9r3bXbpagQnKC/msR5YcOScB3TXs7Vr/Hnzv+6xoOHEhGMySrL0Wd4EqaBv+Q== dependencies: "@popperjs/core" "^2.11.2" - bootstrap-vue "2.20.1" + bootstrap-vue "2.23.1" dompurify "^2.4.0" echarts "^5.3.2" iframe-resizer "^4.3.2" @@ -3118,22 +3118,27 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -bootstrap-vue@2.20.1: - version "2.20.1" - resolved "https://registry.yarnpkg.com/bootstrap-vue/-/bootstrap-vue-2.20.1.tgz#1b6cd4368632c1a6dd4a5ed161242baa131c3cd5" - integrity sha512-s+w83q0T2mo/RbFwTM8gExbLJMEOYpdTUqmyFaHv2Ir+TFprMvTWpeAzeNuawJ130W1gePZ3LW3cNp1t/tZbOw== +bootstrap-vue@2.23.1: + version "2.23.1" + resolved "https://registry.yarnpkg.com/bootstrap-vue/-/bootstrap-vue-2.23.1.tgz#8f866f7cda27eb0e7e13a0bea8d55d8fc7a82199" + integrity sha512-SEWkG4LzmMuWjQdSYmAQk1G/oOKm37dtNfjB5kxq0YafnL2W6qUAmeDTcIZVbPiQd2OQlIkWOMPBRGySk/zGsg== dependencies: "@nuxt/opencollective" "^0.3.2" - bootstrap ">=4.5.3 <5.0.0" + bootstrap "^4.6.1" popper.js "^1.16.1" portal-vue "^2.1.7" vue-functional-data-merge "^3.1.0" -bootstrap@4.5.3, "bootstrap@>=4.5.3 <5.0.0": +bootstrap@4.5.3: version "4.5.3" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.3.tgz#c6a72b355aaf323920be800246a6e4ef30997fe6" integrity sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ== +bootstrap@^4.6.1: + version "4.6.2" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.2.tgz#8e0cd61611728a5bf65a3a2b8d6ff6c77d5d7479" + integrity sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" |