Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
cdd5eba514
commit
c4eeda2d3d
53 changed files with 650 additions and 517 deletions
|
@ -1 +1 @@
|
|||
1fb1d2f7119c5d7bfdbc53e8ae9cc22058921219
|
||||
93d46d0be8467aa4849f981d390357d6a3491420
|
||||
|
|
|
@ -26,7 +26,7 @@ export default {
|
|||
buttonVariant: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'info',
|
||||
default: 'default',
|
||||
},
|
||||
buttonCategory: {
|
||||
type: String,
|
||||
|
|
|
@ -257,7 +257,7 @@ export default {
|
|||
>
|
||||
<gl-button
|
||||
class="flex-grow-1 js-external-dashboard-link"
|
||||
variant="info"
|
||||
variant="confirm"
|
||||
category="primary"
|
||||
:href="externalDashboardUrl"
|
||||
target="_blank"
|
||||
|
|
|
@ -37,7 +37,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
caretIcon() {
|
||||
return this.isCollapsed ? 'angle-right' : 'angle-down';
|
||||
return this.isCollapsed ? 'chevron-lg-right' : 'chevron-lg-down';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -32,25 +32,19 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), {
|
|||
},
|
||||
},
|
||||
[MEMBER_TYPES.group]: {
|
||||
tableFields: gon?.features?.groupMemberInheritedGroup
|
||||
? SHARED_FIELDS.concat(['source', 'granted'])
|
||||
: SHARED_FIELDS.concat(['granted']),
|
||||
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
|
||||
tableAttrs: {
|
||||
table: { 'data-qa-selector': 'groups_list' },
|
||||
tr: { 'data-qa-selector': 'group_row' },
|
||||
},
|
||||
requestFormatter: groupLinkRequestFormatter,
|
||||
...(gon?.features?.groupMemberInheritedGroup
|
||||
? {
|
||||
filteredSearchBar: {
|
||||
show: true,
|
||||
tokens: ['groups_with_inherited_permissions'],
|
||||
searchParam: 'search_groups',
|
||||
placeholder: s__('Members|Filter groups'),
|
||||
recentSearchesStorageKey: 'group_links_members',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
filteredSearchBar: {
|
||||
show: true,
|
||||
tokens: ['groups_with_inherited_permissions'],
|
||||
searchParam: 'search_groups',
|
||||
placeholder: s__('Members|Filter groups'),
|
||||
recentSearchesStorageKey: 'group_links_members',
|
||||
},
|
||||
},
|
||||
[MEMBER_TYPES.invite]: {
|
||||
tableFields: SHARED_FIELDS.concat('invited'),
|
||||
|
|
|
@ -2,6 +2,7 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
|||
|
||||
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
||||
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
|
||||
import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert';
|
||||
import leaveByUrl from '~/namespaces/leave_by_url';
|
||||
import initVueNotificationsDropdown from '~/notifications';
|
||||
import Star from '~/projects/star';
|
||||
|
@ -50,6 +51,7 @@ new ShortcutsNavigation(); // eslint-disable-line no-new
|
|||
initUploadFileTrigger();
|
||||
initInviteMembersModal();
|
||||
initInviteMembersTrigger();
|
||||
initClustersDeprecationAlert();
|
||||
|
||||
initReadMore();
|
||||
new Star(); // eslint-disable-line no-new
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<script>
|
||||
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
inject: ['message'],
|
||||
docsLink: helpPagePath('user/infrastructure/clusters/migrate_to_gitlab_agent.md'),
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-alert :dismissible="false" variant="warning" class="gl-mt-5">
|
||||
<gl-sprintf :message="message">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="$options.docsLink">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-alert>
|
||||
</template>
|
|
@ -0,0 +1,21 @@
|
|||
import Vue from 'vue';
|
||||
import ClustersDeprecationAlert from './components/clusters_deprecation_alert.vue';
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('.js-clusters-deprecation-alert');
|
||||
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { message } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
name: 'ClustersDeprecationAlertRoot',
|
||||
provide: {
|
||||
message,
|
||||
},
|
||||
render: (createElement) => createElement(ClustersDeprecationAlert),
|
||||
});
|
||||
};
|
|
@ -429,7 +429,6 @@ $gl-padding: 16px;
|
|||
$gl-padding-24: 24px;
|
||||
$gl-padding-32: 32px;
|
||||
$gl-padding-50: 50px;
|
||||
$gl-col-padding: 15px;
|
||||
$gl-input-padding: 10px;
|
||||
$gl-vert-padding: 6px;
|
||||
$gl-padding-top: 10px;
|
||||
|
|
|
@ -25,8 +25,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
|||
urgency :low
|
||||
|
||||
def index
|
||||
push_frontend_feature_flag(:group_member_inherited_group, @group)
|
||||
|
||||
@sort = params[:sort].presence || sort_value_name
|
||||
@include_relations ||= requested_relations(:groups_with_inherited_permissions)
|
||||
|
||||
|
|
|
@ -23,21 +23,11 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
|
|||
|
||||
@personal_access_token = result.payload[:personal_access_token]
|
||||
|
||||
if Feature.enabled?(:access_token_ajax)
|
||||
if result.success?
|
||||
render json: { new_token: @personal_access_token.token,
|
||||
active_access_tokens: active_personal_access_tokens }, status: :ok
|
||||
else
|
||||
render json: { errors: result.errors }, status: :unprocessable_entity
|
||||
end
|
||||
if result.success?
|
||||
render json: { new_token: @personal_access_token.token,
|
||||
active_access_tokens: active_personal_access_tokens }, status: :ok
|
||||
else
|
||||
if result.success?
|
||||
PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token)
|
||||
redirect_to profile_personal_access_tokens_path, notice: _("Your new personal access token has been created.")
|
||||
else
|
||||
set_index_vars
|
||||
render :index
|
||||
end
|
||||
render json: { errors: result.errors }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -62,19 +52,10 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
|
|||
def set_index_vars
|
||||
@scopes = Gitlab::Auth.available_scopes_for(current_user)
|
||||
@active_personal_access_tokens = active_personal_access_tokens
|
||||
|
||||
if Feature.disabled?(:access_token_ajax)
|
||||
@new_personal_access_token = PersonalAccessToken.redis_getdel(current_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
def active_personal_access_tokens
|
||||
tokens = finder(state: 'active', sort: 'expires_at_asc').execute
|
||||
|
||||
if Feature.enabled?(:access_token_ajax)
|
||||
::API::Entities::PersonalAccessTokenWithDetails.represent(tokens)
|
||||
else
|
||||
tokens
|
||||
end
|
||||
::API::Entities::PersonalAccessTokenWithDetails.represent(tokens)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,12 +34,14 @@ class ApplicationExperiment < Gitlab::Experiment
|
|||
#
|
||||
# @deprecated
|
||||
def key_for(source, seed = name)
|
||||
# If FIPS is enabled, we simply call the method available in the gem, which
|
||||
# uses SHA2.
|
||||
return super if Gitlab::FIPS.enabled?
|
||||
|
||||
# If FIPS isn't enabled, we use the legacy MD5 logic to keep existing
|
||||
# experiment events working.
|
||||
source = source.keys + source.values if source.is_a?(Hash)
|
||||
|
||||
ingredients = Array(source).map { |v| identify(v) }
|
||||
ingredients.unshift(seed)
|
||||
|
||||
Digest::MD5.hexdigest(ingredients.join('|'))
|
||||
Digest::MD5.hexdigest(Array(source).map { |v| identify(v) }.unshift(seed).join('|'))
|
||||
end
|
||||
|
||||
def nest_experiment(other)
|
||||
|
|
|
@ -60,12 +60,8 @@ module Groups::GroupMembersHelper
|
|||
end
|
||||
|
||||
def group_group_links_list_data(group, include_relations, search)
|
||||
if ::Feature.enabled?(:group_member_inherited_group, group)
|
||||
group_links = group_group_links(group, include_relations)
|
||||
group_links = group_links.search(search) if search
|
||||
else
|
||||
group_links = group.shared_with_group_links
|
||||
end
|
||||
group_links = group_group_links(group, include_relations)
|
||||
group_links = group_links.search(search) if search
|
||||
|
||||
{
|
||||
members: group_group_links_serialized(group, group_links),
|
||||
|
|
|
@ -444,6 +444,18 @@ module ProjectsHelper
|
|||
Gitlab::InactiveProjectsDeletionWarningTracker.new(project.id).scheduled_deletion_date
|
||||
end
|
||||
|
||||
def show_clusters_alert?(project)
|
||||
Gitlab.com? && can_admin_associated_clusters?(project)
|
||||
end
|
||||
|
||||
def clusters_deprecation_alert_message
|
||||
if has_active_license?
|
||||
s_('ClusterIntegration|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of November 2022. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd} or reach out to GitLab support.')
|
||||
else
|
||||
s_('ClusterIntegration|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of November 2022. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd}.')
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def configure_oauth_import_message(provider, help_url)
|
||||
|
@ -743,4 +755,16 @@ module ProjectsHelper
|
|||
end
|
||||
end
|
||||
|
||||
def can_admin_associated_clusters?(project)
|
||||
can_admin_project_clusters?(project) || can_admin_group_clusters?(project)
|
||||
end
|
||||
|
||||
def can_admin_project_clusters?(project)
|
||||
project.clusters.any? && can?(current_user, :admin_cluster, project)
|
||||
end
|
||||
|
||||
def can_admin_group_clusters?(project)
|
||||
project.group && project.group.clusters.any? && can?(current_user, :admin_cluster, project.group)
|
||||
end
|
||||
|
||||
ProjectsHelper.prepend_mod_with('ProjectsHelper')
|
||||
|
|
|
@ -1030,19 +1030,6 @@ module Ci
|
|||
accessibility_report
|
||||
end
|
||||
|
||||
def collect_coverage_reports!(coverage_report)
|
||||
each_report(Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob|
|
||||
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(
|
||||
blob,
|
||||
coverage_report,
|
||||
project_path: project.full_path,
|
||||
worktree_paths: pipeline.all_worktree_paths
|
||||
)
|
||||
end
|
||||
|
||||
coverage_report
|
||||
end
|
||||
|
||||
def collect_codequality_reports!(codequality_report)
|
||||
each_report(Ci::JobArtifact::CODEQUALITY_REPORT_FILE_TYPES) do |file_type, blob|
|
||||
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, codequality_report)
|
||||
|
@ -1175,6 +1162,14 @@ module Ci
|
|||
Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_deployment_job', user_id) if user_id.present? && count_user_deployment?
|
||||
end
|
||||
|
||||
def each_report(report_types)
|
||||
job_artifacts_for_types(report_types).each do |report_artifact|
|
||||
report_artifact.each_blob do |blob|
|
||||
yield report_artifact.file_type, blob, report_artifact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def run_status_commit_hooks!
|
||||
|
@ -1226,14 +1221,6 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
def each_report(report_types)
|
||||
job_artifacts_for_types(report_types).each do |report_artifact|
|
||||
report_artifact.each_blob do |blob|
|
||||
yield report_artifact.file_type, blob, report_artifact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def job_artifacts_for_types(report_types)
|
||||
# Use select to leverage cached associations and avoid N+1 queries
|
||||
job_artifacts.select { |artifact| artifact.file_type.in?(report_types) }
|
||||
|
|
|
@ -335,7 +335,7 @@ module Ci
|
|||
scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) }
|
||||
scope :created_before_id, -> (id) { where('ci_pipelines.id < ?', id) }
|
||||
scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) }
|
||||
scope :with_pipeline_source, -> (source) { where(source: source)}
|
||||
scope :with_pipeline_source, -> (source) { where(source: source) }
|
||||
|
||||
scope :outside_pipeline_family, ->(pipeline) do
|
||||
where.not(id: pipeline.same_family_pipeline_ids)
|
||||
|
@ -1065,6 +1065,10 @@ module Ci
|
|||
latest_report_builds(Ci::JobArtifact.test_reports).preload(:project, :metadata)
|
||||
end
|
||||
|
||||
def latest_report_builds_in_self_and_descendants(reports_scope = ::Ci::JobArtifact.with_reports)
|
||||
builds_in_self_and_descendants.with_reports(reports_scope)
|
||||
end
|
||||
|
||||
def builds_with_coverage
|
||||
builds.latest.with_coverage
|
||||
end
|
||||
|
@ -1081,10 +1085,6 @@ module Ci
|
|||
pipeline_artifacts&.report_exists?(:code_coverage)
|
||||
end
|
||||
|
||||
def can_generate_coverage_reports?
|
||||
has_reports?(Ci::JobArtifact.coverage_reports)
|
||||
end
|
||||
|
||||
def has_codequality_mr_diff_report?
|
||||
pipeline_artifacts&.report_exists?(:code_quality_mr_diff)
|
||||
end
|
||||
|
@ -1115,14 +1115,6 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
def coverage_reports
|
||||
Gitlab::Ci::Reports::CoverageReport.new.tap do |coverage_reports|
|
||||
latest_report_builds(Ci::JobArtifact.coverage_reports).includes(:project).find_each do |build|
|
||||
build.collect_coverage_reports!(coverage_reports)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def codequality_reports
|
||||
Gitlab::Ci::Reports::CodequalityReports.new.tap do |codequality_reports|
|
||||
latest_report_builds(Ci::JobArtifact.codequality_reports).each do |build|
|
||||
|
|
|
@ -2,30 +2,44 @@
|
|||
module Ci
|
||||
module PipelineArtifacts
|
||||
class CoverageReportService
|
||||
def execute(pipeline)
|
||||
return unless pipeline.can_generate_coverage_reports?
|
||||
return if pipeline.has_coverage_reports?
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
file = build_carrierwave_file(pipeline)
|
||||
def initialize(pipeline)
|
||||
@pipeline = pipeline
|
||||
end
|
||||
|
||||
def execute
|
||||
return if pipeline.has_coverage_reports?
|
||||
return if report.empty?
|
||||
|
||||
pipeline.pipeline_artifacts.create!(
|
||||
project_id: pipeline.project_id,
|
||||
file_type: :code_coverage,
|
||||
file_format: Ci::PipelineArtifact::REPORT_TYPES.fetch(:code_coverage),
|
||||
size: file["tempfile"].size,
|
||||
file: file,
|
||||
size: carrierwave_file["tempfile"].size,
|
||||
file: carrierwave_file,
|
||||
expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_carrierwave_file(pipeline)
|
||||
CarrierWaveStringFile.new_file(
|
||||
file_content: pipeline.coverage_reports.to_json,
|
||||
filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_coverage),
|
||||
content_type: 'application/json'
|
||||
)
|
||||
attr_reader :pipeline
|
||||
|
||||
def report
|
||||
strong_memoize(:report) do
|
||||
Gitlab::Ci::Reports::CoverageReportGenerator.new(pipeline).report
|
||||
end
|
||||
end
|
||||
|
||||
def carrierwave_file
|
||||
strong_memoize(:carrier_wave_file) do
|
||||
CarrierWaveStringFile.new_file(
|
||||
file_content: report.to_json,
|
||||
filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_coverage),
|
||||
content_type: 'application/json'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
= form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
|
||||
= form_errors(@milestone)
|
||||
= form_errors(@milestone, pajamas_alert: true)
|
||||
.form-group.row
|
||||
.col-form-label.col-sm-2
|
||||
= f.label :title, _("Title")
|
||||
|
|
|
@ -9,22 +9,22 @@
|
|||
%li.page-item
|
||||
- first_page_path = url_for(page_params.merge(cursor: paginator.cursor_for_first_page))
|
||||
= link_to first_page_path, rel: 'first', class: 'page-link' do
|
||||
= sprite_icon('angle-double-left', size: 8)
|
||||
= sprite_icon('chevron-double-lg-left', size: 8)
|
||||
= s_('Pagination|First')
|
||||
|
||||
%li.page-item.prev
|
||||
= link_to previous_path, rel: 'prev', class: 'page-link' do
|
||||
= sprite_icon('angle-left', size: 8)
|
||||
= sprite_icon('chevron-lg-left', size: 8)
|
||||
= s_('Pagination|Prev')
|
||||
|
||||
- if paginator.has_next_page?
|
||||
%li.page-item.next
|
||||
= link_to next_path, rel: 'next', class: 'page-link' do
|
||||
= s_('Pagination|Next')
|
||||
= sprite_icon('angle-right', size: 8)
|
||||
= sprite_icon('chevron-lg-right', size: 8)
|
||||
- unless without_first_and_last_pages
|
||||
%li.page-item
|
||||
- last_page_path = url_for(page_params.merge(cursor: paginator.cursor_for_last_page))
|
||||
= link_to last_page_path, rel: 'last', class: 'page-link' do
|
||||
= s_('Pagination|Last')
|
||||
= sprite_icon('angle-double-right', size: 8)
|
||||
= sprite_icon('chevron-double-lg-right', size: 8)
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
- if previous_path
|
||||
%li.page-item.prev
|
||||
= link_to previous_path, rel: 'prev', class: 'page-link' do
|
||||
= sprite_icon('angle-left', size: 8)
|
||||
= sprite_icon('chevron-lg-left', size: 8)
|
||||
= s_('Pagination|Prev')
|
||||
- if next_path
|
||||
%li.page-item.next
|
||||
= link_to next_path, rel: 'next', class: 'page-link' do
|
||||
= s_('Pagination|Next')
|
||||
= sprite_icon('angle-right', size: 8)
|
||||
= sprite_icon('chevron-lg-right', size: 8)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
- ajax = Feature.enabled?(:access_token_ajax)
|
||||
- breadcrumb_title s_('AccessTokens|Access Tokens')
|
||||
- page_title s_('AccessTokens|Personal Access Tokens')
|
||||
- type = _('personal access token')
|
||||
|
@ -17,26 +16,15 @@
|
|||
|
||||
.col-lg-8
|
||||
#js-new-access-token-app{ data: { access_token_type: type } }
|
||||
- if @new_personal_access_token
|
||||
= render 'shared/access_tokens/created_container',
|
||||
type: type,
|
||||
new_token_value: @new_personal_access_token
|
||||
|
||||
= render 'shared/access_tokens/form',
|
||||
ajax: ajax,
|
||||
ajax: true,
|
||||
type: type,
|
||||
path: profile_personal_access_tokens_path,
|
||||
token: @personal_access_token,
|
||||
scopes: @scopes,
|
||||
help_path: help_page_path('user/profile/personal_access_tokens.md', anchor: 'personal-access-token-scopes')
|
||||
|
||||
- if ajax
|
||||
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_personal_access_tokens.to_json } }
|
||||
- else
|
||||
= render 'shared/access_tokens/table',
|
||||
type: type,
|
||||
type_plural: type_plural,
|
||||
active_tokens: @active_personal_access_tokens,
|
||||
revoke_route_helper: ->(token) { revoke_profile_personal_access_token_path(token) }
|
||||
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_personal_access_tokens.to_json } }
|
||||
|
||||
#js-tokens-app{ data: { tokens_data: tokens_app_data } }
|
||||
|
|
2
app/views/projects/_clusters_deprecation_alert.html.haml
Normal file
2
app/views/projects/_clusters_deprecation_alert.html.haml
Normal file
|
@ -0,0 +1,2 @@
|
|||
- if show_clusters_alert?(@project)
|
||||
.js-clusters-deprecation-alert{ data: { message: clusters_deprecation_alert_message } }
|
|
@ -6,6 +6,7 @@
|
|||
= render_if_exists 'projects/free_user_cap_alert', project: @project
|
||||
|
||||
= render partial: 'flash_messages', locals: { project: @project }
|
||||
= render 'clusters_deprecation_alert'
|
||||
|
||||
= render "home_panel"
|
||||
= render "archived_notice", project: @project
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
|
||||
- unless hide_gutter_toggle
|
||||
%div
|
||||
%button.gl-button.btn.btn-default.btn-icon.float-right.gl-display-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ type: 'button', class: "#{'gl-md-display-none!' if moved_mr_sidebar_enabled? } #{'gl-sm-display-none!' unless moved_mr_sidebar_enabled?}" }
|
||||
= sprite_icon('chevron-double-lg-left')
|
||||
- display_class = moved_mr_sidebar_enabled? ? 'gl-md-display-none!' : 'gl-sm-display-none!'
|
||||
= render Pajamas::ButtonComponent.new(icon: "chevron-double-lg-left", button_options: { class: "btn-icon float-right gl-display-block gutter-toggle issuable-gutter-toggle js-sidebar-toggle #{display_class}" })
|
||||
|
||||
.detail-page-header-actions.gl-align-self-start.is-merge-request.js-issuable-actions
|
||||
- if can_update_merge_request
|
||||
|
|
|
@ -71,8 +71,10 @@
|
|||
= gl_badge_tag _('Error'), { variant: :danger }, { data: { toggle: 'tooltip', html: 'true', qa_selector: 'mirror_error_badge' }, title: html_escape(mirror.last_error.try(:strip)) }
|
||||
%td.gl-display-flex
|
||||
- if mirror_settings_enabled
|
||||
%button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-icon.gl-button.btn-danger.gl-mr-3{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove')
|
||||
.btn-group.mirror-actions-group{ role: 'group' }
|
||||
- if mirror.ssh_key_auth?
|
||||
= clipboard_button(text: mirror.ssh_public_key, class: 'gl-button btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
|
||||
= clipboard_button(text: mirror.ssh_public_key, class: 'gl-button btn btn-default btn-icon', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
|
||||
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
|
||||
= render Pajamas::ButtonComponent.new(variant: :danger,
|
||||
icon: 'remove',
|
||||
button_options: { class: 'js-delete-mirror qa-delete-mirror rspec-delete-mirror', title: _('Remove'), data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' } })
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
= _('Archive project')
|
||||
- if @project.archived?
|
||||
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'unarchiving-a-project') }
|
||||
%p= _("Unarchiving the project will restore its members' ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
|
||||
%p= _("Unarchiving the project restores its members' ability to make commits, and create issues, comments, and other entities. %{strong_start}After you unarchive the project, it displays in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
|
||||
= link_to _('Unarchive project'), unarchive_project_path(@project),
|
||||
aria: { label: _('Unarchive project') },
|
||||
data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
|
||||
method: :post, class: "gl-button btn btn-confirm"
|
||||
- else
|
||||
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'archiving-a-project') }
|
||||
%p= _("Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
|
||||
%p= _("Archiving the project makes it entirely read-only. It is hidden from the dashboard and doesn't display in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
|
||||
= link_to _('Archive project'), archive_project_path(@project),
|
||||
aria: { label: _('Archive project') },
|
||||
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link', 'confirm-btn-variant': 'confirm' },
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
= render_if_exists 'projects/free_user_cap_alert', project: @project
|
||||
= render_if_exists 'shared/minute_limit_banner', namespace: @project
|
||||
= render partial: 'flash_messages', locals: { project: @project }
|
||||
= render 'clusters_deprecation_alert'
|
||||
|
||||
= render "projects/last_push"
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
- if remote_mirror.update_in_progress?
|
||||
%button.btn.btn-icon.gl-button.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' }, title: _('Updating') }
|
||||
= sprite_icon("retry", css_class: "spin")
|
||||
= render Pajamas::ButtonComponent.new(icon: 'retry',
|
||||
button_options: { class: 'disabled', title: _('Updating'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' } },
|
||||
icon_classes: 'spin')
|
||||
- elsif remote_mirror.enabled?
|
||||
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn btn-icon gl-button qa-update-now-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
|
||||
= sprite_icon("retry")
|
||||
|
|
|
@ -16,7 +16,7 @@ module Ci
|
|||
|
||||
def perform(pipeline_id)
|
||||
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
|
||||
Ci::PipelineArtifacts::CoverageReportService.new.execute(pipeline)
|
||||
Ci::PipelineArtifacts::CoverageReportService.new(pipeline).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: access_token_ajax
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84373
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/359956
|
||||
milestone: '15.0'
|
||||
name: ci_child_pipeline_coverage_reports
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88626
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363557
|
||||
milestone: '15.1'
|
||||
type: development
|
||||
group: group::authentication and authorization
|
||||
group: group::pipeline insights
|
||||
default_enabled: false
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: group_member_inherited_group
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71465
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/357244
|
||||
milestone: '14.10'
|
||||
type: development
|
||||
group: group::workspace
|
||||
default_enabled: false
|
|
@ -150,7 +150,7 @@ In our case, `data-qa-selector="login_field"`, `data-qa-selector="password_field
|
|||
```haml
|
||||
= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required.", data: { qa_selector: 'login_field' }
|
||||
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required.", data: { qa_selector: 'password_field' }
|
||||
= f.submit "Sign in", class: "btn btn-success", data: { qa_selector: 'sign_in_button' }
|
||||
= f.submit "Sign in", class: "btn btn-confirm", data: { qa_selector: 'sign_in_button' }
|
||||
```
|
||||
|
||||
Things to note:
|
||||
|
|
|
@ -78,6 +78,27 @@ The visualization cannot be displayed if the blocking manual job did not run.
|
|||
By default, the [pipeline artifact](../../../ci/pipelines/pipeline_artifacts.md#storage) used
|
||||
to draw the visualization on the merge request expires **one week** after creation.
|
||||
|
||||
### Coverage report from child pipeline
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363301) in GitLab 15.1 [with a flag](../../../administration/feature_flags.md). Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `ci_child_pipeline_coverage_reports`.
|
||||
On GitLab.com, this feature is not available.
|
||||
The feature is not ready for production use.
|
||||
|
||||
If the test coverage is created in jobs that are in a child pipeline, the parent pipeline must use
|
||||
`strategy: depend`.
|
||||
|
||||
```yaml
|
||||
child_test_pipeline:
|
||||
trigger:
|
||||
include:
|
||||
- local: path/to/child_pipeline.yml
|
||||
- template: Security/SAST.gitlab-ci.yml
|
||||
strategy: depend
|
||||
```
|
||||
|
||||
### Automatic class path correction
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217664) in GitLab 13.8.
|
||||
|
|
|
@ -10,6 +10,10 @@ module Gitlab
|
|||
@files = {}
|
||||
end
|
||||
|
||||
def empty?
|
||||
@files.empty?
|
||||
end
|
||||
|
||||
def pick(keys)
|
||||
coverage_files = files.select do |key|
|
||||
keys.include?(key)
|
||||
|
|
53
lib/gitlab/ci/reports/coverage_report_generator.rb
Normal file
53
lib/gitlab/ci/reports/coverage_report_generator.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Reports
|
||||
class CoverageReportGenerator
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(pipeline)
|
||||
@pipeline = pipeline
|
||||
end
|
||||
|
||||
def report
|
||||
coverage_report = Gitlab::Ci::Reports::CoverageReport.new
|
||||
|
||||
# Return an empty report if the pipeline is a child pipeline.
|
||||
# Since the coverage report is used in a merge request report,
|
||||
# we are only interested in the coverage report from the root pipeline.
|
||||
return coverage_report if @pipeline.child?
|
||||
|
||||
coverage_report.tap do |coverage_report|
|
||||
report_builds.find_each do |build|
|
||||
build.each_report(::Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob|
|
||||
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(
|
||||
blob,
|
||||
coverage_report,
|
||||
project_path: @pipeline.project.full_path,
|
||||
worktree_paths: @pipeline.all_worktree_paths
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def report_builds
|
||||
if child_pipeline_feature_enabled?
|
||||
@pipeline.latest_report_builds_in_self_and_descendants(::Ci::JobArtifact.coverage_reports)
|
||||
else
|
||||
@pipeline.latest_report_builds(::Ci::JobArtifact.coverage_reports)
|
||||
end
|
||||
end
|
||||
|
||||
def child_pipeline_feature_enabled?
|
||||
strong_memoize(:feature_enabled) do
|
||||
Feature.enabled?(:ci_child_pipeline_coverage_reports, @pipeline.project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4804,7 +4804,7 @@ msgstr ""
|
|||
msgid "Archived projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}"
|
||||
msgid "Archiving the project makes it entirely read-only. It is hidden from the dashboard and doesn't display in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you ABSOLUTELY SURE you wish to remove this group?"
|
||||
|
@ -8810,6 +8810,12 @@ msgstr ""
|
|||
msgid "ClusterIntegration|The URL used to access the Kubernetes API."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of November 2022. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd} or reach out to GitLab support."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of November 2022. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|The certificate-based method to connect clusters to GitLab was %{linkStart}deprecated%{linkEnd} in GitLab 14.5."
|
||||
msgstr ""
|
||||
|
||||
|
@ -40528,7 +40534,7 @@ msgstr ""
|
|||
msgid "Unarchive project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unarchiving the project will restore its members' ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}"
|
||||
msgid "Unarchiving the project restores its members' ability to make commits, and create issues, comments, and other entities. %{strong_start}After you unarchive the project, it displays in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unassign from commenting user"
|
||||
|
@ -44217,9 +44223,6 @@ msgstr ""
|
|||
msgid "Your new comment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your new personal access token has been created."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
|
||||
msgstr ""
|
||||
|
||||
|
@ -44607,10 +44610,10 @@ msgstr ""
|
|||
msgid "ciReport|%{sameNum} same"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{scanner} detected %{number} new potential %{vulnStr}"
|
||||
msgid "ciReport|%{scanner} detected %{strong_start}%{number}%{strong_end} new potential %{vulnStr}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{scanner} detected no new %{vulnStr}"
|
||||
msgid "ciReport|%{scanner} detected no %{strong_start}new%{strong_end} %{vulnStr}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|: Loading resulted in an error"
|
||||
|
|
|
@ -30,21 +30,9 @@ module QA
|
|||
element :created_access_token
|
||||
end
|
||||
|
||||
# This element will be removed once `access_token_ajax` feature flag is removed
|
||||
# and this work is completed: https://gitlab.com/gitlab-org/gitlab/-/issues/357848
|
||||
base.view 'app/views/shared/access_tokens/_created_container.html.haml' do
|
||||
element :created_access_token
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/access_tokens/components/access_token_table_app.vue' do
|
||||
element :revoke_button
|
||||
end
|
||||
|
||||
# This element will be removed once `access_token_ajax` feature flag is removed
|
||||
# and this work is completed: https://gitlab.com/gitlab-org/gitlab/-/issues/357848
|
||||
base.view 'app/views/shared/access_tokens/_table.html.haml' do
|
||||
element :revoke_button
|
||||
end
|
||||
end
|
||||
|
||||
def fill_token_name(name)
|
||||
|
|
|
@ -66,44 +66,4 @@ RSpec.describe Profiles::PersonalAccessTokensController do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'access_token_ajax feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(access_token_ajax: false)
|
||||
PersonalAccessToken.redis_store!(user.id, token_value)
|
||||
get :index
|
||||
end
|
||||
|
||||
describe '#index' do
|
||||
let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let!(:inactive_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
|
||||
let!(:impersonation_personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
|
||||
let(:token_value) { 's3cr3t' }
|
||||
|
||||
it "retrieves active personal access tokens" do
|
||||
expect(assigns(:active_personal_access_tokens)).to include(active_personal_access_token)
|
||||
end
|
||||
|
||||
it "does not retrieve impersonation tokens or inactive personal access tokens" do
|
||||
expect(assigns(:active_personal_access_tokens)).not_to include(impersonation_personal_access_token)
|
||||
expect(assigns(:active_personal_access_tokens)).not_to include(inactive_personal_access_token)
|
||||
end
|
||||
|
||||
it "retrieves newly created personal access token value" do
|
||||
expect(assigns(:new_personal_access_token)).to eql(token_value)
|
||||
end
|
||||
|
||||
it "sets PAT name and scopes" do
|
||||
name = 'My PAT'
|
||||
scopes = 'api,read_user'
|
||||
|
||||
get :index, params: { name: name, scopes: scopes }
|
||||
|
||||
expect(assigns(:personal_access_token)).to have_attributes(
|
||||
name: eq(name),
|
||||
scopes: contain_exactly(:api, :read_user)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1094,7 +1094,7 @@ RSpec.describe Projects::MergeRequestsController do
|
|||
end
|
||||
|
||||
context 'when processing coverage reports is completed' do
|
||||
let(:report) { { status: :parsed, data: pipeline.coverage_reports } }
|
||||
let(:report) { { status: :parsed, data: { 'files' => {} } } }
|
||||
|
||||
it 'returns coverage reports' do
|
||||
subject
|
||||
|
|
|
@ -11,6 +11,7 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
before do
|
||||
stub_feature_flag_definition(:namespaced_stub, feature_definition)
|
||||
|
||||
allow(Gitlab::FIPS).to receive(:enabled?).and_return(true)
|
||||
allow(application_experiment).to receive(:enabled?).and_return(true)
|
||||
end
|
||||
|
||||
|
@ -137,7 +138,11 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
},
|
||||
{
|
||||
schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
|
||||
data: { experiment: 'namespaced/stub', key: '86208ac54ca798e11f127e8b23ec396a', variant: 'control' }
|
||||
data: {
|
||||
experiment: 'namespaced/stub',
|
||||
key: '300b002687ba1f68591adb2f45ae67f1e56be05ad55f317cc00f1c4aa38f081a',
|
||||
variant: 'control'
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
|
@ -214,8 +219,18 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
end
|
||||
|
||||
describe "#key_for" do
|
||||
it "generates MD5 hashes" do
|
||||
expect(application_experiment.key_for(foo: :bar)).to eq('6f9ac12afdb9b58c2f19a136d09f9153')
|
||||
it "generates FIPS compliant SHA2 hashes" do
|
||||
expect(application_experiment.key_for(foo: :bar))
|
||||
.to eq('1206febc4d022294fc639d68c2905079898ea4fee99290785b822e5010f1a9d1')
|
||||
end
|
||||
|
||||
it "falls back to legacy MD5 when FIPS isn't forced" do
|
||||
# Please see https://gitlab.com/gitlab-org/gitlab/-/issues/334590 about
|
||||
# why this remains and why it hasn't been prioritized.
|
||||
|
||||
allow(Gitlab::FIPS).to receive(:enabled?).and_return(false)
|
||||
expect(application_experiment.key_for(foo: :bar))
|
||||
.to eq('6f9ac12afdb9b58c2f19a136d09f9153')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ RSpec.describe 'Milestone' do
|
|||
end
|
||||
find('input[name="commit"]').click
|
||||
|
||||
expect(find('.alert-danger')).to have_content('already being used for another group or project milestone.')
|
||||
expect(find('.gl-alert-danger')).to have_content('already being used for another group or project milestone.')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -161,126 +161,4 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
|
|||
expect(find("#personal_access_token_scopes_api")).to be_checked
|
||||
expect(find("#personal_access_token_scopes_read_user")).to be_checked
|
||||
end
|
||||
|
||||
context 'access_token_ajax feature flag disabled' do
|
||||
def active_personal_access_tokens
|
||||
find(".table.active-tokens")
|
||||
end
|
||||
|
||||
def no_personal_access_tokens_message
|
||||
find(".settings-message")
|
||||
end
|
||||
|
||||
def created_personal_access_token
|
||||
find("#created-personal-access-token").value
|
||||
end
|
||||
|
||||
def disallow_personal_access_token_saves!
|
||||
allow_next_instance_of(PersonalAccessToken) do |pat|
|
||||
pat.errors.add(:name, 'cannot be nil')
|
||||
end
|
||||
|
||||
allow(PersonalAccessTokens::CreateService).to receive(:new).and_return(pat_create_service)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
stub_feature_flags(access_token_ajax: false)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe "token creation" do
|
||||
it "allows creation of a personal access token" do
|
||||
name = 'My PAT'
|
||||
|
||||
visit profile_personal_access_tokens_path
|
||||
fill_in "Token name", with: name
|
||||
|
||||
# Set date to 1st of next month
|
||||
find_field("Expiration date").click
|
||||
find(".pika-next").click
|
||||
click_on "1"
|
||||
|
||||
# Scopes
|
||||
check "read_api"
|
||||
check "read_user"
|
||||
|
||||
click_on "Create personal access token"
|
||||
|
||||
expect(active_personal_access_tokens).to have_text(name)
|
||||
expect(active_personal_access_tokens).to have_text('in')
|
||||
expect(active_personal_access_tokens).to have_text('read_api')
|
||||
expect(active_personal_access_tokens).to have_text('read_user')
|
||||
expect(created_personal_access_token).not_to be_empty
|
||||
end
|
||||
|
||||
context "when creation fails" do
|
||||
it "displays an error message" do
|
||||
disallow_personal_access_token_saves!
|
||||
visit profile_personal_access_tokens_path
|
||||
fill_in "Token name", with: 'My PAT'
|
||||
|
||||
expect { click_on "Create personal access token" }.not_to change { PersonalAccessToken.count }
|
||||
expect(page).to have_content("Name cannot be nil")
|
||||
expect(page).not_to have_selector("#created-personal-access-token")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'active tokens' do
|
||||
let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
|
||||
let!(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it 'only shows personal access tokens' do
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
expect(active_personal_access_tokens).to have_text(personal_access_token.name)
|
||||
expect(active_personal_access_tokens).not_to have_text(impersonation_token.name)
|
||||
end
|
||||
|
||||
context 'when User#time_display_relative is false' do
|
||||
before do
|
||||
user.update!(time_display_relative: false)
|
||||
end
|
||||
|
||||
it 'shows absolute times for expires_at' do
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "inactive tokens" do
|
||||
let!(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it "allows revocation of an active token" do
|
||||
visit profile_personal_access_tokens_path
|
||||
accept_confirm { click_on "Revoke" }
|
||||
|
||||
expect(page).to have_selector(".settings-message")
|
||||
expect(no_personal_access_tokens_message).to have_text("This user has no active personal access tokens.")
|
||||
end
|
||||
|
||||
it "removes expired tokens from 'active' section" do
|
||||
personal_access_token.update!(expires_at: 5.days.ago)
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
expect(page).to have_selector(".settings-message")
|
||||
expect(no_personal_access_tokens_message).to have_text("This user has no active personal access tokens.")
|
||||
end
|
||||
|
||||
context "when revocation fails" do
|
||||
it "displays an error message" do
|
||||
allow_next_instance_of(PersonalAccessTokens::RevokeService) do |instance|
|
||||
allow(instance).to receive(:revocation_permitted?).and_return(false)
|
||||
end
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
accept_confirm { click_on "Revoke" }
|
||||
expect(active_personal_access_tokens).to have_text(personal_access_token.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,17 +34,17 @@ describe('Graph group component', () => {
|
|||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should show the angle-down caret icon', () => {
|
||||
it('should show the chevron-lg-down caret icon', () => {
|
||||
expect(findContent().isVisible()).toBe(true);
|
||||
expect(findCaretIcon().props('name')).toBe('angle-down');
|
||||
expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
|
||||
});
|
||||
|
||||
it('should show the angle-right caret icon when the user collapses the group', async () => {
|
||||
it('should show the chevron-lg-right caret icon when the user collapses the group', async () => {
|
||||
findToggleButton().trigger('click');
|
||||
|
||||
await nextTick();
|
||||
expect(findContent().isVisible()).toBe(false);
|
||||
expect(findCaretIcon().props('name')).toBe('angle-right');
|
||||
expect(findCaretIcon().props('name')).toBe('chevron-lg-right');
|
||||
});
|
||||
|
||||
it('should contain a tab index for the collapse button', () => {
|
||||
|
@ -60,7 +60,7 @@ describe('Graph group component', () => {
|
|||
|
||||
await nextTick();
|
||||
expect(findContent().isVisible()).toBe(true);
|
||||
expect(findCaretIcon().props('name')).toBe('angle-down');
|
||||
expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -72,15 +72,15 @@ describe('Graph group component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should show the angle-down caret icon when collapseGroup is true', () => {
|
||||
expect(findCaretIcon().props('name')).toBe('angle-right');
|
||||
it('should show the chevron-lg-down caret icon when collapseGroup is true', () => {
|
||||
expect(findCaretIcon().props('name')).toBe('chevron-lg-right');
|
||||
});
|
||||
|
||||
it('should show the angle-right caret icon when collapseGroup is false', async () => {
|
||||
it('should show the chevron-lg-right caret icon when collapseGroup is false', async () => {
|
||||
findToggleButton().trigger('click');
|
||||
|
||||
await nextTick();
|
||||
expect(findCaretIcon().props('name')).toBe('angle-down');
|
||||
expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
|
||||
});
|
||||
|
||||
it('should call collapse the graph group content when enter is pressed on the caret icon', () => {
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { GlAlert, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import ClustersDeprecationAlert from '~/projects/clusters_deprecation_alert/components/clusters_deprecation_alert.vue';
|
||||
|
||||
const message = 'Alert message';
|
||||
|
||||
describe('ClustersDeprecationAlert', () => {
|
||||
let wrapper;
|
||||
|
||||
const provideData = {
|
||||
message,
|
||||
};
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(ClustersDeprecationAlert, {
|
||||
provide: provideData,
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('should render a non-dismissible warning alert', () => {
|
||||
expect(findAlert().props()).toMatchObject({
|
||||
dismissible: false,
|
||||
variant: 'warning',
|
||||
});
|
||||
});
|
||||
|
||||
it('should display the correct message', () => {
|
||||
expect(findAlert().text()).toBe(message);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -135,24 +135,6 @@ RSpec.describe Groups::GroupMembersHelper do
|
|||
expect(subject[:group][:members].map { |link| link[:id] }).to match_array(result)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group_member_inherited_group disabled' do
|
||||
before do
|
||||
stub_feature_flags(group_member_inherited_group: false)
|
||||
end
|
||||
|
||||
where(:include_relations, :result) do
|
||||
[:inherited, :direct] | lazy { [sub_group_group_link.id] }
|
||||
[:inherited] | lazy { [sub_group_group_link.id] }
|
||||
[:direct] | lazy { [sub_group_group_link.id] }
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'always returns direct member links' do
|
||||
expect(subject[:group][:members].map { |link| link[:id] }).to match_array(result)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1190,4 +1190,122 @@ RSpec.describe ProjectsHelper do
|
|||
expect(helper.inactive_project_deletion_date(project)).to eq('2022-03-01')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#can_admin_associated_clusters?' do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be_with_reload(:project) { create(:project) }
|
||||
|
||||
subject { helper.send(:can_admin_associated_clusters?, project) }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(current_user)
|
||||
allow(helper)
|
||||
.to receive(:can?)
|
||||
.with(current_user, :admin_cluster, namespace)
|
||||
.and_return(user_can_admin_cluster)
|
||||
end
|
||||
|
||||
context 'when project has a cluster' do
|
||||
let_it_be(:namespace) { project }
|
||||
|
||||
before do
|
||||
create(:cluster, projects: [namespace])
|
||||
end
|
||||
|
||||
context 'if user can admin cluster' do
|
||||
let_it_be(:user_can_admin_cluster) { true }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'if user can not admin cluster' do
|
||||
let_it_be(:user_can_admin_cluster) { false }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has a group cluster' do
|
||||
let_it_be(:namespace) { create(:group) }
|
||||
|
||||
before do
|
||||
project.update!(namespace: namespace)
|
||||
create(:cluster, :group, groups: [namespace])
|
||||
end
|
||||
|
||||
context 'if user can admin cluster' do
|
||||
let_it_be(:user_can_admin_cluster) { true }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'if user can not admin cluster' do
|
||||
let_it_be(:user_can_admin_cluster) { false }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project doesn\'t have a cluster' do
|
||||
let_it_be(:namespace) { project }
|
||||
|
||||
context 'if user can admin cluster' do
|
||||
let_it_be(:user_can_admin_cluster) { true }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'if user can not admin cluster' do
|
||||
let_it_be(:user_can_admin_cluster) { false }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#show_clusters_alert?' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
subject { helper.show_clusters_alert?(project) }
|
||||
|
||||
where(:is_gitlab_com, :user_can_admin_cluster, :expected) do
|
||||
false | false | false
|
||||
false | true | false
|
||||
true | false | false
|
||||
true | true | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
allow(::Gitlab).to receive(:com?).and_return(is_gitlab_com)
|
||||
allow(helper).to receive(:can_admin_associated_clusters?).and_return(user_can_admin_cluster)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(expected) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clusters_deprecation_alert_message' do
|
||||
subject { helper.clusters_deprecation_alert_message }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:has_active_license?).and_return(has_active_license)
|
||||
end
|
||||
|
||||
context 'if user has an active licence' do
|
||||
let_it_be(:has_active_license) { true }
|
||||
|
||||
it 'displays the correct messagee' do
|
||||
expect(subject).to eq(s_('Clusters|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of November 2022. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd} or reach out to GitLab support.'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'if user doesn\'t have an active licence' do
|
||||
let_it_be(:has_active_license) { false }
|
||||
|
||||
it 'displays the correct message' do
|
||||
expect(subject).to eq(s_('Clusters|The certificate-based Kubernetes integration has been deprecated and will be turned off at the end of November 2022. Please %{linkStart}migrate to the GitLab agent for Kubernetes%{linkEnd}.'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
104
spec/lib/gitlab/ci/reports/coverage_report_generator_spec.rb
Normal file
104
spec/lib/gitlab/ci/reports/coverage_report_generator_spec.rb
Normal file
|
@ -0,0 +1,104 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Reports::CoverageReportGenerator, factory_default: :keep do
|
||||
let_it_be(:project) { create_default(:project, :repository).freeze }
|
||||
let_it_be(:pipeline) { build(:ci_pipeline, :with_coverage_reports) }
|
||||
|
||||
describe '#report' do
|
||||
subject { described_class.new(pipeline).report }
|
||||
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, :success) }
|
||||
|
||||
shared_examples 'having a coverage report' do
|
||||
it 'returns coverage reports with collected data' do
|
||||
expected_files = [
|
||||
"auth/token.go",
|
||||
"auth/rpccredentials.go",
|
||||
"app/controllers/abuse_reports_controller.rb"
|
||||
]
|
||||
|
||||
expect(subject.files.keys).to match_array(expected_files)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline has multiple builds with coverage reports' do
|
||||
let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline) }
|
||||
let!(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline) }
|
||||
|
||||
before do
|
||||
create(:ci_job_artifact, :cobertura, job: build_rspec)
|
||||
create(:ci_job_artifact, :coverage_gocov_xml, job: build_golang)
|
||||
end
|
||||
|
||||
it_behaves_like 'having a coverage report'
|
||||
|
||||
context 'and it is a child pipeline' do
|
||||
let!(:pipeline) { create(:ci_pipeline, :success, child_of: build(:ci_pipeline)) }
|
||||
|
||||
it 'returns empty coverage report' do
|
||||
expect(subject).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when builds are retried' do
|
||||
let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', retried: true, pipeline: pipeline) }
|
||||
let!(:build_golang) { create(:ci_build, :success, name: 'golang', retried: true, pipeline: pipeline) }
|
||||
|
||||
before do
|
||||
create(:ci_job_artifact, :cobertura, job: build_rspec)
|
||||
create(:ci_job_artifact, :coverage_gocov_xml, job: build_golang)
|
||||
end
|
||||
|
||||
it 'does not take retried builds into account' do
|
||||
expect(subject).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline does not have any builds with coverage reports' do
|
||||
it 'returns empty coverage reports' do
|
||||
expect(subject).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline has child pipeline with builds that have coverage reports' do
|
||||
let!(:child_pipeline) { create(:ci_pipeline, :success, child_of: pipeline) }
|
||||
|
||||
let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: child_pipeline) }
|
||||
let!(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: child_pipeline) }
|
||||
|
||||
before do
|
||||
create(:ci_job_artifact, :cobertura, job: build_rspec)
|
||||
create(:ci_job_artifact, :coverage_gocov_xml, job: build_golang)
|
||||
end
|
||||
|
||||
it_behaves_like 'having a coverage report'
|
||||
|
||||
context 'when feature flag ci_child_pipeline_coverage_reports is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_child_pipeline_coverage_reports: false)
|
||||
end
|
||||
|
||||
it 'returns empty coverage reports' do
|
||||
expect(subject).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when both parent and child pipeline have builds with coverage reports' do
|
||||
let!(:child_pipeline) { create(:ci_pipeline, :success, child_of: pipeline) }
|
||||
|
||||
let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline) }
|
||||
let!(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: child_pipeline) }
|
||||
|
||||
before do
|
||||
create(:ci_job_artifact, :cobertura, job: build_rspec)
|
||||
create(:ci_job_artifact, :coverage_gocov_xml, job: build_golang)
|
||||
end
|
||||
|
||||
it_behaves_like 'having a coverage report'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,6 +7,20 @@ RSpec.describe Gitlab::Ci::Reports::CoverageReport do
|
|||
|
||||
it { expect(coverage_report.files).to eq({}) }
|
||||
|
||||
describe '#empty?' do
|
||||
context 'when no file has been added' do
|
||||
it { expect(coverage_report.empty?).to be(true) }
|
||||
end
|
||||
|
||||
context 'when file has been added' do
|
||||
before do
|
||||
coverage_report.add_file('app.rb', { 1 => 0, 2 => 1 })
|
||||
end
|
||||
|
||||
it { expect(coverage_report.empty?).to be(false) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#pick' do
|
||||
before do
|
||||
coverage_report.add_file('app.rb', { 1 => 0, 2 => 1 })
|
||||
|
|
|
@ -4479,68 +4479,6 @@ RSpec.describe Ci::Build do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#collect_coverage_reports!' do
|
||||
subject { build.collect_coverage_reports!(coverage_report) }
|
||||
|
||||
let(:coverage_report) { Gitlab::Ci::Reports::CoverageReport.new }
|
||||
|
||||
it { expect(coverage_report.files).to eq({}) }
|
||||
|
||||
context 'when build has a coverage report' do
|
||||
context 'when there is a Cobertura coverage report from simplecov-cobertura' do
|
||||
before do
|
||||
create(:ci_job_artifact, :cobertura, job: build, project: build.project)
|
||||
end
|
||||
|
||||
it 'parses blobs and add the results to the coverage report' do
|
||||
expect { subject }.not_to raise_error
|
||||
|
||||
expect(coverage_report.files.keys).to match_array(['app/controllers/abuse_reports_controller.rb'])
|
||||
expect(coverage_report.files['app/controllers/abuse_reports_controller.rb'].count).to eq(23)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a Cobertura coverage report from gocov-xml' do
|
||||
before do
|
||||
create(:ci_job_artifact, :coverage_gocov_xml, job: build, project: build.project)
|
||||
end
|
||||
|
||||
it 'parses blobs and add the results to the coverage report' do
|
||||
expect { subject }.not_to raise_error
|
||||
|
||||
expect(coverage_report.files.keys).to match_array(['auth/token.go', 'auth/rpccredentials.go'])
|
||||
expect(coverage_report.files['auth/token.go'].count).to eq(49)
|
||||
expect(coverage_report.files['auth/rpccredentials.go'].count).to eq(10)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a Cobertura coverage report with class filename paths not relative to project root' do
|
||||
before do
|
||||
allow(build.project).to receive(:full_path).and_return('root/javademo')
|
||||
allow(build.pipeline).to receive(:all_worktree_paths).and_return(['src/main/java/com/example/javademo/User.java'])
|
||||
|
||||
create(:ci_job_artifact, :coverage_with_paths_not_relative_to_project_root, job: build, project: build.project)
|
||||
end
|
||||
|
||||
it 'parses blobs and add the results to the coverage report with corrected paths' do
|
||||
expect { subject }.not_to raise_error
|
||||
|
||||
expect(coverage_report.files.keys).to match_array(['src/main/java/com/example/javademo/User.java'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a corrupted Cobertura coverage report' do
|
||||
before do
|
||||
create(:ci_job_artifact, :coverage_with_corrupted_data, job: build, project: build.project)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(Gitlab::Ci::Parsers::Coverage::Cobertura::InvalidLineInformationError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#collect_codequality_reports!' do
|
||||
subject(:codequality_report) { build.collect_codequality_reports!(Gitlab::Ci::Reports::CodequalityReports.new) }
|
||||
|
||||
|
@ -4630,6 +4568,18 @@ RSpec.describe Ci::Build do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#each_report' do
|
||||
let(:report_types) { Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES }
|
||||
|
||||
let!(:codequality) { create(:ci_job_artifact, :codequality, job: build) }
|
||||
let!(:coverage) { create(:ci_job_artifact, :coverage_gocov_xml, job: build) }
|
||||
let!(:junit) { create(:ci_job_artifact, :junit, job: build) }
|
||||
|
||||
it 'yields job artifact blob that matches the type' do
|
||||
expect { |b| build.each_report(report_types, &b) }.to yield_with_args(coverage.file_type, String, coverage)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#report_artifacts' do
|
||||
subject { build.report_artifacts }
|
||||
|
||||
|
|
|
@ -3889,6 +3889,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#latest_report_builds_in_self_and_descendants' do
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let_it_be(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }
|
||||
let_it_be(:grandchild_pipeline) { create(:ci_pipeline, child_of: child_pipeline) }
|
||||
|
||||
it 'returns builds with reports artifacts from pipelines in the hierarcy' do
|
||||
parent_build = create(:ci_build, :test_reports, pipeline: pipeline)
|
||||
child_build = create(:ci_build, :coverage_reports, pipeline: child_pipeline)
|
||||
grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline)
|
||||
|
||||
expect(pipeline.latest_report_builds_in_self_and_descendants).to contain_exactly(parent_build, child_build, grandchild_build)
|
||||
end
|
||||
|
||||
it 'filters builds by scope' do
|
||||
create(:ci_build, :test_reports, pipeline: pipeline)
|
||||
grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline)
|
||||
|
||||
expect(pipeline.latest_report_builds_in_self_and_descendants(Ci::JobArtifact.codequality_reports)).to contain_exactly(grandchild_build)
|
||||
end
|
||||
|
||||
it 'only returns builds that are not retried' do
|
||||
create(:ci_build, :codequality_reports, :retried, pipeline: grandchild_pipeline)
|
||||
grandchild_build = create(:ci_build, :codequality_reports, pipeline: grandchild_pipeline)
|
||||
|
||||
expect(pipeline.latest_report_builds_in_self_and_descendants).to contain_exactly(grandchild_build)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_reports?' do
|
||||
subject { pipeline.has_reports?(Ci::JobArtifact.test_reports) }
|
||||
|
||||
|
@ -3947,38 +3975,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#can_generate_coverage_reports?' do
|
||||
subject { pipeline.can_generate_coverage_reports? }
|
||||
|
||||
context 'when pipeline has builds with coverage reports' do
|
||||
before do
|
||||
create(:ci_build, :coverage_reports, pipeline: pipeline)
|
||||
end
|
||||
|
||||
context 'when pipeline status is running' do
|
||||
let(:pipeline) { create(:ci_pipeline, :running) }
|
||||
|
||||
it { expect(subject).to be_falsey }
|
||||
end
|
||||
|
||||
context 'when pipeline status is success' do
|
||||
let(:pipeline) { create(:ci_pipeline, :success) }
|
||||
|
||||
it { expect(subject).to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline does not have builds with coverage reports' do
|
||||
before do
|
||||
create(:ci_build, :artifacts, pipeline: pipeline)
|
||||
end
|
||||
|
||||
let(:pipeline) { create(:ci_pipeline, :success) }
|
||||
|
||||
it { expect(subject).to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_codequality_mr_diff_report?' do
|
||||
subject { pipeline.has_codequality_mr_diff_report? }
|
||||
|
||||
|
@ -4129,55 +4125,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#coverage_reports' do
|
||||
subject { pipeline.coverage_reports }
|
||||
|
||||
let_it_be(:pipeline) { create(:ci_pipeline) }
|
||||
|
||||
context 'when pipeline has multiple builds with coverage reports' do
|
||||
let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline) }
|
||||
let!(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline) }
|
||||
|
||||
before do
|
||||
create(:ci_job_artifact, :cobertura, job: build_rspec)
|
||||
create(:ci_job_artifact, :coverage_gocov_xml, job: build_golang)
|
||||
end
|
||||
|
||||
it 'returns coverage reports with collected data' do
|
||||
expect(subject.files.keys).to match_array([
|
||||
"auth/token.go",
|
||||
"auth/rpccredentials.go",
|
||||
"app/controllers/abuse_reports_controller.rb"
|
||||
])
|
||||
end
|
||||
|
||||
it 'does not execute N+1 queries' do
|
||||
single_build_pipeline = create(:ci_empty_pipeline, :created)
|
||||
single_rspec = create(:ci_build, :success, name: 'rspec', pipeline: single_build_pipeline)
|
||||
create(:ci_job_artifact, :cobertura, job: single_rspec, project: project)
|
||||
|
||||
control = ActiveRecord::QueryRecorder.new { single_build_pipeline.coverage_reports }
|
||||
|
||||
expect { subject }.not_to exceed_query_limit(control)
|
||||
end
|
||||
|
||||
context 'when builds are retried' do
|
||||
let!(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline) }
|
||||
let!(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline) }
|
||||
|
||||
it 'does not take retried builds into account' do
|
||||
expect(subject.files).to eql({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline does not have any builds with coverage reports' do
|
||||
it 'returns empty coverage reports' do
|
||||
expect(subject.files).to eql({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#codequality_reports' do
|
||||
subject(:codequality_reports) { pipeline.codequality_reports }
|
||||
|
||||
|
|
|
@ -4,17 +4,14 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe ::Ci::PipelineArtifacts::CoverageReportService do
|
||||
describe '#execute' do
|
||||
subject { described_class.new.execute(pipeline) }
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
|
||||
context 'when pipeline has coverage reports' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) }
|
||||
subject { described_class.new(pipeline).execute }
|
||||
|
||||
shared_examples 'creating a pipeline coverage report' do
|
||||
context 'when pipeline is finished' do
|
||||
it 'creates a pipeline artifact' do
|
||||
subject
|
||||
|
||||
expect(Ci::PipelineArtifact.count).to eq(1)
|
||||
expect { subject }.to change { Ci::PipelineArtifact.count }.from(0).to(1)
|
||||
end
|
||||
|
||||
it 'persists the default file name' do
|
||||
|
@ -37,21 +34,32 @@ RSpec.describe ::Ci::PipelineArtifacts::CoverageReportService do
|
|||
end
|
||||
|
||||
context 'when pipeline artifact has already been created' do
|
||||
it 'do not raise an error and do not persist the same artifact twice' do
|
||||
expect { 2.times { described_class.new.execute(pipeline) } }.not_to raise_error
|
||||
it 'does not raise an error and does not persist the same artifact twice' do
|
||||
expect { 2.times { described_class.new(pipeline).execute } }.not_to raise_error
|
||||
|
||||
expect(Ci::PipelineArtifact.count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline has coverage report' do
|
||||
let!(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) }
|
||||
|
||||
it_behaves_like 'creating a pipeline coverage report'
|
||||
end
|
||||
|
||||
context 'when pipeline has coverage report from child pipeline' do
|
||||
let!(:pipeline) { create(:ci_pipeline, :success, project: project) }
|
||||
let!(:child_pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project, child_of: pipeline) }
|
||||
|
||||
it_behaves_like 'creating a pipeline coverage report'
|
||||
end
|
||||
|
||||
context 'when pipeline is running and coverage report does not exist' do
|
||||
let(:pipeline) { create(:ci_pipeline, :running) }
|
||||
|
||||
it 'does not persist data' do
|
||||
subject
|
||||
|
||||
expect(Ci::PipelineArtifact.count).to eq(0)
|
||||
expect { subject }.not_to change { Ci::PipelineArtifact.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -86,6 +86,20 @@ RSpec.describe Tooling::Danger::Datateam do
|
|||
mr_labels: ['type::maintenance', 'Data Warehouse::Impacted'],
|
||||
impacted: false,
|
||||
impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
|
||||
},
|
||||
'with metric status removed' => {
|
||||
modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
|
||||
changed_lines: ['+status: removed'],
|
||||
mr_labels: ['type::maintenance'],
|
||||
impacted: true,
|
||||
impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
|
||||
},
|
||||
'with metric status active' => {
|
||||
modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb),
|
||||
changed_lines: ['+status: active'],
|
||||
mr_labels: ['type::maintenance'],
|
||||
impacted: false,
|
||||
impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
@ -15,6 +15,7 @@ module Tooling
|
|||
DATA_WAREHOUSE_SCOPE = 'Data Warehouse::'
|
||||
FILE_PATH_REGEX = %r{((ee|jh)/)?config/metrics(/.+\.yml)}.freeze
|
||||
PERFORMANCE_INDICATOR_REGEX = %r{gmau|smau|paid_gmau|umau}.freeze
|
||||
METRIC_REMOVED = %r{\+status: removed}.freeze
|
||||
DATABASE_REGEX = %r{\Adb/structure\.sql}.freeze
|
||||
STRUCTURE_SQL_FILE = %w(db/structure.sql).freeze
|
||||
|
||||
|
@ -31,18 +32,18 @@ module Tooling
|
|||
private
|
||||
|
||||
def data_warehouse_impact_files
|
||||
@impacted_files ||= (performance_indicator_changed_files + database_changed_files)
|
||||
@impacted_files ||= (metrics_changed_files + database_changed_files)
|
||||
end
|
||||
|
||||
def labelled_as_datawarehouse?
|
||||
helper.mr_labels.any? { |label| label.start_with?(DATA_WAREHOUSE_SCOPE) }
|
||||
end
|
||||
|
||||
def performance_indicator_changed_files
|
||||
def metrics_changed_files
|
||||
metrics_definitions_files = helper.modified_files.grep(FILE_PATH_REGEX)
|
||||
|
||||
metrics_definitions_files.select do |file|
|
||||
helper.changed_lines(file).any? { |change| change =~ PERFORMANCE_INDICATOR_REGEX }
|
||||
helper.changed_lines(file).any? { |change| performance_indicator_changed?(change) || status_removed?(change) }
|
||||
end.compact
|
||||
end
|
||||
|
||||
|
@ -53,6 +54,14 @@ module Tooling
|
|||
def database_changed_files
|
||||
helper.modified_files & STRUCTURE_SQL_FILE
|
||||
end
|
||||
|
||||
def performance_indicator_changed?(change)
|
||||
change =~ PERFORMANCE_INDICATOR_REGEX
|
||||
end
|
||||
|
||||
def status_removed?(change)
|
||||
change =~ METRIC_REMOVED
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue