Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml4
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml18
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock36
-rw-r--r--app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js7
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue2
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue2
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue76
-rw-r--r--app/assets/javascripts/jira_import/utils/constants.js3
-rw-r--r--app/assets/javascripts/milestones/project_milestone_combobox.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_previewer.js4
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue2
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue2
-rw-r--r--app/assets/javascripts/users_select/index.js6
-rw-r--r--app/assets/stylesheets/fontawesome_custom.scss16
-rw-r--r--app/controllers/concerns/integrations_actions.rb3
-rw-r--r--app/controllers/concerns/wiki_actions.rb5
-rw-r--r--app/graphql/mutations/ci/base.rb17
-rw-r--r--app/graphql/mutations/ci/pipeline_cancel.rb20
-rw-r--r--app/graphql/mutations/ci/pipeline_destroy.rb22
-rw-r--r--app/graphql/mutations/ci/pipeline_retry.rb27
-rw-r--r--app/graphql/resolvers/metrics/dashboard_resolver.rb3
-rw-r--r--app/graphql/types/metrics/dashboard_type.rb7
-rw-r--r--app/graphql/types/milestone_type.rb2
-rw-r--r--app/graphql/types/mutation_type.rb3
-rw-r--r--app/models/blob_viewer/metrics_dashboard_yml.rb26
-rw-r--r--app/models/ci/job_artifact.rb16
-rw-r--r--app/models/concerns/ci/artifactable.rb18
-rw-r--r--app/models/concerns/relative_positioning.rb22
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb14
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/performance_monitoring/prometheus_dashboard.rb11
-rw-r--r--app/models/service.rb328
-rw-r--r--app/services/admin/propagate_integration_service.rb24
-rw-r--r--app/services/ci/cancel_user_pipelines_service.rb4
-rw-r--r--app/services/ci/destroy_pipeline_service.rb4
-rw-r--r--app/services/jira_import/users_mapper_service.rb1
-rw-r--r--app/services/projects/after_rename_service.rb22
-rw-r--r--app/services/projects/transfer_service.rb18
-rw-r--r--app/services/wiki_pages/update_service.rb8
-rw-r--r--app/views/admin/application_settings/_usage.html.haml6
-rw-r--r--app/views/doorkeeper/applications/_delete_form.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml4
-rw-r--r--app/views/projects/packages/packages/_legacy_package_list.html.haml2
-rw-r--r--app/views/projects/triggers/_trigger.html.haml2
-rw-r--r--app/workers/propagate_integration_worker.rb8
-rw-r--r--changelogs/unreleased/225926-replace-fa-tag-s-icons-with-gitlab-svg-icons.yml5
-rw-r--r--changelogs/unreleased/225936-replace-fa-user-s-icons-with-gitlab-svg-user-s-icon.yml5
-rw-r--r--changelogs/unreleased/235889-jira-importer-user-mapping-shows-50-users-max.yml5
-rw-r--r--changelogs/unreleased/239341-fix-snippets-create-without-file-path.yml5
-rw-r--r--changelogs/unreleased/ajk-relative-positioning-safe-move-nulls.yml5
-rw-r--r--changelogs/unreleased/lm-be-pipeline-mutations.yml5
-rw-r--r--changelogs/unreleased/sh-always-retry-read-build-logs.yml5
-rw-r--r--config/feature_flags/development/async_pages_move_project_transfer.yml7
-rw-r--r--config/feature_flags/development/generic_packages.yml7
-rw-r--r--config/feature_flags/development/metrics_dashboard_exhaustive_validations.yml7
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/object_storage.md2
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md7
-rw-r--r--doc/administration/troubleshooting/sidekiq.md2
-rw-r--r--doc/administration/troubleshooting/tracing_correlation_id.md2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql133
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json480
-rw-r--r--doc/api/graphql/reference/index.md41
-rw-r--r--doc/api/issues.md4
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/cicd/templates.md2
-rw-r--r--doc/development/code_review.md2
-rw-r--r--doc/development/documentation/styleguide.md2
-rw-r--r--doc/development/ee_features.md4
-rw-r--r--doc/integration/jira_development_panel.md4
-rw-r--r--doc/integration/omniauth.md2
-rw-r--r--doc/topics/git/numerous_undo_possibilities_in_git/index.md4
-rw-r--r--doc/topics/web_application_firewall/quick_start_guide.md2
-rw-r--r--doc/user/admin_area/monitoring/health_check.md2
-rw-r--r--doc/user/application_security/dast/index.md2
-rw-r--r--doc/user/infrastructure/index.md2
-rw-r--r--doc/user/packages/container_registry/index.md2
-rw-r--r--doc/user/project/import/cvs.md6
-rw-r--r--doc/user/project/integrations/irker.md2
-rw-r--r--doc/user/project/integrations/jira.md2
-rw-r--r--doc/user/project/integrations/overview.md4
-rw-r--r--doc/user/project/merge_requests/code_quality.md4
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/generic_packages.rb34
-rw-r--r--lib/api/wikis.rb5
-rw-r--r--lib/gitlab/application_context.rb2
-rw-r--r--lib/gitlab/background_migration.rb2
-rw-r--r--lib/gitlab/ci/pipeline/artifact/code_coverage.rb8
-rw-r--r--lib/gitlab/ci/trace.rb9
-rw-r--r--lib/gitlab/exclusive_lease_helpers.rb5
-rw-r--r--lib/gitlab/metrics/dashboard/validator.rb16
-rw-r--r--lib/gitlab/metrics/dashboard/validator/client.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/validator/schemas/panel.json2
-rw-r--r--locale/gitlab.pot25
-rw-r--r--spec/controllers/admin/integrations_controller_spec.rb4
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb79
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/broken_yml_syntax.yml13
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap4
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js55
-rw-r--r--spec/frontend/jira_import/mock_data.js3
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap2
-rw-r--r--spec/frontend/snippets/components/edit_spec.js2
-rw-r--r--spec/graphql/types/milestone_type_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb22
-rw-r--r--spec/lib/gitlab/exclusive_lease_helpers_spec.rb10
-rw-r--r--spec/lib/gitlab/metrics/dashboard/validator/errors_spec.rb13
-rw-r--r--spec/lib/gitlab/metrics/dashboard/validator_spec.rb52
-rw-r--r--spec/models/blob_viewer/metrics_dashboard_yml_spec.rb253
-rw-r--r--spec/models/ci/job_artifact_spec.rb36
-rw-r--r--spec/models/concerns/ci/artifactable_spec.rb36
-rw-r--r--spec/models/namespace_spec.rb117
-rw-r--r--spec/models/performance_monitoring/prometheus_dashboard_spec.rb93
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/requests/api/conan_packages_spec.rb1
-rw-r--r--spec/requests/api/generic_packages_spec.rb82
-rw-r--r--spec/requests/api/graphql/metrics/dashboard_query_spec.rb155
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb45
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb31
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb45
-rw-r--r--spec/services/admin/propagate_integration_service_spec.rb36
-rw-r--r--spec/services/ci/cancel_user_pipelines_service_spec.rb12
-rw-r--r--spec/services/ci/generate_coverage_reports_service_spec.rb2
-rw-r--r--spec/services/jira/requests/projects/list_service_spec.rb4
-rw-r--r--spec/services/projects/after_rename_service_spec.rb42
-rw-r--r--spec/services/projects/transfer_service_spec.rb41
-rw-r--r--spec/support/shared_examples/models/relative_positioning_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb10
-rw-r--r--spec/workers/propagate_integration_worker_spec.rb9
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
diff --git a/Gemfile b/Gemfile
index 0af2bff4f2a..fd6a58aa200 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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