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--CONTRIBUTING.md4
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue115
-rw-r--r--app/assets/javascripts/admin/users/components/user_date.vue29
-rw-r--r--app/assets/javascripts/admin/users/components/users_table.vue18
-rw-r--r--app/assets/javascripts/admin/users/constants.js2
-rw-r--r--app/assets/javascripts/admin/users/utils.js7
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js9
-rw-r--r--app/assets/stylesheets/pages/groups.scss1
-rw-r--r--app/models/ci/pipeline.rb3
-rw-r--r--app/models/ci/pipeline_artifact.rb3
-rw-r--r--app/models/project_pages_metadatum.rb1
-rw-r--r--app/services/ci/pipeline_artifacts/create_quality_report_service.rb32
-rw-r--r--app/services/pages/migrate_from_legacy_storage_service.rb71
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb20
-rw-r--r--changelogs/unreleased/231199-yo-gitlab-ui.yml5
-rw-r--r--changelogs/unreleased/remove-tmp-index-oauth-applications.yml5
-rw-r--r--changelogs/unreleased/yo-remove-top-border-usage-quotos.yml5
-rw-r--r--db/migrate/20210120221743_delete_oauth_applications_tmp_index.rb18
-rw-r--r--db/schema_migrations/202101202217431
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/geo/replication/version_specific_updates.md4
-rw-r--r--doc/administration/reference_architectures/1k_users.md2
-rw-r--r--doc/development/agent/routing.md177
-rw-r--r--doc/development/import_project.md2
-rw-r--r--doc/gitlab-basics/create-issue.md4
-rw-r--r--doc/user/index.md2
-rw-r--r--doc/user/project/issues/index.md152
-rw-r--r--doc/user/project/issues/issue_data_and_actions.md2
-rw-r--r--lib/gitlab/import_export/design_repo_restorer.rb7
-rw-r--r--lib/gitlab/import_export/importer.rb6
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb12
-rw-r--r--lib/tasks/gitlab/pages.rake37
-rw-r--r--package.json2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb2
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js138
-rw-r--r--spec/frontend/admin/users/components/user_date_spec.js34
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js26
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js8
-rw-r--r--spec/lib/gitlab/import_export/design_repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb14
-rw-r--r--spec/serializers/pipeline_entity_spec.rb207
-rw-r--r--spec/services/ci/pipeline_artifacts/create_quality_report_service_spec.rb62
-rw-r--r--spec/services/pages/migrate_from_legacy_storage_service_spec.rb92
-rw-r--r--spec/tasks/gitlab/pages_rake_spec.rb60
-rw-r--r--spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb40
-rw-r--r--yarn.lock8
50 files changed, 1061 insertions, 410 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 030624b8df9..1818bca1e46 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -5,11 +5,11 @@ By submitting code as an individual you agree to the
By submitting code as an entity you agree to the
[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
-All Documentation content that resides under the [doc/ directory](/doc) of this
+All Documentation content that resides under the [`doc/` directory](/doc) of this
repository is licensed under Creative Commons:
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
-_This notice should stay as the first item in the CONTRIBUTING.md file._
+_This notice should stay as the first item in the `CONTRIBUTING.md` file._
## Contributing Documentation has been moved
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
new file mode 100644
index 00000000000..6c7c434cdf4
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -0,0 +1,115 @@
+<script>
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
+} from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import { convertArrayToCamelCase } from '~/lib/utils/common_utils';
+import { generateUserPaths } from '../utils';
+
+export default {
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ paths: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ userActions() {
+ return convertArrayToCamelCase(this.user.actions);
+ },
+ dropdownActions() {
+ return this.userActions.filter((a) => a !== 'edit');
+ },
+ dropdownDeleteActions() {
+ return this.dropdownActions.filter((a) => a.includes('delete'));
+ },
+ dropdownSafeActions() {
+ return this.dropdownActions.filter((a) => !this.dropdownDeleteActions.includes(a));
+ },
+ hasDropdownActions() {
+ return this.dropdownActions.length > 0;
+ },
+ hasDeleteActions() {
+ return this.dropdownDeleteActions.length > 0;
+ },
+ hasEditAction() {
+ return this.userActions.includes('edit');
+ },
+ userPaths() {
+ return generateUserPaths(this.paths, this.user.username);
+ },
+ },
+ methods: {
+ isLdapAction(action) {
+ return action === 'ldapBlocked';
+ },
+ },
+ i18n: {
+ edit: __('Edit'),
+ settings: __('Settings'),
+ unlock: __('Unlock'),
+ block: s__('AdminUsers|Block'),
+ unblock: s__('AdminUsers|Unblock'),
+ approve: s__('AdminUsers|Approve'),
+ reject: s__('AdminUsers|Reject'),
+ deactivate: s__('AdminUsers|Deactivate'),
+ activate: s__('AdminUsers|Activate'),
+ ldapBlocked: s__('AdminUsers|Cannot unblock LDAP blocked users'),
+ delete: s__('AdminUsers|Delete user'),
+ deleteWithContributions: s__('AdminUsers|Delete user and contributions'),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button v-if="hasEditAction" data-testid="edit" :href="userPaths.edit">{{
+ $options.i18n.edit
+ }}</gl-button>
+
+ <gl-dropdown
+ v-if="hasDropdownActions"
+ data-testid="actions"
+ right
+ class="gl-ml-2"
+ icon="settings"
+ >
+ <gl-dropdown-section-header>{{ $options.i18n.settings }}</gl-dropdown-section-header>
+
+ <template v-for="action in dropdownSafeActions">
+ <gl-dropdown-item v-if="isLdapAction(action)" :key="action" :data-testid="action">
+ {{ $options.i18n.ldap }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-else :key="action" :href="userPaths[action]" :data-testid="action">
+ {{ $options.i18n[action] }}
+ </gl-dropdown-item>
+ </template>
+
+ <gl-dropdown-divider v-if="hasDeleteActions" />
+
+ <gl-dropdown-item
+ v-for="action in dropdownDeleteActions"
+ :key="action"
+ :href="userPaths[action]"
+ :data-testid="`delete-${action}`"
+ >
+ <span class="gl-text-red-500">{{ $options.i18n[action] }}</span>
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/user_date.vue b/app/assets/javascripts/admin/users/components/user_date.vue
new file mode 100644
index 00000000000..38dddbf72c2
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/user_date.vue
@@ -0,0 +1,29 @@
+<script>
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
+import { SHORT_DATE_FORMAT } from '../constants';
+
+export default {
+ props: {
+ date: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ formattedDate() {
+ const { date } = this;
+ if (date === null) {
+ return __('Never');
+ }
+ return formatDate(new Date(date), SHORT_DATE_FORMAT);
+ },
+ },
+};
+</script>
+<template>
+ <span>
+ {{ formattedDate }}
+ </span>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue
index 15e31935a4c..0eefe1070ff 100644
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ b/app/assets/javascripts/admin/users/components/users_table.vue
@@ -2,6 +2,8 @@
import { GlTable } from '@gitlab/ui';
import { __ } from '~/locale';
import UserAvatar from './user_avatar.vue';
+import UserActions from './user_actions.vue';
+import UserDate from './user_date.vue';
const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
@@ -11,6 +13,8 @@ export default {
components: {
GlTable,
UserAvatar,
+ UserActions,
+ UserDate,
},
props: {
users: {
@@ -62,7 +66,19 @@ export default {
stacked="md"
>
<template #cell(name)="{ item: user }">
- <UserAvatar :user="user" :admin-user-path="paths.adminUser" />
+ <user-avatar :user="user" :admin-user-path="paths.adminUser" />
+ </template>
+
+ <template #cell(createdAt)="{ item: { createdAt } }">
+ <user-date :date="createdAt" />
+ </template>
+
+ <template #cell(lastActivityOn)="{ item: { lastActivityOn } }">
+ <user-date :date="lastActivityOn" show-never />
+ </template>
+
+ <template #cell(settings)="{ item: user }">
+ <user-actions :user="user" :paths="paths" />
</template>
</gl-table>
</div>
diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js
index 675fcf00c39..956c7a15738 100644
--- a/app/assets/javascripts/admin/users/constants.js
+++ b/app/assets/javascripts/admin/users/constants.js
@@ -1 +1,3 @@
export const USER_AVATAR_SIZE = 32;
+
+export const SHORT_DATE_FORMAT = 'd mmm, yyyy';
diff --git a/app/assets/javascripts/admin/users/utils.js b/app/assets/javascripts/admin/users/utils.js
new file mode 100644
index 00000000000..f6c1091ba27
--- /dev/null
+++ b/app/assets/javascripts/admin/users/utils.js
@@ -0,0 +1,7 @@
+export const generateUserPaths = (paths, id) => {
+ return Object.fromEntries(
+ Object.entries(paths).map(([action, genericPath]) => {
+ return [action, genericPath.replace('id', id)];
+ }),
+ );
+};
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index a41e43780bc..d0528204fd5 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -802,3 +802,12 @@ export const removeCookie = (name) => Cookies.remove(name);
* @returns {Boolean} on/off
*/
export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag];
+
+/**
+ * This method takes in array with snake_case strings
+ * and returns a new array with camelCase strings
+ *
+ * @param {Array[String]} array - Array to be converted
+ * @returns {Array[String]} Converted array
+ */
+export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i));
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index aeda91c1714..87307fd682e 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -260,7 +260,6 @@
}
.pipeline-quota {
- border-top: 1px solid $table-border-color;
border-bottom: 1px solid $table-border-color;
margin: 0 0 $gl-padding;
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 93f6c3c59e1..244876a0c05 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -250,6 +250,7 @@ module Ci
after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
pipeline.run_after_commit do
::Ci::PipelineArtifacts::CoverageReportWorker.perform_async(pipeline.id)
+ ::Ci::PipelineArtifacts::CreateQualityReportWorker.perform_async(pipeline.id)
end
end
@@ -1007,6 +1008,8 @@ module Ci
end
def can_generate_codequality_reports?
+ return false unless Feature.enabled?(:codequality_mr_diff, project)
+
has_reports?(Ci::JobArtifact.codequality_reports)
end
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index 437657ade2c..9d7c1c33412 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -14,7 +14,8 @@ module Ci
EXPIRATION_DATE = 1.week.freeze
DEFAULT_FILE_NAMES = {
- code_coverage: 'code_coverage.json'
+ code_coverage: 'code_coverage.json',
+ code_quality: 'code_quality.json'
}.freeze
belongs_to :project, class_name: "Project", inverse_of: :pipeline_artifacts
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index 2bef0056732..347c06db4e6 100644
--- a/app/models/project_pages_metadatum.rb
+++ b/app/models/project_pages_metadatum.rb
@@ -11,4 +11,5 @@ class ProjectPagesMetadatum < ApplicationRecord
scope :deployed, -> { where(deployed: true) }
scope :only_on_legacy_storage, -> { deployed.where(pages_deployment: nil) }
+ scope :with_project_route_and_deployment, -> { preload(project: [:namespace, :route, pages_metadatum: :pages_deployment]) }
end
diff --git a/app/services/ci/pipeline_artifacts/create_quality_report_service.rb b/app/services/ci/pipeline_artifacts/create_quality_report_service.rb
new file mode 100644
index 00000000000..6bc17ff69c0
--- /dev/null
+++ b/app/services/ci/pipeline_artifacts/create_quality_report_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+module Ci
+ module PipelineArtifacts
+ class CreateQualityReportService
+ def execute(pipeline)
+ return unless pipeline.can_generate_codequality_reports?
+ return if pipeline.has_codequality_reports?
+
+ file = build_carrierwave_file(pipeline)
+
+ pipeline.pipeline_artifacts.create!(
+ project_id: pipeline.project_id,
+ file_type: :code_quality,
+ file_format: :raw,
+ size: file["tempfile"].size,
+ file: file,
+ expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now
+ )
+ end
+
+ private
+
+ def build_carrierwave_file(pipeline)
+ CarrierWaveStringFile.new_file(
+ file_content: pipeline.codequality_reports.to_json,
+ filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_quality),
+ content_type: 'application/json'
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/pages/migrate_from_legacy_storage_service.rb b/app/services/pages/migrate_from_legacy_storage_service.rb
new file mode 100644
index 00000000000..e6c9240df52
--- /dev/null
+++ b/app/services/pages/migrate_from_legacy_storage_service.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Pages
+ class MigrateFromLegacyStorageService
+ def initialize(logger, migration_threads, batch_size)
+ @logger = logger
+ @migration_threads = migration_threads
+ @batch_size = batch_size
+
+ @migrated = 0
+ @errored = 0
+ @counters_lock = Mutex.new
+ end
+
+ def execute
+ @queue = SizedQueue.new(1)
+
+ threads = start_migration_threads
+
+ ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: @batch_size) do |batch|
+ @queue.push(batch)
+ end
+
+ @queue.close
+
+ @logger.info("Waiting for threads to finish...")
+ threads.each(&:join)
+
+ { migrated: @migrated, errored: @errored }
+ end
+
+ def start_migration_threads
+ Array.new(@migration_threads) do
+ Thread.new do
+ while batch = @queue.pop
+ process_batch(batch)
+ end
+ end
+ end
+ end
+
+ def process_batch(batch)
+ batch.with_project_route_and_deployment.each do |metadatum|
+ project = metadatum.project
+
+ migrate_project(project)
+ end
+
+ @logger.info("#{@migrated} projects are migrated successfully, #{@errored} projects failed to be migrated")
+ end
+
+ def migrate_project(project)
+ result = nil
+ time = Benchmark.realtime do
+ result = ::Pages::MigrateLegacyStorageToDeploymentService.new(project).execute
+ end
+
+ if result[:status] == :success
+ @logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time} seconds")
+ @counters_lock.synchronize { @migrated += 1 }
+ else
+ @logger.error("project_id: #{project.id} #{project.pages_path} failed to be migrated in #{time} seconds: #{result[:message]}")
+ @counters_lock.synchronize { @errored += 1 }
+ end
+ rescue => e
+ @counters_lock.synchronize { @errored += 1 }
+ @logger.error("#{e.message} project_id: #{project&.id}")
+ Gitlab::ErrorTracking.track_exception(e, project_id: project&.id)
+ end
+ end
+end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 8590e1bc6f7..6179265149e 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1109,6 +1109,14 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: pipeline_background:ci_pipeline_artifacts_create_quality_report
+ :feature_category: :code_testing
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: pipeline_background:ci_pipeline_success_unlock_artifacts
:feature_category: :continuous_integration
:has_external_dependencies:
diff --git a/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb b/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb
new file mode 100644
index 00000000000..abdc2ddbedd
--- /dev/null
+++ b/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Ci
+ module PipelineArtifacts
+ class CreateQualityReportWorker
+ include ApplicationWorker
+
+ queue_namespace :pipeline_background
+ feature_category :code_testing
+
+ idempotent!
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
+ Ci::PipelineArtifacts::CreateQualityReportService.new.execute(pipeline)
+ end
+ end
+ end
+ end
+end
diff --git a/changelogs/unreleased/231199-yo-gitlab-ui.yml b/changelogs/unreleased/231199-yo-gitlab-ui.yml
new file mode 100644
index 00000000000..eaf132aa1cb
--- /dev/null
+++ b/changelogs/unreleased/231199-yo-gitlab-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Apply GitLab UI button styles to button in geo
+merge_request: 51777
+author: Yogi (@yo)
+type: other
diff --git a/changelogs/unreleased/remove-tmp-index-oauth-applications.yml b/changelogs/unreleased/remove-tmp-index-oauth-applications.yml
new file mode 100644
index 00000000000..ae8a4d5b66d
--- /dev/null
+++ b/changelogs/unreleased/remove-tmp-index-oauth-applications.yml
@@ -0,0 +1,5 @@
+---
+title: Remove temp index in oauth_applications table
+merge_request: 52157
+author:
+type: other
diff --git a/changelogs/unreleased/yo-remove-top-border-usage-quotos.yml b/changelogs/unreleased/yo-remove-top-border-usage-quotos.yml
new file mode 100644
index 00000000000..beb7fe11860
--- /dev/null
+++ b/changelogs/unreleased/yo-remove-top-border-usage-quotos.yml
@@ -0,0 +1,5 @@
+---
+title: Remove extra border-top on pipeline quota in the settings page
+merge_request: 52059
+author: Yogi (@yo)
+type: fixed
diff --git a/db/migrate/20210120221743_delete_oauth_applications_tmp_index.rb b/db/migrate/20210120221743_delete_oauth_applications_tmp_index.rb
new file mode 100644
index 00000000000..d29e63ba5da
--- /dev/null
+++ b/db/migrate/20210120221743_delete_oauth_applications_tmp_index.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class DeleteOauthApplicationsTmpIndex < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'tmp_index_oauth_applications_on_id_where_trusted'
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index_by_name :oauth_applications, INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :oauth_applications, :id, where: 'trusted = true', name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20210120221743 b/db/schema_migrations/20210120221743
new file mode 100644
index 00000000000..ce54788b86c
--- /dev/null
+++ b/db/schema_migrations/20210120221743
@@ -0,0 +1 @@
+4bf1d277affdfa9ee772d69cb713f49f257140fb58c40bc8659d563b4cc3de29 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 2939895ec07..2ba8b8a9ff1 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -23448,8 +23448,6 @@ CREATE UNIQUE INDEX term_agreements_unique_index ON term_agreements USING btree
CREATE INDEX tmp_idx_deduplicate_vulnerability_occurrences ON vulnerability_occurrences USING btree (project_id, report_type, location_fingerprint, primary_identifier_id, id);
-CREATE INDEX tmp_index_oauth_applications_on_id_where_trusted ON oauth_applications USING btree (id) WHERE (trusted = true);
-
CREATE INDEX tmp_index_on_vulnerabilities_non_dismissed ON vulnerabilities USING btree (id) WHERE (state <> 2);
CREATE UNIQUE INDEX uniq_pkgs_deb_grp_architectures_on_distribution_id_and_name ON packages_debian_group_architectures USING btree (distribution_id, name);
diff --git a/doc/administration/geo/replication/version_specific_updates.md b/doc/administration/geo/replication/version_specific_updates.md
index 41e9dbe677c..b7095426ad7 100644
--- a/doc/administration/geo/replication/version_specific_updates.md
+++ b/doc/administration/geo/replication/version_specific_updates.md
@@ -11,10 +11,6 @@ Check this document if it includes instructions for the version you are updating
These steps go together with the [general steps](updating_the_geo_nodes.md#general-update-steps)
for updating Geo nodes.
-## Updating to GitLab 13.8
-
-We've detected an issue with the `FetchRemove` call that is used by Geo secondaries. This causes performance issues as we execute reference transaction hooks for each updated reference. Please hold off upgrading until this is in [the 13.8.1 patch release.](https://gitlab.com/gitlab-org/gitaly/-/merge_requests/3002). More details are available [in this issue](https://gitlab.com/gitlab-org/git/-/issues/79).
-
## Updating to GitLab 13.7
We've detected an issue with the `FetchRemove` call that is used by Geo secondaries. This causes performance issues as we execute reference transaction hooks for each updated reference. Please hold off upgrading until this is in [the 13.7.5 patch release.](https://gitlab.com/gitlab-org/gitaly/-/merge_requests/3002). More details are available [in this issue](https://gitlab.com/gitlab-org/git/-/issues/79).
diff --git a/doc/administration/reference_architectures/1k_users.md b/doc/administration/reference_architectures/1k_users.md
index 161964353f5..c1344274c9a 100644
--- a/doc/administration/reference_architectures/1k_users.md
+++ b/doc/administration/reference_architectures/1k_users.md
@@ -45,7 +45,7 @@ To install GitLab for this default reference architecture, use the standard
You can also optionally configure GitLab to use an [external PostgreSQL service](../postgresql/external.md)
or an [external object storage service](../object_storage.md) for added
-performance and reliability at a reduced complexity cost.
+performance and reliability at an increased complexity cost.
## Configure Advanced Search **(STARTER ONLY)**
diff --git a/doc/development/agent/routing.md b/doc/development/agent/routing.md
index 43cc78ccdfb..4b02b65f484 100644
--- a/doc/development/agent/routing.md
+++ b/doc/development/agent/routing.md
@@ -134,90 +134,97 @@ of the `kas` receiving the request from the _external_ endpoint to retry and re-
requests. This method ensures a single central component for each request can determine
how a request is routed, rather than distributing the decision across several `kas` instances.
-### API definitions
+### Reverse gRPC tunnel
+
+This section explains how the `agentk` -> `kas` reverse gRPC tunnel is implemented.
+
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For a video overview of how some of the blocks map to code, see
+[GitLab Kubernetes Agent reverse gRPC tunnel architecture and code overview
+](https://www.youtube.com/watch?v=9pnQF76hyZc).
+
+#### High level schema
+
+In this example, `Server side of module A` exposes its API to get the `Pod` list
+on the `Public API gRPC server`. When it receives a request, it must determine
+the agent ID from it, then call the proxying code which forwards the request to
+a suitable `agentk` that can handle it.
+
+The `Agent side of module A` exposes the same API on the `Internal gRPC server`.
+When it receives the request, it needs to handle it (such as retrieving and returning
+the `Pod` list).
+
+This schema describes how reverse tunneling is handled fully transparently
+for modules, so you can add new features:
+
+```mermaid
+graph TB
+ subgraph kas
+ server-internal-grpc-server[Internal gRPC server]
+ server-api-grpc-server[Public API gRPC server]
+ server-module-a[Server side of module A]
+ server-module-b[Server side of module B]
+ end
+ subgraph agentk
+ agent-internal-grpc-server[Internal gRPC server]
+ agent-module-a[Agent side of module A]
+ agent-module-b[Agent side of module B]
+ end
+
+ agent-internal-grpc-server -- request --> agent-module-a
+ agent-internal-grpc-server -- request --> agent-module-b
+
+ server-module-a-. expose API on .-> server-internal-grpc-server
+ server-module-b-. expose API on .-> server-api-grpc-server
+
+ server-internal-grpc-server -- proxy request --> agent-internal-grpc-server
+ server-api-grpc-server -- proxy request --> agent-internal-grpc-server
+```
+
+#### Implementation schema
+
+`HandleTunnelConnection()` is called with the server-side interface of the reverse
+tunnel. It registers the connection and blocks, waiting for a request to proxy
+through the connection.
+
+`HandleIncomingConnection()` is called with the server-side interface of the incoming
+connection. It registers the connection and blocks, waiting for a matching tunnel
+to proxy the connection through.
+
+After it has two connections that match, `Connection registry` starts bi-directional
+data streaming:
-```proto
-syntax = "proto3";
-
-import "google/protobuf/timestamp.proto";
-
-message KasAddress {
- string ip = 1;
- uint32 port = 2;
-}
-
-message ConnectedAgentInfo {
- // Agent id.
- int64 id = 1;
- // Identifies a particular agentk->kas connection. Randomly generated when agent connects.
- int64 connection_id = 2;
- string version = 3;
- string commit = 4;
- // Pod namespace.
- string pod_namespace = 5;
- // Pod name.
- string pod_name = 6;
- // When the connection was established.
- google.protobuf.Timestamp connected_at = 7;
- KasAddress kas_address = 8;
- // What else do we need?
-}
-
-message KasInstanceInfo {
- string version = 1;
- string commit = 2;
- KasAddress address = 3;
- // What else do we need?
-}
-
-message ConnectedAgentsForProjectRequest {
- int64 project_id = 1;
-}
-
-message ConnectedAgentsForProjectResponse {
- // There may 0 or more agents with the same id, depending on the number of running Pods.
- repeated ConnectedAgentInfo agents = 1;
-}
-
-message ConnectedAgentsByIdRequest {
- int64 agent_id = 1;
-}
-
-message ConnectedAgentsByIdResponse {
- repeated ConnectedAgentInfo agents = 1;
-}
-
-// API for use by GitLab.
-service KasApi {
- // Connected agents for a particular configuration project.
- rpc ConnectedAgentsForProject (ConnectedAgentsForProjectRequest) returns (ConnectedAgentsForProjectResponse) {
- }
- // Connected agents for a particular agent id.
- rpc ConnectedAgentsById (ConnectedAgentsByIdRequest) returns (ConnectedAgentsByIdResponse) {
- }
- // Depends on the need, but here is the call from the example above.
- rpc GetPods (GetPodsRequest) returns (GetPodsResponse) {
- }
-}
-
-message Pod {
- string namespace = 1;
- string name = 2;
-}
-
-message GetPodsRequest {
- int64 agent_id = 1;
- int64 connection_id = 2;
-}
-
-message GetPodsResponse {
- repeated Pod pods = 1;
-}
-
-// Internal API for use by kas for kas -> kas calls.
-service KasInternal {
- // Depends on the need, but here is the call from the example above.
- rpc GetPods (GetPodsRequest) returns (GetPodsResponse) {
- }
-}
+```mermaid
+graph TB
+ subgraph kas
+ server-tunnel-module[Server tunnel module]
+ connection-registry[Connection registry]
+ server-internal-grpc-server[Internal gRPC server]
+ server-api-grpc-server[Public API gRPC server]
+ server-module-a[Server side of module A]
+ server-module-b[Server side of module B]
+ end
+ subgraph agentk
+ agent-internal-grpc-server[Internal gRPC server]
+ agent-tunnel-module[Agent tunnel module]
+ agent-module-a[Agent side of module A]
+ agent-module-b[Agent side of module B]
+ end
+
+ server-tunnel-module -- "HandleTunnelConnection()" --> connection-registry
+ server-internal-grpc-server -- "HandleIncomingConnection()" --> connection-registry
+ server-api-grpc-server -- "HandleIncomingConnection()" --> connection-registry
+ server-module-a-. expose API on .-> server-internal-grpc-server
+ server-module-b-. expose API on .-> server-api-grpc-server
+
+ agent-tunnel-module -- "establish tunnel, receive request" --> server-tunnel-module
+ agent-tunnel-module -- make request --> agent-internal-grpc-server
+ agent-internal-grpc-server -- request --> agent-module-a
+ agent-internal-grpc-server -- request --> agent-module-b
```
+
+### API definitions
+
+- [`agent_tracker/agent_tracker.proto`](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/internal/module/agent_tracker/agent_tracker.proto)
+- [`agent_tracker/rpc/rpc.proto`](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/internal/module/agent_tracker/rpc/rpc.proto)
+- [`reverse_tunnel/rpc/rpc.proto`](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/internal/module/reverse_tunnel/rpc/rpc.proto)
diff --git a/doc/development/import_project.md b/doc/development/import_project.md
index dfe6153ad45..a4917cc0c3d 100644
--- a/doc/development/import_project.md
+++ b/doc/development/import_project.md
@@ -170,7 +170,7 @@ The last option is to import a project using a Rails console:
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path,
shared: shared,
- project: project).restore
+ importable: project).restore
```
We are storing all import failures in the `import_failures` data table.
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index 39e47690264..ab0cec6e058 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -1,8 +1,8 @@
---
-redirect_to: '../user/project/issues/index.md#viewing-and-managing-issues'
+redirect_to: '../user/project/issues/index.md#view-and-manage-issues'
---
-This document was moved to [another location](../user/project/issues/index.md#viewing-and-managing-issues).
+This document was moved to [another location](../user/project/issues/index.md#view-and-manage-issues).
<!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
diff --git a/doc/user/index.md b/doc/user/index.md
index 3b2acfab34c..598c47963b5 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -43,7 +43,7 @@ GitLab is a Git-based platform that integrates a great number of essential tools
- Hosting code in repositories with version control.
- Tracking proposals for new implementations, bug reports, and feedback with a
fully featured [Issue Tracker](project/issues/index.md#issues-list).
-- Organizing and prioritizing with [Issue Boards](project/issues/index.md#issue-boards).
+- Organizing and prioritizing with [Issue Boards](project/issue_board.md).
- Reviewing code in [Merge Requests](project/merge_requests/index.md) with live-preview changes per
branch with [Review Apps](../ci/review_apps/index.md).
- Building, testing, and deploying with built-in [Continuous Integration](../ci/README.md).
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index d07905c0ead..16ef4a05cc2 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -6,33 +6,27 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Issues **(CORE)**
-Issues are the fundamental medium for collaborating on ideas and planning work in GitLab.
+Issues are the fundamental mechanism in GitLab to collaborate on ideas, solve
+problems, and plan work.
-## Overview
+Using issues, you can share and discuss proposals (both before and during their
+implementation) between you and your team, and outside collaborators.
-The GitLab issue tracker is an advanced tool for collaboratively developing ideas, solving problems,
-and planning work.
+You can use issues for many purposes, customized to your needs and workflow.
+Common use cases include:
-Issues can allow sharing and discussion of proposals before, and during,
-their implementation between:
+- Discussing the implementation of a new idea.
+- Tracking tasks and work status.
+- Accepting feature proposals, questions, support requests, or bug reports.
+- Elaborating on new code implementations.
-- You and your team.
-- Outside collaborators.
+For more information about using issues, see the
+[Always start a discussion with an issue](https://about.gitlab.com/blog/2016/03/03/start-with-an-issue/)
+GitLab blog post.
-They can also be used for a variety of other purposes, customized to your
-needs and workflow.
-
-Issues are always associated with a specific project. If you have multiple projects in a group,
-you can view all of the issues collectively at the group level.
-
-**Common use cases include:**
-
-- Discussing the implementation of a new idea
-- Tracking tasks and work status
-- Accepting feature proposals, questions, support requests, or bug reports
-- Elaborating on new code implementations
-
-See also [Always start a discussion with an issue](https://about.gitlab.com/blog/2016/03/03/start-with-an-issue/).
+Issues are always associated with a specific project. If you have multiple
+projects in a group, you can view all of the issues collectively at the group
+level.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
To learn how our Strategic Marketing department uses GitLab issues with [labels](../labels.md) and
@@ -41,63 +35,30 @@ To learn how our Strategic Marketing department uses GitLab issues with [labels]
## Parts of an issue
-Issues contain a variety of content and metadata, enabling a large range of flexibility
-in how they are used. Each issue can contain the following attributes, though not all items
-must be set.
-
-<table class="borderless-table fixed-table">
-<tr>
- <td>
- <ul>
- <li>Content</li>
- <ul>
- <li>Title</li>
- <li>Description and tasks</li>
- <li>Comments and other activity</li>
- </ul>
- <li>People</li>
- <ul>
- <li>Author</li>
- <li>Assignee(s)</li>
- </ul>
- <li>State</li>
- <ul>
- <li>State (open or closed)</li>
- <li>Health status (on track, needs attention, or at risk)</li>
- <li>Confidentiality</li>
- <li>Tasks (completed vs. outstanding)</li>
- </ul>
- </ul>
- </td>
- <td>
- <ul>
- <li>Planning and tracking</li>
- <ul>
- <li>Milestone</li>
- <li>Due date</li>
- <li>Weight</li>
- <li>Time tracking</li>
- <li>Labels</li>
- <li>Votes</li>
- <li>Reaction emoji</li>
- <li>Linked issues</li>
- <li>Assigned epic</li>
- <li>Unique issue number and URL</li>
- </ul>
- </ul>
- </td>
-</tr>
-</table>
-
-## Viewing and managing issues
-
-While you can view and manage details of an issue on the [issue page](#issue-page),
-you can also work with multiple issues at a time using:
-
-- [Issues List](#issues-list).
-- [Issue Boards](#issue-boards).
-- Issue references.
-- [Epics](#epics) **(PREMIUM)**.
+Issues have a flexible content and metadata structure. Here are some of the
+elements you can provide in an issue:
+
+- Title
+- Description and tasks
+- Comments and other activity
+- Author
+- Assignees
+- State (open or closed)
+- Health status (on track, needs attention, or at risk)
+- Confidentiality
+- Tasks (completed vs. outstanding)
+- Milestone
+- Due date
+- Weight
+- Time tracking
+- Labels
+- Votes
+- Reaction emoji
+- Linked issues
+- Assigned epic
+- Unique issue number and URL
+
+## View and manage issues
Key actions for issues include:
@@ -105,7 +66,17 @@ Key actions for issues include:
- [Moving issues](managing_issues.md#moving-issues)
- [Closing issues](managing_issues.md#closing-issues)
- [Deleting issues](managing_issues.md#deleting-issues)
-- [Promoting issues](managing_issues.md#promote-an-issue-to-an-epic) **(PREMIUM)**
+- [Promoting issues](managing_issues.md#promote-an-issue-to-an-epic)
+
+Although you can view and manage details of an issue on the [issue page](#issue-page),
+you can also work with several issues at a time by using these features:
+
+- [Issues List](#issues-list): View a list of issues in a project or group.
+- [Issue Boards](../issue_board.md): Organize issues with a project management
+ workflow for a feature or product release.
+- Issue references
+- [Epics](../../group/epics/index.md): Manage your portfolio of projects by
+ tracking groups of issues with a shared theme.
### Issue page
@@ -125,7 +96,7 @@ To enable it, you need to enable [ActionCable in-app mode](https://docs.gitlab.c
![Project Issues List view](img/project_issues_list_view.png)
-On the Issues List, you can:
+In the Issues List, you can:
- View all issues in a project when opening the Issues List from a project context.
- View all issues in a groups's projects when opening the Issues List from a group context.
@@ -154,31 +125,12 @@ This feature might not be available to you. Check the **version history** note a
In a group, the sidebar displays the total count of open issues and this value is cached if higher
than 1000. The cached value is rounded to thousands (or millions) and updated every 24 hours.
-### Issue boards
-
-![Issue board](img/issue_board.png)
-
-[Issue boards](../issue_board.md) are Kanban boards with columns that display issues based on their
-labels or their assignees**(PREMIUM)**. They offer the flexibility to manage issues using
-highly customizable workflows.
-
-You can reorder issues in the column. If you drag an issue card to another column, its
-associated label or assignee is changed to match that of the new column. The entire
-board can also be filtered to only include issues from a certain milestone or an overarching
-label.
-
### Design Management
With [Design Management](design_management.md), you can upload design
assets to issues and view them all together for sharing and
collaboration with your team.
-### Epics **(PREMIUM)**
-
-[Epics](../../group/epics/index.md) let you manage your portfolio of projects more
-efficiently and with less effort. Epics track groups of issues that share a theme, across
-projects and milestones.
-
### Related issues
You can mark two issues as related, so that when viewing one, the other is always
diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md
index 0bded97f189..5218bb57bca 100644
--- a/doc/user/project/issues/issue_data_and_actions.md
+++ b/doc/user/project/issues/issue_data_and_actions.md
@@ -129,7 +129,7 @@ element. Due dates can be changed as many times as needed.
### Labels
Categorize issues by giving them [labels](../labels.md). They help to organize workflows,
-and they enable you to work with the [GitLab Issue Board](index.md#issue-boards).
+and they enable you to work with the [GitLab Issue Board](../issue_board.md).
Group Labels, which allow you to use the same labels for all projects in the same
group, can also be given to issues. They work exactly the same, but are immediately
diff --git a/lib/gitlab/import_export/design_repo_restorer.rb b/lib/gitlab/import_export/design_repo_restorer.rb
index a702c58a7c2..e093b4b0697 100644
--- a/lib/gitlab/import_export/design_repo_restorer.rb
+++ b/lib/gitlab/import_export/design_repo_restorer.rb
@@ -3,10 +3,11 @@
module Gitlab
module ImportExport
class DesignRepoRestorer < RepoRestorer
- def initialize(project:, shared:, path_to_bundle:)
- super(project: project, shared: shared, path_to_bundle: path_to_bundle)
+ extend ::Gitlab::Utils::Override
- @repository = project.design_repository
+ override :repository
+ def repository
+ @repository ||= importable.design_repository
end
# `restore` method is handled in super class
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index 789249c7d91..390909efe36 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -75,19 +75,19 @@ module Gitlab
def repo_restorer
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path,
shared: shared,
- project: project)
+ importable: project)
end
def wiki_restorer
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path,
shared: shared,
- project: ProjectWiki.new(project))
+ importable: ProjectWiki.new(project))
end
def design_repo_restorer
Gitlab::ImportExport::DesignRepoRestorer.new(path_to_bundle: design_repo_path,
shared: shared,
- project: project)
+ importable: project)
end
def uploads_restorer
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index f808e30bd6e..7701916a855 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -5,10 +5,12 @@ module Gitlab
class RepoRestorer
include Gitlab::ImportExport::CommandLineUtil
- def initialize(project:, shared:, path_to_bundle:)
- @repository = project.repository
+ attr_reader :importable
+
+ def initialize(importable:, shared:, path_to_bundle:)
@path_to_bundle = path_to_bundle
@shared = shared
+ @importable = importable
end
def restore
@@ -22,9 +24,13 @@ module Gitlab
false
end
+ def repository
+ @repository ||= importable.repository
+ end
+
private
- attr_accessor :repository, :path_to_bundle, :shared
+ attr_accessor :path_to_bundle, :shared
def ensure_repository_does_not_exist!
if repository.exists?
diff --git a/lib/tasks/gitlab/pages.rake b/lib/tasks/gitlab/pages.rake
index 107e0d08b70..59c57a66928 100644
--- a/lib/tasks/gitlab/pages.rake
+++ b/lib/tasks/gitlab/pages.rake
@@ -6,37 +6,20 @@ namespace :gitlab do
task migrate_legacy_storage: :gitlab_environment do
logger = Logger.new(STDOUT)
logger.info('Starting to migrate legacy pages storage to zip deployments')
- projects_migrated = 0
- projects_errored = 0
- ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: 10) do |batch|
- batch.preload(project: [:namespace, :route, pages_metadatum: :pages_deployment]).each do |metadatum|
- project = metadatum.project
+ result = ::Pages::MigrateFromLegacyStorageService.new(logger, migration_threads, batch_size).execute
- result = nil
- time = Benchmark.realtime do
- result = ::Pages::MigrateLegacyStorageToDeploymentService.new(project).execute
- end
-
- if result[:status] == :success
- logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time} seconds")
- projects_migrated += 1
- else
- logger.error("project_id: #{project.id} #{project.pages_path} failed to be migrated in #{time} seconds: #{result[:message]}")
- projects_errored += 1
- end
- rescue => e
- projects_errored += 1
- logger.error("#{e.message} project_id: #{project&.id}")
- Gitlab::ErrorTracking.track_exception(e, project_id: project&.id)
- end
+ logger.info("A total of #{result[:migrated] + result[:errored]} projects were processed.")
+ logger.info("- The #{result[:migrated]} projects migrated successfully")
+ logger.info("- The #{result[:errored]} projects failed to be migrated")
+ end
- logger.info("#{projects_migrated} projects are migrated successfully, #{projects_errored} projects failed to be migrated")
- end
+ def migration_threads
+ ENV.fetch('PAGES_MIGRATION_THREADS', '3').to_i
+ end
- logger.info("A total of #{projects_migrated + projects_errored} projects were processed.")
- logger.info("- The #{projects_migrated} projects migrated successfully")
- logger.info("- The #{projects_errored} projects failed to be migrated")
+ def batch_size
+ ENV.fetch('PAGES_MIGRATION_BATCH_SIZE', '10').to_i
end
end
end
diff --git a/package.json b/package.json
index 2b3aae4aa24..a50610489a8 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.179.0",
"@gitlab/tributejs": "1.0.0",
- "@gitlab/ui": "26.1.0",
+ "@gitlab/ui": "26.3.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-4",
"@rails/ujs": "^6.0.3-4",
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
index a2e01398c94..e722710ee00 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
@@ -4,7 +4,7 @@ module QA
RSpec.describe 'Manage', :smoke do
describe 'Project creation' do
it 'user creates a new project',
- testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/429' do
+ testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1234' do
Flow::Login.sign_in
created_project = Resource::Project.fabricate_via_browser_ui! do |project|
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
new file mode 100644
index 00000000000..78bc37233c2
--- /dev/null
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -0,0 +1,138 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDropdownDivider } from '@gitlab/ui';
+import AdminUserActions from '~/admin/users/components/user_actions.vue';
+import { generateUserPaths } from '~/admin/users/utils';
+
+import { users, paths } from '../mock_data';
+
+const BLOCK = 'block';
+const EDIT = 'edit';
+const LDAP = 'ldapBlocked';
+const DELETE = 'delete';
+const DELETE_WITH_CONTRIBUTIONS = 'deleteWithContributions';
+
+describe('AdminUserActions component', () => {
+ let wrapper;
+ const user = users[0];
+ const userPaths = generateUserPaths(paths, user.username);
+
+ const findEditButton = () => wrapper.find('[data-testid="edit"]');
+ const findActionsDropdown = () => wrapper.find('[data-testid="actions"');
+ const findDropdownDivider = () => wrapper.find(GlDropdownDivider);
+
+ const initComponent = ({ actions = [] } = {}) => {
+ wrapper = shallowMount(AdminUserActions, {
+ propsData: {
+ user: {
+ ...user,
+ actions,
+ },
+ paths,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('edit button', () => {
+ describe('when the user has an edit action attached', () => {
+ beforeEach(() => {
+ initComponent({ actions: [EDIT] });
+ });
+
+ it('renders the edit button linking to the user edit path', () => {
+ expect(findEditButton().exists()).toBe(true);
+ expect(findEditButton().attributes('href')).toBe(userPaths.edit);
+ });
+ });
+
+ describe('when there is no edit action attached to the user', () => {
+ beforeEach(() => {
+ initComponent({ actions: [] });
+ });
+
+ it('does not render the edit button linking to the user edit path', () => {
+ expect(findEditButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('actions dropdown', () => {
+ describe('when there are actions', () => {
+ const actions = [EDIT, BLOCK];
+
+ beforeEach(() => {
+ initComponent({ actions });
+ });
+
+ it('renders the actions dropdown', () => {
+ expect(findActionsDropdown().exists()).toBe(true);
+ });
+
+ it.each(actions)('renders a dropdown item for %s', (action) => {
+ const dropdownAction = wrapper.find(`[data-testid="${action}"]`);
+ expect(dropdownAction.exists()).toBe(true);
+ expect(dropdownAction.attributes('href')).toBe(userPaths[action]);
+ });
+
+ describe('when there is a LDAP action', () => {
+ beforeEach(() => {
+ initComponent({ actions: [LDAP] });
+ });
+
+ it('renders the LDAP dropdown item without a link', () => {
+ const dropdownAction = wrapper.find(`[data-testid="${LDAP}"]`);
+ expect(dropdownAction.exists()).toBe(true);
+ expect(dropdownAction.attributes('href')).toBe(undefined);
+ });
+ });
+
+ describe('when there is a delete action', () => {
+ const deleteActions = [DELETE, DELETE_WITH_CONTRIBUTIONS];
+
+ beforeEach(() => {
+ initComponent({ actions: [BLOCK, ...deleteActions] });
+ });
+
+ it('renders a dropdown divider', () => {
+ expect(findDropdownDivider().exists()).toBe(true);
+ });
+
+ it('only renders delete dropdown items for actions containing the word "delete"', () => {
+ const { length } = wrapper.findAll(`[data-testid*="delete-"]`);
+ expect(length).toBe(deleteActions.length);
+ });
+
+ it.each(deleteActions)('renders a delete dropdown item for %s', (action) => {
+ const deleteAction = wrapper.find(`[data-testid="delete-${action}"]`);
+ expect(deleteAction.exists()).toBe(true);
+ expect(deleteAction.attributes('href')).toBe(userPaths[action]);
+ });
+ });
+
+ describe('when there are no delete actions', () => {
+ it('does not render a dropdown divider', () => {
+ expect(findDropdownDivider().exists()).toBe(false);
+ });
+
+ it('does not render a delete dropdown item', () => {
+ const anyDeleteAction = wrapper.find(`[data-testid*="delete-"]`);
+ expect(anyDeleteAction.exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when there are no actions', () => {
+ beforeEach(() => {
+ initComponent({ actions: [] });
+ });
+
+ it('does not render the actions dropdown', () => {
+ expect(findActionsDropdown().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/users/components/user_date_spec.js b/spec/frontend/admin/users/components/user_date_spec.js
new file mode 100644
index 00000000000..6428b10059b
--- /dev/null
+++ b/spec/frontend/admin/users/components/user_date_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+
+import UserDate from '~/admin/users/components/user_date.vue';
+import { users } from '../mock_data';
+
+const mockDate = users[0].createdAt;
+
+describe('FormatDate component', () => {
+ let wrapper;
+
+ const initComponent = (props = {}) => {
+ wrapper = shallowMount(UserDate, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it.each`
+ date | output
+ ${mockDate} | ${'13 Nov, 2020'}
+ ${null} | ${'Never'}
+ ${undefined} | ${'Never'}
+ `('renders $date as $output', ({ date, output }) => {
+ initComponent({ date });
+
+ expect(wrapper.text()).toBe(output);
+ });
+});
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
index b79d2d4d39d..ac48542cec5 100644
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ b/spec/frontend/admin/users/components/users_table_spec.js
@@ -3,6 +3,9 @@ import { mount } from '@vue/test-utils';
import AdminUsersTable from '~/admin/users/components/users_table.vue';
import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
+import AdminUserDate from '~/admin/users/components/user_date.vue';
+import AdminUserActions from '~/admin/users/components/user_actions.vue';
+
import { users, paths } from '../mock_data';
describe('AdminUsersTable component', () => {
@@ -39,18 +42,21 @@ describe('AdminUsersTable component', () => {
initComponent();
});
- it.each`
- key | label
- ${'name'} | ${'Name'}
- ${'projectsCount'} | ${'Projects'}
- ${'createdAt'} | ${'Created on'}
- ${'lastActivityOn'} | ${'Last activity'}
- `('renders users.$key in column $label', ({ key, label }) => {
- expect(getCellByLabel(0, label).text()).toContain(`${user[key]}`);
+ it('renders the projects count', () => {
+ expect(getCellByLabel(0, 'Projects').text()).toContain(`${user.projectsCount}`);
});
- it('renders an AdminUserAvatar component', () => {
- expect(getCellByLabel(0, 'Name').find(AdminUserAvatar).exists()).toBe(true);
+ it('renders the user actions', () => {
+ expect(wrapper.find(AdminUserActions).exists()).toBe(true);
+ });
+
+ it.each`
+ component | label
+ ${AdminUserAvatar} | ${'Name'}
+ ${AdminUserDate} | ${'Created on'}
+ ${AdminUserDate} | ${'Last activity'}
+ `('renders the component for column $label', ({ component, label }) => {
+ expect(getCellByLabel(0, label).find(component).exists()).toBe(true);
});
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 90222f0f718..18be88a0b8b 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1045,4 +1045,12 @@ describe('common_utils', () => {
expect(commonUtils.getDashPath('/some/url')).toEqual(null);
});
});
+
+ describe('convertArrayToCamelCase', () => {
+ it('returns a new array with snake_case string elements converted camelCase', () => {
+ const result = commonUtils.convertArrayToCamelCase(['hello', 'hello_world']);
+
+ expect(result).toEqual(['hello', 'helloWorld']);
+ });
+ });
});
diff --git a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
index a7fd741834b..6680f4e7a03 100644
--- a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Gitlab::ImportExport::DesignRepoRestorer do
let(:restorer) do
described_class.new(path_to_bundle: bundle_path,
shared: shared,
- project: project)
+ importable: project)
end
before do
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index 62b3c0f95e3..65c28a8b8a2 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'forked project import' do
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
let(:repo_restorer) do
- Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, project: project)
+ Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, importable: project)
end
let!(:merge_request) do
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
index 75db3167ebc..20f0f6af6f3 100644
--- a/spec/lib/gitlab/import_export/importer_spec.rb
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -69,8 +69,8 @@ RSpec.describe Gitlab::ImportExport::Importer do
repo_path = File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename)
restorer = double(Gitlab::ImportExport::RepoRestorer)
- expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: repo_path, shared: shared, project: project).and_return(restorer)
- expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, project: ProjectWiki.new(project)).and_return(restorer)
+ expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: repo_path, shared: shared, importable: project).and_return(restorer)
+ expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, importable: ProjectWiki.new(project)).and_return(restorer)
expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).and_call_original
expect(restorer).to receive(:restore).and_return(true).twice
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index ceb34893069..fe43a23e242 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
let(:bundler) { Gitlab::ImportExport::RepoSaver.new(exportable: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
- subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: project) }
+ subject { described_class.new(path_to_bundle: bundle_path, shared: shared, importable: project) }
after do
Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
@@ -65,7 +65,7 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(exportable: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename) }
- subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: ProjectWiki.new(project)) }
+ subject { described_class.new(path_to_bundle: bundle_path, shared: shared, importable: ProjectWiki.new(project)) }
after do
Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index af6d7ab4250..79eb016bc41 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -3522,7 +3522,19 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
context 'when pipeline status is success' do
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
- it { expect(subject).to be_truthy }
+ it 'can generate a codequality report' do
+ expect(subject).to be_truthy
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(codequality_mr_diff: false)
+ end
+
+ it 'can not generate a codequality report' do
+ expect(subject).to be_falsey
+ end
+ end
end
end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index d7cd13edec8..61dbcaae77d 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -7,18 +7,8 @@ RSpec.describe PipelineEntity do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
- let(:request) { double('request') }
-
- before do
- stub_not_protect_default_branch
-
- allow(request).to receive(:current_user).and_return(user)
- allow(request).to receive(:project).and_return(project)
- end
-
- let(:entity) do
- described_class.represent(pipeline, request: request)
- end
+ let(:request) { double('request', current_user: user) }
+ let(:entity) { described_class.represent(pipeline, request: request) }
describe '#as_json' do
subject { entity.as_json }
@@ -54,70 +44,72 @@ RSpec.describe PipelineEntity do
end
end
- context 'when pipeline is retryable' do
- let(:project) { create(:project) }
-
- let(:pipeline) do
- create(:ci_pipeline, status: :success, project: project)
- end
-
+ context 'when default branch not protected' do
before do
- create(:ci_build, :failed, pipeline: pipeline)
+ stub_not_protect_default_branch
end
- it 'does not serialize stage builds' do
- subject.with_indifferent_access.dig(:details, :stages, 0).tap do |stage|
- expect(stage).not_to include(:groups, :latest_statuses, :retries)
+ context 'when pipeline is retryable' do
+ let_it_be(:pipeline) do
+ create(:ci_pipeline, status: :success, project: project)
end
- end
- context 'user has ability to retry pipeline' do
before do
- project.add_developer(user)
- end
-
- it 'contains retry path' do
- expect(subject[:retry_path]).to be_present
+ create(:ci_build, :failed, pipeline: pipeline)
end
- end
- context 'user does not have ability to retry pipeline' do
- it 'does not contain retry path' do
- expect(subject).not_to have_key(:retry_path)
+ it 'does not serialize stage builds' do
+ subject.with_indifferent_access.dig(:details, :stages, 0).tap do |stage|
+ expect(stage).not_to include(:groups, :latest_statuses, :retries)
+ end
end
- end
- end
- context 'when pipeline is cancelable' do
- let(:project) { create(:project) }
+ context 'user has ability to retry pipeline' do
+ before do
+ project.add_developer(user)
+ end
- let(:pipeline) do
- create(:ci_pipeline, status: :running, project: project)
- end
+ it 'contains retry path' do
+ expect(subject[:retry_path]).to be_present
+ end
+ end
- before do
- create(:ci_build, :pending, pipeline: pipeline)
+ context 'user does not have ability to retry pipeline' do
+ it 'does not contain retry path' do
+ expect(subject).not_to have_key(:retry_path)
+ end
+ end
end
- it 'does not serialize stage builds' do
- subject.with_indifferent_access.dig(:details, :stages, 0).tap do |stage|
- expect(stage).not_to include(:groups, :latest_statuses, :retries)
+ context 'when pipeline is cancelable' do
+ let_it_be(:pipeline) do
+ create(:ci_pipeline, status: :running, project: project)
end
- end
- context 'user has ability to cancel pipeline' do
before do
- project.add_developer(user)
+ create(:ci_build, :pending, pipeline: pipeline)
end
- it 'contains cancel path' do
- expect(subject[:cancel_path]).to be_present
+ it 'does not serialize stage builds' do
+ subject.with_indifferent_access.dig(:details, :stages, 0).tap do |stage|
+ expect(stage).not_to include(:groups, :latest_statuses, :retries)
+ end
end
- end
- context 'user does not have ability to cancel pipeline' do
- it 'does not contain cancel path' do
- expect(subject).not_to have_key(:cancel_path)
+ context 'user has ability to cancel pipeline' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'contains cancel path' do
+ expect(subject[:cancel_path]).to be_present
+ end
+ end
+
+ context 'user does not have ability to cancel pipeline' do
+ it 'does not contain cancel path' do
+ expect(subject).not_to have_key(:cancel_path)
+ end
end
end
end
@@ -133,7 +125,6 @@ RSpec.describe PipelineEntity do
end
context 'user does not have ability to delete pipeline' do
- let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'does not contain delete path' do
@@ -167,79 +158,85 @@ RSpec.describe PipelineEntity do
end
end
- context 'when pipeline is detached merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:project) { merge_request.target_project }
- let(:pipeline) { merge_request.pipelines_for_merge_request.first }
-
- it 'makes detached flag true' do
- expect(subject[:flags][:detached_merge_request_pipeline]).to be_truthy
+ context 'when request has a project' do
+ before do
+ allow(request).to receive(:project).and_return(project)
end
- it 'does not expose source sha and target sha' do
- expect(subject[:source_sha]).to be_nil
- expect(subject[:target_sha]).to be_nil
- end
+ context 'when pipeline is detached merge request pipeline' do
+ let_it_be(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:project) { merge_request.target_project }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
- context 'when user is a developer' do
- before do
- project.add_developer(user)
+ it 'makes detached flag true' do
+ expect(subject[:flags][:detached_merge_request_pipeline]).to be_truthy
end
- it 'has merge request information' do
- expect(subject[:merge_request][:iid]).to eq(merge_request.iid)
+ it 'does not expose source sha and target sha' do
+ expect(subject[:source_sha]).to be_nil
+ expect(subject[:target_sha]).to be_nil
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'has merge request information' do
+ expect(subject[:merge_request][:iid]).to eq(merge_request.iid)
- expect(project_merge_request_path(project, merge_request))
- .to include(subject[:merge_request][:path])
+ expect(project_merge_request_path(project, merge_request))
+ .to include(subject[:merge_request][:path])
- expect(subject[:merge_request][:title]).to eq(merge_request.title)
+ expect(subject[:merge_request][:title]).to eq(merge_request.title)
- expect(subject[:merge_request][:source_branch])
- .to eq(merge_request.source_branch)
+ expect(subject[:merge_request][:source_branch])
+ .to eq(merge_request.source_branch)
- expect(project_commits_path(project, merge_request.source_branch))
- .to include(subject[:merge_request][:source_branch_path])
+ expect(project_commits_path(project, merge_request.source_branch))
+ .to include(subject[:merge_request][:source_branch_path])
- expect(subject[:merge_request][:target_branch])
- .to eq(merge_request.target_branch)
+ expect(subject[:merge_request][:target_branch])
+ .to eq(merge_request.target_branch)
- expect(project_commits_path(project, merge_request.target_branch))
- .to include(subject[:merge_request][:target_branch_path])
+ expect(project_commits_path(project, merge_request.target_branch))
+ .to include(subject[:merge_request][:target_branch_path])
+ end
end
- end
- context 'when user is an external user' do
- it 'has no merge request information' do
- expect(subject[:merge_request]).to be_nil
+ context 'when user is an external user' do
+ it 'has no merge request information' do
+ expect(subject[:merge_request]).to be_nil
+ end
end
end
- end
- context 'when pipeline is merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_merge_request_pipeline, merge_sha: 'abc') }
- let(:project) { merge_request.target_project }
- let(:pipeline) { merge_request.pipelines_for_merge_request.first }
+ context 'when pipeline is merge request pipeline' do
+ let_it_be(:merge_request) { create(:merge_request, :with_merge_request_pipeline, merge_sha: 'abc') }
+ let(:project) { merge_request.target_project }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
- it 'makes detached flag false' do
- expect(subject[:flags][:detached_merge_request_pipeline]).to be_falsy
- end
+ it 'makes detached flag false' do
+ expect(subject[:flags][:detached_merge_request_pipeline]).to be_falsy
+ end
- it 'makes atached flag true' do
- expect(subject[:flags][:merge_request_pipeline]).to be_truthy
- end
+ it 'makes atached flag true' do
+ expect(subject[:flags][:merge_request_pipeline]).to be_truthy
+ end
- it 'exposes source sha and target sha' do
- expect(subject[:source_sha]).to be_present
- expect(subject[:target_sha]).to be_present
- end
+ it 'exposes source sha and target sha' do
+ expect(subject[:source_sha]).to be_present
+ expect(subject[:target_sha]).to be_present
+ end
- it 'exposes merge request event type' do
- expect(subject[:merge_request_event_type]).to be_present
+ it 'exposes merge request event type' do
+ expect(subject[:merge_request_event_type]).to be_present
+ end
end
end
context 'when pipeline has failed builds' do
- let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let_it_be(:pipeline) { create(:ci_pipeline, user: user) }
let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
let_it_be(:failed_1) { create(:ci_build, :failed, pipeline: pipeline) }
let_it_be(:failed_2) { create(:ci_build, :failed, pipeline: pipeline) }
diff --git a/spec/services/ci/pipeline_artifacts/create_quality_report_service_spec.rb b/spec/services/ci/pipeline_artifacts/create_quality_report_service_spec.rb
new file mode 100644
index 00000000000..6cb88d1402e
--- /dev/null
+++ b/spec/services/ci/pipeline_artifacts/create_quality_report_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::PipelineArtifacts::CreateQualityReportService do
+ describe '#execute' do
+ subject(:pipeline_artifact) { described_class.new.execute(pipeline) }
+
+ context 'when pipeline has codequality reports' do
+ let(:project) { create(:project, :repository) }
+
+ describe 'pipeline completed status' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :result) do
+ :success | 1
+ :failed | 1
+ :canceled | 1
+ :skipped | 1
+ end
+
+ with_them do
+ let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, status: status, project: project) }
+
+ it 'creates a pipeline artifact' do
+ expect { pipeline_artifact }.to change(Ci::PipelineArtifact, :count).by(result)
+ end
+
+ it 'persists the default file name' do
+ expect(pipeline_artifact.file.filename).to eq('code_quality.json')
+ end
+
+ it 'sets expire_at to 1 week' do
+ freeze_time do
+ expect(pipeline_artifact.expire_at).to eq(1.week.from_now)
+ end
+ end
+ end
+ end
+
+ context 'when pipeline artifact has already been created' do
+ let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) }
+
+ it 'does not persist the same artifact twice' do
+ 2.times { described_class.new.execute(pipeline) }
+
+ expect(Ci::PipelineArtifact.count).to eq(1)
+ end
+ end
+ end
+
+ context 'when pipeline is not completed and codequality report does not exist' do
+ let(:pipeline) { create(:ci_pipeline, :running) }
+
+ it 'does not persist data' do
+ pipeline_artifact
+
+ expect(Ci::PipelineArtifact.count).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
new file mode 100644
index 00000000000..5d335143719
--- /dev/null
+++ b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Pages::MigrateFromLegacyStorageService do
+ let(:service) { described_class.new(Rails.logger, 3, 10) }
+
+ it 'does not try to migrate pages if pages are not deployed' do
+ expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
+
+ expect(service.execute).to eq(migrated: 0, errored: 0)
+ end
+
+ it 'uses multiple threads' do
+ projects = create_list(:project, 20)
+ projects.each do |project|
+ project.mark_pages_as_deployed
+
+ FileUtils.mkdir_p File.join(project.pages_path, "public")
+ File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
+ f.write("Hello!")
+ end
+ end
+
+ service = described_class.new(Rails.logger, 3, 2)
+
+ threads = Concurrent::Set.new
+
+ expect(service).to receive(:migrate_project).exactly(20).times.and_wrap_original do |m, *args|
+ threads.add(Thread.current)
+
+ # sleep to be 100% certain that once thread can't consume all the queue
+ # it works without it, but I want to avoid making this test flaky
+ sleep(0.01)
+
+ m.call(*args)
+ end
+
+ expect(service.execute).to eq(migrated: 20, errored: 0)
+ expect(threads.length).to eq(3)
+ end
+
+ context 'when pages are marked as deployed' do
+ let(:project) { create(:project) }
+
+ before do
+ project.mark_pages_as_deployed
+ end
+
+ context 'when pages directory does not exist' do
+ it 'tries to migrate the project, but does not crash' do
+ expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ expect(service.execute).to eq(migrated: 0, errored: 1)
+ end
+ end
+
+ context 'when pages directory exists on disk' do
+ before do
+ FileUtils.mkdir_p File.join(project.pages_path, "public")
+ File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
+ f.write("Hello!")
+ end
+ end
+
+ it 'migrates pages projects without deployments' do
+ expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ expect do
+ expect(service.execute).to eq(migrated: 1, errored: 0)
+ end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil)
+ end
+
+ context 'when deployed already exists for the project' do
+ before do
+ deployment = create(:pages_deployment, project: project)
+ project.set_first_pages_deployment!(deployment)
+ end
+
+ it 'does not try to migrate project' do
+ expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
+
+ expect(service.execute).to eq(migrated: 0, errored: 0)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/pages_rake_spec.rb b/spec/tasks/gitlab/pages_rake_spec.rb
index 76808f52890..9c26d3d73c8 100644
--- a/spec/tasks/gitlab/pages_rake_spec.rb
+++ b/spec/tasks/gitlab/pages_rake_spec.rb
@@ -9,59 +9,31 @@ RSpec.describe 'gitlab:pages:migrate_legacy_storagerake task' do
subject { run_rake_task('gitlab:pages:migrate_legacy_storage') }
- let(:project) { create(:project) }
-
- it 'does not try to migrate pages if pages are not deployed' do
- expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
+ it 'calls migration service' do
+ expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, 3, 10) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
subject
end
- context 'when pages are marked as deployed' do
- before do
- project.mark_pages_as_deployed
- end
-
- context 'when pages directory does not exist' do
- it 'tries to migrate the project, but does not crash' do
- expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project) do |service|
- expect(service).to receive(:execute).and_call_original
- end
+ it 'uses PAGES_MIGRATION_THREADS environment variable' do
+ stub_env('PAGES_MIGRATION_THREADS', '5')
- subject
- end
+ expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, 5, 10) do |service|
+ expect(service).to receive(:execute).and_call_original
end
- context 'when pages directory exists on disk' do
- before do
- FileUtils.mkdir_p File.join(project.pages_path, "public")
- File.open(File.join(project.pages_path, "public/index.html"), "w") do |f|
- f.write("Hello!")
- end
- end
-
- it 'migrates pages projects without deployments' do
- expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project) do |service|
- expect(service).to receive(:execute).and_call_original
- end
-
- expect do
- subject
- end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil)
- end
-
- context 'when deployed already exists for the project' do
- before do
- deployment = create(:pages_deployment, project: project)
- project.set_first_pages_deployment!(deployment)
- end
+ subject
+ end
- it 'does not try to migrate project' do
- expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
+ it 'uses PAGES_MIGRATION_BATCH_SIZE environment variable' do
+ stub_env('PAGES_MIGRATION_BATCH_SIZE', '100')
- subject
- end
- end
+ expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, 3, 100) do |service|
+ expect(service).to receive(:execute).and_call_original
end
+
+ subject
end
end
diff --git a/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb b/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb
new file mode 100644
index 00000000000..6755c93ae47
--- /dev/null
+++ b/spec/workers/ci/pipeline_artifacts/create_quality_report_worker_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::PipelineArtifacts::CreateQualityReportWorker do
+ describe '#perform' do
+ subject { described_class.new.perform(pipeline_id) }
+
+ context 'when pipeline exists' do
+ let(:pipeline) { create(:ci_pipeline, :with_codequality_reports) }
+ let(:pipeline_id) { pipeline.id }
+
+ it 'calls pipeline codequality report service' do
+ expect_next_instance_of(::Ci::PipelineArtifacts::CreateQualityReportService) do |quality_report_service|
+ expect(quality_report_service).to receive(:execute)
+ end
+
+ subject
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { pipeline_id }
+
+ it 'creates a pipeline artifact' do
+ expect { subject }.to change { pipeline.pipeline_artifacts.count }.by(1)
+ end
+ end
+ end
+
+ context 'when pipeline does not exist' do
+ let(:pipeline_id) { non_existing_record_id }
+
+ it 'does not call pipeline codequality report service' do
+ expect(Ci::PipelineArtifacts::CreateQualityReportService).not_to receive(:execute)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index c8e03828970..6795f0d5a23 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -876,10 +876,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
-"@gitlab/ui@26.1.0":
- version "26.1.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-26.1.0.tgz#c00b221d62b6ad7505bb0025e9becad523e3e00a"
- integrity sha512-strCuRmmVKoOzh8Tlv8AyBvPsqY5l3FOeaySxzykZXS3lKt0iYoMPUv4iDwlg5E9J/1f91xaWryvLAWBV8qWHw==
+"@gitlab/ui@26.3.0":
+ version "26.3.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-26.3.0.tgz#e25b2c67b2df92879711670ce53b26a73cd06f2a"
+ integrity sha512-WRSOp3WJkpxbRgJHq5mfCaVl7s2F6iGYnoMCAfbkt9rG4oLphtF7XWVIm/sVMnnVeqHonIaVJmsLi9rCQdxBzQ==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"