diff options
51 files changed, 1010 insertions, 454 deletions
diff --git a/.markdownlint.yml b/.markdownlint.yml index dd04406f68d..ccf2ef02eee 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -66,6 +66,7 @@ "Grafana", "Gzip", "Helm", + "HipChat", "ID", "Ingress", "jasmine-jquery", @@ -130,6 +131,7 @@ "Ubuntu", "Ultra Auth", "Unicorn", + "unicorn-worker-killer", "URL", "WebdriverIO", "YAML", diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue index 7b169b68d20..1137951ccfc 100644 --- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue +++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue @@ -5,14 +5,14 @@ import highlight from '~/lib/utils/highlight'; import { truncateNamespace } from '~/lib/utils/text_utility'; import { mapVuexModuleState } from '~/lib/utils/vuex_module_mappers'; import Tracking from '~/tracking'; -import Identicon from '~/vue_shared/components/identicon.vue'; +import ProjectAvatar from '~/vue_shared/components/project_avatar.vue'; const trackingMixin = Tracking.mixin(); export default { components: { - Identicon, GlButton, + ProjectAvatar, }, mixins: [trackingMixin], inject: ['vuexModule'], @@ -64,19 +64,12 @@ export default { class="gl-text-left gl-justify-content-start!" @click="track('click_link', { label: `${dropdownType}_dropdown_frequent_items_list_item` })" > - <div - ref="frequentItemsItemAvatarContainer" - class="frequent-items-item-avatar-container avatar-container rect-avatar s32" - > - <img v-if="avatarUrl" ref="frequentItemsItemAvatar" :src="avatarUrl" class="avatar s32" /> - <identicon - v-else - :entity-id="itemId" - :entity-name="itemName" - size-class="s32" - class="rect-avatar" - /> - </div> + <project-avatar + class="gl-float-left gl-mr-3" + :project-avatar-url="avatarUrl" + :project-name="itemName" + aria-hidden="true" + /> <div ref="frequentItemsItemMetadataContainer" class="frequent-items-item-metadata-container"> <div ref="frequentItemsItemTitle" diff --git a/app/assets/javascripts/jobs/components/log/collapsible_section.vue b/app/assets/javascripts/jobs/components/log/collapsible_section.vue index 55cdfb691f4..c0d5fac0e8d 100644 --- a/app/assets/javascripts/jobs/components/log/collapsible_section.vue +++ b/app/assets/javascripts/jobs/components/log/collapsible_section.vue @@ -1,4 +1,6 @@ <script> +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF } from '../../constants'; import LogLine from './line.vue'; import LogLineHeader from './line_header.vue'; @@ -7,7 +9,9 @@ export default { components: { LogLine, LogLineHeader, + CollapsibleLogSection: () => import('./collapsible_section.vue'), }, + mixins: [glFeatureFlagsMixin()], props: { section: { type: Object, @@ -22,6 +26,9 @@ export default { badgeDuration() { return this.section.line && this.section.line.section_duration; }, + infinitelyCollapsibleSectionsFlag() { + return this.glFeatures?.[INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF]; + }, }, methods: { handleOnClickCollapsibleLine(section) { @@ -40,12 +47,26 @@ export default { @toggleLine="handleOnClickCollapsibleLine(section)" /> <template v-if="!section.isClosed"> - <log-line - v-for="line in section.lines" - :key="line.offset" - :line="line" - :path="traceEndpoint" - /> + <template v-if="infinitelyCollapsibleSectionsFlag"> + <template v-for="line in section.lines"> + <collapsible-log-section + v-if="line.isHeader" + :key="line.line.offset" + :section="line" + :trace-endpoint="traceEndpoint" + @onClickCollapsibleLine="handleOnClickCollapsibleLine" + /> + <log-line v-else :key="line.offset" :line="line" :path="traceEndpoint" /> + </template> + </template> + <template v-else> + <log-line + v-for="line in section.lines" + :key="line.offset" + :line="line" + :path="traceEndpoint" + /> + </template> </template> </div> </template> diff --git a/app/assets/javascripts/jobs/components/log/line_number.vue b/app/assets/javascripts/jobs/components/log/line_number.vue index 7ca9154d2fe..c8ceac2c7ff 100644 --- a/app/assets/javascripts/jobs/components/log/line_number.vue +++ b/app/assets/javascripts/jobs/components/log/line_number.vue @@ -1,4 +1,6 @@ <script> +import { INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF } from '../../constants'; + export default { functional: true, props: { @@ -14,7 +16,9 @@ export default { render(h, { props }) { const { lineNumber, path } = props; - const parsedLineNumber = lineNumber + 1; + const parsedLineNumber = gon.features?.[INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF] + ? lineNumber + : lineNumber + 1; const lineId = `L${parsedLineNumber}`; const lineHref = `${path}#${lineId}`; diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js index 3040d4e2379..97f31eee57c 100644 --- a/app/assets/javascripts/jobs/constants.js +++ b/app/assets/javascripts/jobs/constants.js @@ -24,3 +24,5 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = { }; export const SUCCESS_STATUS = 'SUCCESS'; + +export const INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF = 'infinitelyCollapsibleSections'; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index 924b811d0d6..4045d8a0c16 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -1,6 +1,7 @@ import Vue from 'vue'; +import { INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF } from '../constants'; import * as types from './mutation_types'; -import { logLinesParser, updateIncrementalTrace } from './utils'; +import { logLinesParser, logLinesParserLegacy, updateIncrementalTrace } from './utils'; export default { [types.SET_JOB_ENDPOINT](state, endpoint) { @@ -20,12 +21,26 @@ export default { }, [types.RECEIVE_TRACE_SUCCESS](state, log = {}) { + const infinitelyCollapsibleSectionsFlag = + gon.features?.[INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF]; if (log.state) { state.traceState = log.state; } if (log.append) { - state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace; + if (infinitelyCollapsibleSectionsFlag) { + if (log.lines) { + const parsedResult = logLinesParser( + log.lines, + state.auxiliaryPartialTraceHelpers, + state.trace, + ); + state.trace = parsedResult.parsedLines; + state.auxiliaryPartialTraceHelpers = parsedResult.auxiliaryPartialTraceHelpers; + } + } else { + state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace; + } state.traceSize += log.size; } else { @@ -33,7 +48,14 @@ export default { // the trace response will not have a defined // html or size. We keep the old value otherwise these // will be set to `null` - state.trace = log.lines ? logLinesParser(log.lines) : state.trace; + + if (infinitelyCollapsibleSectionsFlag) { + const parsedResult = logLinesParser(log.lines); + state.trace = parsedResult.parsedLines; + state.auxiliaryPartialTraceHelpers = parsedResult.auxiliaryPartialTraceHelpers; + } else { + state.trace = log.lines ? logLinesParserLegacy(log.lines) : state.trace; + } state.traceSize = log.size || state.traceSize; } diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js index 2fe945b2985..718324c8bad 100644 --- a/app/assets/javascripts/jobs/store/state.js +++ b/app/assets/javascripts/jobs/store/state.js @@ -30,4 +30,7 @@ export default () => ({ selectedStage: '', stages: [], jobs: [], + + // to parse partial logs + auxiliaryPartialTraceHelpers: {}, }); diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js index a0e0a0fb8bd..36391a4d433 100644 --- a/app/assets/javascripts/jobs/store/utils.js +++ b/app/assets/javascripts/jobs/store/utils.js @@ -104,7 +104,7 @@ export const getIncrementalLineNumber = (acc) => { * @param Array accumulator * @returns Array parsed log lines */ -export const logLinesParser = (lines = [], accumulator = []) => +export const logLinesParserLegacy = (lines = [], accumulator = []) => lines.reduce( (acc, line, index) => { const lineNumber = accumulator.length > 0 ? getIncrementalLineNumber(acc) : index; @@ -131,6 +131,77 @@ export const logLinesParser = (lines = [], accumulator = []) => [...accumulator], ); +export const logLinesParser = (lines = [], previousTraceState = {}, prevParsedLines = []) => { + let currentLine = previousTraceState?.prevLineCount ?? 0; + let currentHeader = previousTraceState?.currentHeader; + let isPreviousLineHeader = previousTraceState?.isPreviousLineHeader ?? false; + const parsedLines = prevParsedLines.length > 0 ? prevParsedLines : []; + const sectionsQueue = previousTraceState?.sectionsQueue ?? []; + + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + // First run we can use the current index, later runs we have to retrieve the last number of lines + currentLine = previousTraceState?.prevLineCount ? currentLine + 1 : i + 1; + + if (line.section_header && !isPreviousLineHeader) { + // If there's no previous line header that means we're at the root of the log + + isPreviousLineHeader = true; + parsedLines.push(parseHeaderLine(line, currentLine)); + currentHeader = { index: parsedLines.length - 1 }; + } else if (line.section_header && isPreviousLineHeader) { + // If there's a current section, we can't push to the parsedLines array + sectionsQueue.push(currentHeader); + currentHeader = parseHeaderLine(line, currentLine); // Let's parse the incoming header line + } else if (line.section && !line.section_duration) { + // We're inside a collapsible section and want to parse a standard line + if (currentHeader?.index) { + // If the current section header is only an index, add the line as part of the lines + // array of the current collapsible section + parsedLines[currentHeader.index].lines.push(parseLine(line, currentLine)); + } else { + // Otherwise add it to the innermost collapsible section lines array + currentHeader.lines.push(parseLine(line, currentLine)); + } + } else if (line.section && line.section_duration) { + // NOTE: This marks the end of a section_header + const previousSection = sectionsQueue.pop(); + + // Add the duration to section header + // If at the root, just push the end to the current parsedLine, + // otherwise, push it to the previous sections queue + if (currentHeader?.index) { + parsedLines[currentHeader.index].line.section_duration = line.section_duration; + isPreviousLineHeader = false; + currentHeader = null; + } else { + currentHeader.line.section_duration = line.section_duration; + + if (previousSection && previousSection?.index) { + // Is the previous section on root? + parsedLines[previousSection.index].lines.push(currentHeader); + } else if (previousSection && !previousSection?.index) { + previousSection.lines.push(currentHeader); + } + + currentHeader = previousSection; + } + } else { + parsedLines.push(parseLine(line, currentLine)); + } + } + + return { + parsedLines, + auxiliaryPartialTraceHelpers: { + isPreviousLineHeader, + currentHeader, + sectionsQueue, + prevLineCount: lines.length, + }, + }; +}; + /** * Finds the repeated offset, removes the old one * @@ -177,5 +248,5 @@ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => { export const updateIncrementalTrace = (newLog = [], oldParsed = []) => { const parsedLog = findOffsetAndRemove(newLog, oldParsed); - return logLinesParser(newLog, parsedLog); + return logLinesParserLegacy(newLog, parsedLog); }; diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js index d65be6bc69e..6dd21380bec 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js @@ -1,3 +1,3 @@ import initForm from '../shared/init_form'; -document.addEventListener('DOMContentLoaded', initForm); +initForm(); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js index d65be6bc69e..6dd21380bec 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js @@ -1,3 +1,3 @@ import initForm from '../shared/init_form'; -document.addEventListener('DOMContentLoaded', initForm); +initForm(); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js index d65be6bc69e..6dd21380bec 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js @@ -1,3 +1,3 @@ import initForm from '../shared/init_form'; -document.addEventListener('DOMContentLoaded', initForm); +initForm(); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js index d65be6bc69e..6dd21380bec 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js @@ -1,3 +1,3 @@ import initForm from '../shared/init_form'; -document.addEventListener('DOMContentLoaded', initForm); +initForm(); diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue index 06b6d91a216..e14b3b17fa8 100644 --- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue +++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue @@ -1,6 +1,6 @@ <script> import { cloneDeep } from 'lodash'; -import { __, s__ } from '~/locale'; +import { formatNumber, sprintf, __, s__ } from '~/locale'; import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; @@ -58,6 +58,10 @@ export default { type: String, required: true, }, + activeRunnersCount: { + type: Number, + required: true, + }, }, data() { // filtered_search_bar_root.vue may mutate the inital @@ -119,6 +123,11 @@ export default { }, ]; }, + activeRunnersMessage() { + return sprintf(__('Runners currently online: %{active_runners_count}'), { + active_runners_count: formatNumber(this.activeRunnersCount), + }); + }, }, methods: { onFilter(filters) { @@ -144,16 +153,20 @@ export default { }; </script> <template> - <filtered-search - v-bind="$attrs" - :namespace="namespace" - recent-searches-storage-key="runners-search" - :sort-options="$options.sortOptions" - :initial-filter-value="initialFilterValue" - :initial-sort-by="initialSortBy" - :tokens="searchTokens" - :search-input-placeholder="__('Search or filter results...')" - @onFilter="onFilter" - @onSort="onSort" - /> + <div> + <filtered-search + v-bind="$attrs" + :namespace="namespace" + recent-searches-storage-key="runners-search" + :sort-options="$options.sortOptions" + :initial-filter-value="initialFilterValue" + :initial-sort-by="initialSortBy" + :tokens="searchTokens" + :search-input-placeholder="__('Search or filter results...')" + data-testid="runners-filtered-search" + @onFilter="onFilter" + @onSort="onSort" + /> + <div class="gl-text-right" data-testid="active-runners-message">{{ activeRunnersMessage }}</div> + </div> </template> diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue index a3efd51cab5..69a1f106ca8 100644 --- a/app/assets/javascripts/runner/components/runner_list.vue +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -1,7 +1,7 @@ <script> import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { formatNumber, sprintf, __, s__ } from '~/locale'; +import { formatNumber, __, s__ } from '~/locale'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { RUNNER_JOB_COUNT_LIMIT } from '../constants'; import RunnerActionsCell from './cells/runner_actions_cell.vue'; @@ -52,17 +52,6 @@ export default { type: Array, required: true, }, - activeRunnersCount: { - type: Number, - required: true, - }, - }, - computed: { - activeRunnersMessage() { - return sprintf(__('Runners currently online: %{active_runners_count}'), { - active_runners_count: formatNumber(this.activeRunnersCount), - }); - }, }, methods: { formatProjectCount(projectCount) { @@ -101,12 +90,12 @@ export default { </script> <template> <div> - <div class="gl-text-right" data-testid="active-runners-message">{{ activeRunnersMessage }}</div> <gl-table :busy="loading" :items="runners" :fields="$options.fields" :tbody-tr-attr="runnerTrAttr" + data-testid="runner-list" stacked="md" fixed > diff --git a/app/assets/javascripts/runner/runner_list/index.js b/app/assets/javascripts/runner/runner_list/index.js index 5eba14a7948..16616f00d1e 100644 --- a/app/assets/javascripts/runner/runner_list/index.js +++ b/app/assets/javascripts/runner/runner_list/index.js @@ -12,7 +12,8 @@ export const initRunnerList = (selector = '#js-runner-list') => { return null; } - // TODO `activeRunnersCount` should be implemented using a GraphQL API. + // TODO `activeRunnersCount` should be implemented using a GraphQL API + // https://gitlab.com/gitlab-org/gitlab/-/issues/333806 const { activeRunnersCount, registrationToken, runnerInstallHelpPage } = el.dataset; const apolloProvider = new VueApollo({ diff --git a/app/assets/javascripts/runner/runner_list/runner_list_app.vue b/app/assets/javascripts/runner/runner_list/runner_list_app.vue index 8977d2a2a3d..8d39243d609 100644 --- a/app/assets/javascripts/runner/runner_list/runner_list_app.vue +++ b/app/assets/javascripts/runner/runner_list/runner_list_app.vue @@ -116,17 +116,17 @@ export default { </div> </div> - <runner-filtered-search-bar v-model="search" namespace="admin_runners" /> + <runner-filtered-search-bar + v-model="search" + namespace="admin_runners" + :active-runners-count="activeRunnersCount" + /> <div v-if="noRunnersFound" class="gl-text-center gl-p-5"> {{ __('No runners found') }} </div> <template v-else> - <runner-list - :runners="runners.items" - :loading="runnersLoading" - :active-runners-count="activeRunnersCount" - /> + <runner-list :runners="runners.items" :loading="runnersLoading" /> <runner-pagination v-model="search.pagination" :page-info="runners.pageInfo" /> </template> </div> diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 20d8d7d9469..144a396ea65 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -933,13 +933,9 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { } .frequent-items-list-item-container { - .frequent-items-item-avatar-container, - .frequent-items-item-metadata-container { - flex-shrink: 0; - } - .frequent-items-item-metadata-container { display: flex; + flex-shrink: 0; flex-direction: column; justify-content: center; } @@ -951,12 +947,6 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { white-space: nowrap; } - &:hover { - .frequent-items-item-avatar-container .avatar { - border-color: $gray-50; - } - } - .frequent-items-item-title { font-size: $gl-font-size; font-weight: 400; diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index 92442fd4e28..49687a50ff6 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -17,6 +17,10 @@ class Projects::JobsController < Projects::ApplicationController before_action :verify_proxy_request!, only: :proxy_websocket_authorize before_action :push_jobs_table_vue, only: [:index] + before_action do + push_frontend_feature_flag(:infinitely_collapsible_sections, @project, default_enabled: :yaml) + end + layout 'project' feature_category :continuous_integration diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb index a9a8201205e..31d68e65b23 100644 --- a/app/workers/repository_check/single_repository_worker.rb +++ b/app/workers/repository_check/single_repository_worker.rb @@ -46,7 +46,7 @@ module RepositoryCheck true rescue Gitlab::Git::Repository::GitError => e - Gitlab::RepositoryCheckLogger.error(e.message) + Gitlab::RepositoryCheckLogger.error("#{repository.full_path}: #{e.message}") false end diff --git a/config/feature_flags/development/infinitely_collapsible_sections.yml b/config/feature_flags/development/infinitely_collapsible_sections.yml new file mode 100644 index 00000000000..44f37c06d70 --- /dev/null +++ b/config/feature_flags/development/infinitely_collapsible_sections.yml @@ -0,0 +1,8 @@ +--- +name: infinitely_collapsible_sections +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65496 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335297 +milestone: '14.1' +type: development +group: group::pipeline execution +default_enabled: false diff --git a/doc/.vale/gitlab/Acronyms.yml b/doc/.vale/gitlab/Acronyms.yml index d7f5c750ec0..2077cdf9994 100644 --- a/doc/.vale/gitlab/Acronyms.yml +++ b/doc/.vale/gitlab/Acronyms.yml @@ -102,6 +102,7 @@ exceptions: - NFS - NGINX - NOTE + - NPM - NTP - ONLY - OSS diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt index d465767049f..885b7d88e7d 100644 --- a/doc/.vale/gitlab/spelling-exceptions.txt +++ b/doc/.vale/gitlab/spelling-exceptions.txt @@ -247,6 +247,7 @@ Helm Heroku Herokuish Hexo +HipChat hostname hostnames hotfix @@ -295,6 +296,7 @@ keytab keytabs Kibana Kinesis +Klar Knative Kramdown Kroki diff --git a/doc/api/index.md b/doc/api/index.md index f1059904ac3..feb96561ad6 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -492,7 +492,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab response. They have `rel` set to `prev`, `next`, `first`, or `last` and contain the relevant URL. Be sure to use these links instead of generating your own URLs. -For GitLab.com users, [some pagination headers may not be returned](../user/gitlab_com/index.md#pagination-response-headers). +For GitLab SaaS users, [some pagination headers may not be returned](../user/gitlab_com/index.md#pagination-response-headers). In the following cURL example, we limit the output to three items per page (`per_page=3`) and we request the second page (`page=2`) of [comments](notes.md) @@ -836,7 +836,7 @@ languages. For a complete list, visit the [GitLab website](https://about.gitlab. For administrator documentation on rate limit settings, see [Rate limits](../security/rate_limits.md). To find the settings that are specifically used by GitLab.com, see -[GitLab.com-specific rate limits](../user/gitlab_com/index.md#gitlabcom-specific-rate-limits). +[GitLab SaaS-specific rate limits](../user/gitlab_com/index.md#gitlab-saas-specific-rate-limits). ## Content type diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index 8184879a738..666c4d444d8 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -25,9 +25,9 @@ things manually. As with every job, you need to create a valid `.gitlab-ci.yml` describing the build environment. -Let's first specify the PHP image that is used for the job process -(you can read more about what an image means in the runner's lingo reading -about [Using Docker images](../docker/using_docker_images.md#what-is-an-image)). +Let's first specify the PHP image that is used for the job process. +(You can read more about what an image means in the runner's lingo reading +about [Using Docker images](../docker/using_docker_images.md#what-is-an-image).) Start by adding the image to your `.gitlab-ci.yml`: diff --git a/doc/development/documentation/structure.md b/doc/development/documentation/structure.md index ba550184892..ac934673ee2 100644 --- a/doc/development/documentation/structure.md +++ b/doc/development/documentation/structure.md @@ -164,8 +164,15 @@ In most cases, these pages are standalone. ### Tutorials A tutorial is an end-to-end walkthrough of a complex workflow or scenario. -It might include tasks across a variety of GitLab features, tools, and processes. -It does not cover core conceptual information. +In general, you might consider using a tutorial when: + +- The workflow requires a number of sequential steps where each step consists + of sub-steps. +- The steps cover a variety of GitLab features or third-party tools. + +Tutorials are learning aids that complement our core documentation. +They do not introduce new features. +Always use the primary [topic types](#documentation-topic-types) to document new features. Tutorials should be in this format: diff --git a/doc/development/packages.md b/doc/development/packages.md index fa0cdbef92e..45bcfe5e224 100644 --- a/doc/development/packages.md +++ b/doc/development/packages.md @@ -230,7 +230,7 @@ in your local development environment. #### File size limits Files uploaded to the GitLab Package Registry are [limited by format](../administration/instance_limits.md#package-registry-limits). -On GitLab.com, these are typically set to 5GB to help prevent timeout issues and abuse. +On GitLab SaaS, these are typically set to 5GB to help prevent timeout issues and abuse. When a new package type is added to the `Packages::Package` model, a size limit must be added similar to [this example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52639/diffs#382f879fb09b0212e3cedd99e6c46e2083867216), @@ -238,10 +238,10 @@ or the [related test](https://gitlab.com/gitlab-org/gitlab/-/blob/fe4ba437667813 must be updated if file size limits do not apply. The only reason a size limit does not apply is if the package format does not upload and store package files. -#### Rate Limits on GitLab.com +#### Rate Limits on GitLab SaaS Package manager clients can make rapid requests that exceed the -[GitLab.com standard API rate limits](../user/gitlab_com/index.md#gitlabcom-specific-rate-limits). +[GitLab SaaS standard API rate limits](../user/gitlab_com/index.md#gitlab-saas-specific-rate-limits). This results in a `429 Too Many Requests` error. We have opened a set of paths to allow higher rate limits. Unless it is not possible, diff --git a/doc/security/rate_limits.md b/doc/security/rate_limits.md index e698341b4b5..25d4717ec40 100644 --- a/doc/security/rate_limits.md +++ b/doc/security/rate_limits.md @@ -9,7 +9,7 @@ type: reference, howto NOTE: For GitLab.com, please see -[GitLab.com-specific rate limits](../user/gitlab_com/index.md#gitlabcom-specific-rate-limits). +[GitLab SaaS-specific rate limits](../user/gitlab_com/index.md#gitlab-saas-specific-rate-limits). Rate limiting is a common technique used to improve the security and durability of a web application. diff --git a/doc/user/admin_area/monitoring/background_migrations.md b/doc/user/admin_area/monitoring/background_migrations.md index 50593959710..eeedfa25469 100644 --- a/doc/user/admin_area/monitoring/background_migrations.md +++ b/doc/user/admin_area/monitoring/background_migrations.md @@ -4,7 +4,7 @@ group: Database info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Batched Background Migrations **(FREE SELF)** +# Batched background migrations **(FREE SELF)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51332) in GitLab 13.11. > - [Deployed behind a feature flag](../../../user/feature_flags.md), disabled by default. @@ -21,16 +21,19 @@ are created by GitLab developers and run automatically on upgrade. However, such limited in scope to help with migrating some `integer` database columns to `bigint`. This is needed to prevent integer overflow for some tables. -All migrations must be finished before upgrading GitLab. To check the status of the existing -migrations, execute this command: +## Check the status of background migrations **(FREE SELF)** -```ruby -Gitlab::Database::BackgroundMigration::BatchedMigration.pluck(:id, :table_name, :status) -``` +All migrations must have a `Finished` status before updating GitLab. To check the status of the existing +migrations: + +1. On the top bar, select **Menu >** **{admin}** **Admin**. +1. On the left sidebar, select **Monitoring > Background Migrations**. + + ![queued batched background migrations table](img/batched_background_migrations_queued_v14_0.png) -## Enable or disable Batched Background Migrations **(FREE SELF)** +## Enable or disable batched background migrations **(FREE SELF)** -Batched Background Migrations is under development but ready for production use. +Batched background migrations are under development but ready for production use. It is deployed behind a feature flag that is **enabled by default**. [GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) can opt to disable it. @@ -63,7 +66,7 @@ To maximize throughput of batched background migrations (in terms of the number ## Enable or disable automatic batch size optimization **(FREE SELF)** -Automatic batch size optimization for Batched Background Migrations is under development but ready for production use. +Automatic batch size optimization for batched background migrations is under development but ready for production use. It is deployed behind a feature flag that is **enabled by default**. [GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) can opt to disable it. @@ -79,3 +82,27 @@ To disable it: ```ruby Feature.disable(:optimize_batched_migrations) ``` + +## Troubleshooting + +### Database migrations failing because of batched background migration not finished + +When updating to GitLab 14.2 or later there might be a database migration failing with a message like: + +```plaintext +StandardError: An error has occurred, all later migrations canceled: + +Expected batched background migration for the given configuration to be marked as 'finished', but it is 'active': + {:job_class_name=>"CopyColumnUsingBackgroundMigrationJob", :table_name=>"push_event_payloads", :column_name=>"event_id", :job_arguments=>[["event_id"], ["event_id_convert_to_bigint"]]} +``` + +To fix this error: + +1. Update to either 14.0.3 or 14.1. +1. [Check the status](#check-the-status-of-background-migrations) of the batched background migration from the error message, and make sure it is listed as finished. If it is still active, either wait until it is done, or finalize it manually using the command suggested in the error, for example: + +```shell +sudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,push_event_payloads,event_id,'[["event_id"]\, ["event_id_convert_to_bigint"]]'] +``` + +1. You can now update to GitLab 14.2 or higher. diff --git a/doc/user/admin_area/monitoring/img/batched_background_migrations_queued_v14_0.png b/doc/user/admin_area/monitoring/img/batched_background_migrations_queued_v14_0.png Binary files differnew file mode 100644 index 00000000000..0b0792b5e7a --- /dev/null +++ b/doc/user/admin_area/monitoring/img/batched_background_migrations_queued_v14_0.png diff --git a/doc/user/discussions/img/commit_comment_mr_context.png b/doc/user/discussions/img/commit_comment_mr_context.png Binary files differdeleted file mode 100644 index 68f58a57757..00000000000 --- a/doc/user/discussions/img/commit_comment_mr_context.png +++ /dev/null diff --git a/doc/user/discussions/img/commit_comment_mr_discussions_tab.png b/doc/user/discussions/img/commit_comment_mr_discussions_tab.png Binary files differdeleted file mode 100644 index d88f08eae26..00000000000 --- a/doc/user/discussions/img/commit_comment_mr_discussions_tab.png +++ /dev/null diff --git a/doc/user/discussions/img/merge_request_commits_tab.png b/doc/user/discussions/img/merge_request_commits_tab.png Binary files differdeleted file mode 100644 index 267f3a720dc..00000000000 --- a/doc/user/discussions/img/merge_request_commits_tab.png +++ /dev/null diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index e7be08cab56..825f9be6ba6 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -20,7 +20,7 @@ In a comment, you can enter [Markdown](../markdown.md) and use [quick actions](. You can [suggest code changes](../project/merge_requests/reviews/suggestions.md) in your commit diff comment, which the user can accept through the user interface. -## Where you can create comments +## Places you can add comments You can create comments in places like: @@ -34,6 +34,141 @@ You can create comments in places like: Each object can have as many as 5,000 comments. +## Add a comment to a merge request diff + +You can add comments to a merge request diff. These comments +persist, even when you: + +- Force-push after a rebase. +- Amend a commit. + +To add a commit diff comment: + +1. To select a specific commit, on the merge request, select the **Commits** tab, select the commit + message. To view the latest commit, select the **Changes** tab. +1. By the line you want to comment on, hover over the line number and select **{comment}**. + You can select multiple lines by dragging the **{comment}** icon. +1. Type your comment and select **Start a review** or **Add comment now**. + +The comment is displayed on the merge request's **Discussions** tab. + +The comment is not displayed on your project's **Repository > Commits** page. + +NOTE: +When your comment contains a reference to a commit included in the merge request, +it's automatically converted to a link in the context of the current merge request. +For example, `28719b171a056960dfdc0012b625d0b47b123196` becomes +`https://gitlab.example.com/example-group/example-project/-/merge_requests/12345/diffs?commit_id=28719b171a056960dfdc0012b625d0b47b123196`. + +## Add a comment to a commit + +You can add comments and threads to a particular commit. + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Repository > Commits**. +1. Below the commits, in the **Comment** field, enter a comment. +1. Select **Comment** or select the down arrow (**{chevron-down}**) to select **Start thread**. + +WARNING: +Threads created this way are lost if the commit ID changes after a +force push. + +## Add a comment to an image + +In merge requests and commit detail views, you can add a comment to an image. +This comment can also be a thread. + +1. Hover your mouse over the image. +1. Select the location where you want to comment. + +An icon is displayed on the image and a comment field is displayed. + +![Start image thread](img/start_image_discussion.gif) + +## Reply to a comment by sending email + +If you have ["reply by email"](../../administration/reply_by_email.md) configured, +you can reply to comments by sending an email. + +- When you reply to a standard comment, another standard comment is created. +- When you reply to a threaded comment, it creates a reply in the thread. + +You can use [Markdown](../markdown.md) and [quick actions](../project/quick_actions.md) in your email replies. + +## Who can edit comments + +You can edit your own comment at any time. + +Anyone with the [Maintainer role](../permissions.md) or +higher can also edit a comment made by someone else. + +## Prevent comments by locking an issue + +You can prevent public comments in an issue or merge request. +When you do, only project members can add and edit comments. + +Prerequisite: + +- In merge requests, you must have at least the Developer role. +- In issues, you must have at least the Reporter role. + +1. On the right sidebar, next to **Lock issue** or **Lock merge request**, select **Edit**. +1. On the confirmation dialog, select **Lock**. + +Notes are added to the page details. + +If an issue or merge request is locked and closed, you cannot reopen it. + +## Mark a comment as confidential + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207473) in GitLab 13.9. +> - [Deployed behind a feature flag](../feature_flags.md), disabled by default. +> - Disabled on GitLab.com. +> - Not recommended for production use. +> - To use in GitLab self-managed instances, ask a GitLab administrator to enable it. **(FREE SELF)** + +WARNING: +This feature might not be available to you. Check the **version history** note above for details. + +You can make a comment confidential, so that it is visible only to project members +who have at least the Reporter role. + +1. Below the comment, select the **Make this comment confidential** checkbox. +1. Select **Comment**. + +![Confidential comments](img/confidential_comments_v13_9.png) + +## Show only comments + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/26723) in GitLab 11.5. + +For issues and merge requests with many comments, you can filter the page to show comments only. + +1. Open a merge request's **Discussion** tab, or epic or issue's **Overview** tab. +1. On the right side of the page, select from the filter: + - **Show all activity**: Display all user comments and system notes + (issue updates, mentions from other issues, changes to the description, and so on). + - **Show comments only**: Display only user comments. + - **Show history only**: Display only activity notes. + +![Notes filters dropdown options](img/index_notes_filters.png) + +GitLab saves your preference, so it persists when you visit the same page again +from any device you're logged into. + +## Assign an issue to the commenting user + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/191455) in GitLab 13.1. + +You can assign an issue to a user who made a comment. + +1. In the comment, select the **More Actions** menu. +1. Select **Assign to commenting user**. + +![Assign to commenting user](img/quickly_assign_commenter_v13_1.png) + +Select the button again to unassign the commenter. + ## Create a thread by replying to a standard comment > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30299) in GitLab 11.9. @@ -78,23 +213,6 @@ A threaded comment is created. ![Thread comment](img/discussion_comment.png) -## Reply to a comment by sending email - -If you have ["reply by email"](../../administration/reply_by_email.md) configured, -you can reply to comments by sending an email. - -- When you reply to a standard comment, another standard comment is created. -- When you reply to a threaded comment, it creates a reply in the thread. - -You can use [Markdown](../markdown.md) and [quick actions](../project/quick_actions.md) in your email replies. - -## Who can edit comments - -You can edit your own comment at any time. - -Anyone with the [Maintainer role](../permissions.md) or -higher can also edit a comment made by someone else. - ## Resolve a thread > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5022) in GitLab 8.11. @@ -172,133 +290,6 @@ with a new push. Threads are now resolved if a push makes a diff section outdated. Threads on lines that don't change and top-level resolvable threads are not resolved. -## Commit threads in the context of a merge request - -For reviewers with commit-based workflow, it may be useful to add threads to -specific commit diffs in the context of a merge request. These threads -persist through a commit ID change when: - -- force-pushing after a rebase -- amending a commit - -To create a commit diff thread: - -1. Navigate to the merge request **Commits** tab. A list of commits that - constitute the merge request are shown. - - ![Merge request commits tab](img/merge_request_commits_tab.png) - -1. Navigate to a specific commit, select the **Changes** tab (where you - are only be presented diffs from the selected commit), and leave a comment. - - ![Commit diff discussion in merge request context](img/commit_comment_mr_context.png) - -1. Any threads created this way are shown in the merge request's - **Discussions** tab and are resolvable. - - ![Merge request Discussions tab](img/commit_comment_mr_discussions_tab.png) - -Threads created this way only appear in the original merge request -and not when navigating to that commit under your project's -**Repository > Commits** page. - -NOTE: -When a link of a commit reference is found in a thread inside a merge -request, it is automatically converted to a link in the context of the -current merge request. - -## Add a comment to a commit - -You can add comments and threads to a particular commit. - -1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Repository > Commits**. -1. Below the commits, in the **Comment** field, enter a comment. -1. Select **Comment** or select the down arrow (**{chevron-down}**) to select **Start thread**. - -WARNING: -Threads created this way are lost if the commit ID changes after a -force push. - -## Add a comment to an image - -In merge requests and commit detail views, you can add a comment to an image. -This comment can also be a thread. - -1. Hover your mouse over the image. -1. Select the location where you want to comment. - -An icon is displayed on the image and a comment field is displayed. - -![Start image thread](img/start_image_discussion.gif) - -## Prevent comments by locking an issue - -You can prevent public comments in an issue or merge request. -When you do, only project members can add and edit comments. - -Prerequisite: - -- In merge requests, you must have at least the Developer role. -- In issues, you must have at least the Reporter role. - -1. On the right sidebar, next to **Lock issue** or **Lock merge request**, select **Edit**. -1. On the confirmation dialog, select **Lock**. - -Notes are added to the page details. - -If an issue or merge request is locked and closed, you cannot reopen it. - -## Mark a comment as confidential - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207473) in GitLab 13.9. -> - [Deployed behind a feature flag](../feature_flags.md), disabled by default. -> - Disabled on GitLab.com. -> - Not recommended for production use. -> - To use in GitLab self-managed instances, ask a GitLab administrator to enable it. **(FREE SELF)** - -WARNING: -This feature might not be available to you. Check the **version history** note above for details. - -You can make a comment confidential, so that it is visible only to project members -who have at least the Reporter role. - -1. Below the comment, select the **Make this comment confidential** checkbox. -1. Select **Comment**. - -![Confidential comments](img/confidential_comments_v13_9.png) - -## Show only comments - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/26723) in GitLab 11.5. - -For issues and merge requests with many comments, you can filter the page to show comments only. - -1. Open a merge request's **Discussion** tab, or epic or issue's **Overview** tab. -1. On the right side of the page, select from the filter: - - **Show all activity**: Display all user comments and system notes - (issue updates, mentions from other issues, changes to the description, and so on). - - **Show comments only**: Display only user comments. - - **Show history only**: Display only activity notes. - -![Notes filters dropdown options](img/index_notes_filters.png) - -GitLab saves your preference, so it persists when you visit the same page again -from any device you're logged into. - -## Assign an issue to the commenting user - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/191455) in GitLab 13.1. - -You can assign an issue to a user who made a comment. - -1. In the comment, select the **More Actions** menu. -1. Select **Assign to commenting user**. - -![Assign to commenting user](img/quickly_assign_commenter_v13_1.png) - -Select the button again to unassign the commenter. - ## Enable or disable confidential comments **(FREE SELF)** Confidential comments are under development and not ready for production use. The feature is diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 188c80d8992..8190c0e7416 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -4,15 +4,15 @@ group: unassigned info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# GitLab.com settings **(FREE SAAS)** +# GitLab SaaS settings **(FREE SAAS)** This page contains information about the settings that are used on -[GitLab.com](https://about.gitlab.com/pricing/). +[GitLab SaaS](https://about.gitlab.com/pricing/). ## SSH host keys fingerprints -Below are the fingerprints for GitLab.com's SSH host keys. The first time you -connect to a GitLab.com repository, one of these keys is displayed in the output. +Below are the fingerprints for GitLab SaaS's SSH host keys. The first time you +connect to a GitLab SaaS repository, one of these keys is displayed in the output. | Algorithm | MD5 (deprecated) | SHA256 | |------------------|------------------|---------| @@ -34,14 +34,14 @@ gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAA ## Mail configuration -GitLab.com sends emails from the `mg.gitlab.com` domain by using [Mailgun](https://www.mailgun.com/), +GitLab SaaS sends emails from the `mg.gitlab.com` domain by using [Mailgun](https://www.mailgun.com/), and has its own dedicated IP address (`192.237.158.143`). The IP address for `mg.gitlab.com` is subject to change at any time. ### Service Desk custom mailbox -On GitLab.com there's a mailbox configured for Service Desk with the email adress: +On GitLab SaaS, there's a mailbox configured for Service Desk with the email address: `contact-project+%{key}@incoming.gitlab.com`. To use this mailbox, configure the [custom suffix](../project/service_desk.md#configuring-a-custom-email-address-suffix) in project settings. @@ -50,7 +50,7 @@ settings. [See our backup strategy](https://about.gitlab.com/handbook/engineering/infrastructure/production/#backups). -To back up an entire project on GitLab.com, you can export it either: +To back up an entire project on GitLab SaaS, you can export it either: - [Through the UI](../project/settings/import_export.md). - [Through the API](../../api/project_import_export.md#schedule-an-export). You @@ -69,7 +69,7 @@ are included when cloning. ## Alternative SSH port -GitLab.com can be reached by using a [different SSH port](https://about.gitlab.com/blog/2016/02/18/gitlab-dot-com-now-supports-an-alternate-git-plus-ssh-port/) for `git+ssh`. +GitLab SaaS can be reached by using a [different SSH port](https://about.gitlab.com/blog/2016/02/18/gitlab-dot-com-now-supports-an-alternate-git-plus-ssh-port/) for `git+ssh`. | Setting | Value | |------------|---------------------| @@ -91,7 +91,7 @@ Host gitlab.com Below are the settings for [GitLab Pages](https://about.gitlab.com/stages-devops-lifecycle/pages/). -| Setting | GitLab.com | Default | +| Setting | GitLab SaaS | Default | |---------------------------|------------------------|------------------------| | Domain name | `gitlab.io` | - | | IP address | `35.185.44.232` | - | @@ -108,9 +108,9 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/index.md). Any settings or feature limits not listed here are using the defaults listed in the related documentation. -| Setting | GitLab.com | Default | -|-------------------------------------|------------|---------| -| Artifacts maximum size (compressed) | 1 GB | 100 MB | +| Setting | GitLab SaaS | Default | +|-------------------------------------|-------------|---------| +| Artifacts maximum size (compressed) | 1 GB | 100 MB | | Artifacts [expiry time](../../ci/yaml/index.md#artifactsexpire_in) | From June 22, 2020, deleted after 30 days unless otherwise specified (artifacts created before that date have no expiry). | deleted after 30 days unless otherwise specified | | Scheduled Pipeline Cron | `*/5 * * * *` | `3-59/10 * * * *` | | [Max jobs in active pipelines](../../administration/instance_limits.md#number-of-jobs-in-active-pipelines) | `500` for Free tier, unlimited otherwise | Unlimited | @@ -122,17 +122,17 @@ the related documentation. ## Account and limit settings -GitLab.com has the following [account limits](../admin_area/settings/account_and_limit_settings.md) +GitLab SaaS has the following [account limits](../admin_area/settings/account_and_limit_settings.md) enabled. If a setting is not listed, it is set to the default value. If you are near or over the repository size limit, you can either [reduce your repository size with Git](../project/repository/reducing_the_repo_size_using_git.md) or [purchase additional storage](https://about.gitlab.com/pricing/licensing-faq/#can-i-buy-more-storage). -| Setting | GitLab.com | Default | -|-------------------------------|------------|---------| +| Setting | GitLab SaaS | Default | +|-------------------------------|-------------|---------| | [Repository size including LFS](../admin_area/settings/account_and_limit_settings.md#repository-size-limit) | 10 GB | Unlimited | -| Maximum import size | 5 GB | Unlimited ([Modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50MB to unlimited in GitLab 13.8. | -| Maximum attachment size | 10 MB | 10 MB | +| Maximum import size | 5 GB | Unlimited ([Modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50MB to unlimited in GitLab 13.8.) | +| Maximum attachment size | 10 MB | 10 MB | NOTE: `git push` and GitLab project imports are limited to 5 GB per request through @@ -141,11 +141,11 @@ this limit. ## IP range -GitLab.com uses the IP ranges `34.74.90.64/28` and `34.74.226.0/24` for traffic from its Web/API +GitLab SaaS uses the IP ranges `34.74.90.64/28` and `34.74.226.0/24` for traffic from its Web/API fleet. This whole range is solely allocated to GitLab. You can expect connections from webhooks or repository mirroring to come from those IPs and allow them. -GitLab.com is fronted by Cloudflare. For incoming connections to GitLab.com you might need to allow CIDR blocks of Cloudflare ([IPv4](https://www.cloudflare.com/ips-v4) and [IPv6](https://www.cloudflare.com/ips-v6)). +GitLab SaaS is fronted by Cloudflare. For incoming connections to GitLab SaaS, you might need to allow CIDR blocks of Cloudflare ([IPv4](https://www.cloudflare.com/ips-v4) and [IPv6](https://www.cloudflare.com/ips-v6)). For outgoing connections from CI/CD runners, we are not providing static IP addresses. All GitLab runners are deployed into Google Cloud Platform (GCP). Any @@ -156,7 +156,7 @@ IP-based firewall can be configured by looking up all Add these hostnames when you configure allow-lists in local HTTP(S) proxies, or other web-blocking software that governs end-user computers. Pages on -GitLab.com load content from these hostnames: +GitLab SaaS load content from these hostnames: - `gitlab.com` - `*.gitlab.com` @@ -171,32 +171,32 @@ also load certain page content directly from common public CDN hostnames. The following limits apply for [Webhooks](../project/integrations/webhooks.md): -| Setting | GitLab.com | Default | -|----------------------|------------|---------| +| Setting | GitLab SaaS | Default | +|----------------------|-------------|---------| | [Webhook rate limit](../../administration/instance_limits.md#webhook-rate-limit) | `120` calls per minute for GitLab Free, unlimited for GitLab Premium and GitLab Ultimate | Unlimited | | [Number of webhooks](../../administration/instance_limits.md#number-of-webhooks) | `100` per project, `50` per group | `100` per project, `50` per group | -| Maximum payload size | 25 MB | 25 MB | +| Maximum payload size | 25 MB | 25 MB | ## Shared runners -GitLab has shared runners on GitLab.com that you can use to run your CI jobs. +GitLab has shared runners on GitLab SaaS that you can use to run your CI jobs. For more information, see [choosing a runner](../../ci/runners/index.md). ## Sidekiq -GitLab.com runs [Sidekiq](https://sidekiq.org) with arguments `--timeout=4 --concurrency=4` +GitLab SaaS runs [Sidekiq](https://sidekiq.org) with arguments `--timeout=4 --concurrency=4` and the following environment variables: -| Setting | GitLab.com | Default | -|----------------------------------------|------------|-----------| -| `SIDEKIQ_DAEMON_MEMORY_KILLER` | - | `1` | -| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | `2000000` | `2000000` | -| `SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS` | - | - | -| `SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL` | - | `3` | -| `SIDEKIQ_MEMORY_KILLER_GRACE_TIME` | - | `900` | -| `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT` | - | `30` | -| `SIDEKIQ_LOG_ARGUMENTS` | `1` | `1` | +| Setting | GitLab SaaS | Default | +|----------------------------------------|-------------|-----------| +| `SIDEKIQ_DAEMON_MEMORY_KILLER` | - | `1` | +| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | `2000000` | `2000000` | +| `SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS` | - | - | +| `SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL` | - | `3` | +| `SIDEKIQ_MEMORY_KILLER_GRACE_TIME` | - | `900` | +| `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT` | - | `30` | +| `SIDEKIQ_LOG_ARGUMENTS` | `1` | `1` | NOTE: The `SIDEKIQ_MEMORY_KILLER_MAX_RSS` setting is `16000000` on Sidekiq import @@ -204,14 +204,14 @@ nodes and Sidekiq export nodes. ## PostgreSQL -GitLab.com being a fairly large installation of GitLab means we have changed +GitLab SaaS being a fairly large installation of GitLab means we have changed various PostgreSQL settings to better suit our needs. For example, we use streaming replication and servers in hot-standby mode to balance queries across different database servers. -The list of GitLab.com specific settings (and their defaults) is as follows: +The list of GitLab SaaS specific settings (and their defaults) is as follows: -| Setting | GitLab.com | Default | +| Setting | GitLab SaaS | Default | |:--------------------------------------|:--------------------------------------------------------------------|:--------------------------------------| | `archive_command` | `/usr/bin/envdir /etc/wal-e.d/env /opt/wal-e/bin/wal-e wal-push %p` | empty | | `archive_mode` | on | off | @@ -249,9 +249,9 @@ for `shared_buffers` is quite high, and we are ## Puma -GitLab.com uses the default of 60 seconds for [Puma request timeouts](https://docs.gitlab.com/omnibus/settings/puma.html#worker-timeout). +GitLab SaaS uses the default of 60 seconds for [Puma request timeouts](https://docs.gitlab.com/omnibus/settings/puma.html#worker-timeout). -## GitLab.com-specific rate limits +## GitLab SaaS-specific rate limits NOTE: See [Rate limits](../../security/rate_limits.md) for administrator @@ -262,7 +262,7 @@ code. The client should wait before attempting the request again. There are also informational headers with this response detailed in [rate limiting responses](#rate-limiting-responses). -The following table describes the rate limits for GitLab.com, both before and +The following table describes the rate limits for GitLab SaaS, both before and after the limits change in January, 2021: | Rate limit | Before 2021-01-18 | From 2021-01-18 | From 2021-02-12 | @@ -289,7 +289,7 @@ For information on rate limiting responses, see: ### Protected paths throttle -GitLab.com responds with HTTP status code `429` to POST requests at protected +GitLab SaaS responds with HTTP status code `429` to POST requests at protected paths that exceed 10 requests per **minute** per IP address. See the source below for which paths are protected. This includes user creation, @@ -302,20 +302,20 @@ See [Protected Paths](../admin_area/settings/protected_paths.md) for more detail ### IP blocks -IP blocks can occur when GitLab.com receives unusual traffic from a single +IP blocks can occur when GitLab SaaS receives unusual traffic from a single IP address that the system views as potentially malicious. This can be based on rate limit settings. After the unusual traffic ceases, the IP address is automatically released depending on the type of block, as described in a following section. -If you receive a `403 Forbidden` error for all requests to GitLab.com, +If you receive a `403 Forbidden` error for all requests to GitLab SaaS, check for any automated processes that may be triggering a block. For assistance, contact [GitLab Support](https://support.gitlab.com/hc/en-us) with details, such as the affected IP address. #### Git and container registry failed authentication ban -GitLab.com responds with HTTP status code `403` for 1 hour, if 30 failed +GitLab SaaS responds with HTTP status code `403` for 1 hour, if 30 failed authentication requests were received in a 3-minute period from a single IP address. This applies only to Git requests and container registry (`/jwt/auth`) requests @@ -343,7 +343,7 @@ doesn't return the following headers: If created before GitLab 12.2 (July 2019), these items have the [Internal visibility](../../public_access/public_access.md#internal-projects) -setting [disabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/12388): +setting [disabled on GitLab SaaS](https://gitlab.com/gitlab-org/gitlab/-/issues/12388): - Projects - Groups @@ -351,7 +351,7 @@ setting [disabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/1 ### SSH maximum number of connections -GitLab.com defines the maximum number of concurrent, unauthenticated SSH +GitLab SaaS defines the maximum number of concurrent, unauthenticated SSH connections by using the [MaxStartups setting](http://man.openbsd.org/sshd_config.5#MaxStartups). If more than the maximum number of allowed connections occur concurrently, they are dropped and users get @@ -367,9 +367,9 @@ for details. See [non-configurable limits](../../security/rate_limits.md#non-configurable-limits) for information on rate limits that are not configurable, and therefore also -used on GitLab.com. +used on GitLab SaaS. -## GitLab.com Logging +## GitLab SaaS logging We use [Fluentd](https://gitlab.com/gitlab-com/runbooks/tree/master/logging/doc#fluentd) to parse our logs. Fluentd sends our logs to @@ -384,20 +384,20 @@ You can view more information in our runbooks such as: - Our [current log retention policies](https://gitlab.com/gitlab-com/runbooks/-/tree/master/docs/logging#retention) - A [diagram of our logging infrastructure](https://gitlab.com/gitlab-com/runbooks/-/tree/master/docs/logging#logging-infrastructure-overview) -### Job Logs +### Job logs By default, GitLab does not expire job logs. Job logs are retained indefinitely, -and can't be configured on GitLab.com to expire. You can erase job logs +and can't be configured on GitLab SaaS to expire. You can erase job logs [manually with the Jobs API](../../api/jobs.md#erase-a-job) or by [deleting a pipeline](../../ci/pipelines/index.md#delete-a-pipeline). -## GitLab.com at scale +## GitLab SaaS at scale -In addition to the GitLab Enterprise Edition Omnibus install, GitLab.com uses +In addition to the GitLab Enterprise Edition Omnibus install, GitLab SaaS uses the following applications and settings to achieve scale. All settings are publicly available at [chef cookbooks](https://gitlab.com/gitlab-cookbooks). -### Elastic Cluster +### Elastic cluster We use Elasticsearch and Kibana for part of our monitoring solution: diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 88bc1f7ef49..e5949935fcc 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -188,7 +188,7 @@ Use the switches to enable or disable the following features: | **Issues** | ✓ | Activates the GitLab issues tracker | | **Repository** | ✓ | Enables [repository](../repository/) functionality | | **Merge Requests** | ✓ | Enables [merge request](../merge_requests/) functionality; also see [Merge request settings](#merge-request-settings) | -| **Forks** | ✓ | Enables [forking](../working_with_projects.md#fork-a-project) functionality | +| **Forks** | ✓ | Enables [forking](../repository/forking_workflow.md) functionality | | **Pipelines** | ✓ | Enables [CI/CD](../../../ci/index.md) functionality | | **Container Registry** | | Activates a [registry](../../packages/container_registry/) for your Docker images | | **Git Large File Storage** | | Enables the use of [large files](../../../topics/git/lfs/index.md#git-large-file-storage-lfs) | diff --git a/lib/backup/gitaly_backup.rb b/lib/backup/gitaly_backup.rb index d64278cfc92..c15b0ed6a1b 100644 --- a/lib/backup/gitaly_backup.rb +++ b/lib/backup/gitaly_backup.rb @@ -3,9 +3,10 @@ module Backup # Backup and restores repositories using gitaly-backup class GitalyBackup - def initialize(progress, parallel: nil) + def initialize(progress, parallel: nil, parallel_storage: nil) @progress = progress @parallel = parallel + @parallel_storage = parallel_storage end def start(type) @@ -22,6 +23,7 @@ module Backup args = [] args += ['-parallel', @parallel.to_s] if type == :create && @parallel + args += ['-parallel-storage', @parallel_storage.to_s] if type == :create && @parallel_storage @read_io, @write_io = IO.pipe @pid = Process.spawn(bin_path, command, '-path', backup_repos_path, *args, in: @read_io, out: @progress) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 04880ec1594..842ab4f7b80 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -1110,11 +1110,16 @@ module Gitlab Gitlab::AppLogger.warn "Could not find batched background migration for the given configuration: #{configuration}" elsif !migration.finished? raise "Expected batched background migration for the given configuration to be marked as 'finished', " \ - "but it is '#{migration.status}': #{configuration}" \ + "but it is '#{migration.status}':" \ + "\t#{configuration}" \ "\n\n" \ "Finalize it manualy by running" \ "\n\n" \ - "\tgitlab-rake gitlab:background_migrations:finalize[#{job_class_name},#{table_name},#{column_name},'#{job_arguments.inspect.gsub(',', '\,')}']" + "\tsudo gitlab-rake gitlab:background_migrations:finalize[#{job_class_name},#{table_name},#{column_name},'#{job_arguments.inspect.gsub(',', '\,')}']" \ + "\n\n" \ + "For more information, check the documentation" \ + "\n\n" \ + "\thttps://docs.gitlab.com/ee/user/admin_area/monitoring/background_migrations.html#database-migrations-failing-because-of-batched-background-migration-not-finished" end end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 1a0389ed22b..ed74dd472ff 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -299,7 +299,8 @@ namespace :gitlab do def repository_backup_strategy if Feature.enabled?(:gitaly_backup) max_concurrency = ENV['GITLAB_BACKUP_MAX_CONCURRENCY'].presence - Backup::GitalyBackup.new(progress, parallel: max_concurrency) + max_storage_concurrency = ENV['GITLAB_BACKUP_MAX_STORAGE_CONCURRENCY'].presence + Backup::GitalyBackup.new(progress, parallel: max_concurrency, parallel_storage: max_storage_concurrency) else Backup::GitalyRpcBackup.new(progress) end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index d7a267fec69..54c07985a21 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -4,8 +4,6 @@ require 'spec_helper' RSpec.describe "Admin Runners" do include StubENV - include FilteredSearchHelpers - include SortingHelper before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') @@ -14,31 +12,68 @@ RSpec.describe "Admin Runners" do gitlab_enable_admin_mode_sign_in(admin) end - describe "Runners page" do - let(:pipeline) { create(:ci_pipeline) } - - before do - stub_feature_flags(runner_list_view_vue_ui: false) - end + describe "Runners page", :js do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, namespace: namespace, creator: user) } context "when there are runners" do it 'has all necessary texts' do - runner = create(:ci_runner, contacted_at: Time.now) - create(:ci_build, pipeline: pipeline, runner_id: runner.id) + create(:ci_runner, :instance, contacted_at: Time.now) + visit admin_runners_path expect(page).to have_text "Set up a shared runner manually" expect(page).to have_text "Runners currently online: 1" end - describe 'search', :js do + it 'with an instance runner shows an instance badge and no project count' do + runner = create(:ci_runner, :instance) + + visit admin_runners_path + + within "[data-testid='runner-row-#{runner.id}']" do + expect(page).to have_selector '.badge', text: 'shared' + expect(page).to have_text 'n/a' + end + end + + it 'with a group runner shows a group badge and no project count' do + runner = create(:ci_runner, :group, groups: [group]) + + visit admin_runners_path + + within "[data-testid='runner-row-#{runner.id}']" do + expect(page).to have_selector '.badge', text: 'group' + expect(page).to have_text 'n/a' + end + end + + it 'with a project runner shows a project badge and project count' do + runner = create(:ci_runner, :project, projects: [project]) + + visit admin_runners_path + + within "[data-testid='runner-row-#{runner.id}']" do + expect(page).to have_selector '.badge', text: 'specific' + expect(page).to have_text '1' + end + end + + describe 'search' do before do - create(:ci_runner, description: 'runner-foo') - create(:ci_runner, description: 'runner-bar') + create(:ci_runner, :instance, description: 'runner-foo') + create(:ci_runner, :instance, description: 'runner-bar') visit admin_runners_path end + it 'shows runners' do + expect(page).to have_content("runner-foo") + expect(page).to have_content("runner-bar") + end + it 'shows correct runner when description matches' do input_filtered_search_keys('runner-foo') @@ -53,28 +88,29 @@ RSpec.describe "Admin Runners" do end end - describe 'filter by status', :js do + describe 'filter by status' do it 'shows correct runner when status matches' do - create(:ci_runner, description: 'runner-active', active: true) - create(:ci_runner, description: 'runner-paused', active: false) + create(:ci_runner, :instance, description: 'runner-active', active: true) + create(:ci_runner, :instance, description: 'runner-paused', active: false) visit admin_runners_path expect(page).to have_content 'runner-active' expect(page).to have_content 'runner-paused' - input_filtered_search_keys('status:=active') + input_filtered_search_filter_is_only('Status', 'Active') + expect(page).to have_content 'runner-active' expect(page).not_to have_content 'runner-paused' end it 'shows no runner when status does not match' do - create(:ci_runner, :online, description: 'runner-active', active: true) - create(:ci_runner, :online, description: 'runner-paused', active: false) + create(:ci_runner, :instance, description: 'runner-active', active: true) + create(:ci_runner, :instance, description: 'runner-paused', active: false) visit admin_runners_path - input_filtered_search_keys('status:=offline') + input_filtered_search_filter_is_only('Status', 'Online') expect(page).not_to have_content 'runner-active' expect(page).not_to have_content 'runner-paused' @@ -83,46 +119,48 @@ RSpec.describe "Admin Runners" do end it 'shows correct runner when status is selected and search term is entered' do - create(:ci_runner, description: 'runner-a-1', active: true) - create(:ci_runner, description: 'runner-a-2', active: false) - create(:ci_runner, description: 'runner-b-1', active: true) + create(:ci_runner, :instance, description: 'runner-a-1', active: true) + create(:ci_runner, :instance, description: 'runner-a-2', active: false) + create(:ci_runner, :instance, description: 'runner-b-1', active: true) visit admin_runners_path - input_filtered_search_keys('status:=active') + input_filtered_search_filter_is_only('Status', 'Active') + expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-b-1' expect(page).not_to have_content 'runner-a-2' - input_filtered_search_keys('status:=active runner-a') + input_filtered_search_keys('runner-a') + expect(page).to have_content 'runner-a-1' expect(page).not_to have_content 'runner-b-1' expect(page).not_to have_content 'runner-a-2' end end - describe 'filter by type', :js do - it 'shows correct runner when type matches' do - create :ci_runner, :project, description: 'runner-project' - create :ci_runner, :group, description: 'runner-group' + describe 'filter by type' do + before do + create(:ci_runner, :project, description: 'runner-project', projects: [project]) + create(:ci_runner, :group, description: 'runner-group', groups: [group]) + end + it 'shows correct runner when type matches' do visit admin_runners_path expect(page).to have_content 'runner-project' expect(page).to have_content 'runner-group' - input_filtered_search_keys('type:=project_type') + input_filtered_search_filter_is_only('Type', 'project') + expect(page).to have_content 'runner-project' expect(page).not_to have_content 'runner-group' end it 'shows no runner when type does not match' do - create :ci_runner, :project, description: 'runner-project' - create :ci_runner, :group, description: 'runner-group' - visit admin_runners_path - input_filtered_search_keys('type:=instance_type') + input_filtered_search_filter_is_only('Type', 'instance') expect(page).not_to have_content 'runner-project' expect(page).not_to have_content 'runner-group' @@ -131,95 +169,93 @@ RSpec.describe "Admin Runners" do end it 'shows correct runner when type is selected and search term is entered' do - create :ci_runner, :project, description: 'runner-a-1' - create :ci_runner, :instance, description: 'runner-a-2' - create :ci_runner, :project, description: 'runner-b-1' + create(:ci_runner, :project, description: 'runner-2-project', projects: [project]) visit admin_runners_path - input_filtered_search_keys('type:=project_type') - expect(page).to have_content 'runner-a-1' - expect(page).to have_content 'runner-b-1' - expect(page).not_to have_content 'runner-a-2' + input_filtered_search_filter_is_only('Type', 'project') - input_filtered_search_keys('type:=project_type runner-a') - expect(page).to have_content 'runner-a-1' - expect(page).not_to have_content 'runner-b-1' - expect(page).not_to have_content 'runner-a-2' + expect(page).to have_content 'runner-project' + expect(page).to have_content 'runner-2-project' + expect(page).not_to have_content 'runner-group' + + input_filtered_search_keys('runner-project') + + expect(page).to have_content 'runner-project' + expect(page).not_to have_content 'runner-2-project' + expect(page).not_to have_content 'runner-group' end end - describe 'filter by tag', :js do - it 'shows correct runner when tag matches' do - create :ci_runner, description: 'runner-blue', tag_list: ['blue'] - create :ci_runner, description: 'runner-red', tag_list: ['red'] + describe 'filter by tag' do + before do + create(:ci_runner, :instance, description: 'runner-blue', tag_list: ['blue']) + create(:ci_runner, :instance, description: 'runner-red', tag_list: ['red']) + end + it 'shows correct runner when tag matches' do visit admin_runners_path expect(page).to have_content 'runner-blue' expect(page).to have_content 'runner-red' - input_filtered_search_keys('tag:=blue') + input_filtered_search_filter_is_only('Tags', 'blue') expect(page).to have_content 'runner-blue' expect(page).not_to have_content 'runner-red' end it 'shows no runner when tag does not match' do - create :ci_runner, description: 'runner-blue', tag_list: ['blue'] - create :ci_runner, description: 'runner-red', tag_list: ['blue'] - visit admin_runners_path - input_filtered_search_keys('tag:=red') + input_filtered_search_filter_is_only('Tags', 'green') expect(page).not_to have_content 'runner-blue' - expect(page).not_to have_content 'runner-blue' expect(page).to have_text 'No runners found' end it 'shows correct runner when tag is selected and search term is entered' do - create :ci_runner, description: 'runner-a-1', tag_list: ['blue'] - create :ci_runner, description: 'runner-a-2', tag_list: ['red'] - create :ci_runner, description: 'runner-b-1', tag_list: ['blue'] + create(:ci_runner, :instance, description: 'runner-2-blue', tag_list: ['blue']) visit admin_runners_path - input_filtered_search_keys('tag:=blue') + input_filtered_search_filter_is_only('Tags', 'blue') - expect(page).to have_content 'runner-a-1' - expect(page).to have_content 'runner-b-1' - expect(page).not_to have_content 'runner-a-2' + expect(page).to have_content 'runner-blue' + expect(page).to have_content 'runner-2-blue' + expect(page).not_to have_content 'runner-red' - input_filtered_search_keys('tag:=blue runner-a') + input_filtered_search_keys('runner-2-blue') - expect(page).to have_content 'runner-a-1' - expect(page).not_to have_content 'runner-b-1' - expect(page).not_to have_content 'runner-a-2' + expect(page).to have_content 'runner-2-blue' + expect(page).not_to have_content 'runner-blue' + expect(page).not_to have_content 'runner-red' end end - it 'sorts by last contact date', :js do - create(:ci_runner, description: 'runner-1', created_at: '2018-07-12 15:37', contacted_at: '2018-07-12 15:37') - create(:ci_runner, description: 'runner-2', created_at: '2018-07-12 16:37', contacted_at: '2018-07-12 16:37') + it 'sorts by last contact date' do + create(:ci_runner, :instance, description: 'runner-1', created_at: '2018-07-12 15:37', contacted_at: '2018-07-12 15:37') + create(:ci_runner, :instance, description: 'runner-2', created_at: '2018-07-12 16:37', contacted_at: '2018-07-12 16:37') visit admin_runners_path - within '[data-testid="runners-table"] .gl-responsive-table-row:nth-child(2)' do + within '[data-testid="runner-list"] tbody tr:nth-child(1)' do expect(page).to have_content 'runner-2' end - within '[data-testid="runners-table"] .gl-responsive-table-row:nth-child(3)' do + within '[data-testid="runner-list"] tbody tr:nth-child(2)' do expect(page).to have_content 'runner-1' end - sorting_by 'Last Contact' + click_on 'Created date' # Open "sort by" dropdown + click_on 'Last contact' + click_on 'Sort direction: Descending' - within '[data-testid="runners-table"] .gl-responsive-table-row:nth-child(2)' do + within '[data-testid="runner-list"] tbody tr:nth-child(1)' do expect(page).to have_content 'runner-1' end - within '[data-testid="runners-table"] .gl-responsive-table-row:nth-child(3)' do + within '[data-testid="runner-list"] tbody tr:nth-child(2)' do expect(page).to have_content 'runner-2' end end @@ -237,47 +273,6 @@ RSpec.describe "Admin Runners" do end end - context 'group runner' do - let(:group) { create(:group) } - let!(:runner) { create(:ci_runner, :group, groups: [group]) } - - it 'shows the label and does not show the project count' do - visit admin_runners_path - - within "[data-testid='runner-row-#{runner.id}']" do - expect(page).to have_selector '.badge', text: 'group' - expect(page).to have_text 'n/a' - end - end - end - - context 'shared runner' do - it 'shows the label and does not show the project count' do - runner = create(:ci_runner, :instance) - - visit admin_runners_path - - within "[data-testid='runner-row-#{runner.id}']" do - expect(page).to have_selector '.badge', text: 'shared' - expect(page).to have_text 'n/a' - end - end - end - - context 'specific runner' do - it 'shows the label and the project count' do - project = create(:project) - runner = create(:ci_runner, :project, projects: [project]) - - visit admin_runners_path - - within "[data-testid='runner-row-#{runner.id}']" do - expect(page).to have_selector '.badge', text: 'specific' - expect(page).to have_text '1' - end - end - end - describe 'runners registration token' do let!(:token) { Gitlab::CurrentSettings.runners_registration_token } @@ -286,17 +281,23 @@ RSpec.describe "Admin Runners" do end it 'has a registration token' do - expect(page.find('[data-testid="registration_token"]')).to have_content(token) + click_on 'Click to reveal' + expect(page.find('[data-testid="registration-token"]')).to have_content(token) end describe 'reset registration token' do - let(:page_token) { find('[data-testid="registration_token"]').text } + let(:page_token) { find('[data-testid="registration-token"]').text } before do click_button 'Reset registration token' + + page.accept_alert + + wait_for_requests end it 'changes registration token' do + click_on 'Click to reveal' expect(page_token).not_to eq token end end @@ -409,4 +410,43 @@ RSpec.describe "Admin Runners" do end end end + + private + + def search_bar_selector + '[data-testid="runners-filtered-search"]' + end + + # The filters must be clicked first to be able to receive events + # See: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1493 + def focus_filtered_search + page.within(search_bar_selector) do + page.find('.gl-filtered-search-term-token').click + end + end + + def input_filtered_search_keys(search_term) + focus_filtered_search + + page.within(search_bar_selector) do + page.find('input').send_keys(search_term) + click_on 'Search' + end + end + + def input_filtered_search_filter_is_only(filter, value) + focus_filtered_search + + page.within(search_bar_selector) do + click_on filter + + # For OPERATOR_IS_ONLY, clicking the filter + # immediately preselects "=" operator + + page.find('input').send_keys(value) + page.find('input').send_keys(:enter) + + click_on 'Search' + end + end end diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js index a6aa1ece032..5a05265afdc 100644 --- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js +++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js @@ -5,6 +5,7 @@ import { trimText } from 'helpers/text_helper'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; import { createStore } from '~/frequent_items/store'; +import ProjectAvatar from '~/vue_shared/components/project_avatar.vue'; import { mockProject } from '../mock_data'; const localVue = createLocalVue(); @@ -16,12 +17,12 @@ describe('FrequentItemsListItemComponent', () => { let store; const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' }); - const findAvatar = () => wrapper.find({ ref: 'frequentItemsItemAvatar' }); + const findAvatar = () => wrapper.findComponent(ProjectAvatar); const findAllTitles = () => wrapper.findAll({ ref: 'frequentItemsItemTitle' }); const findNamespace = () => wrapper.find({ ref: 'frequentItemsItemNamespace' }); const findAllButtons = () => wrapper.findAllComponents(GlButton); const findAllNamespace = () => wrapper.findAll({ ref: 'frequentItemsItemNamespace' }); - const findAvatarContainer = () => wrapper.findAll({ ref: 'frequentItemsItemAvatarContainer' }); + const findAllAvatars = () => wrapper.findAllComponents(ProjectAvatar); const findAllMetadataContainers = () => wrapper.findAll({ ref: 'frequentItemsItemMetadataContainer' }); @@ -92,16 +93,8 @@ describe('FrequentItemsListItemComponent', () => { createComponent(); }); - it('should render avatar if avatarUrl is present', () => { - wrapper.setProps({ avatarUrl: 'path/to/avatar.png' }); - - return wrapper.vm.$nextTick(() => { - expect(findAvatar().exists()).toBe(true); - }); - }); - - it('should not render avatar if avatarUrl is not present', () => { - expect(findAvatar().exists()).toBe(false); + it('renders avatar', () => { + expect(findAvatar().exists()).toBe(true); }); it('renders root element with the right classes', () => { @@ -111,7 +104,7 @@ describe('FrequentItemsListItemComponent', () => { it.each` name | selector | expected ${'button'} | ${findAllButtons} | ${1} - ${'avatar container'} | ${findAvatarContainer} | ${1} + ${'avatar container'} | ${findAllAvatars} | ${1} ${'metadata container'} | ${findAllMetadataContainers} | ${1} ${'title'} | ${findAllTitles} | ${1} ${'namespace'} | ${findAllNamespace} | ${1} diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js index 871969205b7..1f4dd7d6216 100644 --- a/spec/frontend/jobs/components/job_app_spec.js +++ b/spec/frontend/jobs/components/job_app_spec.js @@ -24,6 +24,7 @@ describe('Job App', () => { let store; let wrapper; let mock; + let origGon; const initSettings = { endpoint: `${TEST_HOST}jobs/123.json`, @@ -85,11 +86,17 @@ describe('Job App', () => { beforeEach(() => { mock = new MockAdapter(axios); store = createStore(); + + origGon = window.gon; + + window.gon = { features: { infinitelyCollapsibleSections: false } }; // NOTE: All of this passes with the feature flag }); afterEach(() => { wrapper.destroy(); mock.restore(); + + window.gon = origGon; }); describe('while loading', () => { diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js index 66f22162c97..4e23a3ba7b8 100644 --- a/spec/frontend/jobs/components/log/collapsible_section_spec.js +++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js @@ -4,6 +4,7 @@ import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data' describe('Job Log Collapsible Section', () => { let wrapper; + let origGon; const traceEndpoint = 'jobs/335'; @@ -18,8 +19,16 @@ describe('Job Log Collapsible Section', () => { }); }; + beforeEach(() => { + origGon = window.gon; + + window.gon = { features: { infinitelyCollapsibleSections: false } }; // NOTE: This also works with true + }); + afterEach(() => { wrapper.destroy(); + + window.gon = origGon; }); describe('with closed section', () => { diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js index b7aff1f3e3b..99fb6846ce5 100644 --- a/spec/frontend/jobs/components/log/log_spec.js +++ b/spec/frontend/jobs/components/log/log_spec.js @@ -1,7 +1,7 @@ import { mount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import Log from '~/jobs/components/log/log.vue'; -import { logLinesParser } from '~/jobs/store/utils'; +import { logLinesParserLegacy, logLinesParser } from '~/jobs/store/utils'; import { jobLog } from './mock_data'; describe('Job Log', () => { @@ -9,6 +9,7 @@ describe('Job Log', () => { let actions; let state; let store; + let origGon; const localVue = createLocalVue(); localVue.use(Vuex); @@ -25,8 +26,12 @@ describe('Job Log', () => { toggleCollapsibleLine: () => {}, }; + origGon = window.gon; + + window.gon = { features: { infinitelyCollapsibleSections: false } }; + state = { - trace: logLinesParser(jobLog), + trace: logLinesParserLegacy(jobLog), traceEndpoint: 'jobs/id', }; @@ -40,6 +45,88 @@ describe('Job Log', () => { afterEach(() => { wrapper.destroy(); + + window.gon = origGon; + }); + + const findCollapsibleLine = () => wrapper.find('.collapsible-line'); + + describe('line numbers', () => { + it('renders a line number for each open line', () => { + expect(wrapper.find('#L1').text()).toBe('1'); + expect(wrapper.find('#L2').text()).toBe('2'); + expect(wrapper.find('#L3').text()).toBe('3'); + }); + + it('links to the provided path and correct line number', () => { + expect(wrapper.find('#L1').attributes('href')).toBe(`${state.traceEndpoint}#L1`); + }); + }); + + describe('collapsible sections', () => { + it('renders a clickable header section', () => { + expect(findCollapsibleLine().attributes('role')).toBe('button'); + }); + + it('renders an icon with the open state', () => { + expect(findCollapsibleLine().find('[data-testid="angle-down-icon"]').exists()).toBe(true); + }); + + describe('on click header section', () => { + it('calls toggleCollapsibleLine', () => { + jest.spyOn(wrapper.vm, 'toggleCollapsibleLine'); + + findCollapsibleLine().trigger('click'); + + expect(wrapper.vm.toggleCollapsibleLine).toHaveBeenCalled(); + }); + }); + }); +}); + +describe('Job Log, infinitelyCollapsibleSections feature flag enabled', () => { + let wrapper; + let actions; + let state; + let store; + let origGon; + + const localVue = createLocalVue(); + localVue.use(Vuex); + + const createComponent = () => { + wrapper = mount(Log, { + localVue, + store, + }); + }; + + beforeEach(() => { + actions = { + toggleCollapsibleLine: () => {}, + }; + + origGon = window.gon; + + window.gon = { features: { infinitelyCollapsibleSections: true } }; + + state = { + trace: logLinesParser(jobLog).parsedLines, + traceEndpoint: 'jobs/id', + }; + + store = new Vuex.Store({ + actions, + state, + }); + + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + + window.gon = origGon; }); const findCollapsibleLine = () => wrapper.find('.collapsible-line'); diff --git a/spec/frontend/jobs/components/log/mock_data.js b/spec/frontend/jobs/components/log/mock_data.js index eb8c4fe8bc9..76c35703106 100644 --- a/spec/frontend/jobs/components/log/mock_data.js +++ b/spec/frontend/jobs/components/log/mock_data.js @@ -58,6 +58,71 @@ export const utilsMockData = [ }, ]; +export const multipleCollapsibleSectionsMockData = [ + { + offset: 1001, + content: [{ text: ' on docker-auto-scale-com 8a6210b8' }], + }, + { + offset: 1002, + content: [ + { + text: 'Executing "step_script" stage of the job script', + }, + ], + section: 'step-script', + section_header: true, + }, + { + offset: 1003, + content: [{ text: 'sleep 60' }], + section: 'step-script', + }, + { + offset: 1004, + content: [ + { + text: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam lorem dolor, congue ac condimentum vitae', + }, + ], + section: 'step-script', + }, + { + offset: 1005, + content: [{ text: 'executing...' }], + section: 'step-script', + }, + { + offset: 1006, + content: [{ text: '1st collapsible section' }], + section: 'collapsible-1', + section_header: true, + }, + { + offset: 1007, + content: [ + { + text: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam lorem dolor, congue ac condimentum vitae', + }, + ], + section: 'collapsible-1', + }, + { + offset: 1008, + content: [], + section: 'collapsible-1', + section_duration: '01:00', + }, + { + offset: 1009, + content: [], + section: 'step-script', + section_duration: '10:00', + }, +]; + export const originalTrace = [ { offset: 1, diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js index 1c7e45dfb3d..159315330e4 100644 --- a/spec/frontend/jobs/store/mutations_spec.js +++ b/spec/frontend/jobs/store/mutations_spec.js @@ -4,12 +4,21 @@ import state from '~/jobs/store/state'; describe('Jobs Store Mutations', () => { let stateCopy; + let origGon; const html = 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- : Writing /builds/ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3.png<br>I'; beforeEach(() => { stateCopy = state(); + + origGon = window.gon; + + window.gon = { features: { infinitelyCollapsibleSections: false } }; + }); + + afterEach(() => { + window.gon = origGon; }); describe('SET_JOB_ENDPOINT', () => { @@ -267,3 +276,88 @@ describe('Jobs Store Mutations', () => { }); }); }); + +describe('Job Store mutations, feature flag ON', () => { + let stateCopy; + let origGon; + + const html = + 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- : Writing /builds/ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3.png<br>I'; + + beforeEach(() => { + stateCopy = state(); + + origGon = window.gon; + + window.gon = { features: { infinitelyCollapsibleSections: true } }; + }); + + afterEach(() => { + window.gon = origGon; + }); + + describe('RECEIVE_TRACE_SUCCESS', () => { + describe('with new job log', () => { + describe('log.lines', () => { + describe('when append is true', () => { + it('sets the parsed log ', () => { + mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, { + append: true, + size: 511846, + complete: true, + lines: [ + { + offset: 1, + content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }], + }, + ], + }); + + expect(stateCopy.trace).toEqual([ + { + offset: 1, + content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }], + lineNumber: 1, + }, + ]); + }); + }); + + describe('when lines are defined', () => { + it('sets the parsed log ', () => { + mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, { + append: false, + size: 511846, + complete: true, + lines: [ + { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }] }, + ], + }); + + expect(stateCopy.trace).toEqual([ + { + offset: 0, + content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }], + lineNumber: 1, + }, + ]); + }); + }); + + describe('when lines are null', () => { + it('sets the default value', () => { + mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, { + append: true, + html, + size: 511846, + complete: false, + lines: null, + }); + + expect(stateCopy.trace).toEqual([]); + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js index e50d304bb08..35ac2945ab5 100644 --- a/spec/frontend/jobs/store/utils_spec.js +++ b/spec/frontend/jobs/store/utils_spec.js @@ -1,5 +1,6 @@ import { logLinesParser, + logLinesParserLegacy, updateIncrementalTrace, parseHeaderLine, parseLine, @@ -17,6 +18,7 @@ import { headerTraceIncremental, collapsibleTrace, collapsibleTraceIncremental, + multipleCollapsibleSectionsMockData, } from '../components/log/mock_data'; describe('Jobs Store Utils', () => { @@ -175,11 +177,11 @@ describe('Jobs Store Utils', () => { expect(isCollapsibleSection()).toEqual(false); }); }); - describe('logLinesParser', () => { + describe('logLinesParserLegacy', () => { let result; beforeEach(() => { - result = logLinesParser(utilsMockData); + result = logLinesParserLegacy(utilsMockData); }); describe('regular line', () => { @@ -216,6 +218,87 @@ describe('Jobs Store Utils', () => { }); }); + describe('logLinesParser', () => { + let result; + + beforeEach(() => { + result = logLinesParser(utilsMockData); + }); + + describe('regular line', () => { + it('adds a lineNumber property with correct index', () => { + expect(result.parsedLines[0].lineNumber).toEqual(1); + expect(result.parsedLines[1].line.lineNumber).toEqual(2); + }); + }); + + describe('collapsible section', () => { + it('adds a `isClosed` property', () => { + expect(result.parsedLines[1].isClosed).toEqual(false); + }); + + it('adds a `isHeader` property', () => { + expect(result.parsedLines[1].isHeader).toEqual(true); + }); + + it('creates a lines array property with the content of the collapsible section', () => { + expect(result.parsedLines[1].lines.length).toEqual(2); + expect(result.parsedLines[1].lines[0].content).toEqual(utilsMockData[2].content); + expect(result.parsedLines[1].lines[1].content).toEqual(utilsMockData[3].content); + }); + }); + + describe('section duration', () => { + it('adds the section information to the header section', () => { + expect(result.parsedLines[1].line.section_duration).toEqual( + utilsMockData[4].section_duration, + ); + }); + + it('does not add section duration as a line', () => { + expect(result.parsedLines[1].lines.includes(utilsMockData[4])).toEqual(false); + }); + }); + + describe('multiple collapsible sections', () => { + beforeEach(() => { + result = logLinesParser(multipleCollapsibleSectionsMockData); + }); + + it('should contain a section inside another section', () => { + const innerSection = [ + { + isClosed: false, + isHeader: true, + line: { + content: [{ text: '1st collapsible section' }], + lineNumber: 6, + offset: 1006, + section: 'collapsible-1', + section_duration: '01:00', + section_header: true, + }, + lines: [ + { + content: [ + { + text: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam lorem dolor, congue ac condimentum vitae', + }, + ], + lineNumber: 7, + offset: 1007, + section: 'collapsible-1', + }, + ], + }, + ]; + + expect(result.parsedLines[1].lines).toEqual(expect.arrayContaining(innerSection)); + }); + }); + }); + describe('findOffsetAndRemove', () => { describe('when last item is header', () => { const existingLog = [ @@ -391,7 +474,7 @@ describe('Jobs Store Utils', () => { describe('updateIncrementalTrace', () => { describe('without repeated section', () => { it('concats and parses both arrays', () => { - const oldLog = logLinesParser(originalTrace); + const oldLog = logLinesParserLegacy(originalTrace); const result = updateIncrementalTrace(regularIncremental, oldLog); expect(result).toEqual([ @@ -419,7 +502,7 @@ describe('Jobs Store Utils', () => { describe('with regular line repeated offset', () => { it('updates the last line and formats with the incremental part', () => { - const oldLog = logLinesParser(originalTrace); + const oldLog = logLinesParserLegacy(originalTrace); const result = updateIncrementalTrace(regularIncrementalRepeated, oldLog); expect(result).toEqual([ @@ -438,7 +521,7 @@ describe('Jobs Store Utils', () => { describe('with header line repeated', () => { it('updates the header line and formats with the incremental part', () => { - const oldLog = logLinesParser(headerTrace); + const oldLog = logLinesParserLegacy(headerTrace); const result = updateIncrementalTrace(headerTraceIncremental, oldLog); expect(result).toEqual([ @@ -464,7 +547,7 @@ describe('Jobs Store Utils', () => { describe('with collapsible line repeated', () => { it('updates the collapsible line and formats with the incremental part', () => { - const oldLog = logLinesParser(collapsibleTrace); + const oldLog = logLinesParserLegacy(collapsibleTrace); const result = updateIncrementalTrace(collapsibleTraceIncremental, oldLog); expect(result).toEqual([ diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js index 836f57efa16..85cf7ea92df 100644 --- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js +++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js @@ -13,6 +13,7 @@ describe('RunnerList', () => { const findFilteredSearch = () => wrapper.findComponent(FilteredSearch); const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); const findSortOptions = () => wrapper.findAllComponents(GlDropdownItem); + const findActiveRunnersMessage = () => wrapper.findByTestId('active-runners-message'); const mockDefaultSort = 'CREATED_DESC'; const mockOtherSort = 'CONTACTED_DESC'; @@ -20,6 +21,7 @@ describe('RunnerList', () => { { type: PARAM_KEY_STATUS, value: { data: 'ACTIVE', operator: '=' } }, { type: 'filtered-search-term', value: { data: '' } }, ]; + const mockActiveRunnersCount = 2; const createComponent = ({ props = {}, options = {} } = {}) => { wrapper = extendedWrapper( @@ -30,6 +32,7 @@ describe('RunnerList', () => { filters: [], sort: mockDefaultSort, }, + activeRunnersCount: mockActiveRunnersCount, ...props, }, stubs: { @@ -55,6 +58,18 @@ describe('RunnerList', () => { expect(findFilteredSearch().props('namespace')).toBe('runners'); }); + it('Displays an active runner count', () => { + expect(findActiveRunnersMessage().text()).toBe( + `Runners currently online: ${mockActiveRunnersCount}`, + ); + }); + + it('Displays a large active runner count', () => { + createComponent({ props: { activeRunnersCount: 2000 } }); + + expect(findActiveRunnersMessage().text()).toBe('Runners currently online: 2,000'); + }); + it('sets sorting options', () => { const SORT_OPTIONS_COUNT = 2; diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js index 059e7179b19..5fff3581e39 100644 --- a/spec/frontend/runner/components/runner_list_spec.js +++ b/spec/frontend/runner/components/runner_list_spec.js @@ -12,7 +12,6 @@ const mockActiveRunnersCount = mockRunners.length; describe('RunnerList', () => { let wrapper; - const findActiveRunnersMessage = () => wrapper.findByTestId('active-runners-message'); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findTable = () => wrapper.findComponent(GlTable); const findHeaders = () => wrapper.findAll('th'); @@ -40,18 +39,6 @@ describe('RunnerList', () => { wrapper.destroy(); }); - it('Displays active runner count', () => { - expect(findActiveRunnersMessage().text()).toBe( - `Runners currently online: ${mockActiveRunnersCount}`, - ); - }); - - it('Displays a large active runner count', () => { - createComponent({ props: { activeRunnersCount: 2000 } }); - - expect(findActiveRunnersMessage().text()).toBe('Runners currently online: 2,000'); - }); - it('Displays headers', () => { const headerLabels = findHeaders().wrappers.map((w) => w.text()); diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb index 6341f496517..cdb35c0ce01 100644 --- a/spec/lib/backup/gitaly_backup_spec.rb +++ b/spec/lib/backup/gitaly_backup_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Backup::GitalyBackup do let(:parallel) { nil } + let(:parallel_storage) { nil } let(:progress) do Tempfile.new('progress').tap do |progress| progress.unlink @@ -14,7 +15,7 @@ RSpec.describe Backup::GitalyBackup do progress.close end - subject { described_class.new(progress, parallel: parallel) } + subject { described_class.new(progress, parallel: parallel, parallel_storage: parallel_storage) } context 'unknown' do it 'fails to start unknown' do @@ -59,6 +60,17 @@ RSpec.describe Backup::GitalyBackup do end end + context 'parallel_storage option set' do + let(:parallel_storage) { 3 } + + it 'passes parallel option through' do + expect(Process).to receive(:spawn).with(anything, 'create', '-path', anything, '-parallel-storage', '3', { in: anything, out: progress }).and_call_original + + subject.start(:create) + subject.wait + end + end + it 'raises when the exit code not zero' do expect(subject).to receive(:bin_path).and_return(Gitlab::Utils.which('false')) diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index b8a8d429e14..8e25f9249fe 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -2063,11 +2063,16 @@ RSpec.describe Gitlab::Database::MigrationHelpers do create(:batched_background_migration, configuration.merge(status: :active)) expect { ensure_batched_background_migration_is_finished } - .to raise_error "Expected batched background migration for the given configuration to be marked as 'finished', but it is 'active': #{configuration}" \ + .to raise_error "Expected batched background migration for the given configuration to be marked as 'finished', but it is 'active':" \ + "\t#{configuration}" \ "\n\n" \ "Finalize it manualy by running" \ "\n\n" \ - "\tgitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,events,id,'[[\"id\"]\\, [\"id_convert_to_bigint\"]]']" + "\tsudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,events,id,'[[\"id\"]\\, [\"id_convert_to_bigint\"]]']" \ + "\n\n" \ + "For more information, check the documentation" \ + "\n\n" \ + "\thttps://docs.gitlab.com/ee/user/admin_area/monitoring/background_migrations.html#database-migrations-failing-because-of-batched-background-migration-not-finished" end it 'does not raise error when migration exists and is marked as finished' do diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index ae46d0b5e7a..ebaaf179546 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -407,7 +407,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do .with(max_concurrency: 5, max_storage_concurrency: 2) .and_call_original end - expect(::Backup::GitalyBackup).to receive(:new).with(anything, parallel: 5).and_call_original + expect(::Backup::GitalyBackup).to receive(:new).with(anything, parallel: 5, parallel_storage: 2).and_call_original expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process end |