Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7e81076708
commit
852877d997
50 changed files with 1065 additions and 414 deletions
|
@ -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
|
||||
|
||||
|
|
115
app/assets/javascripts/admin/users/components/user_actions.vue
Normal file
115
app/assets/javascripts/admin/users/components/user_actions.vue
Normal file
|
@ -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>
|
29
app/assets/javascripts/admin/users/components/user_date.vue
Normal file
29
app/assets/javascripts/admin/users/components/user_date.vue
Normal file
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export const USER_AVATAR_SIZE = 32;
|
||||
|
||||
export const SHORT_DATE_FORMAT = 'd mmm, yyyy';
|
||||
|
|
7
app/assets/javascripts/admin/users/utils.js
Normal file
7
app/assets/javascripts/admin/users/utils.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const generateUserPaths = (paths, id) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(paths).map(([action, genericPath]) => {
|
||||
return [action, genericPath.replace('id', id)];
|
||||
}),
|
||||
);
|
||||
};
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
71
app/services/pages/migrate_from_legacy_storage_service.rb
Normal file
71
app/services/pages/migrate_from_legacy_storage_service.rb
Normal file
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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
|
5
changelogs/unreleased/231199-yo-gitlab-ui.yml
Normal file
5
changelogs/unreleased/231199-yo-gitlab-ui.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Apply GitLab UI button styles to button in geo
|
||||
merge_request: 51777
|
||||
author: Yogi (@yo)
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove temp index in oauth_applications table
|
||||
merge_request: 52157
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove extra border-top on pipeline quota in the settings page
|
||||
merge_request: 52059
|
||||
author: Yogi (@yo)
|
||||
type: fixed
|
|
@ -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
|
1
db/schema_migrations/20210120221743
Normal file
1
db/schema_migrations/20210120221743
Normal file
|
@ -0,0 +1 @@
|
|||
4bf1d277affdfa9ee772d69cb713f49f257140fb58c40bc8659d563b4cc3de29
|
|
@ -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);
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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)**
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
### 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:
|
||||
|
||||
```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
|
||||
|
||||
```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) {
|
||||
}
|
||||
}
|
||||
```
|
||||
- [`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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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.
|
||||
Issues have a flexible content and metadata structure. Here are some of the
|
||||
elements you can provide in an issue:
|
||||
|
||||
<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>
|
||||
- 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
|
||||
|
||||
## 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)**.
|
||||
## 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
||||
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
|
||||
def migration_threads
|
||||
ENV.fetch('PAGES_MIGRATION_THREADS', '3').to_i
|
||||
end
|
||||
|
||||
logger.info("#{projects_migrated} projects are migrated successfully, #{projects_errored} projects failed to be migrated")
|
||||
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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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|
|
||||
|
|
138
spec/frontend/admin/users/components/user_actions_spec.js
Normal file
138
spec/frontend/admin/users/components/user_actions_spec.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
34
spec/frontend/admin/users/components/user_date_spec.js
Normal file
34
spec/frontend/admin/users/components/user_date_spec.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
create(:ci_build, :failed, pipeline: pipeline)
|
||||
end
|
||||
|
||||
it 'contains retry path' do
|
||||
expect(subject[:retry_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
|
||||
|
||||
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
|
||||
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)
|
||||
end
|
||||
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)
|
||||
context 'when pipeline is cancelable' do
|
||||
let_it_be(:pipeline) do
|
||||
create(:ci_pipeline, status: :running, project: project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline is cancelable' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
let(:pipeline) do
|
||||
create(:ci_pipeline, status: :running, project: project)
|
||||
end
|
||||
|
||||
before do
|
||||
create(:ci_build, :pending, pipeline: pipeline)
|
||||
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)
|
||||
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
|
||||
|
||||
expect(project_merge_request_path(project, merge_request))
|
||||
.to include(subject[:merge_request][:path])
|
||||
context 'when user is a developer' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
expect(subject[:merge_request][:title]).to eq(merge_request.title)
|
||||
it 'has merge request information' do
|
||||
expect(subject[:merge_request][:iid]).to eq(merge_request.iid)
|
||||
|
||||
expect(subject[:merge_request][:source_branch])
|
||||
.to eq(merge_request.source_branch)
|
||||
expect(project_merge_request_path(project, merge_request))
|
||||
.to include(subject[:merge_request][:path])
|
||||
|
||||
expect(project_commits_path(project, merge_request.source_branch))
|
||||
.to include(subject[:merge_request][:source_branch_path])
|
||||
expect(subject[:merge_request][:title]).to eq(merge_request.title)
|
||||
|
||||
expect(subject[:merge_request][:target_branch])
|
||||
.to eq(merge_request.target_branch)
|
||||
expect(subject[:merge_request][:source_branch])
|
||||
.to eq(merge_request.source_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.source_branch))
|
||||
.to include(subject[:merge_request][:source_branch_path])
|
||||
|
||||
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])
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
context 'when user is an external user' do
|
||||
it 'has no merge request information' do
|
||||
expect(subject[:merge_request]).to be_nil
|
||||
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
|
||||
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 }
|
||||
it 'makes atached flag true' do
|
||||
expect(subject[:flags][:merge_request_pipeline]).to be_truthy
|
||||
end
|
||||
|
||||
it 'makes detached flag false' do
|
||||
expect(subject[:flags][:detached_merge_request_pipeline]).to be_falsy
|
||||
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 '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 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) }
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
it 'uses PAGES_MIGRATION_THREADS environment variable' do
|
||||
stub_env('PAGES_MIGRATION_THREADS', '5')
|
||||
|
||||
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, 5, 10) do |service|
|
||||
expect(service).to receive(:execute).and_call_original
|
||||
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
|
||||
subject
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
it 'uses PAGES_MIGRATION_BATCH_SIZE environment variable' do
|
||||
stub_env('PAGES_MIGRATION_BATCH_SIZE', '100')
|
||||
|
||||
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, 3, 100) 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
|
||||
|
||||
it 'does not try to migrate project' do
|
||||
expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue