Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-13 15:09:34 +00:00
parent cdd5eba514
commit c4eeda2d3d
53 changed files with 650 additions and 517 deletions

View file

@ -1 +1 @@
1fb1d2f7119c5d7bfdbc53e8ae9cc22058921219
93d46d0be8467aa4849f981d390357d6a3491420

View file

@ -26,7 +26,7 @@ export default {
buttonVariant: {
type: String,
required: false,
default: 'info',
default: 'default',
},
buttonCategory: {
type: String,

View file

@ -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"

View file

@ -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: {

View file

@ -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'),

View file

@ -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

View file

@ -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>

View file

@ -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),
});
};

View file

@ -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;

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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),

View file

@ -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')

View file

@ -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) }

View file

@ -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|

View file

@ -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

View file

@ -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")

View file

@ -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)

View file

@ -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)

View file

@ -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 } }

View file

@ -0,0 +1,2 @@
- if show_clusters_alert?(@project)
.js-clusters-deprecation-alert{ data: { message: clusters_deprecation_alert_message } }

View file

@ -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

View file

@ -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

View file

@ -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' } })

View file

@ -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' },

View file

@ -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"

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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.

View file

@ -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)

View 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

View file

@ -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"

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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', () => {

View file

@ -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);
});
});
});

View file

@ -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

View file

@ -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

View 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

View file

@ -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 })

View file

@ -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 }

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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