diff options
132 files changed, 2446 insertions, 664 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 006956457aa..33c9a8ba987 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -53,7 +53,7 @@ review-build-cng: review-deploy: extends: - .review-workflow-base - - .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise + - .review:rules:review-deploy stage: review dependencies: [] resource_group: "review/${CI_COMMIT_REF_NAME}" @@ -172,7 +172,7 @@ review-qa-all: review-performance: extends: - .default-retry - - .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise + - .review:rules:review-performance image: name: sitespeedio/sitespeed.io:6.3.1 entrypoint: [""] diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index fbadec1f63d..16193f2b1b0 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -669,7 +669,23 @@ allow_failure: true - <<: *if-dot-com-gitlab-org-schedule -.review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise: +.review:rules:review-deploy: + rules: + - <<: *if-not-ee + when: never + - <<: *if-dot-com-gitlab-org-merge-request + changes: *ci-review-patterns + - <<: *if-dot-com-gitlab-org-merge-request + changes: *frontend-patterns + allow_failure: true + - <<: *if-dot-com-gitlab-org-merge-request + changes: *code-qa-patterns + when: manual + allow_failure: true + - <<: *if-dot-com-gitlab-org-schedule + allow_failure: true + +.review:rules:review-performance: rules: - if: '$DAST_RUN == "true"' # Skip this job when DAST is run when: never @@ -260,7 +260,7 @@ gem 'ruby-fogbugz', '~> 0.2.1' gem 'kubeclient', '~> 4.6.0' # Sanitize user input -gem 'sanitize', '~> 4.6' +gem 'sanitize', '~> 5.2.1' gem 'babosa', '~> 1.0.2' # Sanitizes SVG input @@ -328,7 +328,7 @@ gem 'snowplow-tracker', '~> 0.6.1' # Metrics group :metrics do - gem 'method_source', '~> 0.8', require: false + gem 'method_source', '~> 1.0', require: false # Prometheus gem 'prometheus-client-mmap', '~> 0.11.0' @@ -351,7 +351,7 @@ end group :development, :test do gem 'bullet', '~> 6.1.0' - gem 'pry-byebug', '~> 3.5.1', platform: :mri + gem 'pry-byebug', '~> 3.9.0', platform: :mri gem 'pry-rails', '~> 0.3.9' gem 'awesome_print', require: false diff --git a/Gemfile.lock b/Gemfile.lock index d71e4249577..d2dca0ef497 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -151,7 +151,7 @@ GEM bundler-audit (0.6.1) bundler (>= 1.2.0, < 3) thor (~> 0.18) - byebug (9.1.0) + byebug (11.1.3) capybara (3.33.0) addressable mini_mime (>= 0.1.3) @@ -177,7 +177,7 @@ GEM cork nap open4 (~> 1.3) - coderay (1.1.2) + coderay (1.1.3) colored2 (3.1.2) commonmarker (0.20.1) ruby-enum (~> 0.5) @@ -676,7 +676,7 @@ GEM memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) memory_profiler (0.9.14) - method_source (0.9.2) + method_source (1.0.0) mime-types (3.3.1) mime-types-data (~> 3.2015) mime-types-data (3.2020.0512) @@ -713,10 +713,10 @@ GEM netrc (0.11.0) nio4r (2.5.2) no_proxy_fix (0.1.2) - nokogiri (1.10.9) + nokogiri (1.10.10) mini_portile2 (~> 2.4.0) - nokogumbo (1.5.0) - nokogiri + nokogumbo (2.0.2) + nokogiri (~> 1.8, >= 1.8.4) notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) @@ -832,12 +832,12 @@ GEM unparser procto (0.0.3) prometheus-client-mmap (0.11.0) - pry (0.11.3) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry-byebug (3.5.1) - byebug (~> 9.1) - pry (~> 0.10) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.9.0) + byebug (~> 11.0) + pry (~> 0.13.0) pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.3) @@ -1028,10 +1028,10 @@ GEM rubyzip (2.0.0) rugged (0.28.4.1) safe_yaml (1.0.4) - sanitize (4.6.6) + sanitize (5.2.1) crass (~> 1.0.2) - nokogiri (>= 1.4.4) - nokogumbo (~> 1.4) + nokogiri (>= 1.8.0) + nokogumbo (~> 2.0) sass (3.5.5) sass-listen (~> 4.0.0) sass-listen (4.0.0) @@ -1360,7 +1360,7 @@ DEPENDENCIES mail (= 2.7.1) marginalia (~> 1.9.0) memory_profiler (~> 0.9) - method_source (~> 0.8) + method_source (~> 1.0) mimemagic (~> 0.3.2) mini_magick minitest (~> 5.11.0) @@ -1397,7 +1397,7 @@ DEPENDENCIES png_quantizator (~> 0.2.1) premailer-rails (~> 1.10.3) prometheus-client-mmap (~> 0.11.0) - pry-byebug (~> 3.5.1) + pry-byebug (~> 3.9.0) pry-rails (~> 0.3.9) rack (~> 2.0.9) rack-attack (~> 6.3.0) @@ -1437,7 +1437,7 @@ DEPENDENCIES ruby_parser (~> 3.8) rubyzip (~> 2.0.0) rugged (~> 0.28) - sanitize (~> 4.6) + sanitize (~> 5.2.1) sassc-rails (~> 2.1.0) scss_lint (~> 0.56.0) seed-fu (~> 2.3.7) diff --git a/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js b/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js index b4803be4d52..f89533aeb1d 100644 --- a/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js +++ b/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js @@ -1,8 +1,7 @@ import PayloadPreviewer from '~/pages/admin/application_settings/payload_previewer'; export default () => { - new PayloadPreviewer( - document.querySelector('.js-usage-ping-payload-trigger'), - document.querySelector('.js-usage-ping-payload'), - ).init(); + Array.from(document.querySelectorAll('.js-payload-preview-trigger')).forEach(trigger => { + new PayloadPreviewer(trigger).init(); + }); }; diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue index d22fef27964..0e09ae108ea 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue @@ -67,7 +67,7 @@ export default { </script> <template> <gl-deprecated-dropdown :text="value"> - <gl-search-box-by-type v-model.trim="searchTerm" class="m-2" /> + <gl-search-box-by-type v-model.trim="searchTerm" class="gl-m-3" /> <gl-deprecated-dropdown-item v-for="environment in filteredResults" :key="environment" diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue index 0194a8e5ac1..2617ea0bdea 100644 --- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue +++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue @@ -130,7 +130,7 @@ export default { <gl-search-box-by-type v-model.trim="searchQuery" :placeholder="s__('ClusterIntegration|Search domains')" - class="m-2" + class="gl-m-3" /> <gl-deprecated-dropdown-item v-for="domain in filteredDomains" diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue index b5d17398f3a..42fdba12f13 100644 --- a/app/assets/javascripts/jira_import/components/jira_import_form.vue +++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue @@ -23,6 +23,7 @@ import { addInProgressImportToStore } from '../utils/cache_update'; import { debounceWait, dropdownLabel, + userMappingsPageSize, previousImportsMessage, tableConfig, userMappingMessage, @@ -74,12 +75,15 @@ export default { }, data() { return { + hasMoreUsers: false, isFetching: false, + isLoadingMoreUsers: false, isSubmitting: false, searchTerm: '', selectedProject: undefined, selectState: null, userMappings: [], + userMappingsStartAt: 0, users: [], }; }, @@ -101,6 +105,9 @@ export default { ? `jira-import::${this.selectedProject}-${this.numberOfPreviousImports + 1}` : 'jira-import::KEY-1'; }, + isInitialLoadingState() { + return this.isLoadingMoreUsers && !this.hasMoreUsers; + }, }, watch: { searchTerm: debounce(function debouncedUserSearch() { @@ -108,23 +115,7 @@ export default { }, debounceWait), }, mounted() { - this.$apollo - .mutate({ - mutation: getJiraUserMappingMutation, - variables: { - input: { - projectPath: this.projectPath, - }, - }, - }) - .then(({ data }) => { - if (data.jiraImportUsers.errors.length) { - this.$emit('error', data.jiraImportUsers.errors.join('. ')); - } else { - this.userMappings = data.jiraImportUsers.jiraUsers; - } - }) - .catch(() => this.$emit('error', __('There was an error retrieving the Jira users.'))); + this.getJiraUserMapping(); this.searchUsers() .then(data => { @@ -133,6 +124,36 @@ export default { .catch(() => {}); }, methods: { + getJiraUserMapping() { + this.isLoadingMoreUsers = true; + + this.$apollo + .mutate({ + mutation: getJiraUserMappingMutation, + variables: { + input: { + projectPath: this.projectPath, + startAt: this.userMappingsStartAt, + }, + }, + }) + .then(({ data }) => { + if (data.jiraImportUsers.errors.length) { + this.$emit('error', data.jiraImportUsers.errors.join('. ')); + return; + } + + this.userMappings = this.userMappings.concat(data.jiraImportUsers.jiraUsers); + this.hasMoreUsers = data.jiraImportUsers.jiraUsers.length === userMappingsPageSize; + this.userMappingsStartAt += userMappingsPageSize; + }) + .catch(() => { + this.$emit('error', __('There was an error retrieving the Jira users.')); + }) + .finally(() => { + this.isLoadingMoreUsers = false; + }); + }, searchUsers() { const params = { active: true, @@ -187,7 +208,9 @@ export default { this.selectedProject = undefined; } }) - .catch(() => this.$emit('error', __('There was an error importing the Jira project.'))) + .catch(() => { + this.$emit('error', __('There was an error importing the Jira project.')); + }) .finally(() => { this.isSubmitting = false; }); @@ -278,11 +301,9 @@ export default { " @hide="resetDropdown" > - <gl-search-box-by-type v-model.trim="searchTerm" class="m-2" /> + <gl-search-box-by-type v-model.trim="searchTerm" class="gl-m-3" /> - <div v-if="isFetching" class="gl-text-center"> - <gl-loading-icon /> - </div> + <gl-loading-icon v-if="isFetching" /> <gl-new-dropdown-item v-for="user in users" @@ -300,6 +321,17 @@ export default { </template> </gl-table> + <gl-loading-icon v-if="isInitialLoadingState" /> + + <gl-button + v-if="hasMoreUsers" + :loading="isLoadingMoreUsers" + data-testid="load-more-users-button" + @click="getJiraUserMapping" + > + {{ __('Load more users') }} + </gl-button> + <div class="footer-block row-content-block d-flex justify-content-between"> <gl-button type="submit" diff --git a/app/assets/javascripts/jira_import/utils/constants.js b/app/assets/javascripts/jira_import/utils/constants.js index 6adc3e5306c..178159be009 100644 --- a/app/assets/javascripts/jira_import/utils/constants.js +++ b/app/assets/javascripts/jira_import/utils/constants.js @@ -27,3 +27,6 @@ export const tableConfig = [ export const userMappingMessage = __(`Jira users have been imported from the configured Jira instance. They can be mapped by selecting a GitLab user from the dropdown in the "GitLab username" column. When the form appears, the dropdown defaults to the user conducting the import.`); + +// pageSize must match the MAX_USERS value in app/services/jira_import/users_mapper_service.rb +export const userMappingsPageSize = 50; diff --git a/app/assets/javascripts/milestones/project_milestone_combobox.vue b/app/assets/javascripts/milestones/project_milestone_combobox.vue index d0179ab5509..4e61e8b4262 100644 --- a/app/assets/javascripts/milestones/project_milestone_combobox.vue +++ b/app/assets/javascripts/milestones/project_milestone_combobox.vue @@ -184,7 +184,7 @@ export default { <gl-search-box-by-type v-model.trim="searchQuery" - class="m-2" + class="gl-m-3" :placeholder="this.$options.translations.searchMilestones" @input="searchMilestones" /> diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue index 266f3bb3fd5..67c56766d99 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_header.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue @@ -192,7 +192,7 @@ export default { > <div class="d-flex flex-column overflow-hidden"> <gl-new-dropdown-header>{{ __('Environment') }}</gl-new-dropdown-header> - <gl-search-box-by-type class="m-2" @input="debouncedEnvironmentsSearch" /> + <gl-search-box-by-type class="gl-m-3" @input="debouncedEnvironmentsSearch" /> <gl-loading-icon v-if="environmentsLoading" :inline="true" /> <div v-else class="flex-fill overflow-auto"> diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue index dd15d1e2804..a4d388fb064 100644 --- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue +++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue @@ -83,7 +83,7 @@ export default { <gl-search-box-by-type ref="monitorDashboardsDropdownSearch" v-model="searchTerm" - class="m-2" + class="gl-m-3" /> <div class="flex-fill overflow-auto"> diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js index bbaaeb55c65..a2fca238613 100644 --- a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js @@ -1,3 +1,3 @@ -import setup from 'ee_else_ce/admin/application_settings/setup_metrics_and_profiling'; +import setup from '~/admin/application_settings/setup_metrics_and_profiling'; document.addEventListener('DOMContentLoaded', setup); diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js index e7b468f039f..f8fc53799a8 100644 --- a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js +++ b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js @@ -3,9 +3,9 @@ import { __ } from '../../../locale'; import { deprecatedCreateFlash as flash } from '../../../flash'; export default class PayloadPreviewer { - constructor(trigger, container) { + constructor(trigger) { this.trigger = trigger; - this.container = container; + this.container = document.querySelector(trigger.dataset.payloadSelector); this.isVisible = false; this.isInserted = false; } diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue index a8589b50899..6b414d958ed 100644 --- a/app/assets/javascripts/projects/commits/components/author_select.vue +++ b/app/assets/javascripts/projects/commits/components/author_select.vue @@ -119,7 +119,7 @@ export default { <gl-new-dropdown-divider /> <gl-search-box-by-type v-model.trim="authorInput" - class="m-2" + class="gl-m-3" :placeholder="__('Search')" @input="searchAuthors" /> diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue index 227d9043d7f..3d2eaebf1cb 100644 --- a/app/assets/javascripts/snippets/components/edit.vue +++ b/app/assets/javascripts/snippets/components/edit.vue @@ -63,7 +63,7 @@ export default { return this.actions.length > 0; }, hasValidBlobs() { - return this.actions.every(x => x.filePath && x.content); + return this.actions.every(x => x.content); }, updatePrevented() { return this.snippet.title === '' || !this.hasValidBlobs || this.isUpdating; diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js index 8dccd7d6f5f..ad86929ec0a 100644 --- a/app/assets/javascripts/users_select/index.js +++ b/app/assets/javascripts/users_select/index.js @@ -11,7 +11,7 @@ import { import axios from '../lib/utils/axios_utils'; import { s__, __, sprintf } from '../locale'; import ModalStore from '../boards/stores/modal_store'; -import { parseBoolean } from '../lib/utils/common_utils'; +import { parseBoolean, spriteIcon } from '../lib/utils/common_utils'; import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; @@ -225,7 +225,9 @@ function UsersSelect(currentUser, els, options = {}) { }); }; collapsedAssigneeTemplate = template( - '<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>', + `<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> ${spriteIcon( + 'user', + )} <% } %>`, ); assigneeTemplate = template( `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss index 4ffcf84c790..964dc33658b 100644 --- a/app/assets/stylesheets/fontawesome_custom.scss +++ b/app/assets/stylesheets/fontawesome_custom.scss @@ -130,10 +130,6 @@ content: '\f101'; } -.fa-trash::before { - content: '\f1f8'; -} - .fa-angle-double-left::before { content: '\f100'; } @@ -186,10 +182,6 @@ content: '\f1a0'; } -.fa-user::before { - content: '\f007'; -} - .fa-exclamation-circle::before { content: '\f06a'; } @@ -210,10 +202,6 @@ content: '\f016'; } -.fa-users::before { - content: '\f0c0'; -} - .fa-tags::before { content: '\f02c'; } @@ -266,10 +254,6 @@ content: '\f061'; } -.fa-user-secret::before { - content: '\f21b'; -} - .fa-search-plus::before { content: '\f00e'; } diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb index 9a8e5d14123..8c9aea6d868 100644 --- a/app/controllers/concerns/integrations_actions.rb +++ b/app/controllers/concerns/integrations_actions.rb @@ -16,12 +16,11 @@ module IntegrationsActions def update saved = integration.update(service_params[:service]) - overwrite = Gitlab::Utils.to_boolean(params[:overwrite]) respond_to do |format| format.html do if saved - PropagateIntegrationWorker.perform_async(integration.id, overwrite) + PropagateIntegrationWorker.perform_async(integration.id, false) redirect_to scoped_edit_integration_path(integration), notice: success_message else render 'shared/integrations/edit' diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb index b403f5b1d8f..5a5b634da40 100644 --- a/app/controllers/concerns/wiki_actions.rb +++ b/app/controllers/concerns/wiki_actions.rb @@ -93,9 +93,10 @@ module WikiActions def update return render('shared/wikis/empty') unless can?(current_user, :create_wiki, container) - @page = WikiPages::UpdateService.new(container: container, current_user: current_user, params: wiki_params).execute(page) + response = WikiPages::UpdateService.new(container: container, current_user: current_user, params: wiki_params).execute(page) + @page = response.payload[:page] - if page.valid? + if response.success? redirect_to( wiki_page_path(wiki, page), notice: _('Wiki was successfully updated.') diff --git a/app/graphql/mutations/ci/base.rb b/app/graphql/mutations/ci/base.rb new file mode 100644 index 00000000000..09df4487a50 --- /dev/null +++ b/app/graphql/mutations/ci/base.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Mutations + module Ci + class Base < BaseMutation + argument :id, ::Types::GlobalIDType[::Ci::Pipeline], + required: true, + description: 'The id of the pipeline to mutate' + + private + + def find_object(id:) + GlobalID::Locator.locate(id) + end + end + end +end diff --git a/app/graphql/mutations/ci/pipeline_cancel.rb b/app/graphql/mutations/ci/pipeline_cancel.rb new file mode 100644 index 00000000000..2f50540255e --- /dev/null +++ b/app/graphql/mutations/ci/pipeline_cancel.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Mutations + module Ci + class PipelineCancel < BaseMutation + graphql_name 'PipelineCancel' + + authorize :update_pipeline + + def resolve + result = ::Ci::CancelUserPipelinesService.new.execute(current_user) + + { + success: result.success?, + errors: [result&.message] + } + end + end + end +end diff --git a/app/graphql/mutations/ci/pipeline_destroy.rb b/app/graphql/mutations/ci/pipeline_destroy.rb new file mode 100644 index 00000000000..bb24d416583 --- /dev/null +++ b/app/graphql/mutations/ci/pipeline_destroy.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutations + module Ci + class PipelineDestroy < Base + graphql_name 'PipelineDestroy' + + authorize :destroy_pipeline + + def resolve(id:) + pipeline = authorized_find!(id: id) + project = pipeline.project + + result = ::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline) + { + success: result.success?, + errors: result.errors + } + end + end + end +end diff --git a/app/graphql/mutations/ci/pipeline_retry.rb b/app/graphql/mutations/ci/pipeline_retry.rb new file mode 100644 index 00000000000..0669bfc449c --- /dev/null +++ b/app/graphql/mutations/ci/pipeline_retry.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Mutations + module Ci + class PipelineRetry < Base + graphql_name 'PipelineRetry' + + field :pipeline, + Types::Ci::PipelineType, + null: true, + description: 'The pipeline after mutation' + + authorize :update_pipeline + + def resolve(id:) + pipeline = authorized_find!(id: id) + project = pipeline.project + + ::Ci::RetryPipelineService.new(project, current_user).execute(pipeline) + { + pipeline: pipeline, + errors: errors_on_object(pipeline) + } + end + end + end +end diff --git a/app/graphql/resolvers/metrics/dashboard_resolver.rb b/app/graphql/resolvers/metrics/dashboard_resolver.rb index 05d82ca0f46..18a654c7dc5 100644 --- a/app/graphql/resolvers/metrics/dashboard_resolver.rb +++ b/app/graphql/resolvers/metrics/dashboard_resolver.rb @@ -14,7 +14,8 @@ module Resolvers def resolve(**args) return unless environment - ::PerformanceMonitoring::PrometheusDashboard.find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment }) + ::PerformanceMonitoring::PrometheusDashboard + .find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment }) end end end diff --git a/app/graphql/types/metrics/dashboard_type.rb b/app/graphql/types/metrics/dashboard_type.rb index bbcce2d9596..47502356773 100644 --- a/app/graphql/types/metrics/dashboard_type.rb +++ b/app/graphql/types/metrics/dashboard_type.rb @@ -16,6 +16,13 @@ module Types field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true, description: 'Annotations added to the dashboard', resolver: Resolvers::Metrics::Dashboards::AnnotationResolver + + # In order to maintain backward compatibility we need to return NULL when there are no warnings + # and dashboard validation returns an empty array when there are no issues. + def schema_validation_warnings + warnings = object.schema_validation_warnings + warnings unless warnings.empty? + end end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb index ca606c9da44..91527f9dcd4 100644 --- a/app/graphql/types/milestone_type.rb +++ b/app/graphql/types/milestone_type.rb @@ -60,3 +60,5 @@ module Types end end end + +Types::MilestoneType.prepend_if_ee('::EE::Types::MilestoneType') diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index e143d14676e..aea79f4f423 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -62,6 +62,9 @@ module Types mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true mount_mutation Mutations::DesignManagement::Move mount_mutation Mutations::ContainerExpirationPolicies::Update + mount_mutation Mutations::Ci::PipelineCancel + mount_mutation Mutations::Ci::PipelineDestroy + mount_mutation Mutations::Ci::PipelineRetry end end diff --git a/app/models/blob_viewer/metrics_dashboard_yml.rb b/app/models/blob_viewer/metrics_dashboard_yml.rb index c05fb5d88d6..88643253d3d 100644 --- a/app/models/blob_viewer/metrics_dashboard_yml.rb +++ b/app/models/blob_viewer/metrics_dashboard_yml.rb @@ -25,20 +25,30 @@ module BlobViewer private def parse_blob_data - yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw! + if Feature.enabled?(:metrics_dashboard_exhaustive_validations, project) + exhaustive_metrics_dashboard_validation + else + old_metrics_dashboard_validation + end + end + def old_metrics_dashboard_validation + yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw! ::PerformanceMonitoring::PrometheusDashboard.from_json(yaml) - nil + [] rescue Gitlab::Config::Loader::FormatError => error - wrap_yml_syntax_error(error) + ["YAML syntax: #{error.message}"] rescue ActiveModel::ValidationError => invalid - invalid.model.errors + invalid.model.errors.messages.map { |messages| messages.join(': ') } end - def wrap_yml_syntax_error(error) - ::PerformanceMonitoring::PrometheusDashboard.new.errors.tap do |errors| - errors.add(:'YAML syntax', error.message) - end + def exhaustive_metrics_dashboard_validation + yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw! + Gitlab::Metrics::Dashboard::Validator + .errors(yaml, dashboard_path: blob.path, project: project) + .map(&:message) + rescue Gitlab::Config::Loader::FormatError => error + [error.message] end end end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 75c3ce98c95..ae1b5bab7fb 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -12,8 +12,6 @@ module Ci include FileStoreMounter extend Gitlab::Ci::Model - NotSupportedAdapterError = Class.new(StandardError) - ignore_columns :locked, remove_after: '2020-07-22', remove_with: '13.4' TEST_REPORT_FILE_TYPES = %w[junit].freeze @@ -271,16 +269,6 @@ module Ci end end - def each_blob(&blk) - unless file_format_adapter_class - raise NotSupportedAdapterError, 'This file format requires a dedicated adapter' - end - - file.open do |stream| - file_format_adapter_class.new(stream).each_blob(&blk) - end - end - def self.archived_trace_exists_for?(job_id) where(job_id: job_id).trace.take&.file&.file&.exists? end @@ -298,10 +286,6 @@ module Ci private - def file_format_adapter_class - FILE_FORMAT_ADAPTERS[file_format.to_sym] - end - def set_size self.size = file.size end diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb index 54fb9021f2f..37cd0d954b0 100644 --- a/app/models/concerns/ci/artifactable.rb +++ b/app/models/concerns/ci/artifactable.rb @@ -4,6 +4,8 @@ module Ci module Artifactable extend ActiveSupport::Concern + NotSupportedAdapterError = Class.new(StandardError) + FILE_FORMAT_ADAPTERS = { gzip: Gitlab::Ci::Build::Artifacts::Adapters::GzipStream, raw: Gitlab::Ci::Build::Artifacts::Adapters::RawStream @@ -16,5 +18,21 @@ module Ci gzip: 3 }, _suffix: true end + + def each_blob(&blk) + unless file_format_adapter_class + raise NotSupportedAdapterError, 'This file format requires a dedicated adapter' + end + + file.open do |stream| + file_format_adapter_class.new(stream).each_blob(&blk) + end + end + + private + + def file_format_adapter_class + FILE_FORMAT_ADAPTERS[file_format.to_sym] + end end end diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 74a1e15f42b..a6fd912e135 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -143,7 +143,7 @@ module RelativePositioning return 0 if objects.empty? representative = objects.first - number_of_gaps = objects.size + 1 # 1 at left, one between each, and one at right + number_of_gaps = objects.size # 1 to the nearest neighbour, and one between each position = if at_end representative.max_relative_position else @@ -152,16 +152,21 @@ module RelativePositioning position ||= START_POSITION # If there are no positioned siblings, start from START_POSITION - gap, position = gap_size(representative, gaps: number_of_gaps, at_end: at_end, starting_from: position) - - # Raise if we could not make enough space - raise NoSpaceLeft if gap < MIN_GAP + gap = 0 + attempts = 10 # consolidate up to 10 gaps to find enough space + while gap < 1 && attempts > 0 + gap, position = gap_size(representative, gaps: number_of_gaps, at_end: at_end, starting_from: position) + attempts -= 1 + end - indexed = objects.each_with_index.to_a - starting_from = at_end ? position : position - (gap * number_of_gaps) + # Allow placing items next to each other, if we have to. + gap = 1 if gap < MIN_GAP + delta = at_end ? gap : -gap + indexed = (at_end ? objects : objects.reverse).each_with_index # Some classes are polymorphic, and not all siblings are in the same table. by_model = indexed.group_by { |pair| pair.first.class } + lower_bound, upper_bound = at_end ? [position, MAX_POSITION] : [MIN_POSITION, position] by_model.each do |model, pairs| model.transaction do @@ -169,7 +174,8 @@ module RelativePositioning # These are known to be integers, one from the DB, and the other # calculated by us, and thus safe to interpolate values = batch.map do |obj, i| - pos = starting_from + gap * (i + 1) + desired_pos = position + delta * (i + 1) + pos = desired_pos.clamp(lower_bound, upper_bound) obj.relative_position = pos "(#{obj.id}, #{pos})" end.join(', ') diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index 42159a8349f..57cb887d12a 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -25,8 +25,10 @@ module Storage Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path) if ::Feature.enabled?(:async_pages_move_namespace_transfer, self) - run_after_commit do - Gitlab::PagesTransfer.new.async.move_namespace(path, former_parent_full_path, parent_full_path) + if any_project_with_pages_deployed? + run_after_commit do + Gitlab::PagesTransfer.new.async.move_namespace(path, former_parent_full_path, parent_full_path) + end end else Gitlab::PagesTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path) @@ -35,10 +37,12 @@ module Storage Gitlab::UploadsTransfer.new.rename_namespace(full_path_before_last_save, full_path) if ::Feature.enabled?(:async_pages_move_namespace_rename, self) - full_path_was = full_path_before_last_save + if any_project_with_pages_deployed? + full_path_was = full_path_before_last_save - run_after_commit do - Gitlab::PagesTransfer.new.async.rename_namespace(full_path_was, full_path) + run_after_commit do + Gitlab::PagesTransfer.new.async.rename_namespace(full_path_was, full_path) + end end else Gitlab::PagesTransfer.new.rename_namespace(full_path_before_last_save, full_path) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 48e10ad4fee..527fa9d52d0 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -353,6 +353,10 @@ class Namespace < ApplicationRecord ) end + def any_project_with_pages_deployed? + all_projects.with_pages_deployed.any? + end + def closest_setting(name) self_and_ancestors(hierarchy_order: :asc) .find { |n| !n.read_attribute(name).nil? } diff --git a/app/models/performance_monitoring/prometheus_dashboard.rb b/app/models/performance_monitoring/prometheus_dashboard.rb index bf87d2c3916..40d14aaa1de 100644 --- a/app/models/performance_monitoring/prometheus_dashboard.rb +++ b/app/models/performance_monitoring/prometheus_dashboard.rb @@ -53,14 +53,23 @@ module PerformanceMonitoring # This method is planned to be refactored as a part of https://gitlab.com/gitlab-org/gitlab/-/issues/219398 # implementation. For new existing logic was reused to faster deliver MVC def schema_validation_warnings + return run_custom_validation.map(&:message) if Feature.enabled?(:metrics_dashboard_exhaustive_validations, environment&.project) + self.class.from_json(reload_schema) - nil + [] + rescue Gitlab::Metrics::Dashboard::Errors::LayoutError => error + [error.message] rescue ActiveModel::ValidationError => exception exception.model.errors.map { |attr, error| "#{attr}: #{error}" } end private + def run_custom_validation + Gitlab::Metrics::Dashboard::Validator + .errors(reload_schema, dashboard_path: path, project: environment&.project) + end + # dashboard finder methods are somehow limited, #find includes checking if # user is authorised to view selected dashboard, but modifies schema, which in some cases may # cause false positives returned from validation, and #find_raw does not authorise users diff --git a/app/models/service.rb b/app/models/service.rb index 75ad2cdbc83..148c554119f 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -26,16 +26,17 @@ class Service < ApplicationRecord default_value_for :active, false default_value_for :alert_events, true - default_value_for :push_events, true - default_value_for :issues_events, true - default_value_for :confidential_issues_events, true + default_value_for :category, 'common' default_value_for :commit_events, true - default_value_for :merge_requests_events, true - default_value_for :tag_push_events, true - default_value_for :note_events, true + default_value_for :confidential_issues_events, true default_value_for :confidential_note_events, true + default_value_for :issues_events, true default_value_for :job_events, true + default_value_for :merge_requests_events, true + default_value_for :note_events, true default_value_for :pipeline_events, true + default_value_for :push_events, true + default_value_for :tag_push_events, true default_value_for :wiki_page_events, true after_initialize :initialize_properties @@ -81,7 +82,163 @@ class Service < ApplicationRecord scope :alert_hooks, -> { where(alert_events: true, active: true) } scope :deployment, -> { where(category: 'deployment') } - default_value_for :category, 'common' + # Provide convenient accessor methods for each serialized property. + # Also keep track of updated properties in a similar way as ActiveModel::Dirty + def self.prop_accessor(*args) + args.each do |arg| + class_eval <<~RUBY, __FILE__, __LINE__ + 1 + unless method_defined?(arg) + def #{arg} + properties['#{arg}'] + end + end + + def #{arg}=(value) + self.properties ||= {} + updated_properties['#{arg}'] = #{arg} unless #{arg}_changed? + self.properties['#{arg}'] = value + end + + def #{arg}_changed? + #{arg}_touched? && #{arg} != #{arg}_was + end + + def #{arg}_touched? + updated_properties.include?('#{arg}') + end + + def #{arg}_was + updated_properties['#{arg}'] + end + RUBY + end + end + + # Provide convenient boolean accessor methods for each serialized property. + # Also keep track of updated properties in a similar way as ActiveModel::Dirty + def self.boolean_accessor(*args) + self.prop_accessor(*args) + + args.each do |arg| + class_eval <<~RUBY, __FILE__, __LINE__ + 1 + def #{arg}? + # '!!' is used because nil or empty string is converted to nil + !!ActiveRecord::Type::Boolean.new.cast(#{arg}) + end + RUBY + end + end + + def self.to_param + raise NotImplementedError + end + + def self.event_names + self.supported_events.map { |event| ServicesHelper.service_event_field_name(event) } + end + + def self.supported_event_actions + %w[] + end + + def self.supported_events + %w[commit push tag_push issue confidential_issue merge_request wiki_page] + end + + def self.event_description(event) + ServicesHelper.service_event_description(event) + end + + def self.find_or_create_templates + create_nonexistent_templates + for_template + end + + def self.create_nonexistent_templates + nonexistent_services = list_nonexistent_services_for(for_template) + return if nonexistent_services.empty? + + # Create within a transaction to perform the lowest possible SQL queries. + transaction do + nonexistent_services.each do |service_type| + service_type.constantize.create(template: true) + end + end + end + private_class_method :create_nonexistent_templates + + def self.find_or_initialize_integration(name, instance: false, group_id: nil) + if name.in?(available_services_names) + "#{name}_service".camelize.constantize.find_or_initialize_by(instance: instance, group_id: group_id) + end + end + + def self.find_or_initialize_all(scope) + scope + build_nonexistent_services_for(scope) + end + + def self.build_nonexistent_services_for(scope) + list_nonexistent_services_for(scope).map do |service_type| + service_type.constantize.new + end + end + private_class_method :build_nonexistent_services_for + + def self.list_nonexistent_services_for(scope) + # Using #map instead of #pluck to save one query count. This is because + # ActiveRecord loaded the object here, so we don't need to query again later. + available_services_types - scope.map(&:type) + end + private_class_method :list_nonexistent_services_for + + def self.available_services_names + service_names = services_names + service_names += dev_services_names + + service_names.sort_by(&:downcase) + end + + def self.services_names + SERVICE_NAMES + end + + def self.dev_services_names + return [] unless Rails.env.development? + + DEV_SERVICE_NAMES + end + + def self.available_services_types + available_services_names.map { |service_name| "#{service_name}_service".camelize } + end + + def self.services_types + services_names.map { |service_name| "#{service_name}_service".camelize } + end + + def self.build_from_integration(project_id, integration) + service = integration.dup + + if integration.supports_data_fields? + data_fields = integration.data_fields.dup + data_fields.service = service + end + + service.template = false + service.instance = false + service.inherit_from_id = integration.id if integration.instance? + service.project_id = project_id + service.active = false if service.invalid? + service + end + + def self.instance_exists_for?(type) + exists?(instance: true, type: type) + end + + def self.instance_for(type) + find_by(instance: true, type: type) + end def activated? active @@ -124,10 +281,6 @@ class Service < ApplicationRecord self.class.to_param end - def self.to_param - raise NotImplementedError - end - def fields # implement inside child [] @@ -137,7 +290,7 @@ class Service < ApplicationRecord # # This list is used in `Service#as_json(only: json_fields)`. def json_fields - %w(active) + %w[active] end def to_service_hash @@ -156,10 +309,6 @@ class Service < ApplicationRecord self.class.event_names end - def self.event_names - self.supported_events.map { |event| ServicesHelper.service_event_field_name(event) } - end - def event_field(event) nil end @@ -188,18 +337,10 @@ class Service < ApplicationRecord self.class.supported_event_actions end - def self.supported_event_actions - %w() - end - def supported_events self.class.supported_events end - def self.supported_events - %w(commit push tag_push issue confidential_issue merge_request wiki_page) - end - def execute(data) # implement inside child end @@ -216,55 +357,6 @@ class Service < ApplicationRecord !instance? end - # Provide convenient accessor methods - # for each serialized property. - # Also keep track of updated properties in a similar way as ActiveModel::Dirty - def self.prop_accessor(*args) - args.each do |arg| - class_eval <<~RUBY, __FILE__, __LINE__ + 1 - unless method_defined?(arg) - def #{arg} - properties['#{arg}'] - end - end - - def #{arg}=(value) - self.properties ||= {} - updated_properties['#{arg}'] = #{arg} unless #{arg}_changed? - self.properties['#{arg}'] = value - end - - def #{arg}_changed? - #{arg}_touched? && #{arg} != #{arg}_was - end - - def #{arg}_touched? - updated_properties.include?('#{arg}') - end - - def #{arg}_was - updated_properties['#{arg}'] - end - RUBY - end - end - - # Provide convenient boolean accessor methods - # for each serialized property. - # Also keep track of updated properties in a similar way as ActiveModel::Dirty - def self.boolean_accessor(*args) - self.prop_accessor(*args) - - args.each do |arg| - class_eval <<~RUBY, __FILE__, __LINE__ + 1 - def #{arg}? - # '!!' is used because nil or empty string is converted to nil - !!ActiveRecord::Type::Boolean.new.cast(#{arg}) - end - RUBY - end - end - # Returns a hash of the properties that have been assigned a new value since last save, # indicating their original values (attr => original value). # ActiveRecord does not provide a mechanism to track changes in serialized keys, @@ -289,92 +381,6 @@ class Service < ApplicationRecord self.category == :issue_tracker end - def self.find_or_create_templates - create_nonexistent_templates - for_template - end - - private_class_method def self.create_nonexistent_templates - nonexistent_services = list_nonexistent_services_for(for_template) - return if nonexistent_services.empty? - - # Create within a transaction to perform the lowest possible SQL queries. - transaction do - nonexistent_services.each do |service_type| - service_type.constantize.create(template: true) - end - end - end - - def self.find_or_initialize_integration(name, instance: false, group_id: nil) - if name.in?(available_services_names) - "#{name}_service".camelize.constantize.find_or_initialize_by(instance: instance, group_id: group_id) - end - end - - def self.find_or_initialize_all(scope) - scope + build_nonexistent_services_for(scope) - end - - private_class_method def self.build_nonexistent_services_for(scope) - list_nonexistent_services_for(scope).map do |service_type| - service_type.constantize.new - end - end - - private_class_method def self.list_nonexistent_services_for(scope) - available_services_types - scope.map(&:type) - end - - def self.available_services_names - service_names = services_names - service_names += dev_services_names - - service_names.sort_by(&:downcase) - end - - def self.services_names - SERVICE_NAMES - end - - def self.dev_services_names - return [] unless Rails.env.development? - - DEV_SERVICE_NAMES - end - - def self.available_services_types - available_services_names.map { |service_name| "#{service_name}_service".camelize } - end - - def self.services_types - services_names.map { |service_name| "#{service_name}_service".camelize } - end - - def self.build_from_integration(project_id, integration) - service = integration.dup - - if integration.supports_data_fields? - data_fields = integration.data_fields.dup - data_fields.service = service - end - - service.template = false - service.instance = false - service.inherit_from_id = integration.id if integration.instance? - service.project_id = project_id - service.active = false if service.invalid? - service - end - - def self.instance_exists_for?(type) - exists?(instance: true, type: type) - end - - def self.instance_for(type) - find_by(instance: true, type: type) - end - # override if needed def supports_data_fields? false @@ -402,10 +408,6 @@ class Service < ApplicationRecord end end - def self.event_description(event) - ServicesHelper.service_event_description(event) - end - def valid_recipients? activated? && !importing? end diff --git a/app/services/admin/propagate_integration_service.rb b/app/services/admin/propagate_integration_service.rb index 9a5ce58ee2c..6af6239d442 100644 --- a/app/services/admin/propagate_integration_service.rb +++ b/app/services/admin/propagate_integration_service.rb @@ -6,42 +6,30 @@ module Admin delegate :data_fields_present?, to: :integration - def self.propagate(integration:, overwrite:) - new(integration, overwrite).propagate + def self.propagate(integration) + new(integration).propagate end - def initialize(integration, overwrite) + def initialize(integration) @integration = integration - @overwrite = overwrite end def propagate - if overwrite - update_integration_for_all_projects - else - update_integration_for_inherited_projects - end - + update_inherited_integrations create_integration_for_projects_without_integration end private - attr_reader :integration, :overwrite + attr_reader :integration # rubocop: disable Cop/InBatches # rubocop: disable CodeReuse/ActiveRecord - def update_integration_for_inherited_projects + def update_inherited_integrations Service.where(type: integration.type, inherit_from_id: integration.id).in_batches(of: BATCH_SIZE) do |batch| bulk_update_from_integration(batch) end end - - def update_integration_for_all_projects - Service.where(type: integration.type).in_batches(of: BATCH_SIZE) do |batch| - bulk_update_from_integration(batch) - end - end # rubocop: enable Cop/InBatches # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/ci/cancel_user_pipelines_service.rb b/app/services/ci/cancel_user_pipelines_service.rb index bcafb6b4a35..3a8b5e91088 100644 --- a/app/services/ci/cancel_user_pipelines_service.rb +++ b/app/services/ci/cancel_user_pipelines_service.rb @@ -7,6 +7,10 @@ module Ci # https://gitlab.com/gitlab-org/gitlab/issues/32332 def execute(user) user.pipelines.cancelable.find_each(&:cancel_running) + + ServiceResponse.success(message: 'Pipeline canceled') + rescue ActiveRecord::StaleObjectError + ServiceResponse.error(message: 'Error canceling pipeline') end # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb index 9aea20c45f7..1d9533ed76f 100644 --- a/app/services/ci/destroy_pipeline_service.rb +++ b/app/services/ci/destroy_pipeline_service.rb @@ -8,6 +8,10 @@ module Ci Ci::ExpirePipelineCacheService.new.execute(pipeline, delete: true) pipeline.destroy! + + ServiceResponse.success(message: 'Pipeline not found') + rescue ActiveRecord::RecordNotFound + ServiceResponse.error(message: 'Pipeline not found') end end end diff --git a/app/services/jira_import/users_mapper_service.rb b/app/services/jira_import/users_mapper_service.rb index b5997d77215..480c034f952 100644 --- a/app/services/jira_import/users_mapper_service.rb +++ b/app/services/jira_import/users_mapper_service.rb @@ -2,6 +2,7 @@ module JiraImport class UsersMapperService + # MAX_USERS must match the pageSize value in app/assets/javascripts/jira_import/utils/constants.js MAX_USERS = 50 attr_reader :jira_service, :start_at diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb index f9022b3afe3..4abe83868b3 100644 --- a/app/services/projects/after_rename_service.rb +++ b/app/services/projects/after_rename_service.rb @@ -97,16 +97,18 @@ module Projects end if ::Feature.enabled?(:async_pages_move_project_rename, project) - # Block will be evaluated in the context of project so we need - # to bind to a local variable to capture it, as the instance - # variable and method aren't available on Project - path_before_local = @path_before - - project.run_after_commit_or_now do - Gitlab::PagesTransfer - .new - .async - .rename_project(path_before_local, path, namespace.full_path) + if project.pages_deployed? + # Block will be evaluated in the context of project so we need + # to bind to a local variable to capture it, as the instance + # variable and method aren't available on Project + path_before_local = @path_before + + project.run_after_commit_or_now do + Gitlab::PagesTransfer + .new + .async + .rename_project(path_before_local, path, namespace.full_path) + end end else Gitlab::PagesTransfer diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 0fb70feec86..2e91ea8e0d8 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -88,15 +88,14 @@ module Projects # Move uploads move_project_uploads(project) - # Move pages - Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) - project.old_path_with_namespace = @old_path update_repository_configuration(@new_path) execute_system_hooks end + + move_pages(project) rescue Exception # rubocop:disable Lint/RescueException rollback_side_effects raise @@ -181,6 +180,19 @@ module Projects ) end + def move_pages(project) + transfer = Gitlab::PagesTransfer.new + + if Feature.enabled?(:async_pages_move_project_transfer, project) + # Avoid scheduling moves for directories that don't exist. + return unless project.pages_deployed? + + transfer = transfer.async + end + + transfer.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) + end + def old_wiki_repo_path "#{old_path}#{::Gitlab::GlRepository::WIKI.path_suffix}" end diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb index 5ac6902e0b0..2b7a6cd2e40 100644 --- a/app/services/wiki_pages/update_service.rb +++ b/app/services/wiki_pages/update_service.rb @@ -8,9 +8,13 @@ module WikiPages if page.update(@params) execute_hooks(page) + ServiceResponse.success(payload: { page: page }) + else + ServiceResponse.error( + message: _('Could not udpdate wiki page'), + payload: { page: page } + ) end - - page end def usage_counter_action diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index 68c8ff2b73d..e3f699c1597 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -1,3 +1,5 @@ +- payload_class = 'js-usage-ping-payload' + = form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) @@ -25,10 +27,10 @@ - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path } %p.mb-2= s_('%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe } - %button.btn.js-usage-ping-payload-trigger{ type: 'button' } + %button.btn.js-payload-preview-trigger{ type: 'button', data: { payload_selector: ".#{payload_class}" } } .spinner.js-spinner.d-none .js-text.d-inline= _('Preview payload') - %pre.usage-data.js-usage-ping-payload.js-syntax-highlight.code.highlight.mt-2.d-none{ data: { endpoint: usage_data_admin_application_settings_path(format: :html) } } + %pre.usage-data.js-syntax-highlight.code.highlight.mt-2.d-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } } - else = _('The usage ping is disabled, and cannot be configured through this form.') - deactivating_usage_ping_path = help_page_path('development/telemetry/usage_ping', anchor: 'disable-usage-ping') diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml index ac5cac50699..77b7a50338c 100644 --- a/app/views/doorkeeper/applications/_delete_form.html.haml +++ b/app/views/doorkeeper/applications/_delete_form.html.haml @@ -5,6 +5,6 @@ = button_tag type: "submit", class: "btn btn-transparent", data: { confirm: _("Are you sure?") } do %span.sr-only = _('Destroy') - = icon('trash') + = sprite_icon('remove') - else = submit_tag _('Destroy'), data: { confirm: _("Are you sure?") }, class: submit_btn_css diff --git a/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml b/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml index 9ec1d7d0d67..de9c6c5320f 100644 --- a/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml +++ b/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml @@ -5,7 +5,7 @@ = icon('warning fw') = _('Metrics Dashboard YAML definition is invalid:') %ul - - viewer.errors.messages.each do |error| - %li= error.join(': ') + - viewer.errors.each do |error| + %li= error = link_to _('Learn more'), help_page_path('operations/metrics/dashboards/index.md') diff --git a/app/views/projects/packages/packages/_legacy_package_list.html.haml b/app/views/projects/packages/packages/_legacy_package_list.html.haml index 43dbb5c3eee..d94062a1a12 100644 --- a/app/views/projects/packages/packages/_legacy_package_list.html.haml +++ b/app/views/projects/packages/packages/_legacy_package_list.html.haml @@ -52,7 +52,7 @@ - if can_destroy_package .float-right = link_to project_package_path(@project, package), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-remove", title: _('Delete Package') do - = icon('trash') + = sprite_icon('remove') = paginate @packages, theme: "gitlab" - else .row.empty-state diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index 3036e918160..579b8ba2766 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -34,4 +34,4 @@ = sprite_icon('pencil') - if can?(current_user, :manage_trigger, trigger) = link_to project_trigger_path(@project, trigger), data: { confirm: revoke_trigger_confirmation }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do - %i.fa.fa-trash + = sprite_icon('remove') diff --git a/app/workers/propagate_integration_worker.rb b/app/workers/propagate_integration_worker.rb index 15c0e761a0a..68e38386372 100644 --- a/app/workers/propagate_integration_worker.rb +++ b/app/workers/propagate_integration_worker.rb @@ -7,10 +7,8 @@ class PropagateIntegrationWorker idempotent! loggable_arguments 1 - def perform(integration_id, overwrite) - Admin::PropagateIntegrationService.propagate( - integration: Service.find(integration_id), - overwrite: overwrite - ) + # Keep overwrite parameter for backwards compatibility. + def perform(integration_id, overwrite = nil) + Admin::PropagateIntegrationService.propagate(Service.find(integration_id)) end end diff --git a/changelogs/unreleased/225926-replace-fa-tag-s-icons-with-gitlab-svg-icons.yml b/changelogs/unreleased/225926-replace-fa-tag-s-icons-with-gitlab-svg-icons.yml new file mode 100644 index 00000000000..eee4518a4e7 --- /dev/null +++ b/changelogs/unreleased/225926-replace-fa-tag-s-icons-with-gitlab-svg-icons.yml @@ -0,0 +1,5 @@ +--- +title: Replace fa-trash icons with GitLab SVG remove icon +merge_request: 40579 +author: +type: changed diff --git a/changelogs/unreleased/225936-replace-fa-user-s-icons-with-gitlab-svg-user-s-icon.yml b/changelogs/unreleased/225936-replace-fa-user-s-icons-with-gitlab-svg-user-s-icon.yml new file mode 100644 index 00000000000..0077210064b --- /dev/null +++ b/changelogs/unreleased/225936-replace-fa-user-s-icons-with-gitlab-svg-user-s-icon.yml @@ -0,0 +1,5 @@ +--- +title: Replace fa-user(s) icons with GitLab SVG user(s) icon +merge_request: 39165 +author: +type: changed diff --git a/changelogs/unreleased/235889-jira-importer-user-mapping-shows-50-users-max.yml b/changelogs/unreleased/235889-jira-importer-user-mapping-shows-50-users-max.yml new file mode 100644 index 00000000000..1fcf6c768e7 --- /dev/null +++ b/changelogs/unreleased/235889-jira-importer-user-mapping-shows-50-users-max.yml @@ -0,0 +1,5 @@ +--- +title: Fix Jira importer user mapping limit +merge_request: 40310 +author: +type: fixed diff --git a/changelogs/unreleased/239341-fix-snippets-create-without-file-path.yml b/changelogs/unreleased/239341-fix-snippets-create-without-file-path.yml new file mode 100644 index 00000000000..9e3e412638e --- /dev/null +++ b/changelogs/unreleased/239341-fix-snippets-create-without-file-path.yml @@ -0,0 +1,5 @@ +--- +title: Fix snippet save button disabled with empty file path +merge_request: 40412 +author: +type: fixed diff --git a/changelogs/unreleased/ajk-relative-positioning-safe-move-nulls.yml b/changelogs/unreleased/ajk-relative-positioning-safe-move-nulls.yml new file mode 100644 index 00000000000..8a989a70416 --- /dev/null +++ b/changelogs/unreleased/ajk-relative-positioning-safe-move-nulls.yml @@ -0,0 +1,5 @@ +--- +title: Avoid raising errors when moving unpositioned items +merge_request: 40152 +author: +type: fixed diff --git a/changelogs/unreleased/lm-be-pipeline-mutations.yml b/changelogs/unreleased/lm-be-pipeline-mutations.yml new file mode 100644 index 00000000000..650722506df --- /dev/null +++ b/changelogs/unreleased/lm-be-pipeline-mutations.yml @@ -0,0 +1,5 @@ +--- +title: 'GraphQL: Pipeline mutations for retry, cancel, and destroy' +merge_request: 39780 +author: +type: added diff --git a/changelogs/unreleased/sh-always-retry-read-build-logs.yml b/changelogs/unreleased/sh-always-retry-read-build-logs.yml new file mode 100644 index 00000000000..da4b185277d --- /dev/null +++ b/changelogs/unreleased/sh-always-retry-read-build-logs.yml @@ -0,0 +1,5 @@ +--- +title: Always attempt retry of job trace read when file is missing +merge_request: 40516 +author: +type: fixed diff --git a/config/feature_flags/development/async_pages_move_project_transfer.yml b/config/feature_flags/development/async_pages_move_project_transfer.yml new file mode 100644 index 00000000000..6f4c9538970 --- /dev/null +++ b/config/feature_flags/development/async_pages_move_project_transfer.yml @@ -0,0 +1,7 @@ +--- +name: async_pages_move_project_transfer +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40492 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235757 +group: team::Scalability +type: development +default_enabled: false diff --git a/config/feature_flags/development/generic_packages.yml b/config/feature_flags/development/generic_packages.yml new file mode 100644 index 00000000000..99b89b196ea --- /dev/null +++ b/config/feature_flags/development/generic_packages.yml @@ -0,0 +1,7 @@ +--- +name: generic_packages +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40045 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/239133 +group: group::release management +type: development +default_enabled: false diff --git a/config/feature_flags/development/metrics_dashboard_exhaustive_validations.yml b/config/feature_flags/development/metrics_dashboard_exhaustive_validations.yml new file mode 100644 index 00000000000..3e3a5b9de9a --- /dev/null +++ b/config/feature_flags/development/metrics_dashboard_exhaustive_validations.yml @@ -0,0 +1,7 @@ +--- +name: metrics_dashboard_exhaustive_validations +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40103 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/241697 +group: group::apm +type: development +default_enabled: false
\ No newline at end of file diff --git a/doc/README.md b/doc/README.md index b2412ca2929..7f5efaf213a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -93,7 +93,7 @@ The following documentation relates to the DevOps **Manage** stage: |:--------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Authentication and<br/>Authorization](administration/auth/README.md) **(CORE ONLY)** | Supported authentication and authorization providers. | | [GitLab Value Stream Analytics](user/project/cycle_analytics.md) | Measure the time it takes to go from an [idea to production](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab) for each project you have. | -| [Instance-level Anlytics](user/admin_area/analytics/index.md) | Discover statistics on how many GitLab features you use and user activity. | +| [Instance-level Analytics](user/admin_area/analytics/index.md) | Discover statistics on how many GitLab features you use and user activity. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md index 49716883310..8668200ce44 100644 --- a/doc/administration/object_storage.md +++ b/doc/administration/object_storage.md @@ -279,7 +279,7 @@ This is not compatible with the consolidated object storage form. OpenStack Swift is only supported with the storage-specific form. See the [S3 settings](#s3-compatible-connection-settings) if you want to use the consolidated form. -While OpenStack Swift provides S3 compatibliity, some users may want to use the +While OpenStack Swift provides S3 compatibility, some users may want to use the [Swift API](https://docs.openstack.org/swift/latest/api/object_api_v1_overview.html). Here are the valid connection settings below for the Swift API, provided by [fog-openstack](https://github.com/fog/fog-openstack): diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md index 22d699b424b..f4345153b3d 100644 --- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md +++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md @@ -43,10 +43,13 @@ instance_of_object.method(:foo).source_location project.method(:private?).source_location ``` -## Query an object +## Query the database using an ActiveRecord Model ```ruby -o = Object.where('attribute like ?', 'ex') +m = Model.where('attribute like ?', 'ex%') + +# for example to query the projects +projects = Project.where('path like ?', 'Oumua%') ``` ## View all keys in cache diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md index c628ed837eb..404e806c5d9 100644 --- a/doc/administration/troubleshooting/sidekiq.md +++ b/doc/administration/troubleshooting/sidekiq.md @@ -217,7 +217,7 @@ to perform a number of troubleshooting steps on Sidekiq. These are the administrative commands and it should only be used if currently admin interface is not suitable due to scale of installation. -All this commands should be run using `gitlab-rails console`. +All these commands should be run using `gitlab-rails console`. ### View the queue size diff --git a/doc/administration/troubleshooting/tracing_correlation_id.md b/doc/administration/troubleshooting/tracing_correlation_id.md index f716e74f36a..03c342595a3 100644 --- a/doc/administration/troubleshooting/tracing_correlation_id.md +++ b/doc/administration/troubleshooting/tracing_correlation_id.md @@ -21,7 +21,7 @@ You can find your correlation ID by searching in either place. You can use your browser's developer tools to monitor and inspect network activity with the site that you're visiting. See the links below for network monitoring -documenation for some popular browsers. +documentation for some popular browsers. - [Network Monitor - Firefox Developer Tools](https://developer.mozilla.org/en-US/docs/Tools/Network_Monitor) - [Inspect Network Activity In Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools/network/) diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 4d244e8b6f6..4e745e905bb 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -1427,6 +1427,36 @@ type Branch { name: String! } +""" +Represents the total number of issues and their weights for a particular day. +""" +type BurnupChartDailyTotals { + """ + Number of closed issues as of this day + """ + completedCount: Int! + + """ + Total weight of closed issues as of this day + """ + completedWeight: Int! + + """ + Date for burnup totals + """ + date: ISO8601Date! + + """ + Number of issues as of this day + """ + scopeCount: Int! + + """ + Total weight of issues as of this day + """ + scopeWeight: Int! +} + type CiGroup { """ Jobs in group @@ -1566,6 +1596,11 @@ type CiJobEdge { node: CiJob } +""" +Identifier of Ci::Pipeline +""" +scalar CiPipelineID + type CiStage { """ Group of jobs for the stage @@ -9550,6 +9585,11 @@ Represents a milestone. """ type Milestone { """ + Daily scope and completed totals for burnup charts + """ + burnupTimeSeries: [BurnupChartDailyTotals!] + + """ Timestamp of milestone creation """ createdAt: Time! @@ -9760,6 +9800,9 @@ type Mutation { """ mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload + pipelineCancel(input: PipelineCancelInput!): PipelineCancelPayload + pipelineDestroy(input: PipelineDestroyInput!): PipelineDestroyPayload + pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload @deprecated(reason: "Use awardEmojiRemove. Deprecated in 13.2") removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload runDastScan(input: RunDASTScanInput!): RunDASTScanPayload @@ -10554,6 +10597,31 @@ type Pipeline { userPermissions: PipelinePermissions! } +""" +Autogenerated input type of PipelineCancel +""" +input PipelineCancelInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String +} + +""" +Autogenerated return type of PipelineCancel +""" +type PipelineCancelPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Errors encountered during execution of the mutation. + """ + errors: [String!]! +} + enum PipelineConfigSourceEnum { AUTO_DEVOPS_SOURCE BRIDGE_SOURCE @@ -10591,6 +10659,36 @@ type PipelineConnection { } """ +Autogenerated input type of PipelineDestroy +""" +input PipelineDestroyInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the pipeline to mutate + """ + id: CiPipelineID! +} + +""" +Autogenerated return type of PipelineDestroy +""" +type PipelineDestroyPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Errors encountered during execution of the mutation. + """ + errors: [String!]! +} + +""" An edge in a connection. """ type PipelineEdge { @@ -10622,6 +10720,41 @@ type PipelinePermissions { updatePipeline: Boolean! } +""" +Autogenerated input type of PipelineRetry +""" +input PipelineRetryInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The id of the pipeline to mutate + """ + id: CiPipelineID! +} + +""" +Autogenerated return type of PipelineRetry +""" +type PipelineRetryPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Errors encountered during execution of the mutation. + """ + errors: [String!]! + + """ + The pipeline after mutation + """ + pipeline: Pipeline +} + enum PipelineStatusEnum { CANCELED CREATED diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 479ac8981fe..1d8cd04633c 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -3853,6 +3853,109 @@ }, { "kind": "OBJECT", + "name": "BurnupChartDailyTotals", + "description": "Represents the total number of issues and their weights for a particular day.", + "fields": [ + { + "name": "completedCount", + "description": "Number of closed issues as of this day", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "completedWeight", + "description": "Total weight of closed issues as of this day", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "date", + "description": "Date for burnup totals", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ISO8601Date", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "scopeCount", + "description": "Number of issues as of this day", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "scopeWeight", + "description": "Total weight of issues as of this day", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "CiGroup", "description": null, "fields": [ @@ -4250,6 +4353,16 @@ "possibleTypes": null }, { + "kind": "SCALAR", + "name": "CiPipelineID", + "description": "Identifier of Ci::Pipeline", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { "kind": "OBJECT", "name": "CiStage", "description": null, @@ -26710,6 +26823,28 @@ "description": "Represents a milestone.", "fields": [ { + "name": "burnupTimeSeries", + "description": "Daily scope and completed totals for burnup charts", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "BurnupChartDailyTotals", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "createdAt", "description": "Timestamp of milestone creation", "args": [ @@ -28797,6 +28932,87 @@ "deprecationReason": null }, { + "name": "pipelineCancel", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PipelineCancelInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PipelineCancelPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pipelineDestroy", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PipelineDestroyInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PipelineDestroyPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pipelineRetry", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PipelineRetryInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "PipelineRetryPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "removeAwardEmoji", "description": null, "args": [ @@ -31571,6 +31787,80 @@ "possibleTypes": null }, { + "kind": "INPUT_OBJECT", + "name": "PipelineCancelInput", + "description": "Autogenerated input type of PipelineCancel", + "fields": null, + "inputFields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PipelineCancelPayload", + "description": "Autogenerated return type of PipelineCancel", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Errors encountered during execution of the mutation.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { "kind": "ENUM", "name": "PipelineConfigSourceEnum", "description": null, @@ -31715,6 +32005,94 @@ "possibleTypes": null }, { + "kind": "INPUT_OBJECT", + "name": "PipelineDestroyInput", + "description": "Autogenerated input type of PipelineDestroy", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "The id of the pipeline to mutate", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CiPipelineID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PipelineDestroyPayload", + "description": "Autogenerated return type of PipelineDestroy", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Errors encountered during execution of the mutation.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { "kind": "OBJECT", "name": "PipelineEdge", "description": "An edge in a connection.", @@ -31827,6 +32205,108 @@ "possibleTypes": null }, { + "kind": "INPUT_OBJECT", + "name": "PipelineRetryInput", + "description": "Autogenerated input type of PipelineRetry", + "fields": null, + "inputFields": [ + { + "name": "id", + "description": "The id of the pipeline to mutate", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CiPipelineID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "PipelineRetryPayload", + "description": "Autogenerated return type of PipelineRetry", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Errors encountered during execution of the mutation.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pipeline", + "description": "The pipeline after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Pipeline", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { "kind": "ENUM", "name": "PipelineStatusEnum", "description": null, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index e0c8e903aba..d4bd5b60aa6 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -245,6 +245,18 @@ Autogenerated return type of BoardListUpdateLimitMetrics | `commit` | Commit | Commit for the branch | | `name` | String! | Name of the branch | +## BurnupChartDailyTotals + +Represents the total number of issues and their weights for a particular day. + +| Name | Type | Description | +| --- | ---- | ---------- | +| `completedCount` | Int! | Number of closed issues as of this day | +| `completedWeight` | Int! | Total weight of closed issues as of this day | +| `date` | ISO8601Date! | Date for burnup totals | +| `scopeCount` | Int! | Number of issues as of this day | +| `scopeWeight` | Int! | Total weight of issues as of this day | + ## CiGroup | Name | Type | Description | @@ -1477,6 +1489,7 @@ Represents a milestone. | Name | Type | Description | | --- | ---- | ---------- | +| `burnupTimeSeries` | BurnupChartDailyTotals! => Array | Daily scope and completed totals for burnup charts | | `createdAt` | Time! | Timestamp of milestone creation | | `description` | String | Description of the milestone | | `dueDate` | Time | Timestamp of the milestone due date | @@ -1622,6 +1635,24 @@ Information about pagination in a connection. | `user` | User | Pipeline user | | `userPermissions` | PipelinePermissions! | Permissions for the current user on the resource | +## PipelineCancelPayload + +Autogenerated return type of PipelineCancel + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Errors encountered during execution of the mutation. | + +## PipelineDestroyPayload + +Autogenerated return type of PipelineDestroy + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Errors encountered during execution of the mutation. | + ## PipelinePermissions | Name | Type | Description | @@ -1630,6 +1661,16 @@ Information about pagination in a connection. | `destroyPipeline` | Boolean! | Indicates the user can perform `destroy_pipeline` on this resource | | `updatePipeline` | Boolean! | Indicates the user can perform `update_pipeline` on this resource | +## PipelineRetryPayload + +Autogenerated return type of PipelineRetry + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Errors encountered during execution of the mutation. | +| `pipeline` | Pipeline | The pipeline after mutation | + ## Project | Name | Type | Description | diff --git a/doc/api/issues.md b/doc/api/issues.md index 93f788d62a3..b762698bd5b 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -1333,8 +1333,8 @@ PUT /projects/:id/issues/:issue_iid/reorder |-------------|---------|----------|--------------------------------------| | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `issue_iid` | integer | yes | The internal ID of a project's issue | -| `move_after_id` | integer | no | The ID of a projet's issue to move this issue after | -| `move_before_id` | integer | no | The ID of a projet's issue to move this issue before | +| `move_after_id` | integer | no | The ID of a project's issue to move this issue after | +| `move_before_id` | integer | no | The ID of a project's issue to move this issue before | ```shell curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/issues/85/reorder?move_after_id=51&move_before_id=92" diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 56fd338c3ce..de3b0281647 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -4246,7 +4246,7 @@ script: - ls -al cache/ ``` -The configurtion above will result in `git fetch` being called this way: +The configuration above will result in `git fetch` being called this way: ```shell git fetch origin $REFSPECS --depth 50 --prune diff --git a/doc/development/cicd/templates.md b/doc/development/cicd/templates.md index 0169ca42ac6..44bbd4c83f0 100644 --- a/doc/development/cicd/templates.md +++ b/doc/development/cicd/templates.md @@ -13,7 +13,7 @@ This document explains how to develop [GitLab CI/CD templates](../../ci/examples All template files reside in the `lib/gitlab/ci/templates` directory, and are categorized by the following sub-directories: -| Sub-directroy | Content | [Selectable in UI](#make-sure-the-new-template-can-be-selected-in-ui) | +| Sub-directory | Content | [Selectable in UI](#make-sure-the-new-template-can-be-selected-in-ui) | |---------------|--------------------------------------------------------------|-----------------------------------------------------------------------| | `/AWS/*` | Cloud Deployment (AWS) related jobs | No | | `/Jobs/*` | Auto DevOps related jobs | Yes | diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 283f88ec0e1..9a18af34866 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -138,7 +138,7 @@ up confusion or verify that the end result matches what they had in mind, to database specialists to get input on the data model or specific queries, or to any other developer to get an in-depth review of the solution. -If an author is unsure if a merge request needs a [domain experts's](#domain-experts) opinion, that's +If an author is unsure if a merge request needs a [domain expert's](#domain-experts) opinion, that's usually a pretty good sign that it does, since without it the required level of confidence in their solution will not have been reached. diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index 4c77d95c89f..d37c00ef69b 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -1288,7 +1288,7 @@ However, the following might help the reader connect the text to the user interf | **{monitor}** Monitoring | View GitLab system information, and information on background jobs, logs, health checks, requests profiles, and audit logs. | | **{messages}** Messages | Send and manage broadcast messages for your users. | -Use an icon when you find youself having to describe an interface element. For example: +Use an icon when you find yourself having to describe an interface element. For example: - Do: Click the Admin Area icon ( **{admin}** ). - Don't: Click the Admin Area icon (the wrench icon). diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index e7954fa910b..6616c350e2b 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -929,7 +929,7 @@ export default { - We can use slots and/or scoped slots to achieve the same thing as we did with mixins. If you only need an EE component there is no need to create the CE component. -1. First, we have a CE component that can render a slot incase we need EE template and functionality to be decorated on top of the CE base. +1. First, we have a CE component that can render a slot in case we need EE template and functionality to be decorated on top of the CE base. ```vue // ./ce/my_component.vue @@ -1030,7 +1030,7 @@ separate SCSS file in an appropriate directory within `app/assets/stylesheets`. In some cases, this is not entirely possible or creating dedicated SCSS file is an overkill, e.g. a text style of some component is different for EE. In such cases, -styles are usually kept in stylesheet that is common for both CE and EE, and it is wise +styles are usually kept in a stylesheet that is common for both CE and EE, and it is wise to isolate such ruleset from rest of CE rules (along with adding comment describing the same) to avoid conflicts during CE to EE merge. diff --git a/doc/integration/jira_development_panel.md b/doc/integration/jira_development_panel.md index c4d72593fc7..5a4296f67a1 100644 --- a/doc/integration/jira_development_panel.md +++ b/doc/integration/jira_development_panel.md @@ -205,7 +205,7 @@ Potential resolutions: [Contact GitLab Support](https://about.gitlab.com/support) if none of these reasons apply. -#### Fixing synchonization issues +#### Fixing synchronization issues If Jira displays incorrect information (such as deleted branches), you may need to resynchronize the information. To do so: @@ -239,7 +239,7 @@ For a walkthrough of the integration with GitLab for Jira, watch [Configure GitL NOTE: **Note:** The GitLab user only needs access when adding a new namespace. For syncing with Jira, we do not depend on the user's token. - ![Confure namespace on GitLab Jira App](img/jira_dev_panel_setup_com_3.png) + ![Configure namespace on GitLab Jira App](img/jira_dev_panel_setup_com_3.png) After a namespace is added, all future commits, branches and merge requests of all projects under that namespace will be synced to Jira. Past data cannot be synced at the moment. diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 9dd7f2cd9e1..9eb90d38457 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -299,7 +299,7 @@ providers without two factor authentication. Define the allowed providers using an array, e.g. `["twitter", 'google_oauth2']`, or as `true`/`false` to allow all providers or none. This option should only be configured for providers which already have two factor authentication (default: false). -This configration dose not apply to SAML. +This configuration dose not apply to SAML. ```ruby gitlab_rails['omniauth_allow_bypass_two_factor'] = ['twitter', 'google_oauth2'] diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md index 285ab133196..b59bdf12371 100644 --- a/doc/topics/git/numerous_undo_possibilities_in_git/index.md +++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md @@ -243,7 +243,7 @@ git bisect A..E Bisect will provide us with commit ID of the middle commit to test, and then guide us through simple bisection process. You can read more about it [in official Git Tools](https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git) -In our example we will end up with commit `B`, that introduced bug/error. We have +In our example we will end up with commit `B`, that introduced the bug/error. We have 4 options on how to remove it (or part of it) from our repository. - Undo (swap additions and deletions) changes introduced by commit `B`: @@ -409,7 +409,7 @@ the cleanup of detached commits (happens automatically). ### Where modifying history is generally acceptable Modified history breaks the development chain of other developers, as changed -history does not have matching commits'ids. For that reason it should not be +history does not have matching commit IDs. For that reason it should not be used on any public branch or on branch that *might* be used by other developers. When contributing to big open source repositories (for example, [GitLab](https://gitlab.com/gitlab-org/gitlab/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria) itself), it is acceptable to *squash* commits into a single one, to present a diff --git a/doc/topics/web_application_firewall/quick_start_guide.md b/doc/topics/web_application_firewall/quick_start_guide.md index 9e69bc7e7c7..0996a928508 100644 --- a/doc/topics/web_application_firewall/quick_start_guide.md +++ b/doc/topics/web_application_firewall/quick_start_guide.md @@ -102,7 +102,7 @@ for you to install. For this guide, we need to install Ingress. Ingress provides load balancing, SSL termination, and name-based virtual hosting, using NGINX behind -the scenes. Make sure to switch the toogle to the enabled position before installing. +the scenes. Make sure to switch the toggle to the enabled position before installing. Both logging and blocking modes are available for WAF. While logging mode is useful for auditing anomalous traffic, blocking mode ensures the traffic doesn't reach past Ingress. diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md index 329b6ff5bb0..2a38ccb31f0 100644 --- a/doc/user/admin_area/monitoring/health_check.md +++ b/doc/user/admin_area/monitoring/health_check.md @@ -153,7 +153,7 @@ https://gitlab.example.com/-/readiness?token=ACCESS_TOKEN ``` NOTE: **Note:** -In case the database or Redis service are unaccessible, the probe endpoints response is not guaranteed to be correct. +In case the database or Redis service are inaccessible, the probe endpoints response is not guaranteed to be correct. You should switch to [IP whitelist](#ip-whitelist) from deprecated access token to avoid it. <!-- ## Troubleshooting diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 3a9327555fc..56a373a8856 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -460,7 +460,7 @@ DAST can be [configured](#customizing-the-dast-settings) using environment varia | `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` | boolean | Set to `true` to require [domain validation](#domain-validation) when running DAST full scans. Not supported for API scans. Default: `false` | | `DAST_AUTO_UPDATE_ADDONS` | boolean | ZAP add-ons are pinned to specific versions in the DAST Docker image. Set to `true` to download the latest versions when the scan starts. Default: `false` | | `DAST_API_HOST_OVERRIDE` | string | Used to override domains defined in API specification files. Only supported when importing the API specification from a URL. Example: `example.com:8080` | -| `DAST_EXCLUDE_RULES` | string | Set to a comma-separated list of Vulnerability Rule IDs to exclude them from running during the scan. Rule IDs are numbers and can be found from the DAST log or on the [ZAP project](https://github.com/zaproxy/zaproxy/blob/develop/docs/scanners.md). For example, `HTTP Parameter Override` has a rule ID of `10026`. **Note:** In earlier versions of GitLab the excluded rules were executed but alerts they generated were supressed. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118641) in GitLab 12.10. | +| `DAST_EXCLUDE_RULES` | string | Set to a comma-separated list of Vulnerability Rule IDs to exclude them from running during the scan. Rule IDs are numbers and can be found from the DAST log or on the [ZAP project](https://github.com/zaproxy/zaproxy/blob/develop/docs/scanners.md). For example, `HTTP Parameter Override` has a rule ID of `10026`. **Note:** In earlier versions of GitLab the excluded rules were executed but alerts they generated were suppressed. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118641) in GitLab 12.10. | | `DAST_REQUEST_HEADERS` | string | Set to a comma-separated list of request header names and values. Headers are added to every request made by DAST. For example, `Cache-control: no-cache,User-Agent: DAST/1.0` | | `DAST_DEBUG` | boolean | Enable debug message output. Default: `false` | | `DAST_SPIDER_MINS` | number | The maximum duration of the spider scan in minutes. Set to `0` for unlimited. Default: One minute, or unlimited when the scan is a full scan. | diff --git a/doc/user/infrastructure/index.md b/doc/user/infrastructure/index.md index 2a49ca18aaf..7ee09e8c857 100644 --- a/doc/user/infrastructure/index.md +++ b/doc/user/infrastructure/index.md @@ -433,7 +433,7 @@ apply: ### Multiple Terraform Plan reports -Starting with 13.2, you can display mutiple reports on the Merge Request page. The reports will also display the `artifacts: name:`. See example below for a suggested setup. +Starting with 13.2, you can display multiple reports on the Merge Request page. The reports will also display the `artifacts: name:`. See example below for a suggested setup. ```yaml image: diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index f46ad99e573..b103486928a 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -75,7 +75,7 @@ This view allows you to: ### Control Container Registry for your group -Navigate to your groups's **{package}** **Packages & Registries > Container Registry**. +Navigate to your group's **{package}** **Packages & Registries > Container Registry**. ![Container Registry group repositories](img/container_registry_group_repositories_v13_1.png) diff --git a/doc/user/project/import/cvs.md b/doc/user/project/import/cvs.md index d2e79458526..2957b33c20e 100644 --- a/doc/user/project/import/cvs.md +++ b/doc/user/project/import/cvs.md @@ -25,10 +25,10 @@ The following list illustrates the main differences between CVS and Git: are not atomic. If an operation on the repository is interrupted in the middle, the repository can be left in an inconsistent state. - **Storage method.** Changes in CVS are per file (changeset), while in Git - a committed file(s) is stored in its entirety (snapshot). That means that's + a committed file(s) is stored in its entirety (snapshot). That means it's very easy in Git to revert or undo a whole change. - **Revision IDs.** The fact that in CVS changes are per files, the revision ID - is depicted by version numbers, for example `1.4` reflects how many time a + is depicted by version numbers, for example `1.4` reflects how many times a given file has been changed. In Git, each version of a project as a whole (each commit) has its unique name given by SHA-1. - **Merge tracking.** Git uses a commit-before-merge approach rather than @@ -54,7 +54,7 @@ Wikipedia article on [comparing the different version control software](https:// CVS is old with no new release since 2008. Git provides more tools to work with (`git bisect` for one) which makes for a more productive workflow. -Migrating to Git/GitLab there is: +Migrating to Git/GitLab will benefit you: - **Shorter learning curve**, Git has a big community and a vast number of tutorials to get you started (see our [Git topic](../../../topics/git/index.md)). diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md index f2e769dcfc0..443ca11be27 100644 --- a/doc/user/project/integrations/irker.md +++ b/doc/user/project/integrations/irker.md @@ -53,7 +53,7 @@ Irker accepts channel names of the form `chan` and `#chan`, both for the case, `Aorimn` is treated as a nick and no more as a channel name. Irker can also join password-protected channels. Users need to append -`?key=thesecretpassword` to the chan name. When using this feature remember to +`?key=thesecretpassword` to the channel name. When using this feature remember to **not** put the `#` sign in front of the channel name; failing to do so will result on irker joining a channel literally named `#chan?key=password` henceforth leaking the channel key through the `/whois` IRC command (depending on IRC server diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 1c7b00b184a..3e0b6492477 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # GitLab Jira integration -If you need to use Jira to track work that's implemented in GitLab, GitLab's Jira integrations make the process of working across systems more efficent. +If you need to use Jira to track work that's implemented in GitLab, GitLab's Jira integrations make the process of working across systems more efficient. This page is about the GitLab Jira integration, which is available in every GitLab project by default, allowing you to connect it to any Jira instance, whether Cloud or self-managed. To compare features with the complementary Jira Development Panel integration, see [Jira integrations](jira_integrations.md). diff --git a/doc/user/project/integrations/overview.md b/doc/user/project/integrations/overview.md index ffa1167d5a4..9499c76a1e2 100644 --- a/doc/user/project/integrations/overview.md +++ b/doc/user/project/integrations/overview.md @@ -82,9 +82,9 @@ Read more about [Service templates](services_templates.md). ## Project integration management -Project integraton management lets you control integration settings across all projects +Project integration management lets you control integration settings across all projects of an instance. On the project level, administrators you can choose whether to inherit the -instance configuraton or provide custom settings. +instance configuration or provide custom settings. Read more about [Project integration management](../../admin_area/settings/project_integration_management.md). diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md index 3c697e22cf5..9fc824e2f44 100644 --- a/doc/user/project/merge_requests/code_quality.md +++ b/doc/user/project/merge_requests/code_quality.md @@ -69,7 +69,7 @@ For instance, consider the following workflow: This example shows how to run Code Quality on your code by using GitLab CI/CD and Docker. It requires GitLab 11.11 or later, and GitLab Runner 11.5 or later. If you are using -GitLab 11.4 or ealier, you can view the deprecated job definitions in the +GitLab 11.4 or earlier, you can view the deprecated job definitions in the [documentation archive](https://docs.gitlab.com/12.10/ee/user/project/merge_requests/code_quality.html#previous-job-definitions). First, you need GitLab Runner configured: @@ -276,7 +276,7 @@ This adds SonarJava to the `plugins:` section of the [default `.codeclimate.yml` included in your project. Changes to the `plugins:` section do not affect the `exclude_patterns` section of the -defeault `.codeclimate.yml`. See the Code Climate documentation for +default `.codeclimate.yml`. See the Code Climate documentation for [excluding files and folders](https://docs.codeclimate.com/docs/excluding-files-and-folders) for more details. diff --git a/lib/api/api.rb b/lib/api/api.rb index cf367438ff4..308c9d68d7b 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -197,6 +197,7 @@ module API mount ::API::ConanPackages mount ::API::MavenPackages mount ::API::NpmPackages + mount ::API::GenericPackages mount ::API::GoProxy mount ::API::Pages mount ::API::PagesDomains diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb new file mode 100644 index 00000000000..98b8a40c7c9 --- /dev/null +++ b/lib/api/generic_packages.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module API + class GenericPackages < Grape::API::Instance + before do + require_packages_enabled! + authenticate! + + require_generic_packages_available! + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + route_setting :authentication, job_token_allowed: true + + namespace ':id/packages/generic' do + get 'ping' do + :pong + end + end + end + + helpers do + include ::API::Helpers::PackagesHelpers + + def require_generic_packages_available! + not_found! unless Feature.enabled?(:generic_packages, user_project) + end + end + end +end diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 0c3fb3c8093..4eba12157bd 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -83,11 +83,12 @@ module API put ':id/wikis/:slug' do authorize! :create_wiki, container - page = WikiPages::UpdateService + response = WikiPages::UpdateService .new(container: container, current_user: current_user, params: params) .execute(wiki_page) + page = response.payload[:page] - if page.valid? + if response.success? present page, with: Entities::WikiPage else render_validation_error!(page) diff --git a/lib/gitlab/application_context.rb b/lib/gitlab/application_context.rb index a3feda9bb59..30cb74bcf54 100644 --- a/lib/gitlab/application_context.rb +++ b/lib/gitlab/application_context.rb @@ -29,7 +29,7 @@ module Gitlab Labkit::Context.current.to_h.include?(Labkit::Context.log_key(attribute_name)) end - def initialize(**args) + def initialize(args) unknown_attributes = args.keys - APPLICATION_ATTRIBUTES.map(&:name) raise ArgumentError, "#{unknown_attributes} are not known keys" if unknown_attributes.any? diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb index ea0307e8bd6..d1b9062a23c 100644 --- a/lib/gitlab/background_migration.rb +++ b/lib/gitlab/background_migration.rb @@ -9,7 +9,7 @@ module Gitlab # Begins stealing jobs from the background migrations queue, blocking the # caller until all jobs have been completed. # - # When a migration raises a StandardError is is going to be retries up to + # When a migration raises a StandardError it is going to retry up to # three times, for example, to recover from a deadlock. # # When Exception is being raised, it enqueues the migration again, and diff --git a/lib/gitlab/ci/pipeline/artifact/code_coverage.rb b/lib/gitlab/ci/pipeline/artifact/code_coverage.rb index f8a004e8512..d8f28bde7ce 100644 --- a/lib/gitlab/ci/pipeline/artifact/code_coverage.rb +++ b/lib/gitlab/ci/pipeline/artifact/code_coverage.rb @@ -5,6 +5,8 @@ module Gitlab module Pipeline module Artifact class CodeCoverage + include Gitlab::Utils::StrongMemoize + def initialize(pipeline_artifact) @pipeline_artifact = pipeline_artifact end @@ -18,7 +20,11 @@ module Gitlab private def raw_report - @raw_report ||= Gitlab::Json.parse(@pipeline_artifact.file.read) + strong_memoize(:raw_report) do + @pipeline_artifact.each_blob do |blob| + Gitlab::Json.parse(blob) + end + end end end end diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb index 4b194810c9e..348e5472cb4 100644 --- a/lib/gitlab/ci/trace.rb +++ b/lib/gitlab/ci/trace.rb @@ -79,9 +79,7 @@ module Gitlab job.trace_chunks.any? || current_path.present? || old_trace.present? end - def read(&block) - should_retry = true if lock_taken?(lock_key) - + def read(should_retry: true, &block) read_stream(&block) rescue Errno::ENOENT raise unless should_retry @@ -195,13 +193,10 @@ module Gitlab end def in_write_lock(&blk) + lock_key = "trace:write:lock:#{job.id}" in_lock(lock_key, ttl: LOCK_TTL, retries: LOCK_RETRIES, sleep_sec: LOCK_SLEEP, &blk) end - def lock_key - "trace:write:lock:#{job.id}" - end - def archive_stream!(stream) clone_file!(stream, JobArtifactUploader.workhorse_upload_path) do |clone_path| create_build_trace!(job, clone_path) diff --git a/lib/gitlab/exclusive_lease_helpers.rb b/lib/gitlab/exclusive_lease_helpers.rb index 0de45414955..10762d83588 100644 --- a/lib/gitlab/exclusive_lease_helpers.rb +++ b/lib/gitlab/exclusive_lease_helpers.rb @@ -39,10 +39,5 @@ module Gitlab ensure lease&.cancel end - - def lock_taken?(key) - lease = Gitlab::ExclusiveLease.new(key, timeout: 0) - lease.exists? - end end end diff --git a/lib/gitlab/metrics/dashboard/validator.rb b/lib/gitlab/metrics/dashboard/validator.rb index 8edd9c397f9..1e8dc059968 100644 --- a/lib/gitlab/metrics/dashboard/validator.rb +++ b/lib/gitlab/metrics/dashboard/validator.rb @@ -4,24 +4,22 @@ module Gitlab module Metrics module Dashboard module Validator - DASHBOARD_SCHEMA_PATH = 'lib/gitlab/metrics/dashboard/validator/schemas/dashboard.json'.freeze + DASHBOARD_SCHEMA_PATH = Rails.root.join(*%w[lib gitlab metrics dashboard validator schemas dashboard.json]).freeze class << self def validate(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil) - errors = _validate(content, schema_path, dashboard_path: dashboard_path, project: project) - errors.empty? + errors(content, schema_path, dashboard_path: dashboard_path, project: project).empty? end def validate!(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil) - errors = _validate(content, schema_path, dashboard_path: dashboard_path, project: project) + errors = errors(content, schema_path, dashboard_path: dashboard_path, project: project) errors.empty? || raise(errors.first) end - private - - def _validate(content, schema_path, dashboard_path: nil, project: nil) - client = Validator::Client.new(content, schema_path, dashboard_path: dashboard_path, project: project) - client.execute + def errors(content, schema_path = DASHBOARD_SCHEMA_PATH, dashboard_path: nil, project: nil) + Validator::Client + .new(content, schema_path, dashboard_path: dashboard_path, project: project) + .execute end end end diff --git a/lib/gitlab/metrics/dashboard/validator/client.rb b/lib/gitlab/metrics/dashboard/validator/client.rb index c63415abcfc..588c677ca28 100644 --- a/lib/gitlab/metrics/dashboard/validator/client.rb +++ b/lib/gitlab/metrics/dashboard/validator/client.rb @@ -46,7 +46,7 @@ module Gitlab def validate_against_schema schemer.validate(content).map do |error| - Errors::SchemaValidationError.new(error) + ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new(error) end end end diff --git a/lib/gitlab/metrics/dashboard/validator/schemas/panel.json b/lib/gitlab/metrics/dashboard/validator/schemas/panel.json index 011eef53e40..2ae9608036e 100644 --- a/lib/gitlab/metrics/dashboard/validator/schemas/panel.json +++ b/lib/gitlab/metrics/dashboard/validator/schemas/panel.json @@ -4,7 +4,7 @@ "properties": { "type": { "type": "string", - "enum": ["area-chart", "anomaly-chart", "bar", "column", "stacked-column", "single-stat", "heatmap"], + "enum": ["area-chart", "line-chart", "anomaly-chart", "bar", "column", "stacked-column", "single-stat", "heatmap", "gauge"], "default": "area-chart" }, "title": { "type": "string" }, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0009047a5d6..7ea0c30f089 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4190,6 +4190,9 @@ msgstr "" msgid "Burnup chart" msgstr "" +msgid "Burnup chart could not be generated due to too many events" +msgstr "" + msgid "Business" msgstr "" @@ -7060,6 +7063,9 @@ msgstr "" msgid "Could not save prometheus manual configuration" msgstr "" +msgid "Could not udpdate wiki page" +msgstr "" + msgid "Could not update the LDAP settings" msgstr "" @@ -11299,6 +11305,12 @@ msgstr "" msgid "Geo|Geo Status" msgstr "" +msgid "Geo|Go to the primary site" +msgstr "" + +msgid "Geo|If you want to make changes, you must visit the primary site." +msgstr "" + msgid "Geo|In progress" msgstr "" @@ -11431,10 +11443,10 @@ msgstr "" msgid "Geo|Waiting for scheduler" msgstr "" -msgid "Geo|You are on a secondary, %{b_open}read-only%{b_close} Geo node. If you want to make changes, you must visit this page on the %{node_link_open}primary node%{node_link_close}." +msgid "Geo|You are on a secondary, %{b_open}read-only%{b_close} Geo node." msgstr "" -msgid "Geo|You are on a secondary, %{b_open}read-only%{b_close} Geo node. You may be able to make a limited amount of changes or perform a limited amount of actions on this page." +msgid "Geo|You may be able to make a limited amount of changes or perform a limited amount of actions on this page." msgstr "" msgid "Geo|misconfigured" @@ -14594,6 +14606,9 @@ msgstr "" msgid "Load more" msgstr "" +msgid "Load more users" +msgstr "" + msgid "Loading" msgstr "" @@ -15702,12 +15717,18 @@ msgid_plural "Milestones" msgstr[0] "" msgstr[1] "" +msgid "Milestone does not support burnup charts" +msgstr "" + msgid "Milestone lists not available with your current license" msgstr "" msgid "Milestone lists show all issues from the selected milestone." msgstr "" +msgid "Milestone must have a start and due date" +msgstr "" + msgid "MilestoneSidebar|Closed:" msgstr "" diff --git a/spec/controllers/admin/integrations_controller_spec.rb b/spec/controllers/admin/integrations_controller_spec.rb index 4a5d5ede728..353f6f11e93 100644 --- a/spec/controllers/admin/integrations_controller_spec.rb +++ b/spec/controllers/admin/integrations_controller_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Admin::IntegrationsController do before do allow(PropagateIntegrationWorker).to receive(:perform_async) - put :update, params: { id: integration.class.to_param, overwrite: true, service: { url: url } } + put :update, params: { id: integration.class.to_param, service: { url: url } } end context 'valid params' do @@ -40,7 +40,7 @@ RSpec.describe Admin::IntegrationsController do end it 'calls to PropagateIntegrationWorker' do - expect(PropagateIntegrationWorker).to have_received(:perform_async).with(integration.id, true) + expect(PropagateIntegrationWorker).to have_received(:perform_async).with(integration.id, false) end end diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 3032d115a00..7c564d76f70 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -564,35 +564,76 @@ RSpec.describe 'File blob', :js do file_path: '.gitlab/dashboards/custom-dashboard.yml', file_content: file_content ).execute - - visit_blob('.gitlab/dashboards/custom-dashboard.yml') end - context 'valid dashboard file' do - let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) } + context 'with metrics_dashboard_exhaustive_validations feature flag off' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: false) + visit_blob('.gitlab/dashboards/custom-dashboard.yml') + end - it 'displays an auxiliary viewer' do - aggregate_failures do - # shows that dashboard yaml is valid - expect(page).to have_content('Metrics Dashboard YAML definition is valid.') + context 'valid dashboard file' do + let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) } + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that dashboard yaml is valid + expect(page).to have_content('Metrics Dashboard YAML definition is valid.') + + # shows a learn more link + expect(page).to have_link('Learn more') + end + end + end + + context 'invalid dashboard file' do + let(:file_content) { "dashboard: 'invalid'" } - # shows a learn more link - expect(page).to have_link('Learn more') + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that dashboard yaml is invalid + expect(page).to have_content('Metrics Dashboard YAML definition is invalid:') + expect(page).to have_content("panel_groups: should be an array of panel_groups objects") + + # shows a learn more link + expect(page).to have_link('Learn more') + end end end end - context 'invalid dashboard file' do - let(:file_content) { "dashboard: 'invalid'" } + context 'with metrics_dashboard_exhaustive_validations feature flag on' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: true) + visit_blob('.gitlab/dashboards/custom-dashboard.yml') + end - it 'displays an auxiliary viewer' do - aggregate_failures do - # shows that dashboard yaml is invalid - expect(page).to have_content('Metrics Dashboard YAML definition is invalid:') - expect(page).to have_content("panel_groups: should be an array of panel_groups objects") + context 'valid dashboard file' do + let(:file_content) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) } - # shows a learn more link - expect(page).to have_link('Learn more') + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that dashboard yaml is valid + expect(page).to have_content('Metrics Dashboard YAML definition is valid.') + + # shows a learn more link + expect(page).to have_link('Learn more') + end + end + end + + context 'invalid dashboard file' do + let(:file_content) { "dashboard: 'invalid'" } + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows that dashboard yaml is invalid + expect(page).to have_content('Metrics Dashboard YAML definition is invalid:') + expect(page).to have_content("root is missing required keys: panel_groups") + + # shows a learn more link + expect(page).to have_link('Learn more') + end end end end diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/broken_yml_syntax.yml b/spec/fixtures/lib/gitlab/metrics/dashboard/broken_yml_syntax.yml new file mode 100644 index 00000000000..d551ad2dcdb --- /dev/null +++ b/spec/fixtures/lib/gitlab/metrics/dashboard/broken_yml_syntax.yml @@ -0,0 +1,13 @@ +dashboard: 'Test Dashboard' + panel_groups: + - group: Group B + panels: + - title: "Super Chart B" + type: "area-chart" + y_label: "y_label" + metrics: + - id: metric_b + query_range: 'query' + unit: unit + label: Legend Label +- group: Group A diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap index 975c31bb59c..eede5426f42 100644 --- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap +++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap @@ -114,7 +114,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] = <!----> <div - class="gl-search-box-by-type m-2" + class="gl-search-box-by-type gl-m-3" > <svg class="gl-search-box-by-type-search-icon gl-icon s16" @@ -225,7 +225,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] = <!----> <div - class="gl-search-box-by-type m-2" + class="gl-search-box-by-type gl-m-3" > <svg class="gl-search-box-by-type-search-icon gl-icon s16" diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js index 7cc7b40f4c8..6ef28a71f48 100644 --- a/spec/frontend/jira_import/components/jira_import_form_spec.js +++ b/spec/frontend/jira_import/components/jira_import_form_spec.js @@ -10,6 +10,7 @@ import { imports, issuesPath, jiraProjects, + jiraUsersResponse, projectId, projectPath, userMappings as defaultUserMappings, @@ -38,7 +39,10 @@ describe('JiraImportForm', () => { const getHeader = name => getByRole(wrapper.element, 'columnheader', { name }); + const findLoadMoreUsersButton = () => wrapper.find('[data-testid="load-more-users-button"]'); + const mountComponent = ({ + hasMoreUsers = false, isSubmitting = false, loading = false, mutate = mutateSpy, @@ -55,6 +59,7 @@ describe('JiraImportForm', () => { projectPath, }, data: () => ({ + hasMoreUsers, isFetching: false, isSubmitting, searchTerm: '', @@ -300,6 +305,7 @@ describe('JiraImportForm', () => { variables: { input: { projectPath, + startAt: 0, }, }, }; @@ -318,4 +324,53 @@ describe('JiraImportForm', () => { }); }); }); + + describe('load more users button', () => { + describe('when all users have been loaded', () => { + it('is not shown', () => { + wrapper = mountComponent(); + + expect(findLoadMoreUsersButton().exists()).toBe(false); + }); + }); + + describe('when all users have not been loaded', () => { + it('is shown', () => { + wrapper = mountComponent({ hasMoreUsers: true }); + + expect(findLoadMoreUsersButton().exists()).toBe(true); + }); + }); + + describe('when clicked', () => { + beforeEach(() => { + mutateSpy = jest.fn(() => + Promise.resolve({ + data: { + jiraImportStart: { errors: [] }, + jiraImportUsers: { jiraUsers: jiraUsersResponse, errors: [] }, + }, + }), + ); + + wrapper = mountComponent({ hasMoreUsers: true }); + }); + + it('calls the GraphQL user mapping mutation', async () => { + const mutationArguments = { + mutation: getJiraUserMappingMutation, + variables: { + input: { + projectPath, + startAt: 0, + }, + }, + }; + + findLoadMoreUsersButton().vm.$emit('click'); + + expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments)); + }); + }); + }); }); diff --git a/spec/frontend/jira_import/mock_data.js b/spec/frontend/jira_import/mock_data.js index 8ea40080f32..51dd939283e 100644 --- a/spec/frontend/jira_import/mock_data.js +++ b/spec/frontend/jira_import/mock_data.js @@ -1,5 +1,6 @@ import getJiraImportDetailsQuery from '~/jira_import/queries/get_jira_import_details.query.graphql'; import { IMPORT_STATE } from '~/jira_import/utils/jira_import_utils'; +import { userMappingsPageSize } from '~/jira_import/utils/constants'; export const fullPath = 'gitlab-org/gitlab-test'; @@ -87,6 +88,8 @@ export const jiraProjects = [ { text: 'Migrate to GitLab (MTG)', value: 'MTG' }, ]; +export const jiraUsersResponse = new Array(userMappingsPageSize); + export const imports = [ { jiraProjectKey: 'MTG', diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap index 7ef956f8e05..80eacbe0a6a 100644 --- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap +++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap @@ -52,7 +52,7 @@ exports[`Dashboard template matches the default snapshot 1`] = ` </gl-new-dropdown-header-stub> <gl-search-box-by-type-stub - class="m-2" + class="gl-m-3" clearbuttontitle="Clear" value="" /> diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index dff9bf088c0..25cef3a8045 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -183,7 +183,7 @@ describe('Snippet Edit app', () => { ${'foo'} | ${[]} | ${false} ${'foo'} | ${[TEST_ACTIONS.VALID]} | ${false} ${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_CONTENT]} | ${true} - ${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${true} + ${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${false} `( 'should handle submit disable (title=$title, actions=$actions, shouldDisable=$shouldDisable)', async ({ title, actions, shouldDisable }) => { diff --git a/spec/graphql/types/milestone_type_spec.rb b/spec/graphql/types/milestone_type_spec.rb index 2315c10433b..806495250ac 100644 --- a/spec/graphql/types/milestone_type_spec.rb +++ b/spec/graphql/types/milestone_type_spec.rb @@ -15,7 +15,7 @@ RSpec.describe GitlabSchema.types['Milestone'] do stats ] - expect(described_class).to have_graphql_fields(*expected_fields) + expect(described_class).to have_graphql_fields(*expected_fields).at_least end describe 'stats field' do diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index 0326fb2b061..171877dbaee 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -23,26 +23,14 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state do artifact1.file.migrate!(ObjectStorage::Store::REMOTE) end - context 'when write lock is not present' do - it 'raises an exception' do - expect { artifact2.job.trace.raw }.to raise_error(Errno::ENOENT) - end - end + it 'reloads the trace after is it migrated' do + stub_const('Gitlab::HttpIO::BUFFER_SIZE', test_data.length) - context 'when write lock is present', :clean_gitlab_redis_shared_state do - before do - Gitlab::ExclusiveLease.new("trace:write:lock:#{job.id}", timeout: 10.seconds).try_obtain + expect_next_instance_of(Gitlab::HttpIO) do |http_io| + expect(http_io).to receive(:get_chunk).and_return(test_data, "") end - it 'reloads the trace after is it migrated' do - stub_const('Gitlab::HttpIO::BUFFER_SIZE', test_data.length) - - expect_next_instance_of(Gitlab::HttpIO) do |http_io| - expect(http_io).to receive(:get_chunk).and_return(test_data, "") - end - - expect(artifact2.job.trace.raw).to eq(test_data) - end + expect(artifact2.job.trace.raw).to eq(test_data) end end diff --git a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb index e4733d4fe5e..01e2fe8ce17 100644 --- a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb @@ -111,14 +111,4 @@ RSpec.describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state d end end end - - describe '#lock_taken?' do - it 'returns true when lock has been taken' do - expect(class_instance.lock_taken?(unique_key)).to be false - - class_instance.in_lock(unique_key) do - expect(class_instance.lock_taken?(unique_key)).to be true - end - end - end end diff --git a/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb index f0db1bd0d33..fdbba6c31b5 100644 --- a/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb @@ -34,6 +34,17 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do it { is_expected.to eq 'root is missing required keys: one' } end + + context 'when there is type mismatch' do + %w(null string boolean integer number array object).each do |expected_type| + context "on type: #{expected_type}" do + let(:type) { expected_type } + let(:details) { nil } + + it { is_expected.to eq "'property_name' at root is not of type: #{expected_type}" } + end + end + end end context 'for nested object' do @@ -52,8 +63,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator::Errors do let(:type) { expected_type } let(:details) { nil } - subject { described_class.new(error_hash).message } - it { is_expected.to eq "'property_name' at /nested_objects/0 is not of type: #{expected_type}" } end end diff --git a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb index c4cda271408..eb67ea2b7da 100644 --- a/spec/lib/gitlab/metrics/dashboard/validator_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/validator_spec.rb @@ -143,4 +143,56 @@ RSpec.describe Gitlab::Metrics::Dashboard::Validator do end end end + + describe '#errors' do + context 'valid dashboard schema' do + it 'returns no errors' do + expect(described_class.errors(valid_dashboard)).to eq [] + end + + context 'with duplicate metric_ids' do + it 'returns errors' do + expect(described_class.errors(duplicate_id_dashboard)).to eq [Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds.new] + end + end + + context 'with dashboard_path and project' do + subject { described_class.errors(valid_dashboard, dashboard_path: 'test/path.yml', project: project) } + + context 'with no conflicting metric identifiers in db' do + it { is_expected.to eq [] } + end + + context 'with metric identifier present in current dashboard' do + before do + create(:prometheus_metric, + identifier: 'metric_a1', + dashboard_path: 'test/path.yml', + project: project + ) + end + + it { is_expected.to eq [] } + end + + context 'with metric identifier present in another dashboard' do + before do + create(:prometheus_metric, + identifier: 'metric_a1', + dashboard_path: 'some/other/dashboard/path.yml', + project: project + ) + end + + it { is_expected.to eq [Gitlab::Metrics::Dashboard::Validator::Errors::DuplicateMetricIds.new] } + end + end + end + + context 'invalid dashboard schema' do + it 'returns collection of validation errors' do + expect(described_class.errors(invalid_dashboard)).to all be_kind_of(Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError) + end + end + end end diff --git a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb index 057f0f32158..84dfc5186a8 100644 --- a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb +++ b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb @@ -9,119 +9,228 @@ RSpec.describe BlobViewer::MetricsDashboardYml do let_it_be(:project) { create(:project, :repository) } let(:blob) { fake_blob(path: '.gitlab/dashboards/custom-dashboard.yml', data: data) } let(:sha) { sample_commit.id } + let(:data) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') } subject(:viewer) { described_class.new(blob) } - context 'when the definition is valid' do - let(:data) { File.read(Rails.root.join('config/prometheus/common_metrics.yml')) } + context 'with metrics_dashboard_exhaustive_validations feature flag on' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: true) + end + + context 'when the definition is valid' do + before do + allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([]) + end + + describe '#valid?' do + it 'calls prepare! on the viewer' do + expect(viewer).to receive(:prepare!) + + viewer.valid? + end + + it 'processes dashboard yaml and returns true', :aggregate_failures do + yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw! + + expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader| + expect(loader).to receive(:load_raw!).and_call_original + end + expect(Gitlab::Metrics::Dashboard::Validator) + .to receive(:errors) + .with(yml, dashboard_path: '.gitlab/dashboards/custom-dashboard.yml', project: project) + .and_call_original + expect(viewer.valid?).to be true + end + end + + describe '#errors' do + it 'returns empty array' do + expect(viewer.errors).to eq [] + end + end + end + + context 'when definition is invalid' do + let(:error) { ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new } + let(:data) do + <<~YAML + dashboard: + YAML + end + + before do + allow(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).and_return([error]) + end - describe '#valid?' do - it 'calls prepare! on the viewer' do - allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json) + describe '#valid?' do + it 'returns false' do + expect(viewer.valid?).to be false + end + end - expect(viewer).to receive(:prepare!) + describe '#errors' do + it 'returns validation errors' do + expect(viewer.errors).to eq ["Dashboard failed schema validation"] + end + end + end - viewer.valid? + context 'when YAML syntax is invalid' do + let(:data) do + <<~YAML + dashboard: 'empty metrics' + panel_groups: + - group: 'Group Title' + YAML end - it 'returns true', :aggregate_failures do - yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw! + describe '#valid?' do + it 'returns false' do + expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors) + expect(viewer.valid?).to be false + end + end - expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader| - expect(loader).to receive(:load_raw!).and_call_original + describe '#errors' do + it 'returns validation errors' do + expect(viewer.errors).to all be_kind_of String end - expect(PerformanceMonitoring::PrometheusDashboard) - .to receive(:from_json) - .with(yml) - .and_call_original - expect(viewer.valid?).to be_truthy end end - describe '#errors' do - it 'returns nil' do - allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json) + context 'when YAML loader raises error' do + let(:data) do + <<~YAML + large yaml file + YAML + end + + before do + allow(::Gitlab::Config::Loader::Yaml).to receive(:new) + .and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big') + end - expect(viewer.errors).to be nil + it 'is invalid' do + expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors) + expect(viewer.valid?).to be false + end + + it 'returns validation errors' do + expect(viewer.errors).to eq ['The parsed YAML is too big'] end end end - context 'when definition is invalid' do - let(:error) { ActiveModel::ValidationError.new(PerformanceMonitoring::PrometheusDashboard.new.tap(&:validate)) } - let(:data) do - <<~YAML - dashboard: - YAML + context 'with metrics_dashboard_exhaustive_validations feature flag off' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: false) end - describe '#valid?' do - it 'returns false' do - expect(PerformanceMonitoring::PrometheusDashboard) - .to receive(:from_json).and_raise(error) + context 'when the definition is valid' do + describe '#valid?' do + before do + allow(PerformanceMonitoring::PrometheusDashboard).to receive(:from_json) + end + + it 'calls prepare! on the viewer' do + expect(viewer).to receive(:prepare!) - expect(viewer.valid?).to be_falsey + viewer.valid? + end + + it 'processes dashboard yaml and returns true', :aggregate_failures do + yml = ::Gitlab::Config::Loader::Yaml.new(data).load_raw! + + expect_next_instance_of(::Gitlab::Config::Loader::Yaml, data) do |loader| + expect(loader).to receive(:load_raw!).and_call_original + end + expect(PerformanceMonitoring::PrometheusDashboard) + .to receive(:from_json) + .with(yml) + .and_call_original + expect(viewer.valid?).to be true + end + end + + describe '#errors' do + it 'returns empty array' do + expect(viewer.errors).to eq [] + end end end - describe '#errors' do - it 'returns validation errors' do - allow(PerformanceMonitoring::PrometheusDashboard) - .to receive(:from_json).and_raise(error) + context 'when definition is invalid' do + let(:error) { ActiveModel::ValidationError.new(PerformanceMonitoring::PrometheusDashboard.new.tap(&:validate)) } + let(:data) do + <<~YAML + dashboard: + YAML + end + + describe '#valid?' do + it 'returns false' do + expect(PerformanceMonitoring::PrometheusDashboard) + .to receive(:from_json).and_raise(error) - expect(viewer.errors).to be error.model.errors + expect(viewer.valid?).to be false + end + end + + describe '#errors' do + it 'returns validation errors' do + allow(PerformanceMonitoring::PrometheusDashboard) + .to receive(:from_json).and_raise(error) + + expect(viewer.errors).to eq error.model.errors.messages.map { |messages| messages.join(': ') } + end end end - end - context 'when YAML syntax is invalid' do - let(:data) do - <<~YAML + context 'when YAML syntax is invalid' do + let(:data) do + <<~YAML dashboard: 'empty metrics' panel_groups: - group: 'Group Title' - YAML - end - - describe '#valid?' do - it 'returns false' do - expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json) - expect(viewer.valid?).to be_falsey + YAML end - end - describe '#errors' do - it 'returns validation errors' do - yaml_wrapped_errors = { 'YAML syntax': ["(<unknown>): did not find expected key while parsing a block mapping at line 1 column 1"] } + describe '#valid?' do + it 'returns false' do + expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json) + expect(viewer.valid?).to be false + end + end - expect(viewer.errors).to be_kind_of ActiveModel::Errors - expect(viewer.errors.messages).to eql(yaml_wrapped_errors) + describe '#errors' do + it 'returns validation errors' do + expect(viewer.errors).to eq ["YAML syntax: (<unknown>): did not find expected key while parsing a block mapping at line 1 column 1"] + end end end - end - context 'when YAML loader raises error' do - let(:data) do - <<~YAML + context 'when YAML loader raises error' do + let(:data) do + <<~YAML large yaml file - YAML - end - - before do - allow(::Gitlab::Config::Loader::Yaml).to receive(:new) - .and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big') - end + YAML + end - it 'is invalid' do - expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json) - expect(viewer.valid?).to be(false) - end + before do + allow(::Gitlab::Config::Loader::Yaml).to( + receive(:new).and_raise(::Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big') + ) + end - it 'returns validation errors' do - yaml_wrapped_errors = { 'YAML syntax': ["The parsed YAML is too big"] } + it 'is invalid' do + expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:from_json) + expect(viewer.valid?).to be false + end - expect(viewer.errors).to be_kind_of(ActiveModel::Errors) - expect(viewer.errors.messages).to eq(yaml_wrapped_errors) + it 'returns validation errors' do + expect(viewer.errors).to eq ["YAML syntax: The parsed YAML is too big"] + end end end end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 91a669aa3f4..0518b2c7540 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -343,42 +343,6 @@ RSpec.describe Ci::JobArtifact do end end - describe '#each_blob' do - context 'when file format is gzip' do - context 'when gzip file contains one file' do - let(:artifact) { build(:ci_job_artifact, :junit) } - - it 'iterates blob once' do - expect { |b| artifact.each_blob(&b) }.to yield_control.once - end - end - - context 'when gzip file contains three files' do - let(:artifact) { build(:ci_job_artifact, :junit_with_three_testsuites) } - - it 'iterates blob three times' do - expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(3).times - end - end - end - - context 'when file format is raw' do - let(:artifact) { build(:ci_job_artifact, :codequality, file_format: :raw) } - - it 'iterates blob once' do - expect { |b| artifact.each_blob(&b) }.to yield_control.once - end - end - - context 'when there are no adapters for the file format' do - let(:artifact) { build(:ci_job_artifact, :junit, file_format: :zip) } - - it 'raises an error' do - expect { |b| artifact.each_blob(&b) }.to raise_error(described_class::NotSupportedAdapterError) - end - end - end - describe 'expired?' do subject { artifact.expired? } diff --git a/spec/models/concerns/ci/artifactable_spec.rb b/spec/models/concerns/ci/artifactable_spec.rb index 13c2ff5efe5..f05189abdd2 100644 --- a/spec/models/concerns/ci/artifactable_spec.rb +++ b/spec/models/concerns/ci/artifactable_spec.rb @@ -18,4 +18,40 @@ RSpec.describe Ci::Artifactable do it { is_expected.to be_const_defined(:FILE_FORMAT_ADAPTERS) } end end + + describe '#each_blob' do + context 'when file format is gzip' do + context 'when gzip file contains one file' do + let(:artifact) { build(:ci_job_artifact, :junit) } + + it 'iterates blob once' do + expect { |b| artifact.each_blob(&b) }.to yield_control.once + end + end + + context 'when gzip file contains three files' do + let(:artifact) { build(:ci_job_artifact, :junit_with_three_testsuites) } + + it 'iterates blob three times' do + expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(3).times + end + end + end + + context 'when file format is raw' do + let(:artifact) { build(:ci_job_artifact, :codequality, file_format: :raw) } + + it 'iterates blob once' do + expect { |b| artifact.each_blob(&b) }.to yield_control.once + end + end + + context 'when there are no adapters for the file format' do + let(:artifact) { build(:ci_job_artifact, :junit, file_format: :zip) } + + it 'raises an error' do + expect { |b| artifact.each_blob(&b) }.to raise_error(described_class::NotSupportedAdapterError) + end + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index f685fe12e13..39ece3d1cb3 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -391,14 +391,14 @@ RSpec.describe Namespace do let(:uploads_dir) { FileUploader.root } let(:pages_dir) { File.join(TestEnv.pages_path) } - def expect_project_directories_at(namespace_path) + def expect_project_directories_at(namespace_path, with_pages: true) expected_repository_path = File.join(TestEnv.repos_path, namespace_path, 'the-project.git') expected_upload_path = File.join(uploads_dir, namespace_path, 'the-project') expected_pages_path = File.join(pages_dir, namespace_path, 'the-project') expect(File.directory?(expected_repository_path)).to be_truthy expect(File.directory?(expected_upload_path)).to be_truthy - expect(File.directory?(expected_pages_path)).to be_truthy + expect(File.directory?(expected_pages_path)).to be(with_pages) end before do @@ -412,7 +412,7 @@ RSpec.describe Namespace do FileUtils.remove_entry(File.join(TestEnv.repos_path, new_parent.full_path), true) FileUtils.remove_entry(File.join(TestEnv.repos_path, child.full_path), true) FileUtils.remove_entry(File.join(uploads_dir, project.full_path), true) - FileUtils.remove_entry(File.join(pages_dir, project.full_path), true) + FileUtils.remove_entry(pages_dir, true) end context 'renaming child' do @@ -426,10 +426,22 @@ RSpec.describe Namespace do end end - it 'correctly moves the repository, uploads and pages', :sidekiq_inline do - child.update!(path: 'renamed') + context 'when no projects have pages deployed' do + it 'moves the repository and uploads', :sidekiq_inline do + project.pages_metadatum.update!(deployed: false) + child.update!(path: 'renamed') + + expect_project_directories_at('parent/renamed', with_pages: false) + end + end + + context 'when the project has pages deployed' do + it 'correctly moves the repository, uploads and pages', :sidekiq_inline do + project.pages_metadatum.update!(deployed: true) + child.update!(path: 'renamed') - expect_project_directories_at('parent/renamed') + expect_project_directories_at('parent/renamed') + end end end @@ -444,10 +456,22 @@ RSpec.describe Namespace do end end - it 'correctly moves the repository, uploads and pages', :sidekiq_inline do - parent.update!(path: 'renamed') + context 'when no projects have pages deployed' do + it 'moves the repository and uploads', :sidekiq_inline do + project.pages_metadatum.update!(deployed: false) + parent.update!(path: 'renamed') + + expect_project_directories_at('renamed/child', with_pages: false) + end + end + + context 'when the project has pages deployed' do + it 'correctly moves the repository, uploads and pages', :sidekiq_inline do + project.pages_metadatum.update!(deployed: true) + parent.update!(path: 'renamed') - expect_project_directories_at('renamed/child') + expect_project_directories_at('renamed/child') + end end end @@ -462,10 +486,22 @@ RSpec.describe Namespace do end end - it 'correctly moves the repository, uploads and pages', :sidekiq_inline do - child.update!(parent: new_parent) + context 'when no projects have pages deployed' do + it 'moves the repository and uploads', :sidekiq_inline do + project.pages_metadatum.update!(deployed: false) + child.update!(parent: new_parent) + + expect_project_directories_at('new_parent/child', with_pages: false) + end + end + + context 'when the project has pages deployed' do + it 'correctly moves the repository, uploads and pages', :sidekiq_inline do + project.pages_metadatum.update!(deployed: true) + child.update!(parent: new_parent) - expect_project_directories_at('new_parent/child') + expect_project_directories_at('new_parent/child') + end end end @@ -480,10 +516,22 @@ RSpec.describe Namespace do end end - it 'correctly moves the repository, uploads and pages', :sidekiq_inline do - child.update!(parent: nil) + context 'when no projects have pages deployed' do + it 'moves the repository and uploads', :sidekiq_inline do + project.pages_metadatum.update!(deployed: false) + child.update!(parent: nil) + + expect_project_directories_at('child', with_pages: false) + end + end + + context 'when the project has pages deployed' do + it 'correctly moves the repository, uploads and pages', :sidekiq_inline do + project.pages_metadatum.update!(deployed: true) + child.update!(parent: nil) - expect_project_directories_at('child') + expect_project_directories_at('child') + end end end @@ -498,10 +546,22 @@ RSpec.describe Namespace do end end - it 'correctly moves the repository, uploads and pages', :sidekiq_inline do - parent.update!(parent: new_parent) + context 'when no projects have pages deployed' do + it 'moves the repository and uploads', :sidekiq_inline do + project.pages_metadatum.update!(deployed: false) + parent.update!(parent: new_parent) - expect_project_directories_at('new_parent/parent/child') + expect_project_directories_at('new_parent/parent/child', with_pages: false) + end + end + + context 'when the project has pages deployed' do + it 'correctly moves the repository, uploads and pages', :sidekiq_inline do + project.pages_metadatum.update!(deployed: true) + parent.update!(parent: new_parent) + + expect_project_directories_at('new_parent/parent/child') + end end end end @@ -1174,6 +1234,27 @@ RSpec.describe Namespace do end end + describe '#any_project_with_pages_deployed?' do + it 'returns true if any project nested under the group has pages deployed' do + parent_1 = create(:group) # Three projects, one with pages + child_1_1 = create(:group, parent: parent_1) # Two projects, one with pages + child_1_2 = create(:group, parent: parent_1) # One project, no pages + parent_2 = create(:group) # No projects + + create(:project, group: child_1_1).tap do |project| + project.pages_metadatum.update!(deployed: true) + end + + create(:project, group: child_1_1) + create(:project, group: child_1_2) + + expect(parent_1.any_project_with_pages_deployed?).to be(true) + expect(child_1_1.any_project_with_pages_deployed?).to be(true) + expect(child_1_2.any_project_with_pages_deployed?).to be(false) + expect(parent_2.any_project_with_pages_deployed?).to be(false) + end + end + describe '#has_parent?' do it 'returns true when the group has a parent' do group = create(:group, :nested) diff --git a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb index 61174a7d0c5..634690d5d0b 100644 --- a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb +++ b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb @@ -219,20 +219,93 @@ RSpec.describe PerformanceMonitoring::PrometheusDashboard do end describe '#schema_validation_warnings' do - context 'when schema is valid' do - it 'returns nil' do - expect(described_class).to receive(:from_json) - expect(described_class.new.schema_validation_warnings).to be_nil + let(:environment) { create(:environment, project: project) } + let(:path) { '.gitlab/dashboards/test.yml' } + let(:project) { create(:project, :repository, :custom_repo, files: { path => dashboard_schema.to_yaml }) } + + subject(:schema_validation_warnings) { described_class.new(dashboard_schema.merge(path: path, environment: environment)).schema_validation_warnings } + + before do + allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find_raw).with(project, dashboard_path: path).and_call_original + end + + context 'metrics_dashboard_exhaustive_validations is on' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: true) + end + + context 'when schema is valid' do + let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) } + + it 'returns empty array' do + expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([]) + + expect(schema_validation_warnings).to eq [] + end + end + + context 'when schema is invalid' do + let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) } + + it 'returns array with errors messages' do + error = ::Gitlab::Metrics::Dashboard::Validator::Errors::SchemaValidationError.new + + expect(Gitlab::Metrics::Dashboard::Validator).to receive(:errors).with(dashboard_schema, dashboard_path: path, project: project).and_return([error]) + + expect(schema_validation_warnings).to eq [error.message] + end + end + + context 'when YAML has wrong syntax' do + let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) } + + subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings } + + it 'returns array with errors messages' do + expect(Gitlab::Metrics::Dashboard::Validator).not_to receive(:errors) + + expect(schema_validation_warnings).to eq ['Invalid yaml'] + end end end - context 'when schema is invalid' do - it 'returns array with errors messages' do - instance = described_class.new - instance.errors.add(:test, 'test error') + context 'metrics_dashboard_exhaustive_validations is off' do + before do + stub_feature_flags(metrics_dashboard_exhaustive_validations: false) + end + + context 'when schema is valid' do + let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) } + + it 'returns empty array' do + expect(described_class).to receive(:from_json).with(dashboard_schema) + + expect(schema_validation_warnings).to eq [] + end + end + + context 'when schema is invalid' do + let(:dashboard_schema) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/dashboard_missing_panel_groups.yml')) } + + it 'returns array with errors messages' do + instance = described_class.new + instance.errors.add(:test, 'test error') + + expect(described_class).to receive(:from_json).and_raise(ActiveModel::ValidationError.new(instance)) + expect(described_class.new.schema_validation_warnings).to eq ['test: test error'] + end + end - expect(described_class).to receive(:from_json).and_raise(ActiveModel::ValidationError.new(instance)) - expect(described_class.new.schema_validation_warnings).to eq ['test: test error'] + context 'when YAML has wrong syntax' do + let(:project) { create(:project, :repository, :custom_repo, files: { path => fixture_file('lib/gitlab/metrics/dashboard/broken_yml_syntax.yml') }) } + + subject(:schema_validation_warnings) { described_class.new(path: path, environment: environment).schema_validation_warnings } + + it 'returns array with errors messages' do + expect(described_class).not_to receive(:from_json) + + expect(schema_validation_warnings).to eq ['Invalid yaml'] + end end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f9b819e22cd..9cd666e541f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1648,7 +1648,7 @@ RSpec.describe User do # add user to project project.add_maintainer(user) - # create invite to projet + # create invite to project create(:project_member, :developer, project: project, invite_token: '1234', invite_email: 'inviteduser1@example.com') # create request to join project diff --git a/spec/requests/api/conan_packages_spec.rb b/spec/requests/api/conan_packages_spec.rb index 5ad7fe5d579..6569ce91bb8 100644 --- a/spec/requests/api/conan_packages_spec.rb +++ b/spec/requests/api/conan_packages_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'spec_helper' RSpec.describe API::ConanPackages do diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb new file mode 100644 index 00000000000..e08637629cc --- /dev/null +++ b/spec/requests/api/generic_packages_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::GenericPackages do + let_it_be(:personal_access_token) { create(:personal_access_token) } + let_it_be(:project) { create(:project) } + + describe 'GET /api/v4/projects/:id/packages/generic/ping' do + let(:user) { personal_access_token.user } + let(:auth_token) { personal_access_token.token } + + before do + project.add_developer(user) + end + + context 'packages feature is disabled' do + it 'responds with 404 Not Found' do + stub_packages_setting(enabled: false) + + ping(personal_access_token: auth_token) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'generic_packages feature flag is disabled' do + it 'responds with 404 Not Found' do + stub_feature_flags(generic_packages: false) + + ping(personal_access_token: auth_token) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'generic_packages feature flag is enabled' do + before do + stub_feature_flags(generic_packages: true) + end + + context 'authenticating using personal access token' do + it 'responds with 200 OK when valid personal access token is provided' do + ping(personal_access_token: auth_token) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'responds with 401 Unauthorized when invalid personal access token provided' do + ping(personal_access_token: 'invalid-token') + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'authenticating using job token' do + it 'responds with 200 OK when valid job token is provided' do + job_token = create(:ci_build, user: user).token + + ping(job_token: job_token) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'responds with 401 Unauthorized when invalid job token provided' do + ping(job_token: 'invalid-token') + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + def ping(personal_access_token: nil, job_token: nil) + headers = { + Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => personal_access_token.presence, + Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_token.presence + }.compact + + get api('/projects/%d/packages/generic/ping' % project.id), headers: headers + end + end +end diff --git a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb index 456b0a5dea1..e01f59ee6a0 100644 --- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb +++ b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb @@ -7,7 +7,7 @@ RSpec.describe 'Getting Metrics Dashboard' do let_it_be(:current_user) { create(:user) } let(:project) { create(:project) } - let!(:environment) { create(:environment, project: project) } + let(:environment) { create(:environment, project: project) } let(:query) do graphql_query_for( @@ -25,73 +25,156 @@ RSpec.describe 'Getting Metrics Dashboard' do ) end - context 'for anonymous user' do + context 'with metrics_dashboard_exhaustive_validations feature flag off' do before do - post_graphql(query, current_user: current_user) + stub_feature_flags(metrics_dashboard_exhaustive_validations: false) end - context 'requested dashboard is available' do - let(:path) { 'config/prometheus/common_metrics.yml' } + context 'for anonymous user' do + before do + post_graphql(query, current_user: current_user) + end + + context 'requested dashboard is available' do + let(:path) { 'config/prometheus/common_metrics.yml' } + + it_behaves_like 'a working graphql query' + + it 'returns nil' do + dashboard = graphql_data.dig('project', 'environments', 'nodes') + + expect(dashboard).to be_nil + end + end + end + + context 'for user with developer access' do + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + context 'requested dashboard is available' do + let(:path) { 'config/prometheus/common_metrics.yml' } + + it_behaves_like 'a working graphql query' + + it 'returns metrics dashboard' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') + + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil) + end + + context 'invalid dashboard' do + let(:path) { '.gitlab/dashboards/metrics.yml' } + let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) } + + it 'returns metrics dashboard' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') + + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"]) + end + end + + context 'empty dashboard' do + let(:path) { '.gitlab/dashboards/metrics.yml' } + let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) } + + it 'returns metrics dashboard' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - it_behaves_like 'a working graphql query' + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"]) + end + end + end + + context 'requested dashboard can not be found' do + let(:path) { 'config/prometheus/i_am_not_here.yml' } - it 'returns nil' do - dashboard = graphql_data.dig('project', 'environments', 'nodes') + it_behaves_like 'a working graphql query' - expect(dashboard).to be_nil + it 'returns nil' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') + + expect(dashboard).to be_nil + end end end end - context 'for user with developer access' do + context 'with metrics_dashboard_exhaustive_validations feature flag on' do before do - project.add_developer(current_user) - post_graphql(query, current_user: current_user) + stub_feature_flags(metrics_dashboard_exhaustive_validations: true) end - context 'requested dashboard is available' do - let(:path) { 'config/prometheus/common_metrics.yml' } + context 'for anonymous user' do + before do + post_graphql(query, current_user: current_user) + end + + context 'requested dashboard is available' do + let(:path) { 'config/prometheus/common_metrics.yml' } + + it_behaves_like 'a working graphql query' - it_behaves_like 'a working graphql query' + it 'returns nil' do + dashboard = graphql_data.dig('project', 'environments', 'nodes') - it 'returns metrics dashboard' do - dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard'] + expect(dashboard).to be_nil + end + end + end - expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil) + context 'for user with developer access' do + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) end - context 'invalid dashboard' do - let(:path) { '.gitlab/dashboards/metrics.yml' } - let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) } + context 'requested dashboard is available' do + let(:path) { 'config/prometheus/common_metrics.yml' } + + it_behaves_like 'a working graphql query' it 'returns metrics dashboard' do dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"]) + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil) end - end - context 'empty dashboard' do - let(:path) { '.gitlab/dashboards/metrics.yml' } - let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) } + context 'invalid dashboard' do + let(:path) { '.gitlab/dashboards/metrics.yml' } + let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) } - it 'returns metrics dashboard' do - dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') + it 'returns metrics dashboard' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"]) + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: panel_groups"]) + end + end + + context 'empty dashboard' do + let(:path) { '.gitlab/dashboards/metrics.yml' } + let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) } + + it 'returns metrics dashboard' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') + + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: dashboard, panel_groups"]) + end end end - end - context 'requested dashboard can not be found' do - let(:path) { 'config/prometheus/i_am_not_here.yml' } + context 'requested dashboard can not be found' do + let(:path) { 'config/prometheus/i_am_not_here.yml' } - it_behaves_like 'a working graphql query' + it_behaves_like 'a working graphql query' - it 'returns nil' do - dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard'] + it 'returns nil' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - expect(dashboard).to be_nil + expect(dashboard).to be_nil + end end end end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb new file mode 100644 index 00000000000..5a4dc3ce187 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'PipelineCancel' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, :running, project: project, user: user) } + + let(:mutation) { graphql_mutation(:pipeline_cancel, {}, 'errors') } + + let(:mutation_response) { graphql_mutation_response(:pipeline_cancel) } + + before_all do + project.add_maintainer(user) + end + + it 'reports the service-level error' do + service = double(execute: ServiceResponse.error(message: 'Error canceling pipeline')) + allow(::Ci::CancelUserPipelinesService).to receive(:new).and_return(service) + + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(mutation_response).to include('errors' => ['Error canceling pipeline']) + end + + it 'does not change any pipelines not owned by the current user' do + build = create(:ci_build, :running, pipeline: pipeline) + + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(build).not_to be_canceled + end + + it "cancels all of the current user's cancelable pipelines" do + build = create(:ci_build, :running, pipeline: pipeline) + + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(build.reload).to be_canceled + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb new file mode 100644 index 00000000000..08959d354e2 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'PipelineDestroy' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { project.owner } + let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project, user: user) } + + let(:mutation) do + variables = { + id: pipeline.to_global_id.to_s + } + graphql_mutation(:pipeline_destroy, variables, 'errors') + end + + it 'returns an error if the user is not allowed to destroy the pipeline' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + + it 'destroys a pipeline' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound) + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb new file mode 100644 index 00000000000..f6acf29c321 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'PipelineRetry' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + + let(:mutation) do + variables = { + id: pipeline.to_global_id.to_s + } + graphql_mutation(:pipeline_retry, variables, + <<-QL + errors + pipeline { + id + } + QL + ) + end + + let(:mutation_response) { graphql_mutation_response(:pipeline_retry) } + + before_all do + project.add_maintainer(user) + end + + it 'returns an error if the user is not allowed to retry the pipeline' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + + it 'retries a pipeline' do + pipeline_id = ::Gitlab::GlobalId.build(pipeline, id: pipeline.id).to_s + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['pipeline']['id']).to eq(pipeline_id) + end +end diff --git a/spec/services/admin/propagate_integration_service_spec.rb b/spec/services/admin/propagate_integration_service_spec.rb index 2e879cf06d1..aded0f08ae8 100644 --- a/spec/services/admin/propagate_integration_service_spec.rb +++ b/spec/services/admin/propagate_integration_service_spec.rb @@ -43,7 +43,7 @@ RSpec.describe Admin::PropagateIntegrationService do ) end - let!(:another_inherited_integration) do + let!(:different_type_inherited_integration) do BambooService.create!( project: create(:project), inherit_from_id: instance_integration.id, @@ -59,7 +59,7 @@ RSpec.describe Admin::PropagateIntegrationService do shared_examples 'inherits settings from integration' do it 'updates the inherited integrations' do - described_class.propagate(integration: instance_integration, overwrite: overwrite) + described_class.propagate(instance_integration) expect(integration.reload.inherit_from_id).to eq(instance_integration.id) expect(integration.attributes.except(*excluded_attributes)) @@ -70,7 +70,7 @@ RSpec.describe Admin::PropagateIntegrationService do let(:excluded_attributes) { %w[id service_id created_at updated_at] } it 'updates the data fields from inherited integrations' do - described_class.propagate(integration: instance_integration, overwrite: overwrite) + described_class.propagate(instance_integration) expect(integration.reload.data_fields.attributes.except(*excluded_attributes)) .to eq(instance_integration.data_fields.attributes.except(*excluded_attributes)) @@ -80,7 +80,7 @@ RSpec.describe Admin::PropagateIntegrationService do shared_examples 'does not inherit settings from integration' do it 'does not update the not inherited integrations' do - described_class.propagate(integration: instance_integration, overwrite: overwrite) + described_class.propagate(instance_integration) expect(integration.reload.attributes.except(*excluded_attributes)) .not_to eq(instance_integration.attributes.except(*excluded_attributes)) @@ -88,8 +88,6 @@ RSpec.describe Admin::PropagateIntegrationService do end context 'update only inherited integrations' do - let(:overwrite) { false } - it_behaves_like 'inherits settings from integration' do let(:integration) { inherited_integration } end @@ -99,27 +97,7 @@ RSpec.describe Admin::PropagateIntegrationService do end it_behaves_like 'does not inherit settings from integration' do - let(:integration) { another_inherited_integration } - end - - it_behaves_like 'inherits settings from integration' do - let(:integration) { project.jira_service } - end - end - - context 'update all integrations' do - let(:overwrite) { true } - - it_behaves_like 'inherits settings from integration' do - let(:integration) { inherited_integration } - end - - it_behaves_like 'inherits settings from integration' do - let(:integration) { not_inherited_integration } - end - - it_behaves_like 'does not inherit settings from integration' do - let(:integration) { another_inherited_integration } + let(:integration) { different_type_inherited_integration } end it_behaves_like 'inherits settings from integration' do @@ -128,7 +106,7 @@ RSpec.describe Admin::PropagateIntegrationService do end it 'updates project#has_external_issue_tracker for issue tracker services' do - described_class.propagate(integration: instance_integration, overwrite: true) + described_class.propagate(instance_integration) expect(project.reload.has_external_issue_tracker).to eq(true) end @@ -141,7 +119,7 @@ RSpec.describe Admin::PropagateIntegrationService do external_wiki_url: 'http://external-wiki-url.com' ) - described_class.propagate(integration: instance_integration, overwrite: true) + described_class.propagate(instance_integration) expect(project.reload.has_external_wiki).to eq(true) end diff --git a/spec/services/ci/cancel_user_pipelines_service_spec.rb b/spec/services/ci/cancel_user_pipelines_service_spec.rb index 12117051b64..8491242dfd5 100644 --- a/spec/services/ci/cancel_user_pipelines_service_spec.rb +++ b/spec/services/ci/cancel_user_pipelines_service_spec.rb @@ -19,5 +19,17 @@ RSpec.describe Ci::CancelUserPipelinesService do expect(build.reload).to be_canceled end end + + context 'when an error ocurrs' do + it 'raises a service level error' do + service = double(execute: ServiceResponse.error(message: 'Error canceling pipeline')) + allow(::Ci::CancelUserPipelinesService).to receive(:new).and_return(service) + + result = subject + + expect(result).to be_a(ServiceResponse) + expect(result).to be_error + end + end end end diff --git a/spec/services/ci/generate_coverage_reports_service_spec.rb b/spec/services/ci/generate_coverage_reports_service_spec.rb index 76d446c1d2c..722b92ea3b6 100644 --- a/spec/services/ci/generate_coverage_reports_service_spec.rb +++ b/spec/services/ci/generate_coverage_reports_service_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Ci::GenerateCoverageReportsService do end end - context 'when head pipeline has corrupted coverage reports' do + context 'when head pipeline does not have a coverage report artifact' do let!(:merge_request) { create(:merge_request, :with_coverage_reports, source_project: project) } let!(:service) { described_class.new(project, nil, id: merge_request.id) } let!(:head_pipeline) { merge_request.head_pipeline } diff --git a/spec/services/jira/requests/projects/list_service_spec.rb b/spec/services/jira/requests/projects/list_service_spec.rb index b4db77f8104..415dd42c795 100644 --- a/spec/services/jira/requests/projects/list_service_spec.rb +++ b/spec/services/jira/requests/projects/list_service_spec.rb @@ -69,7 +69,7 @@ RSpec.describe Jira::Requests::Projects::ListService do expect(client).to receive(:get).and_return([{ 'key' => 'pr1', 'name' => 'First Project' }, { 'key' => 'pr2', 'name' => 'Second Project' }]) end - it 'returns a paylod with Jira projets' do + it 'returns a paylod with Jira projects' do payload = subject.payload expect(subject.success?).to be_truthy @@ -80,7 +80,7 @@ RSpec.describe Jira::Requests::Projects::ListService do context 'when filtering projects by name' do let(:params) { { query: 'first' } } - it 'returns a paylod with Jira projets' do + it 'returns a paylod with Jira procjets' do payload = subject.payload expect(subject.success?).to be_truthy diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb index 11e3604c38a..89971a3edfb 100644 --- a/spec/services/projects/after_rename_service_spec.rb +++ b/spec/services/projects/after_rename_service_spec.rb @@ -75,10 +75,25 @@ RSpec.describe Projects::AfterRenameService do end end - it 'schedules a move of the pages directory' do - expect(PagesTransferWorker).to receive(:perform_async).with('rename_project', anything) + context 'when the project has pages deployed' do + it 'schedules a move of the pages directory' do + allow(project).to receive(:pages_deployed?).and_return(true) - service_execute + expect(PagesTransferWorker).to receive(:perform_async).with('rename_project', anything) + + service_execute + end + end + + context 'when the project does not have pages deployed' do + it 'does nothing with the pages directory' do + allow(project).to receive(:pages_deployed?).and_return(false) + + expect(PagesTransferWorker).not_to receive(:perform_async) + expect(Gitlab::PagesTransfer).not_to receive(:new) + + service_execute + end end end @@ -180,10 +195,25 @@ RSpec.describe Projects::AfterRenameService do end end - it 'schedules a move of the pages directory' do - expect(PagesTransferWorker).to receive(:perform_async).with('rename_project', anything) + context 'when the project has pages deployed' do + it 'schedules a move of the pages directory' do + allow(project).to receive(:pages_deployed?).and_return(true) - service_execute + expect(PagesTransferWorker).to receive(:perform_async).with('rename_project', anything) + + service_execute + end + end + + context 'when the project does not have pages deployed' do + it 'does nothing with the pages directory' do + allow(project).to receive(:pages_deployed?).and_return(false) + + expect(PagesTransferWorker).not_to receive(:perform_async) + expect(Gitlab::PagesTransfer).not_to receive(:new) + + service_execute + end end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 3362b333c6e..01af999f117 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -5,8 +5,8 @@ require 'spec_helper' RSpec.describe Projects::TransferService do include GitHelpers - let(:user) { create(:user) } - let(:group) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } let(:project) { create(:project, :repository, :legacy_storage, namespace: user.namespace) } subject(:execute_transfer) { described_class.new(project, user).execute(group) } @@ -489,6 +489,43 @@ RSpec.describe Projects::TransferService do end end + context 'moving pages' do + let_it_be(:project) { create(:project, namespace: user.namespace) } + + before do + group.add_owner(user) + end + + it 'schedules a job when pages are deployed' do + project.mark_pages_as_deployed + + expect(PagesTransferWorker).to receive(:perform_async) + .with("move_project", [project.path, user.namespace.full_path, group.full_path]) + + execute_transfer + end + + it 'does not schedule a job when no pages are deployed' do + expect(PagesTransferWorker).not_to receive(:perform_async) + + execute_transfer + end + + context 'when async_pages_move_project_transfer is disabled' do + before do + stub_feature_flags(async_pages_move_project_transfer: false) + end + + it 'moves pages inline' do + expect_next_instance_of(Gitlab::PagesTransfer) do |transfer| + expect(transfer).to receive(:move_project).with(project.path, user.namespace.full_path, group.full_path) + end + + execute_transfer + end + end + end + def rugged_config rugged_repo(project.repository).config end diff --git a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb index 18f4932907a..ebb91a2fbad 100644 --- a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb +++ b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb @@ -70,6 +70,37 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(items.sort_by(&:relative_position)).to eq(items) end + it 'manages to move nulls to the end even if there is not enough space' do + run = run_at_end(20).to_a + bunch_a = create_items_with_positions(run[0..18]) + bunch_b = create_items_with_positions([run.last]) + + nils = create_items_with_positions([nil] * 4) + described_class.move_nulls_to_end(nils) + + items = [*bunch_a, *bunch_b, *nils] + items.each(&:reset) + + expect(items.map(&:relative_position)).to all(be_valid_position) + expect(items.reverse.sort_by(&:relative_position)).to eq(items) + end + + it 'manages to move nulls to the end, stacking if we cannot create enough space' do + run = run_at_end(40).to_a + bunch = create_items_with_positions(run.select(&:even?)) + + nils = create_items_with_positions([nil] * 20) + described_class.move_nulls_to_end(nils) + + items = [*bunch, *nils] + items.each(&:reset) + + expect(items.map(&:relative_position)).to all(be_valid_position) + expect(bunch.reverse.sort_by(&:relative_position)).to eq(bunch) + expect(nils.reverse.sort_by(&:relative_position)).not_to eq(nils) + expect(bunch.map(&:relative_position)).to all(be < nils.map(&:relative_position).min) + end + it 'does not have an N+1 issue' do create_items_with_positions(10..12) @@ -130,6 +161,37 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(described_class.move_nulls_to_start([item1])).to be(0) expect(item1.reload.relative_position).to be(1) end + + it 'manages to move nulls to the start even if there is not enough space' do + run = run_at_start(20).to_a + bunch_a = create_items_with_positions([run.first]) + bunch_b = create_items_with_positions(run[2..]) + + nils = create_items_with_positions([nil, nil, nil, nil]) + described_class.move_nulls_to_start(nils) + + items = [*nils, *bunch_a, *bunch_b] + items.each(&:reset) + + expect(items.map(&:relative_position)).to all(be_valid_position) + expect(items.reverse.sort_by(&:relative_position)).to eq(items) + end + + it 'manages to move nulls to the end, stacking if we cannot create enough space' do + run = run_at_start(40).to_a + bunch = create_items_with_positions(run.select(&:even?)) + + nils = create_items_with_positions([nil].cycle.take(20)) + described_class.move_nulls_to_start(nils) + + items = [*nils, *bunch] + items.each(&:reset) + + expect(items.map(&:relative_position)).to all(be_valid_position) + expect(bunch.reverse.sort_by(&:relative_position)).to eq(bunch) + expect(nils.reverse.sort_by(&:relative_position)).not_to eq(nils) + expect(bunch.map(&:relative_position)).to all(be > nils.map(&:relative_position).max) + end end describe '#max_relative_position' do diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb index 0191a6dfbc9..fd10dd4367e 100644 --- a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb @@ -19,8 +19,10 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type| subject(:service) { described_class.new(container: container, current_user: user, params: opts) } it 'updates the wiki page' do - updated_page = service.execute(page) + response = service.execute(page) + updated_page = response.payload[:page] + expect(response).to be_success expect(updated_page).to be_valid expect(updated_page.message).to eq(opts[:message]) expect(updated_page.content).to eq(opts[:content]) @@ -81,7 +83,11 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type| end it 'reports the error' do - expect(service.execute(page)).to be_invalid + response = service.execute(page) + page = response.payload[:page] + + expect(response).to be_error + expect(page).to be_invalid .and have_attributes(errors: be_present) end end diff --git a/spec/workers/propagate_integration_worker_spec.rb b/spec/workers/propagate_integration_worker_spec.rb index 3fe76f14750..b8c7f2bebe7 100644 --- a/spec/workers/propagate_integration_worker_spec.rb +++ b/spec/workers/propagate_integration_worker_spec.rb @@ -17,8 +17,13 @@ RSpec.describe PropagateIntegrationWorker do end it 'calls the propagate service with the integration' do - expect(Admin::PropagateIntegrationService).to receive(:propagate) - .with(integration: integration, overwrite: true) + expect(Admin::PropagateIntegrationService).to receive(:propagate).with(integration) + + subject.perform(integration.id) + end + + it 'ignores overwrite parameter from previous version' do + expect(Admin::PropagateIntegrationService).to receive(:propagate).with(integration) subject.perform(integration.id, true) end |