Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ea044b0c4c
commit
b4c39709e3
|
|
@ -139,6 +139,7 @@ linters:
|
|||
- Style/HashSyntax
|
||||
- Style/IdenticalConditionalBranches
|
||||
- Style/IfInsideElse
|
||||
- Style/InlineDisableAnnotation
|
||||
- Style/NegatedIf
|
||||
- Style/NestedTernaryOperator
|
||||
- Style/RedundantInterpolation
|
||||
|
|
|
|||
|
|
@ -925,6 +925,9 @@ Cop/UserAdmin:
|
|||
- 'spec/**/*.rb'
|
||||
- 'ee/spec/**/*.rb'
|
||||
|
||||
Style/InlineDisableAnnotation:
|
||||
Enabled: true
|
||||
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/327495
|
||||
Style/RegexpLiteral:
|
||||
Enabled: false
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -329,7 +329,7 @@ export default {
|
|||
@mouseout="hideTooltips"
|
||||
>
|
||||
<div class="gl-display-flex gl-align-items-center gl-flex-grow-1">
|
||||
<ci-icon :status="job.status" size="md" :use-link="false" />
|
||||
<ci-icon :status="job.status" :use-link="false" />
|
||||
<div class="gl-pl-3 gl-pr-3 gl-display-flex gl-flex-direction-column gl-pipeline-job-width">
|
||||
<div class="gl-text-truncate gl-pr-9 gl-line-height-normal">{{ job.name }}</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -81,11 +81,6 @@ export default {
|
|||
// detailedStatus is graphQL, details.status is REST
|
||||
return pipeline?.detailedStatus || pipeline?.details?.status;
|
||||
},
|
||||
triggerButtonClass(pipeline) {
|
||||
const { group } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
|
||||
|
||||
return `ci-status-icon-${group}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -105,7 +100,6 @@ export default {
|
|||
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
|
||||
:status="pipelineStatus(pipeline)"
|
||||
:show-tooltip="false"
|
||||
:class="triggerButtonClass(pipeline)"
|
||||
class="linked-pipeline-mini-item gl-mb-0!"
|
||||
data-testid="linked-pipeline-mini-item"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,20 +6,11 @@ import { GlBadge, GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
|||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* icon: "status_running" // used to render the icon and CSS class
|
||||
* text: "Running",
|
||||
* detailsPath: '/project1/jobs/1' // can also be details_path
|
||||
* }
|
||||
*
|
||||
* Used in:
|
||||
* - Extended MR Popover
|
||||
* - Jobs show view header
|
||||
* - Jobs show view sidebar
|
||||
* - Jobs table
|
||||
* - Linked pipelines
|
||||
* - Pipeline graph
|
||||
* - Pipeline mini graph
|
||||
* - Pipeline show view badge
|
||||
* - Pipelines table Badge
|
||||
*/
|
||||
|
||||
export default {
|
||||
|
|
@ -35,13 +26,8 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
validator(status) {
|
||||
const { group, icon } = status;
|
||||
return (
|
||||
typeof group === 'string' &&
|
||||
group.length &&
|
||||
typeof icon === 'string' &&
|
||||
icon.startsWith('status_')
|
||||
);
|
||||
const { icon } = status;
|
||||
return typeof icon === 'string' && icon.startsWith('status_');
|
||||
},
|
||||
},
|
||||
showStatusText: {
|
||||
|
|
@ -62,66 +48,47 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.showTooltip && !this.showStatusText ? this.status?.text : '';
|
||||
},
|
||||
detailsPath() {
|
||||
// For now, this can either come from graphQL with camelCase or REST API in snake_case
|
||||
if (!this.useLink) {
|
||||
return null;
|
||||
if (this.showTooltip) {
|
||||
// show tooltip only when not showing text already
|
||||
return !this.showStatusText ? this.status?.text : null;
|
||||
}
|
||||
return this.status.detailsPath || this.status.details_path;
|
||||
return null;
|
||||
},
|
||||
wrapperStyleClasses() {
|
||||
const status = this.status.group;
|
||||
return `ci-status-icon ci-status-icon-${status} gl-rounded-full gl-justify-content-center gl-line-height-0`;
|
||||
ariaLabel() {
|
||||
// show aria-label only when text is not rendered
|
||||
if (!this.showStatusText) {
|
||||
return this.status?.text;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
href() {
|
||||
// href can come from GraphQL (camelCase) or REST API (snake_case)
|
||||
if (this.useLink) {
|
||||
return this.status.detailsPath || this.status.details_path;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
icon() {
|
||||
return this.status.icon;
|
||||
if (this.status.icon) {
|
||||
return `${this.status.icon}_borderless`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
badgeStyles() {
|
||||
variant() {
|
||||
switch (this.status.icon) {
|
||||
case 'status_success':
|
||||
return {
|
||||
textColor: 'gl-text-green-700',
|
||||
variant: 'success',
|
||||
};
|
||||
return 'success';
|
||||
case 'status_warning':
|
||||
return {
|
||||
textColor: 'gl-text-orange-700',
|
||||
variant: 'warning',
|
||||
};
|
||||
case 'status_failed':
|
||||
return {
|
||||
textColor: 'gl-text-red-700',
|
||||
variant: 'danger',
|
||||
};
|
||||
case 'status_running':
|
||||
return {
|
||||
textColor: 'gl-text-blue-700',
|
||||
variant: 'info',
|
||||
};
|
||||
case 'status_pending':
|
||||
return {
|
||||
textColor: 'gl-text-orange-700',
|
||||
variant: 'warning',
|
||||
};
|
||||
case 'status_canceled':
|
||||
return {
|
||||
textColor: 'gl-text-gray-700',
|
||||
variant: 'neutral',
|
||||
};
|
||||
case 'status_manual':
|
||||
return {
|
||||
textColor: 'gl-text-gray-700',
|
||||
variant: 'neutral',
|
||||
};
|
||||
return 'warning';
|
||||
case 'status_failed':
|
||||
return 'danger';
|
||||
case 'status_running':
|
||||
return 'info';
|
||||
// default covers the styles for the remainder of CI
|
||||
// statuses that are not explicitly stated here
|
||||
default:
|
||||
return {
|
||||
textColor: 'gl-text-gray-600',
|
||||
variant: 'muted',
|
||||
};
|
||||
return 'neutral';
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -131,30 +98,18 @@ export default {
|
|||
<gl-badge
|
||||
v-gl-tooltip
|
||||
class="ci-icon gl-p-2"
|
||||
:class="`ci-icon-variant-${variant}`"
|
||||
:variant="variant"
|
||||
:title="title"
|
||||
:aria-label="title"
|
||||
:href="detailsPath"
|
||||
:aria-label="ariaLabel"
|
||||
:href="href"
|
||||
size="md"
|
||||
:variant="badgeStyles.variant"
|
||||
data-testid="ci-icon"
|
||||
@click="$emit('ciStatusBadgeClick')"
|
||||
>
|
||||
<span
|
||||
class="ci-icon-wrapper"
|
||||
:class="[
|
||||
wrapperStyleClasses,
|
||||
{
|
||||
'gl-display-inline-block gl-vertical-align-top': showStatusText,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<gl-icon :name="icon" :aria-label="status.icon" /> </span
|
||||
><span
|
||||
v-if="showStatusText"
|
||||
class="gl-mx-2 gl-white-space-nowrap"
|
||||
:class="badgeStyles.textColor"
|
||||
data-testid="ci-icon-text"
|
||||
>{{ status.text }}</span
|
||||
>
|
||||
<span class="ci-icon-gl-icon-wrapper"><gl-icon :name="icon" /></span
|
||||
><span v-if="showStatusText" class="gl-mx-2 gl-white-space-nowrap" data-testid="ci-icon-text">{{
|
||||
status.text
|
||||
}}</span>
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,37 +1,18 @@
|
|||
@mixin icon-styles($primary-color, $svg-color) {
|
||||
@mixin icon-styles($color) {
|
||||
svg,
|
||||
.gl-icon {
|
||||
fill: $primary-color;
|
||||
}
|
||||
|
||||
// For the pipeline mini graph, we pass a custom 'gl-border' so that we can enforce
|
||||
// a border of 1px instead of the thicker svg borders to adhere to design standards.
|
||||
// If we implement the component with 'isBorderless' and also pass that border,
|
||||
// this css is to dynamically apply the correct border color for those specific icons.
|
||||
&.borderless {
|
||||
border-color: $primary-color;
|
||||
}
|
||||
|
||||
&.interactive {
|
||||
&:hover {
|
||||
background: $svg-color;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
box-shadow: 0 0 0 1px $primary-color;
|
||||
}
|
||||
fill: $color;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-success,
|
||||
.ci-status-icon-passed {
|
||||
@include icon-styles($green-500, $green-100);
|
||||
@include icon-styles($green-500);
|
||||
}
|
||||
|
||||
.ci-status-icon-error,
|
||||
.ci-status-icon-failed {
|
||||
@include icon-styles($red-500, $red-100);
|
||||
@include icon-styles($red-500);
|
||||
}
|
||||
|
||||
.ci-status-icon-pending,
|
||||
|
|
@ -39,18 +20,18 @@
|
|||
.ci-status-icon-waiting-for-callback,
|
||||
.ci-status-icon-failed-with-warnings,
|
||||
.ci-status-icon-success-with-warnings {
|
||||
@include icon-styles($orange-500, $orange-100);
|
||||
@include icon-styles($orange-500);
|
||||
}
|
||||
|
||||
.ci-status-icon-running {
|
||||
@include icon-styles($blue-500, $blue-100);
|
||||
@include icon-styles($blue-500);
|
||||
}
|
||||
|
||||
.ci-status-icon-canceled,
|
||||
.ci-status-icon-disabled,
|
||||
.ci-status-icon-scheduled,
|
||||
.ci-status-icon-manual {
|
||||
@include icon-styles($gray-900, $gray-100);
|
||||
@include icon-styles($gray-900);
|
||||
}
|
||||
|
||||
.ci-status-icon-notification,
|
||||
|
|
@ -58,7 +39,58 @@
|
|||
.ci-status-icon-created,
|
||||
.ci-status-icon-skipped,
|
||||
.ci-status-icon-notfound {
|
||||
@include icon-styles($gray-500, $gray-100);
|
||||
@include icon-styles($gray-500);
|
||||
}
|
||||
|
||||
.ci-icon {
|
||||
// .ci-icon class is used at
|
||||
// - app/assets/javascripts/vue_shared/components/ci_icon.vue
|
||||
// - app/helpers/ci/status_helper.rb
|
||||
.ci-icon-gl-icon-wrapper {
|
||||
@include gl-rounded-full;
|
||||
@include gl-line-height-0;
|
||||
}
|
||||
|
||||
// Makes the borderless CI icons appear slightly bigger than the default 16px.
|
||||
// Could be fixed by making the SVG fill up the canvas in a follow up issue.
|
||||
.gl-icon {
|
||||
// fill: currentColor;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: -2px;
|
||||
}
|
||||
|
||||
@mixin ci-icon-style($bg-color, $color, $gl-dark-bg-color: null, $gl-dark-color: null) {
|
||||
.ci-icon-gl-icon-wrapper {
|
||||
background-color: $bg-color;
|
||||
color: $color;
|
||||
|
||||
.gl-dark & {
|
||||
background-color: $gl-dark-bg-color;
|
||||
color: $gl-dark-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ci-icon-variant-success {
|
||||
@include ci-icon-style($green-500, $white, $green-600, $green-50)
|
||||
}
|
||||
|
||||
&.ci-icon-variant-warning {
|
||||
@include ci-icon-style($orange-500, $white, $orange-600, $orange-50)
|
||||
}
|
||||
|
||||
&.ci-icon-variant-danger {
|
||||
@include ci-icon-style($red-500, $white, $red-600, $red-50)
|
||||
}
|
||||
|
||||
&.ci-icon-variant-info {
|
||||
@include ci-icon-style($white, $blue-500, $blue-600, $blue-50)
|
||||
}
|
||||
|
||||
&.ci-icon-variant-neutral {
|
||||
@include ci-icon-style($white, $gray-500)
|
||||
}
|
||||
}
|
||||
|
||||
.password-status-icon-success {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ module Types
|
|||
# rubocop: disable Graphql/AuthorizeTypes -- The resolver authorizes the request
|
||||
class DegradationType < BaseObject
|
||||
graphql_name 'CodequalityReportsComparerReportDegradation'
|
||||
|
||||
description 'Represents a degradation on the compared codequality report.'
|
||||
|
||||
field :description, GraphQL::Types::String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Security
|
||||
module CodequalityReportsComparer
|
||||
class ReportGenerationStatusEnum < BaseEnum
|
||||
graphql_name 'CodequalityReportsComparerReportGenerationStatus'
|
||||
description 'Represents the generation status of the compared codequality report.'
|
||||
|
||||
value 'PARSED', value: :parsed, description: 'Report was generated.'
|
||||
value 'PARSING', value: :parsing, description: 'Report is being generated.'
|
||||
value 'ERROR', value: :error, description: 'An error happened while generating the report.'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,11 +4,11 @@ module Types
|
|||
module Security
|
||||
module CodequalityReportsComparer
|
||||
class StatusEnum < BaseEnum
|
||||
graphql_name 'CodequalityReportsComparerReportStatus'
|
||||
description 'Report comparison status'
|
||||
graphql_name 'CodequalityReportsComparerStatus'
|
||||
description 'Represents the state of the code quality report.'
|
||||
|
||||
value 'SUCCESS', value: 'success', description: 'Report successfully generated.'
|
||||
value 'FAILED', value: 'failed', description: 'Report failed to generate.'
|
||||
value 'SUCCESS', value: 'success', description: 'No degradations found in the head pipeline report.'
|
||||
value 'FAILED', value: 'failed', description: 'Report generated and there are new code quality degradations.'
|
||||
value 'NOT_FOUND', value: 'not_found', description: 'Head report or base report not found.'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ module Types
|
|||
|
||||
description 'Represents reports comparison for code quality.'
|
||||
|
||||
field :status,
|
||||
type: CodequalityReportsComparer::ReportGenerationStatusEnum,
|
||||
null: true,
|
||||
description: 'Compared codequality report generation status.'
|
||||
|
||||
field :report,
|
||||
type: CodequalityReportsComparer::ReportType,
|
||||
null: true,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ module Ci
|
|||
end
|
||||
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def ci_icon_for_status(status, size: 16)
|
||||
def ci_icon_for_status(status, size: 24)
|
||||
icon_name =
|
||||
if detailed_status?(status)
|
||||
status.icon
|
||||
|
|
@ -50,16 +50,12 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
icon_name = icon_name == 'play' ? icon_name : "#{icon_name}_borderless"
|
||||
|
||||
sprite_icon(icon_name, size: size, css_class: 'gl-icon')
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
def ci_icon_class_for_status(status)
|
||||
group = detailed_status?(status) ? status.group : status.dasherize
|
||||
|
||||
"ci-status-icon-#{group}"
|
||||
end
|
||||
|
||||
def pipeline_status_cache_key(pipeline_status)
|
||||
"pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}"
|
||||
end
|
||||
|
|
@ -85,16 +81,17 @@ module Ci
|
|||
show_status_text: false
|
||||
)
|
||||
variant = badge_variant(status)
|
||||
klass = "js-ci-status-badge-legacy ci-status-icon #{ci_icon_class_for_status(status)} gl-rounded-full gl-justify-content-center gl-line-height-0"
|
||||
badge_classes = "ci-icon ci-icon-variant-#{variant} gl-p-2 #{option_css_classes}"
|
||||
title = "#{_('Pipeline')}: #{ci_label_for_status(status)}"
|
||||
data = { toggle: 'tooltip', placement: tooltip_placement, container: container, testid: 'ci-icon' }
|
||||
badge_classes = "ci-icon gl-p-2 #{option_css_classes}"
|
||||
|
||||
icon_wrapper_class = "js-ci-status-badge-legacy ci-icon-gl-icon-wrapper"
|
||||
|
||||
gl_badge_tag(variant: variant, size: :md, href: path, class: badge_classes, title: title, data: data) do
|
||||
if show_status_text
|
||||
content_tag(:span, ci_icon_for_status(status), { class: klass }) + content_tag(:span, status.label, { class: 'gl-mx-2 gl-white-space-nowrap' })
|
||||
content_tag(:span, ci_icon_for_status(status), { class: icon_wrapper_class }) + content_tag(:span, status.label, { class: 'gl-mx-2 gl-white-space-nowrap', data: { testid: 'ci-icon-text' } })
|
||||
else
|
||||
content_tag(:span, ci_icon_for_status(status), { class: klass })
|
||||
content_tag(:span, ci_icon_for_status(status), { class: icon_wrapper_class })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -135,16 +132,18 @@ module Ci
|
|||
case variant
|
||||
when 'success'
|
||||
:success
|
||||
when 'success-with-warnings', 'pending'
|
||||
when 'success-with-warnings'
|
||||
:warning
|
||||
when 'pending'
|
||||
:warning
|
||||
when 'waiting-for-resource'
|
||||
:warning
|
||||
when 'failed'
|
||||
:danger
|
||||
when 'running'
|
||||
:info
|
||||
when 'canceled', 'manual'
|
||||
:neutral
|
||||
else
|
||||
:muted
|
||||
:neutral
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -132,6 +132,17 @@ module Nav
|
|||
)
|
||||
end
|
||||
|
||||
if Feature.enabled?(:ui_for_organizations, current_user) && current_user.can?(:create_organization)
|
||||
menu_items.push(
|
||||
::Gitlab::Nav::TopNavMenuItem.build(
|
||||
id: 'general_new_organization',
|
||||
title: s_('Organization|New organization'),
|
||||
href: new_organization_path,
|
||||
data: { track_action: 'click_link_new_organization_parent', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global_new_organization_link' }
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
if current_user.can?(:create_snippet)
|
||||
menu_items.push(
|
||||
::Gitlab::Nav::TopNavMenuItem.build(
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ module Ci
|
|||
DEFAULT_CONFIG_PATH = '.gitlab-ci.yml'
|
||||
|
||||
CANCELABLE_STATUSES = (Ci::HasStatus::CANCELABLE_STATUSES + ['manual']).freeze
|
||||
UNLOCKABLE_STATUSES = (Ci::Pipeline.completed_statuses + [:manual]).freeze
|
||||
|
||||
paginates_per 15
|
||||
|
||||
|
|
@ -272,6 +273,32 @@ module Ci
|
|||
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
|
||||
end
|
||||
|
||||
after_transition any => UNLOCKABLE_STATUSES do |pipeline|
|
||||
# This is a temporary flag that we added just in case we need to totally
|
||||
# stop unlocking pipelines due to unexpected issues during rollout.
|
||||
next if Feature.enabled?(:ci_stop_unlock_pipelines, pipeline.project)
|
||||
|
||||
next unless Feature.enabled?(:ci_unlock_non_successful_pipelines, pipeline.project)
|
||||
|
||||
pipeline.run_after_commit do
|
||||
Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(pipeline.ci_ref_id)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Remove this block once we've completed roll-out of ci_unlock_non_successful_pipelines
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/428408
|
||||
after_transition any => :success do |pipeline|
|
||||
# This is a temporary flag that we added just in case we need to totally
|
||||
# stop unlocking pipelines due to unexpected issues during rollout.
|
||||
next if Feature.enabled?(:ci_stop_unlock_pipelines, pipeline.project)
|
||||
|
||||
next unless Feature.disabled?(:ci_unlock_non_successful_pipelines, pipeline.project)
|
||||
|
||||
pipeline.run_after_commit do
|
||||
Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(pipeline.ci_ref_id)
|
||||
end
|
||||
end
|
||||
|
||||
after_transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success do |pipeline|
|
||||
# We wait a little bit to ensure that all Ci::BuildFinishedWorkers finish first
|
||||
# because this is where some metrics like code coverage is parsed and stored
|
||||
|
|
@ -395,6 +422,7 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
scope :with_unlockable_status, -> { with_status(*UNLOCKABLE_STATUSES) }
|
||||
scope :internal, -> { where(source: internal_sources) }
|
||||
scope :no_child, -> { where.not(source: :parent_pipeline) }
|
||||
scope :ci_sources, -> { where(source: Enums::Ci::Pipeline.ci_sources.values) }
|
||||
|
|
|
|||
|
|
@ -30,15 +30,6 @@ module Ci
|
|||
state :fixed, value: 3
|
||||
state :broken, value: 4
|
||||
state :still_failing, value: 5
|
||||
|
||||
after_transition any => [:fixed, :success] do |ci_ref|
|
||||
# Do not try to unlock if no artifacts are locked
|
||||
next unless ci_ref.artifacts_locked?
|
||||
|
||||
ci_ref.run_after_commit do
|
||||
Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(ci_ref.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
|
|
@ -75,5 +66,13 @@ module Ci
|
|||
self.status_name
|
||||
end
|
||||
end
|
||||
|
||||
def last_successful_ci_source_pipeline
|
||||
pipelines.ci_sources.success.order(id: :desc).first
|
||||
end
|
||||
|
||||
def last_unlockable_ci_source_pipeline
|
||||
pipelines.ci_sources.with_unlockable_status.order(id: :desc).first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ module Packages
|
|||
|
||||
validates :package_name_pattern, presence: true, uniqueness: { scope: [:project_id, :package_type] },
|
||||
length: { maximum: 255 }
|
||||
validates :package_name_pattern,
|
||||
format: {
|
||||
with: Gitlab::Regex.protection_rules_npm_package_name_pattern_regex,
|
||||
message: ->(_object, _data) { _('should be a valid NPM package name with optional wildcard characters.') }
|
||||
},
|
||||
if: :npm?
|
||||
validates :package_type, presence: true
|
||||
validates :push_protected_up_to_access_level, presence: true
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,12 @@ module Ci
|
|||
|
||||
BATCH_SIZE = 50
|
||||
ENQUEUE_INTERVAL_SECONDS = 0.1
|
||||
EXCLUDED_IDS_LIMIT = 1000
|
||||
|
||||
def execute(ci_ref, before_pipeline: nil)
|
||||
pipelines_scope = ci_ref.pipelines.artifacts_locked
|
||||
pipelines_scope = pipelines_scope.before_pipeline(before_pipeline) if before_pipeline
|
||||
total_new_entries = 0
|
||||
|
||||
pipelines_scope.each_batch(of: BATCH_SIZE) do |batch|
|
||||
pipelines_scope(ci_ref, before_pipeline).each_batch(of: BATCH_SIZE) do |batch|
|
||||
pipeline_ids = batch.pluck(:id) # rubocop: disable CodeReuse/ActiveRecord
|
||||
total_added = Ci::UnlockPipelineRequest.enqueue(pipeline_ids)
|
||||
total_new_entries += total_added
|
||||
|
|
@ -27,6 +26,34 @@ module Ci
|
|||
total_new_entries: total_new_entries
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pipelines_scope(ci_ref, before_pipeline)
|
||||
scope = ci_ref.pipelines.artifacts_locked
|
||||
|
||||
if before_pipeline
|
||||
# We use `same_family_pipeline_ids.map(&:id)` to force run the query and
|
||||
# specifically pass the array of IDs to the NOT IN condition. If not, we would
|
||||
# end up running the subquery for same_family_pipeline_ids on each batch instead.
|
||||
excluded_ids = before_pipeline.same_family_pipeline_ids.map(&:id)
|
||||
scope = scope.created_before_id(before_pipeline.id)
|
||||
|
||||
# When unlocking previous pipelines, we still want to keep the
|
||||
# last successful CI source pipeline locked.
|
||||
# If before_pipeline is not provided, like in the case of deleting a ref,
|
||||
# we want to unlock all pipelines instead.
|
||||
ci_ref.last_successful_ci_source_pipeline.try do |pipeline|
|
||||
excluded_ids.concat(pipeline.same_family_pipeline_ids.map(&:id))
|
||||
end
|
||||
|
||||
# We add a limit to the excluded IDs just to be safe and avoid any
|
||||
# arity issues with the NOT IN query.
|
||||
scope = scope.where.not(id: excluded_ids.take(EXCLUDED_IDS_LIMIT)) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
scope
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ module Ci
|
|||
|
||||
def perform(ref_id)
|
||||
::Ci::Ref.find_by_id(ref_id).try do |ref|
|
||||
pipeline = ref.last_finished_pipeline
|
||||
next unless ref.artifacts_locked?
|
||||
|
||||
pipeline = ref.last_unlockable_ci_source_pipeline
|
||||
result = ::Ci::Refs::EnqueuePipelinesToUnlockService.new.execute(ref, before_pipeline: pipeline)
|
||||
|
||||
log_extra_metadata_on_done(:total_pending_entries, result[:total_pending_entries])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_stop_unlock_pipelines
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134967
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428408
|
||||
milestone: '16.6'
|
||||
type: development
|
||||
group: group::pipeline security
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_unlock_non_successful_pipelines
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134967
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/428408
|
||||
milestone: '16.5'
|
||||
type: development
|
||||
group: group::pipeline security
|
||||
default_enabled: false
|
||||
|
|
@ -5,4 +5,4 @@ rollout_issue_url:
|
|||
milestone: '16.5'
|
||||
type: ops
|
||||
group: group::tenant scale
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
gitlab_schema_validation.add_suggestions_on_using_clusterwide_schema
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../tooling/danger/gitlab_schema_validation_suggestion'
|
||||
|
||||
module Danger
|
||||
class GitlabSchemaValidation < ::Danger::Plugin
|
||||
include Tooling::Danger::GitlabSchemaValidationSuggestion
|
||||
end
|
||||
end
|
||||
|
|
@ -15393,6 +15393,7 @@ Represents reports comparison for code quality.
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="codequalityreportscomparerreport"></a>`report` | [`CodequalityReportsComparerReport`](#codequalityreportscomparerreport) | Compared codequality report. |
|
||||
| <a id="codequalityreportscomparerstatus"></a>`status` | [`CodequalityReportsComparerReportGenerationStatus`](#codequalityreportscomparerreportgenerationstatus) | Compared codequality report generation status. |
|
||||
|
||||
### `CodequalityReportsComparerReport`
|
||||
|
||||
|
|
@ -15405,7 +15406,7 @@ Represents compared code quality report.
|
|||
| <a id="codequalityreportscomparerreportexistingerrors"></a>`existingErrors` | [`[CodequalityReportsComparerReportDegradation!]`](#codequalityreportscomparerreportdegradation) | All code quality degradations. |
|
||||
| <a id="codequalityreportscomparerreportnewerrors"></a>`newErrors` | [`[CodequalityReportsComparerReportDegradation!]!`](#codequalityreportscomparerreportdegradation) | New code quality degradations. |
|
||||
| <a id="codequalityreportscomparerreportresolvederrors"></a>`resolvedErrors` | [`[CodequalityReportsComparerReportDegradation!]`](#codequalityreportscomparerreportdegradation) | Resolved code quality degradations. |
|
||||
| <a id="codequalityreportscomparerreportstatus"></a>`status` | [`CodequalityReportsComparerReportStatus!`](#codequalityreportscomparerreportstatus) | Status of report. |
|
||||
| <a id="codequalityreportscomparerreportstatus"></a>`status` | [`CodequalityReportsComparerStatus!`](#codequalityreportscomparerstatus) | Status of report. |
|
||||
| <a id="codequalityreportscomparerreportsummary"></a>`summary` | [`CodequalityReportsComparerReportSummary!`](#codequalityreportscomparerreportsummary) | Codequality report summary. |
|
||||
|
||||
### `CodequalityReportsComparerReportDegradation`
|
||||
|
|
@ -28124,15 +28125,25 @@ Values for sorting variables.
|
|||
| <a id="codequalitydegradationseverityminor"></a>`MINOR` | Code Quality degradation has a status of minor. |
|
||||
| <a id="codequalitydegradationseverityunknown"></a>`UNKNOWN` | Code Quality degradation has a status of unknown. |
|
||||
|
||||
### `CodequalityReportsComparerReportStatus`
|
||||
### `CodequalityReportsComparerReportGenerationStatus`
|
||||
|
||||
Report comparison status.
|
||||
Represents the generation status of the compared codequality report.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="codequalityreportscomparerreportstatusfailed"></a>`FAILED` | Report failed to generate. |
|
||||
| <a id="codequalityreportscomparerreportstatusnot_found"></a>`NOT_FOUND` | Head report or base report not found. |
|
||||
| <a id="codequalityreportscomparerreportstatussuccess"></a>`SUCCESS` | Report successfully generated. |
|
||||
| <a id="codequalityreportscomparerreportgenerationstatuserror"></a>`ERROR` | An error happened while generating the report. |
|
||||
| <a id="codequalityreportscomparerreportgenerationstatusparsed"></a>`PARSED` | Report was generated. |
|
||||
| <a id="codequalityreportscomparerreportgenerationstatusparsing"></a>`PARSING` | Report is being generated. |
|
||||
|
||||
### `CodequalityReportsComparerStatus`
|
||||
|
||||
Represents the state of the code quality report.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="codequalityreportscomparerstatusfailed"></a>`FAILED` | Report generated and there are new code quality degradations. |
|
||||
| <a id="codequalityreportscomparerstatusnot_found"></a>`NOT_FOUND` | Head report or base report not found. |
|
||||
| <a id="codequalityreportscomparerstatussuccess"></a>`SUCCESS` | No degradations found in the head pipeline report. |
|
||||
|
||||
### `CommitActionMode`
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
## Implementing an experiment
|
||||
|
||||
[Examples](https://gitlab.com/gitlab-org/growth/growth/-/wikis/GLEX-Framework-code-examples)
|
||||
[Examples](https://gitlab.com/groups/gitlab-org/growth/-/wikis/GLEX-How-Tos)
|
||||
|
||||
Start by generating a feature flag using the `bin/feature-flag` command as you
|
||||
usually would for a development feature flag, making sure to use `experiment` for
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ RSpec/FeatureCategory:
|
|||
Style/HashSyntax:
|
||||
Enabled: false
|
||||
|
||||
Style/InlineDisableAnnotation:
|
||||
Enabled: false
|
||||
|
||||
Style/Lambda:
|
||||
EnforcedStyle: literal
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ module Gitlab
|
|||
extend self
|
||||
extend MergeRequests
|
||||
extend Packages
|
||||
extend Packages::Protection::Rules
|
||||
|
||||
def project_name_regex
|
||||
# The character range \p{Alnum} overlaps with \u{00A9}-\u{1f9ff}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
module Gitlab
|
||||
module Regex
|
||||
module Packages
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
|
||||
CONAN_RECIPE_FILES = %w[conanfile.py conanmanifest.txt conan_sources.tgz conan_export.tgz].freeze
|
||||
CONAN_PACKAGE_FILES = %w[conaninfo.txt conanmanifest.txt conan_package.tgz].freeze
|
||||
|
||||
|
|
@ -74,8 +76,10 @@ module Gitlab
|
|||
maven_app_name_regex
|
||||
end
|
||||
|
||||
def npm_package_name_regex
|
||||
@npm_package_name_regex ||= %r{\A(?:@(#{Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX})/)?[-+\.\_a-zA-Z0-9]+\z}o
|
||||
def npm_package_name_regex(other_accepted_chars = nil)
|
||||
strong_memoize_with(:npm_package_name_regex, other_accepted_chars) do
|
||||
%r{\A(?:@(#{Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX})/)?[-+\.\_a-zA-Z0-9#{other_accepted_chars}]+\z}
|
||||
end
|
||||
end
|
||||
|
||||
def npm_package_name_regex_message
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Regex
|
||||
module Packages
|
||||
module Protection
|
||||
module Rules
|
||||
def protection_rules_npm_package_name_pattern_regex
|
||||
@protection_rules_npm_package_name_pattern_regex ||= npm_package_name_regex('*')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -44313,6 +44313,9 @@ msgstr ""
|
|||
msgid "Session duration (minutes)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Session|There was a error loading the user verification challenge. Refresh to try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set %{epic_ref} as the parent epic."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -57830,6 +57833,9 @@ msgstr ""
|
|||
msgid "severity|Unknown"
|
||||
msgstr ""
|
||||
|
||||
msgid "should be a valid NPM package name with optional wildcard characters."
|
||||
msgstr ""
|
||||
|
||||
msgid "should be an array of %{object_name} objects"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ module QA
|
|||
def has_build?(name, status: :success, wait: nil)
|
||||
if status
|
||||
within_element('job-item-container', text: name) do
|
||||
has_selector?(".ci-status-icon-#{status}", **{ wait: wait }.compact)
|
||||
has_selector?("[data-testid='status_#{status}_borderless-icon']", **{ wait: wait }.compact)
|
||||
end
|
||||
else
|
||||
has_element?('job-item-container', text: name)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
module QA
|
||||
RSpec.describe 'Verify', :runner, product_group: :pipeline_security do
|
||||
describe "Unlocking job artifacts across pipelines" do
|
||||
describe "Unlocking job artifacts across pipelines", feature_flag: { name: 'ci_unlock_non_successful_pipelines,
|
||||
scope: :project' } do
|
||||
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
|
||||
let(:project) { create(:project, name: 'unlock-job-artifacts-project') }
|
||||
|
||||
|
|
@ -15,11 +16,13 @@ module QA
|
|||
end
|
||||
|
||||
before do
|
||||
Runtime::Feature.enable(:ci_unlock_non_successful_pipelines, project: project)
|
||||
Flow::Login.sign_in
|
||||
project.visit!
|
||||
end
|
||||
|
||||
after do
|
||||
Runtime::Feature.disable(:ci_unlock_non_successful_pipelines, project: project)
|
||||
runner.remove_via_api!
|
||||
end
|
||||
|
||||
|
|
@ -59,11 +62,7 @@ module QA
|
|||
end
|
||||
|
||||
it 'keeps job artifacts from latest failed pipelines and from latest successful pipeline',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/394808',
|
||||
quarantine: {
|
||||
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/266958',
|
||||
type: :bug
|
||||
} do
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/394808' do
|
||||
update_ci_file(job_name: 'failed_job_1', script: 'exit 1')
|
||||
Flow::Pipeline.wait_for_latest_pipeline(status: 'Failed')
|
||||
|
||||
|
|
@ -94,11 +93,7 @@ module QA
|
|||
end
|
||||
|
||||
it 'keeps job artifacts from the latest blocked pipeline and from latest successful pipeline',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/395511',
|
||||
quarantine: {
|
||||
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/387087',
|
||||
type: :bug
|
||||
} do
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/395511' do
|
||||
update_ci_with_manual_job(job_name: 'successful_job_with_manual_1', script: 'echo test')
|
||||
Flow::Pipeline.wait_for_latest_pipeline(status: 'Blocked')
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
module Style
|
||||
# rubocop:disable Lint/RedundantCopDisableDirective -- For examples
|
||||
# Checks that rubocop inline disabling is formatted according
|
||||
# to guidelines.
|
||||
# See: https://docs.gitlab.com/ee/development/rubocop_development_guide.html#disabling-rules-inline,
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/428762
|
||||
#
|
||||
# # bad
|
||||
# # rubocop:disable Some/Cop, Another/Cop
|
||||
#
|
||||
# # good
|
||||
# # rubocop:disable Some/Cop, Another/Cop -- Some reason
|
||||
#
|
||||
# rubocop:enable Lint/RedundantCopDisableDirective
|
||||
class InlineDisableAnnotation < RuboCop::Cop::Base
|
||||
include RangeHelp
|
||||
|
||||
COP_DISABLE = '#\s*rubocop\s*:\s*(?:disable|todo)\s+'
|
||||
BAD_DISABLE = %r{\A(?<line>(?<disabling>#{COP_DISABLE}(?:[\w/]+(?:\s*,\s*[\w/]+)*))\s*)(?!.*\s*--\s\S).*}
|
||||
COP_DISABLE_LINE = /\A(?<line>#{COP_DISABLE}.*)\Z/
|
||||
MSG = <<~MESSAGE
|
||||
Inline disabling a cop needs to follow the format of `%{disable} -- Some reason`.
|
||||
See https://docs.gitlab.com/ee/development/rubocop_development_guide.html#disabling-rules-inline.
|
||||
MESSAGE
|
||||
|
||||
def on_new_investigation
|
||||
processed_source.comments.each do |comment|
|
||||
candidate_match = COP_DISABLE_LINE.match(comment.text)
|
||||
# Pre-filter to ensure we are on a comment that is for a rubocop disabling
|
||||
next unless candidate_match
|
||||
|
||||
bad_match = BAD_DISABLE.match(comment.text)
|
||||
# Only the badly formatted lines make it past this.
|
||||
next unless bad_match
|
||||
|
||||
add_offense(
|
||||
source_range(
|
||||
processed_source.buffer, comment.loc.line, comment.loc.column, candidate_match[:line].length
|
||||
),
|
||||
message: format(MSG, disable: bad_match[:disabling])
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -82,7 +82,7 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
|
|||
it 'shows correct build status from default branch' do
|
||||
page.within("//li[@id='commit-#{pipeline.short_sha}']") do
|
||||
expect(page).to have_css("[data-testid='ci-icon']")
|
||||
expect(page).to have_css('.ci-status-icon-success')
|
||||
expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects
|
|||
page.within('[data-testid="project_controls"]') do
|
||||
expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
|
||||
expect(page).to have_css("[data-testid='ci-icon']")
|
||||
expect(page).to have_css('.ci-status-icon-success')
|
||||
expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
|
||||
expect(page).to have_link('Pipeline: passed')
|
||||
end
|
||||
end
|
||||
|
|
@ -166,7 +166,7 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects
|
|||
page.within('[data-testid="project_controls"]') do
|
||||
expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
|
||||
expect(page).not_to have_css("[data-testid='ci-icon']")
|
||||
expect(page).not_to have_css('.ci-status-icon-success')
|
||||
expect(page).not_to have_css('[data-testid="status_success_borderless-icon"]')
|
||||
expect(page).not_to have_link('Pipeline: passed')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
|
|||
expect(page).to have_content("Merge blocked")
|
||||
expect(page).to have_content(
|
||||
"pipeline must succeed. It's waiting for a manual action to continue.")
|
||||
expect(page).to have_css('.ci-status-icon-manual')
|
||||
expect(page).to have_css('[data-testid="status_manual_borderless-icon"]')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -941,9 +941,7 @@ RSpec.describe 'File blob', :js, feature_category: :groups_and_projects do
|
|||
|
||||
it 'shows the realtime pipeline status' do
|
||||
page.within('.commit-actions') do
|
||||
expect(page).to have_css('.ci-status-icon')
|
||||
expect(page).to have_css('.ci-status-icon-running')
|
||||
expect(page).to have_selector('[data-testid="status_running-icon"]')
|
||||
expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -299,13 +299,13 @@ RSpec.describe 'Branches', feature_category: :groups_and_projects do
|
|||
|
||||
it 'shows pipeline status when available' do
|
||||
page.within first('.all-branches li') do
|
||||
expect(page).to have_css 'a.gl-badge .ci-status-icon-success'
|
||||
expect(page).to have_css '[data-testid="status_success_borderless-icon"]'
|
||||
end
|
||||
end
|
||||
|
||||
it 'displays a placeholder when not available' do
|
||||
page.all('.all-branches li') do |li|
|
||||
expect(li).to have_css '.pipeline-status svg.s16'
|
||||
expect(li).to have_css '.pipeline-status svg.s24'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -317,7 +317,7 @@ RSpec.describe 'Branches', feature_category: :groups_and_projects do
|
|||
|
||||
it 'does not show placeholder or pipeline status' do
|
||||
page.all('.all-branches') do |branches|
|
||||
expect(branches).not_to have_css '.pipeline-status svg.s16'
|
||||
expect(branches).not_to have_css '.pipeline-status svg.s24'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :sou
|
|||
end
|
||||
|
||||
it 'display icon with status' do
|
||||
expect(page).to have_selector('.ci-status-icon-running')
|
||||
expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
|
||||
end
|
||||
|
||||
it 'displays a mini pipeline graph' do
|
||||
|
|
@ -63,7 +63,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :sou
|
|||
wait_for_requests
|
||||
|
||||
page.within '.js-builds-dropdown-list' do
|
||||
expect(page).to have_selector('.ci-status-icon-running')
|
||||
expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
|
||||
expect(page).to have_content(build.stage_name)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ RSpec.describe 'user reads pipeline status', :js, feature_category: :groups_and_
|
|||
|
||||
page.within('.commit-detail') do
|
||||
expect(page).to have_link('', href: project_pipeline_path(project, expected_pipeline))
|
||||
expect(page).to have_selector(".ci-status-icon-#{expected_pipeline.status}")
|
||||
expect(page).to have_selector("[data-testid='status_#{expected_pipeline.status}_borderless-icon']")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
|
||||
it 'shows a running icon and a cancel action for the running build' do
|
||||
page.within('#ci-badge-deploy') do
|
||||
expect(page).to have_selector('[data-testid="status_running-icon"]')
|
||||
expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
|
||||
expect(page).to have_selector('.js-icon-cancel')
|
||||
expect(page).to have_content('deploy')
|
||||
end
|
||||
|
|
@ -187,7 +187,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
|
||||
it 'shows a preparing icon and a cancel action' do
|
||||
page.within('#ci-badge-prepare') do
|
||||
expect(page).to have_selector('[data-testid="status_preparing-icon"]')
|
||||
expect(page).to have_selector('[data-testid="status_preparing_borderless-icon"]')
|
||||
expect(page).to have_selector('.js-icon-cancel')
|
||||
expect(page).to have_content('prepare')
|
||||
end
|
||||
|
|
@ -209,7 +209,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
|
||||
it 'shows the success icon and a retry action for the successful build' do
|
||||
page.within('#ci-badge-build') do
|
||||
expect(page).to have_selector('[data-testid="status_success-icon"]')
|
||||
expect(page).to have_selector('[data-testid="status_success_borderless-icon"]')
|
||||
expect(page).to have_content('build')
|
||||
end
|
||||
|
||||
|
|
@ -238,7 +238,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
|
||||
it 'shows the scheduled icon and an unschedule action for the delayed job' do
|
||||
page.within('#ci-badge-delayed-job') do
|
||||
expect(page).to have_selector('[data-testid="status_scheduled-icon"]')
|
||||
expect(page).to have_selector('[data-testid="status_scheduled_borderless-icon"]')
|
||||
expect(page).to have_content('delayed-job')
|
||||
end
|
||||
|
||||
|
|
@ -263,7 +263,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
|
||||
it 'shows the failed icon and a retry action for the failed build' do
|
||||
page.within('#ci-badge-test') do
|
||||
expect(page).to have_selector('[data-testid="status_failed-icon"]')
|
||||
expect(page).to have_selector('[data-testid="status_failed_borderless-icon"]')
|
||||
expect(page).to have_content('test')
|
||||
end
|
||||
|
||||
|
|
@ -297,7 +297,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
|
||||
it 'shows the skipped icon and a play action for the manual build' do
|
||||
page.within('#ci-badge-manual-build') do
|
||||
expect(page).to have_selector('[data-testid="status_manual-icon"]')
|
||||
expect(page).to have_selector('[data-testid="status_manual_borderless-icon"]')
|
||||
expect(page).to have_content('manual')
|
||||
end
|
||||
|
||||
|
|
@ -323,7 +323,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
end
|
||||
|
||||
it 'shows the success icon and the generic comit status build' do
|
||||
expect(page).to have_selector('[data-testid="status_success-icon"]')
|
||||
expect(page).to have_selector('[data-testid="status_success_borderless-icon"]')
|
||||
expect(page).to have_content('jenkins')
|
||||
expect(page).to have_link('jenkins', href: 'http://gitlab.com/status')
|
||||
end
|
||||
|
|
@ -358,7 +358,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
let(:status) { :success }
|
||||
|
||||
it 'does not show the cancel or retry action' do
|
||||
expect(page).to have_selector('.ci-status-icon-success')
|
||||
expect(page).to have_selector('[data-testid="status_success_borderless-icon"]')
|
||||
expect(page).not_to have_selector('button[aria-label="Retry downstream pipeline"]')
|
||||
expect(page).not_to have_selector('button[aria-label="Cancel downstream pipeline"]')
|
||||
end
|
||||
|
|
@ -379,7 +379,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
|
||||
it 'shows the pipeline as canceled with the retry action' do
|
||||
expect(page).to have_selector('button[aria-label="Retry downstream pipeline"]')
|
||||
expect(page).to have_selector('.ci-status-icon-canceled')
|
||||
expect(page).to have_selector('[data-testid="status_canceled_borderless-icon"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -398,7 +398,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
end
|
||||
|
||||
it 'shows running pipeline with the cancel action' do
|
||||
expect(page).to have_selector('.ci-status-icon-running')
|
||||
expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
|
||||
expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]')
|
||||
end
|
||||
end
|
||||
|
|
@ -418,7 +418,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
end
|
||||
|
||||
it 'shows running pipeline with the cancel action' do
|
||||
expect(page).to have_selector('.ci-status-icon-running')
|
||||
expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
|
||||
expect(page).to have_selector('button[aria-label="Cancel downstream pipeline"]')
|
||||
end
|
||||
end
|
||||
|
|
@ -438,7 +438,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
end
|
||||
|
||||
it 'does not show the retry button' do
|
||||
expect(page).to have_selector('.ci-status-icon-failed')
|
||||
expect(page).to have_selector('[data-testid="status_failed_borderless-icon"]')
|
||||
expect(page).not_to have_selector('button[aria-label="Retry downstream pipeline"]')
|
||||
end
|
||||
end
|
||||
|
|
@ -782,8 +782,8 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
expect(page).to have_content('Cancel pipeline')
|
||||
end
|
||||
|
||||
it 'does not link to job', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/408215' do
|
||||
expect(page).not_to have_selector('.js-pipeline-graph-job-link')
|
||||
it 'does link to job' do
|
||||
expect(page).to have_selector('.js-pipeline-graph-job-link')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -906,12 +906,12 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
within('.js-pipeline-graph') do
|
||||
within(all('[data-testid="stage-column"]')[0]) do
|
||||
expect(page).to have_content('test')
|
||||
expect(page).to have_css('.ci-status-icon-pending')
|
||||
expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
|
||||
end
|
||||
|
||||
within(all('[data-testid="stage-column"]')[1]) do
|
||||
expect(page).to have_content('deploy')
|
||||
expect(page).to have_css('.ci-status-icon-created')
|
||||
expect(page).to have_css('[data-testid="status_created_borderless-icon"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -931,12 +931,12 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
within('.js-pipeline-graph') do
|
||||
within(all('[data-testid="stage-column"]')[0]) do
|
||||
expect(page).to have_content('test')
|
||||
expect(page).to have_css('.ci-status-icon-success')
|
||||
expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
|
||||
end
|
||||
|
||||
within(all('[data-testid="stage-column"]')[1]) do
|
||||
expect(page).to have_content('deploy')
|
||||
expect(page).to have_css('.ci-status-icon-pending')
|
||||
expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -960,7 +960,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
within('.js-pipeline-graph') do
|
||||
within(all('[data-testid="stage-column"]')[1]) do
|
||||
expect(page).to have_content('deploy')
|
||||
expect(page).to have_css('.ci-status-icon-waiting-for-resource')
|
||||
expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -980,7 +980,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
within('.js-pipeline-graph') do
|
||||
within(all('[data-testid="stage-column"]')[1]) do
|
||||
expect(page).to have_content('deploy')
|
||||
expect(page).to have_css('.ci-status-icon-pending')
|
||||
expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1008,7 +1008,7 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
|
|||
within('.js-pipeline-graph') do
|
||||
within(all('[data-testid="stage-column"]')[1]) do
|
||||
expect(page).to have_content('deploy')
|
||||
expect(page).to have_css('.ci-status-icon-waiting-for-resource')
|
||||
expect(page).to have_css('[data-testid="status_pending_borderless-icon"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ describe('pipeline graph job item', () => {
|
|||
expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`);
|
||||
|
||||
expect(findJobCiIcon().exists()).toBe(true);
|
||||
expect(findJobCiIcon().find('.ci-status-icon-success').exists()).toBe(true);
|
||||
expect(findJobCiIcon().find('[data-testid="status_success_borderless-icon"]').exists()).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toBe(mockJob.name);
|
||||
});
|
||||
|
|
@ -106,7 +108,9 @@ describe('pipeline graph job item', () => {
|
|||
|
||||
it('should render status and name', () => {
|
||||
expect(findJobCiIcon().exists()).toBe(true);
|
||||
expect(findJobCiIcon().find('.ci-status-icon-success').exists()).toBe(true);
|
||||
expect(findJobCiIcon().find('[data-testid="status_success_borderless-icon"]').exists()).toBe(
|
||||
true,
|
||||
);
|
||||
expect(findJobLink().exists()).toBe(false);
|
||||
|
||||
expect(wrapper.text()).toBe(mockJobWithoutDetails.name);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@ describe('job name component', () => {
|
|||
|
||||
it('should render an icon with the provided status', () => {
|
||||
expect(wrapper.findComponent(CiIcon).exists()).toBe(true);
|
||||
expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ describe('Linked pipeline', () => {
|
|||
});
|
||||
|
||||
it('should render the pipeline status icon svg', () => {
|
||||
expect(wrapper.find('.ci-status-icon-success svg').exists()).toBe(true);
|
||||
expect(wrapper.findByTestId('status_success_borderless-icon').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have a ci-status child component', () => {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ describe('Linked pipeline mini list', () => {
|
|||
});
|
||||
|
||||
it('should render the correct ci status icon', () => {
|
||||
expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
|
||||
expect(wrapper.find('[data-testid="status_running_borderless-icon"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have an activated tooltip', () => {
|
||||
|
|
@ -95,7 +95,7 @@ describe('Linked pipeline mini list', () => {
|
|||
});
|
||||
|
||||
it('should render the correct ci status icon', () => {
|
||||
expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
|
||||
expect(wrapper.find('[data-testid="status_running_borderless-icon"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should have an activated tooltip', () => {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ describe('IDE job description', () => {
|
|||
});
|
||||
|
||||
it('renders CI icon', () => {
|
||||
expect(wrapper.find('.ci-status-icon').findComponent(GlIcon).exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="ci-icon"]').findComponent(GlIcon).exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders bridge job details without the job link', () => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ describe('IDE jobs item', () => {
|
|||
});
|
||||
|
||||
it('renders CI icon', () => {
|
||||
expect(wrapper.find('[data-testid="status_success-icon"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="ci-icon"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render view logs button if not started', async () => {
|
||||
|
|
|
|||
|
|
@ -2,52 +2,135 @@ import { GlIcon } from '@gitlab/ui';
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
|
||||
const mockStatus = {
|
||||
group: 'success',
|
||||
icon: 'status_success',
|
||||
text: 'Success',
|
||||
};
|
||||
|
||||
describe('CI Icon component', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props) => {
|
||||
const createComponent = ({ props = {} } = {}) => {
|
||||
wrapper = shallowMount(CiIcon, {
|
||||
propsData: {
|
||||
status: mockStatus,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('should render a span element with an svg', () => {
|
||||
createComponent({
|
||||
status: {
|
||||
group: 'success',
|
||||
icon: 'status_success',
|
||||
},
|
||||
});
|
||||
const findIcon = () => wrapper.findComponent(GlIcon);
|
||||
|
||||
expect(wrapper.find('span').exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
|
||||
it('should render a span element and an icon', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.attributes('size')).toBe('md');
|
||||
expect(findIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('rendering a status', () => {
|
||||
it.each`
|
||||
icon | group | cssClass
|
||||
${'status_success'} | ${'success'} | ${'ci-status-icon-success'}
|
||||
${'status_failed'} | ${'failed'} | ${'ci-status-icon-failed'}
|
||||
${'status_warning'} | ${'warning'} | ${'ci-status-icon-warning'}
|
||||
${'status_pending'} | ${'pending'} | ${'ci-status-icon-pending'}
|
||||
${'status_running'} | ${'running'} | ${'ci-status-icon-running'}
|
||||
${'status_created'} | ${'created'} | ${'ci-status-icon-created'}
|
||||
${'status_skipped'} | ${'skipped'} | ${'ci-status-icon-skipped'}
|
||||
${'status_canceled'} | ${'canceled'} | ${'ci-status-icon-canceled'}
|
||||
${'status_manual'} | ${'manual'} | ${'ci-status-icon-manual'}
|
||||
`('should render a $group status', ({ icon, group, cssClass }) => {
|
||||
wrapper = shallowMount(CiIcon, {
|
||||
propsData: {
|
||||
status: {
|
||||
icon,
|
||||
group,
|
||||
describe.each`
|
||||
showStatusText | showTooltip | expectedText | expectedTooltip | expectedAriaLabel
|
||||
${true} | ${true} | ${'Success'} | ${undefined} | ${undefined}
|
||||
${true} | ${false} | ${'Success'} | ${undefined} | ${undefined}
|
||||
${false} | ${true} | ${''} | ${'Success'} | ${'Success'}
|
||||
${false} | ${false} | ${''} | ${undefined} | ${'Success'}
|
||||
`(
|
||||
'when showStatusText is %{showStatusText} and showTooltip is %{showTooltip}',
|
||||
({ showStatusText, showTooltip, expectedText, expectedTooltip, expectedAriaLabel }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
showStatusText,
|
||||
showTooltip,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`aria-label is ${expectedAriaLabel}`, () => {
|
||||
expect(wrapper.attributes('aria-label')).toBe(expectedAriaLabel);
|
||||
});
|
||||
|
||||
it(`text shown is ${expectedAriaLabel}`, () => {
|
||||
expect(wrapper.text()).toBe(expectedText);
|
||||
});
|
||||
|
||||
it(`tooltip shown is ${expectedAriaLabel}`, () => {
|
||||
expect(wrapper.attributes('title')).toBe(expectedTooltip);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('when appearing as a link', () => {
|
||||
it('shows a GraphQL path', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
status: {
|
||||
...mockStatus,
|
||||
detailsPath: '/path',
|
||||
},
|
||||
useLink: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.ci-icon-wrapper').classes()).toContain(cssClass);
|
||||
expect(wrapper.attributes('href')).toBe('/path');
|
||||
});
|
||||
|
||||
it('shows a REST API path', () => {
|
||||
createComponent({
|
||||
props: {
|
||||
status: {
|
||||
...mockStatus,
|
||||
details_path: '/path',
|
||||
},
|
||||
useLink: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.attributes('href')).toBe('/path');
|
||||
});
|
||||
|
||||
it('shows no path', () => {
|
||||
createComponent({
|
||||
status: {
|
||||
detailsPath: '/path',
|
||||
details_path: '/path',
|
||||
},
|
||||
props: {
|
||||
useLink: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.attributes('href')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering a status icon and class', () => {
|
||||
it.each`
|
||||
icon | variant
|
||||
${'status_success'} | ${'success'}
|
||||
${'status_warning'} | ${'warning'}
|
||||
${'status_pending'} | ${'warning'}
|
||||
${'status_failed'} | ${'danger'}
|
||||
${'status_running'} | ${'info'}
|
||||
${'status_created'} | ${'neutral'}
|
||||
${'status_skipped'} | ${'neutral'}
|
||||
${'status_canceled'} | ${'neutral'}
|
||||
${'status_manual'} | ${'neutral'}
|
||||
`('should render a $group status', ({ icon, variant }) => {
|
||||
createComponent({
|
||||
props: {
|
||||
status: {
|
||||
...mockStatus,
|
||||
icon,
|
||||
},
|
||||
showStatusText: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper.attributes('variant')).toBe(variant);
|
||||
expect(wrapper.classes(`ci-icon-variant-${variant}`)).toBe(true);
|
||||
|
||||
expect(findIcon().props('name')).toBe(`${icon}_borderless`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['CodequalityReportsComparerReportGenerationStatus'], feature_category: :code_quality do
|
||||
specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparerReportGenerationStatus') }
|
||||
|
||||
it 'exposes all codequality report status values' do
|
||||
expect(described_class.values.keys).to contain_exactly('PARSED', 'PARSING', 'ERROR')
|
||||
end
|
||||
end
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['CodequalityReportsComparerReportStatus'], feature_category: :code_quality do
|
||||
specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparerReportStatus') }
|
||||
RSpec.describe GitlabSchema.types['CodequalityReportsComparerStatus'], feature_category: :code_quality do
|
||||
specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparerStatus') }
|
||||
|
||||
it 'exposes all codequality report status values' do
|
||||
expect(described_class.values.keys).to contain_exactly('SUCCESS', 'FAILED', 'NOT_FOUND')
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ RSpec.describe GitlabSchema.types['CodequalityReportsComparer'], feature_categor
|
|||
specify { expect(described_class.graphql_name).to eq('CodequalityReportsComparer') }
|
||||
|
||||
it 'has expected fields' do
|
||||
expect(described_class).to have_graphql_fields(:report)
|
||||
expect(described_class).to have_graphql_fields(:status, :report)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ RSpec.describe Ci::StatusHelper do
|
|||
end
|
||||
|
||||
it "has the success status icon" do
|
||||
is_expected.to include("ci-status-icon-success")
|
||||
is_expected.to include("ci-icon-variant-success")
|
||||
end
|
||||
|
||||
context "when pipeline has commit path" do
|
||||
|
|
@ -44,7 +44,19 @@ RSpec.describe Ci::StatusHelper do
|
|||
end
|
||||
|
||||
it "has the correct status icon" do
|
||||
is_expected.to include("ci-status-icon-success")
|
||||
is_expected.to include("ci-icon-variant-success")
|
||||
end
|
||||
end
|
||||
|
||||
context "when showing status text" do
|
||||
subject do
|
||||
detailed_status = Gitlab::Ci::Status::Success.new(build(:ci_build, :success), build(:user))
|
||||
helper.render_ci_icon(detailed_status, show_status_text: true)
|
||||
end
|
||||
|
||||
it "contains status text" do
|
||||
is_expected.to include("data-testid=\"ci-icon-text\"")
|
||||
is_expected.to include("passed")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -79,56 +91,35 @@ RSpec.describe Ci::StatusHelper do
|
|||
is_expected.to include('gl-badge badge badge-pill badge-neutral')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#badge_variant' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
describe 'badge and icon appearance' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:status, :expected_badge_variant_class) do
|
||||
'success' | 'badge-success'
|
||||
'success-with-warnings' | 'badge-warning'
|
||||
'pending' | 'badge-warning'
|
||||
'failed' | 'badge-danger'
|
||||
'running' | 'badge-info'
|
||||
'canceled' | 'badge-neutral'
|
||||
'manual' | 'badge-neutral'
|
||||
'other-status' | 'badge-muted'
|
||||
end
|
||||
|
||||
with_them do
|
||||
subject { helper.render_ci_icon(status) }
|
||||
|
||||
it 'uses the correct badge variant classes for gl-badge' do
|
||||
is_expected.to include("gl-badge badge badge-pill #{expected_badge_variant_class}")
|
||||
where(:status, :icon, :badge_variant) do
|
||||
'success' | 'status_success_borderless' | 'success'
|
||||
'success-with-warnings' | 'status_warning_borderless' | 'warning'
|
||||
'pending' | 'status_pending_borderless' | 'warning'
|
||||
'waiting-for-resource' | 'status_pending_borderless' | 'warning'
|
||||
'failed' | 'status_failed_borderless' | 'danger'
|
||||
'running' | 'status_running_borderless' | 'info'
|
||||
'preparing' | 'status_preparing_borderless' | 'neutral'
|
||||
'canceled' | 'status_canceled_borderless' | 'neutral'
|
||||
'created' | 'status_created_borderless' | 'neutral'
|
||||
'scheduled' | 'status_scheduled_borderless' | 'neutral'
|
||||
'play' | 'play' | 'neutral'
|
||||
'skipped' | 'status_skipped_borderless' | 'neutral'
|
||||
'manual' | 'status_manual_borderless' | 'neutral'
|
||||
'other-status' | 'status_canceled_borderless' | 'neutral'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ci_icon_for_status' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
with_them do
|
||||
subject { helper.render_ci_icon(status) }
|
||||
|
||||
where(:status, :icon_variant) do
|
||||
'success' | 'status_success'
|
||||
'success-with-warnings' | 'status_warning'
|
||||
'preparing' | 'status_preparing'
|
||||
'pending' | 'status_pending'
|
||||
'waiting-for-resource' | 'status_pending'
|
||||
'failed' | 'status_failed'
|
||||
'running' | 'status_running'
|
||||
'canceled' | 'status_canceled'
|
||||
'created' | 'status_created'
|
||||
'scheduled' | 'status_scheduled'
|
||||
'play' | 'play'
|
||||
'skipped' | 'status_skipped'
|
||||
'manual' | 'status_manual'
|
||||
end
|
||||
|
||||
with_them do
|
||||
subject { helper.render_ci_icon(status).to_s }
|
||||
|
||||
it 'uses the correct icon variant for status' do
|
||||
is_expected.to include("ci-status-icon-#{status}")
|
||||
is_expected.to include(icon_variant)
|
||||
it 'uses the correct variant and icon for status' do
|
||||
is_expected.to include("gl-badge badge badge-pill badge-#{badge_variant}")
|
||||
is_expected.to include("ci-icon-variant-#{badge_variant}")
|
||||
is_expected.to include("data-testid=\"#{icon}-icon\"")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
|
|||
let(:with_can_create_project) { false }
|
||||
let(:with_can_create_group) { false }
|
||||
let(:with_can_create_snippet) { false }
|
||||
let(:with_can_create_organization) { false }
|
||||
let(:title) { 'Create new...' }
|
||||
|
||||
subject(:view_model) do
|
||||
|
|
@ -24,6 +25,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
|
|||
allow(user).to receive(:can_create_group?) { with_can_create_group }
|
||||
allow(user).to receive(:can?).and_call_original
|
||||
allow(user).to receive(:can?).with(:create_snippet) { with_can_create_snippet }
|
||||
allow(user).to receive(:can?).with(:create_organization) { with_can_create_organization }
|
||||
end
|
||||
|
||||
shared_examples 'invite member item' do |partial|
|
||||
|
|
@ -135,6 +137,39 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when can create organization' do
|
||||
let(:with_can_create_organization) { true }
|
||||
|
||||
it 'has new organization menu item' do
|
||||
expect(view_model[:menu_sections]).to eq(
|
||||
expected_menu_section(
|
||||
title: _('In GitLab'),
|
||||
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
|
||||
id: 'general_new_organization',
|
||||
title: s_('Organization|New organization'),
|
||||
href: '/-/organizations/new',
|
||||
data: {
|
||||
track_action: 'click_link_new_organization_parent',
|
||||
track_label: 'plus_menu_dropdown',
|
||||
track_property: 'navigation_top',
|
||||
testid: 'global_new_organization_link'
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
context 'when ui_for_organizations feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ui_for_organizations: false)
|
||||
end
|
||||
|
||||
it 'does not have new organization menu item' do
|
||||
expect(view_model[:menu_sections]).to match_array([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with persisted group' do
|
||||
|
|
|
|||
|
|
@ -287,6 +287,9 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do
|
|||
{ href: "/groups/new", text: "New group",
|
||||
component: nil,
|
||||
extraAttrs: extra_attrs.call("general_new_group") },
|
||||
{ href: "/-/organizations/new", text: s_('Organization|New organization'),
|
||||
component: nil,
|
||||
extraAttrs: extra_attrs.call("general_new_organization") },
|
||||
{ href: "/-/snippets/new", text: "New snippet",
|
||||
component: nil,
|
||||
extraAttrs: extra_attrs.call("general_new_snippet") }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,18 @@ require 'spec_helper'
|
|||
# Only specs that *cannot* be run with fast_spec_helper only
|
||||
# See regex_spec for tests that do not require the full spec_helper
|
||||
RSpec.describe Gitlab::Regex, feature_category: :tooling do
|
||||
shared_examples_for 'npm package name regex' do
|
||||
it { is_expected.to match('@scope/package') }
|
||||
it { is_expected.to match('unscoped-package') }
|
||||
it { is_expected.not_to match('@first-scope@second-scope/package') }
|
||||
it { is_expected.not_to match('scope-without-at-symbol/package') }
|
||||
it { is_expected.not_to match('@not-a-scoped-package') }
|
||||
it { is_expected.not_to match('@scope/sub/package') }
|
||||
it { is_expected.not_to match('@scope/../../package') }
|
||||
it { is_expected.not_to match('@scope%2e%2e%2fpackage') }
|
||||
it { is_expected.not_to match('@%2e%2e%2f/package') }
|
||||
end
|
||||
|
||||
describe '.debian_architecture_regex' do
|
||||
subject { described_class.debian_architecture_regex }
|
||||
|
||||
|
|
@ -37,15 +49,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
|
|||
describe '.npm_package_name_regex' do
|
||||
subject { described_class.npm_package_name_regex }
|
||||
|
||||
it { is_expected.to match('@scope/package') }
|
||||
it { is_expected.to match('unscoped-package') }
|
||||
it { is_expected.not_to match('@first-scope@second-scope/package') }
|
||||
it { is_expected.not_to match('scope-without-at-symbol/package') }
|
||||
it { is_expected.not_to match('@not-a-scoped-package') }
|
||||
it { is_expected.not_to match('@scope/sub/package') }
|
||||
it { is_expected.not_to match('@scope/../../package') }
|
||||
it { is_expected.not_to match('@scope%2e%2e%2fpackage') }
|
||||
it { is_expected.not_to match('@%2e%2e%2f/package') }
|
||||
it_behaves_like 'npm package name regex'
|
||||
|
||||
context 'capturing group' do
|
||||
[
|
||||
|
|
@ -63,6 +67,24 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.protection_rules_npm_package_name_pattern_regex' do
|
||||
subject { described_class.protection_rules_npm_package_name_pattern_regex }
|
||||
|
||||
it_behaves_like 'npm package name regex'
|
||||
|
||||
it { is_expected.to match('@scope/package-*') }
|
||||
it { is_expected.to match('@my-scope/*my-package-with-wildcard-inbetween') }
|
||||
it { is_expected.to match('@my-scope/*my-package-with-wildcard-start') }
|
||||
it { is_expected.to match('@my-scope/my-*package-*with-wildcard-multiple-*') }
|
||||
it { is_expected.to match('@my-scope/my-package-with_____underscore') }
|
||||
it { is_expected.to match('@my-scope/my-package-with-wildcard-end*') }
|
||||
it { is_expected.to match('@my-scope/my-package-with-regex-characters.+') }
|
||||
|
||||
it { is_expected.not_to match('@my-scope/my-package-with-percent-sign-%') }
|
||||
it { is_expected.not_to match('*@my-scope/my-package-with-wildcard-start') }
|
||||
it { is_expected.not_to match('@my-scope/my-package-with-backslash-\*') }
|
||||
end
|
||||
|
||||
describe '.debian_distribution_regex' do
|
||||
subject { described_class.debian_distribution_regex }
|
||||
|
||||
|
|
|
|||
|
|
@ -157,6 +157,106 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
|
|||
end
|
||||
end
|
||||
|
||||
describe 'unlocking pipelines based on state transition' do
|
||||
let(:ci_ref) { create(:ci_ref) }
|
||||
let(:unlock_previous_pipelines_worker_spy) { class_spy(::Ci::Refs::UnlockPreviousPipelinesWorker) }
|
||||
|
||||
before do
|
||||
stub_const('Ci::Refs::UnlockPreviousPipelinesWorker', unlock_previous_pipelines_worker_spy)
|
||||
stub_feature_flags(ci_stop_unlock_pipelines: false)
|
||||
end
|
||||
|
||||
shared_examples 'not unlocking pipelines' do |event:|
|
||||
context "on #{event}" do
|
||||
let(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :artifacts_locked) }
|
||||
|
||||
it 'does not unlock previous pipelines' do
|
||||
pipeline.fire_events!(event)
|
||||
|
||||
expect(unlock_previous_pipelines_worker_spy).not_to have_received(:perform_async)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'unlocking pipelines' do |event:|
|
||||
context "on #{event}" do
|
||||
before do
|
||||
pipeline.fire_events!(event)
|
||||
end
|
||||
|
||||
let(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :artifacts_locked) }
|
||||
|
||||
it 'unlocks previous pipelines' do
|
||||
expect(unlock_previous_pipelines_worker_spy).to have_received(:perform_async).with(ci_ref.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when transitioning to unlockable states' do
|
||||
before do
|
||||
pipeline.run
|
||||
end
|
||||
|
||||
it_behaves_like 'unlocking pipelines', event: :succeed
|
||||
it_behaves_like 'unlocking pipelines', event: :drop
|
||||
it_behaves_like 'unlocking pipelines', event: :skip
|
||||
it_behaves_like 'unlocking pipelines', event: :cancel
|
||||
it_behaves_like 'unlocking pipelines', event: :block
|
||||
|
||||
context 'and ci_stop_unlock_pipelines is enabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_stop_unlock_pipelines: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'not unlocking pipelines', event: :succeed
|
||||
it_behaves_like 'not unlocking pipelines', event: :drop
|
||||
it_behaves_like 'not unlocking pipelines', event: :skip
|
||||
it_behaves_like 'not unlocking pipelines', event: :cancel
|
||||
it_behaves_like 'not unlocking pipelines', event: :block
|
||||
end
|
||||
|
||||
context 'and ci_unlock_non_successful_pipelines is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_unlock_non_successful_pipelines: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'unlocking pipelines', event: :succeed
|
||||
it_behaves_like 'not unlocking pipelines', event: :drop
|
||||
it_behaves_like 'not unlocking pipelines', event: :skip
|
||||
it_behaves_like 'not unlocking pipelines', event: :cancel
|
||||
it_behaves_like 'not unlocking pipelines', event: :block
|
||||
|
||||
context 'and ci_stop_unlock_pipelines is enabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_stop_unlock_pipelines: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'not unlocking pipelines', event: :succeed
|
||||
it_behaves_like 'not unlocking pipelines', event: :drop
|
||||
it_behaves_like 'not unlocking pipelines', event: :skip
|
||||
it_behaves_like 'not unlocking pipelines', event: :cancel
|
||||
it_behaves_like 'not unlocking pipelines', event: :block
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when transitioning to a non-unlockable state' do
|
||||
before do
|
||||
pipeline.enqueue
|
||||
end
|
||||
|
||||
it_behaves_like 'not unlocking pipelines', event: :run
|
||||
|
||||
context 'and ci_unlock_non_successful_pipelines is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_unlock_non_successful_pipelines: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'not unlocking pipelines', event: :run
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'pipeline age metric' do
|
||||
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }
|
||||
|
||||
|
|
@ -220,6 +320,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
|
|||
end
|
||||
end
|
||||
|
||||
describe '.with_unlockable_status' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
let!(:pipeline) { create(:ci_pipeline, project: project, status: status) }
|
||||
|
||||
subject(:result) { described_class.with_unlockable_status }
|
||||
|
||||
described_class::UNLOCKABLE_STATUSES.map(&:to_s).each do |s|
|
||||
context "when pipeline status is #{s}" do
|
||||
let(:status) { s }
|
||||
|
||||
it 'includes the pipeline in the result' do
|
||||
expect(result).to include(pipeline)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
(Ci::HasStatus::AVAILABLE_STATUSES - described_class::UNLOCKABLE_STATUSES.map(&:to_s)).each do |s|
|
||||
context "when pipeline status is #{s}" do
|
||||
let(:status) { s }
|
||||
|
||||
it 'does excludes the pipeline in the result' do
|
||||
expect(result).not_to include(pipeline)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.processables' do
|
||||
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }
|
||||
|
||||
|
|
|
|||
|
|
@ -7,61 +7,6 @@ RSpec.describe Ci::Ref, feature_category: :continuous_integration do
|
|||
|
||||
it { is_expected.to belong_to(:project) }
|
||||
|
||||
describe 'state machine transitions' do
|
||||
context 'unlock artifacts transition' do
|
||||
let(:ci_ref) { create(:ci_ref) }
|
||||
let(:unlock_previous_pipelines_worker_spy) { class_spy(::Ci::Refs::UnlockPreviousPipelinesWorker) }
|
||||
|
||||
before do
|
||||
stub_const('Ci::Refs::UnlockPreviousPipelinesWorker', unlock_previous_pipelines_worker_spy)
|
||||
end
|
||||
|
||||
context 'pipeline is locked' do
|
||||
let!(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :artifacts_locked) }
|
||||
|
||||
where(:initial_state, :action, :count) do
|
||||
:unknown | :succeed! | 1
|
||||
:unknown | :do_fail! | 0
|
||||
:success | :succeed! | 1
|
||||
:success | :do_fail! | 0
|
||||
:failed | :succeed! | 1
|
||||
:failed | :do_fail! | 0
|
||||
:fixed | :succeed! | 1
|
||||
:fixed | :do_fail! | 0
|
||||
:broken | :succeed! | 1
|
||||
:broken | :do_fail! | 0
|
||||
:still_failing | :succeed | 1
|
||||
:still_failing | :do_fail | 0
|
||||
end
|
||||
|
||||
with_them do
|
||||
context "when transitioning states" do
|
||||
before do
|
||||
status_value = Ci::Ref.state_machines[:status].states[initial_state].value
|
||||
ci_ref.update!(status: status_value)
|
||||
end
|
||||
|
||||
it 'calls pipeline complete unlock artifacts service' do
|
||||
ci_ref.send(action)
|
||||
|
||||
expect(unlock_previous_pipelines_worker_spy).to have_received(:perform_async).exactly(count).times
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'pipeline is unlocked' do
|
||||
let!(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :unlocked) }
|
||||
|
||||
it 'does not unlock pipelines' do
|
||||
ci_ref.succeed!
|
||||
|
||||
expect(unlock_previous_pipelines_worker_spy).not_to have_received(:perform_async)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.ensure_for' do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
|
||||
|
|
@ -241,4 +186,117 @@ RSpec.describe Ci::Ref, feature_category: :continuous_integration do
|
|||
let!(:model) { create(:ci_ref, project: parent) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#last_successful_ci_source_pipeline' do
|
||||
let_it_be(:ci_ref) { create(:ci_ref) }
|
||||
|
||||
let(:ci_source) { Enums::Ci::Pipeline.sources[:push] }
|
||||
let(:dangling_source) { Enums::Ci::Pipeline.sources[:parent_pipeline] }
|
||||
|
||||
subject(:result) { ci_ref.last_successful_ci_source_pipeline }
|
||||
|
||||
context 'when there are no successful CI source pipelines' do
|
||||
let!(:running_ci_source) { create(:ci_pipeline, :running, ci_ref: ci_ref, source: ci_source) }
|
||||
let!(:successful_dangling_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source) }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when there are successful CI source pipelines' do
|
||||
context 'and the latest pipeline is a successful CI source pipeline' do
|
||||
let!(:failed_ci_source) { create(:ci_pipeline, :failed, ci_ref: ci_ref, source: ci_source) }
|
||||
let!(:successful_dangling_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source, child_of: failed_ci_source) }
|
||||
let!(:successful_ci_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
|
||||
|
||||
it 'returns the last successful CI source pipeline' do
|
||||
expect(result).to eq(successful_ci_source)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and there is a newer successful dangling source pipeline than the successful CI source pipelines' do
|
||||
let!(:successful_ci_source_1) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
|
||||
let!(:successful_ci_source_2) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
|
||||
let!(:failed_ci_source) { create(:ci_pipeline, :failed, ci_ref: ci_ref, source: ci_source) }
|
||||
let!(:successful_dangling_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source, child_of: failed_ci_source) }
|
||||
|
||||
it 'returns the last successful CI source pipeline' do
|
||||
expect(result).to eq(successful_ci_source_2)
|
||||
end
|
||||
|
||||
context 'and the newer successful dangling source is a child of a successful CI source pipeline' do
|
||||
let!(:parent_ci_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: ci_source) }
|
||||
let!(:successful_child_source) { create(:ci_pipeline, :success, ci_ref: ci_ref, source: dangling_source, child_of: parent_ci_source) }
|
||||
|
||||
it 'returns the parent pipeline instead' do
|
||||
expect(result).to eq(parent_ci_source)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#last_unlockable_ci_source_pipeline' do
|
||||
let(:ci_source) { Enums::Ci::Pipeline.sources[:push] }
|
||||
let(:dangling_source) { Enums::Ci::Pipeline.sources[:parent_pipeline] }
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:ci_ref) { create(:ci_ref, project: project) }
|
||||
|
||||
subject(:result) { ci_ref.last_unlockable_ci_source_pipeline }
|
||||
|
||||
context 'when there are unlockable pipelines in the ref' do
|
||||
context 'and the last CI source pipeline in the ref is unlockable' do
|
||||
let!(:unlockable_ci_source_1) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: ci_source) }
|
||||
let!(:unlockable_ci_source_2) { create(:ci_pipeline, :blocked, project: project, ci_ref: ci_ref, source: ci_source) }
|
||||
|
||||
it 'returns the CI source pipeline' do
|
||||
expect(result).to eq(unlockable_ci_source_2)
|
||||
end
|
||||
|
||||
context 'and it has unlockable child pipelines' do
|
||||
let!(:child) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source_2) }
|
||||
let!(:child_2) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source_2) }
|
||||
|
||||
it 'returns the parent CI source pipeline' do
|
||||
expect(result).to eq(unlockable_ci_source_2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'and it has a non-unlockable child pipeline' do
|
||||
let!(:child) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source_2) }
|
||||
|
||||
it 'returns the parent CI source pipeline' do
|
||||
expect(result).to eq(unlockable_ci_source_2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'and the last CI source pipeline in the ref is not unlockable' do
|
||||
let!(:unlockable_ci_source) { create(:ci_pipeline, :skipped, project: project, ci_ref: ci_ref, source: ci_source) }
|
||||
let!(:unlockable_dangling_source) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: unlockable_ci_source) }
|
||||
let!(:non_unlockable_ci_source) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: ci_source) }
|
||||
let!(:non_unlockable_ci_source_2) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: ci_source) }
|
||||
|
||||
it 'returns the last unlockable CI source pipeline before it' do
|
||||
expect(result).to eq(unlockable_ci_source)
|
||||
end
|
||||
|
||||
context 'and it has unlockable child pipelines' do
|
||||
let!(:child) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: non_unlockable_ci_source) }
|
||||
let!(:child_2) { create(:ci_pipeline, :success, project: project, ci_ref: ci_ref, source: dangling_source, child_of: non_unlockable_ci_source) }
|
||||
|
||||
it 'returns the last unlockable CI source pipeline before it' do
|
||||
expect(result).to eq(unlockable_ci_source)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no unlockable pipelines in the ref' do
|
||||
let!(:non_unlockable_pipeline) { create(:ci_pipeline, :running, project: project, ci_ref: ci_ref, source: ci_source) }
|
||||
let!(:pipeline_from_another_ref) { create(:ci_pipeline, :success, source: ci_source) }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,6 +34,32 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
|
|||
it { is_expected.to validate_presence_of(:package_name_pattern) }
|
||||
it { is_expected.to validate_uniqueness_of(:package_name_pattern).scoped_to(:project_id, :package_type) }
|
||||
it { is_expected.to validate_length_of(:package_name_pattern).is_at_most(255) }
|
||||
|
||||
[
|
||||
'@my-scope/my-package',
|
||||
'@my-scope/*my-package-with-wildcard-inbetween',
|
||||
'@my-scope/*my-package-with-wildcard-start',
|
||||
'@my-scope/my-*package-*with-wildcard-multiple-*',
|
||||
'@my-scope/my-package-with_____underscore',
|
||||
'@my-scope/my-package-with-regex-characters.+',
|
||||
'@my-scope/my-package-with-wildcard-end*'
|
||||
].each do |package_name_pattern|
|
||||
it { is_expected.to allow_value(package_name_pattern).for(:package_name_pattern) }
|
||||
end
|
||||
|
||||
[
|
||||
'@my-scope/my-package-with-percent-sign-%',
|
||||
'*@my-scope/my-package-with-wildcard-start',
|
||||
'@my-scope/my-package-with-backslash-\*'
|
||||
].each do |package_name_pattern|
|
||||
it {
|
||||
is_expected.not_to(
|
||||
allow_value(package_name_pattern)
|
||||
.for(:package_name_pattern)
|
||||
.with_message(_('should be a valid NPM package name with optional wildcard characters.'))
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe '#package_type' do
|
||||
|
|
@ -51,14 +77,13 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
|
|||
|
||||
context 'with different package name patterns' do
|
||||
where(:package_name_pattern, :expected_pattern_query) do
|
||||
'@my-scope/my-package' | '@my-scope/my-package'
|
||||
'*@my-scope/my-package-with-wildcard-start' | '%@my-scope/my-package-with-wildcard-start'
|
||||
'@my-scope/my-package-with-wildcard-end*' | '@my-scope/my-package-with-wildcard-end%'
|
||||
'@my-scope/*my-package-with-wildcard-inbetween' | '@my-scope/%my-package-with-wildcard-inbetween'
|
||||
'**@my-scope/**my-package-with-wildcard-multiple**' | '%%@my-scope/%%my-package-with-wildcard-multiple%%'
|
||||
'@my-scope/my-package-with_____underscore' | '@my-scope/my-package-with\_\_\_\_\_underscore'
|
||||
'@my-scope/my-package-with-percent-sign-%' | '@my-scope/my-package-with-percent-sign-\%'
|
||||
'@my-scope/my-package-with-regex-characters.+' | '@my-scope/my-package-with-regex-characters.+'
|
||||
'@my-scope/my-package' | '@my-scope/my-package'
|
||||
'@my-scope/*my-package-with-wildcard-start' | '@my-scope/%my-package-with-wildcard-start'
|
||||
'@my-scope/my-package-with-wildcard-end*' | '@my-scope/my-package-with-wildcard-end%'
|
||||
'@my-scope/my-package*with-wildcard-inbetween' | '@my-scope/my-package%with-wildcard-inbetween'
|
||||
'@my-scope/**my-package-**-with-wildcard-multiple**' | '@my-scope/%%my-package-%%-with-wildcard-multiple%%'
|
||||
'@my-scope/my-package-with_____underscore' | '@my-scope/my-package-with\_\_\_\_\_underscore'
|
||||
'@my-scope/my-package-with-regex-characters.+' | '@my-scope/my-package-with-regex-characters.+'
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
@ -74,7 +99,7 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
|
|||
end
|
||||
|
||||
let_it_be(:ppr_with_wildcard_start) do
|
||||
create(:package_protection_rule, package_name_pattern: '*@my-scope/my_package-with-wildcard-start')
|
||||
create(:package_protection_rule, package_name_pattern: '@my-scope/*my_package-with-wildcard-start')
|
||||
end
|
||||
|
||||
let_it_be(:ppr_with_wildcard_end) do
|
||||
|
|
@ -82,11 +107,11 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
|
|||
end
|
||||
|
||||
let_it_be(:ppr_with_wildcard_inbetween) do
|
||||
create(:package_protection_rule, package_name_pattern: '@my-scope/*my_package-with-wildcard-inbetween')
|
||||
create(:package_protection_rule, package_name_pattern: '@my-scope/my_package*with-wildcard-inbetween')
|
||||
end
|
||||
|
||||
let_it_be(:ppr_with_wildcard_multiples) do
|
||||
create(:package_protection_rule, package_name_pattern: '**@my-scope/**my_package-with-wildcard-multiple**')
|
||||
create(:package_protection_rule, package_name_pattern: '@my-scope/**my_package**with-wildcard-multiple**')
|
||||
end
|
||||
|
||||
let_it_be(:ppr_with_underscore) do
|
||||
|
|
@ -103,46 +128,47 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
|
|||
|
||||
context 'with several package protection rule scenarios' do
|
||||
where(:package_name, :expected_package_protection_rules) do
|
||||
'@my-scope/my_package' | [ref(:package_protection_rule)]
|
||||
'@my-scope/my2package' | []
|
||||
'@my-scope/my_package-2' | []
|
||||
'@my-scope/my_package' | [ref(:package_protection_rule)]
|
||||
'@my-scope/my2package' | []
|
||||
'@my-scope/my_package-2' | []
|
||||
|
||||
# With wildcard pattern at the start
|
||||
'@my-scope/my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
|
||||
'@my-scope/my_package-with-wildcard-start-any' | []
|
||||
'prefix-@my-scope/my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
|
||||
'prefix-@my-scope/my_package-with-wildcard-start-any' | []
|
||||
'@my-scope/my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
|
||||
'@my-scope/my_package-with-wildcard-start-any' | []
|
||||
'@my-scope/any-my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)]
|
||||
'@my-scope/any-my_package-with-wildcard-start-any' | []
|
||||
|
||||
# With wildcard pattern at the end
|
||||
'@my-scope/my_package-with-wildcard-end' | [ref(:ppr_with_wildcard_end)]
|
||||
'@my-scope/my_package-with-wildcard-end:1234567890' | [ref(:ppr_with_wildcard_end)]
|
||||
'prefix-@my-scope/my_package-with-wildcard-end' | []
|
||||
'prefix-@my-scope/my_package-with-wildcard-end:1234567890' | []
|
||||
'@my-scope/my_package-with-wildcard-end' | [ref(:ppr_with_wildcard_end)]
|
||||
'@my-scope/my_package-with-wildcard-end:1234567890' | [ref(:ppr_with_wildcard_end)]
|
||||
'@my-scope/any-my_package-with-wildcard-end' | []
|
||||
'@my-scope/any-my_package-with-wildcard-end:1234567890' | []
|
||||
|
||||
# With wildcard pattern inbetween
|
||||
'@my-scope/my_package-with-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
|
||||
'@my-scope/any-my_package-with-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
|
||||
'@my-scope/any-my_package-my_package-wildcard-inbetween-any' | []
|
||||
'@my-scope/my_packagewith-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
|
||||
'@my-scope/my_package-any-with-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)]
|
||||
'@my-scope/any-my_package-my_package-wildcard-inbetween-any' | []
|
||||
|
||||
# With multiple wildcard pattern are used
|
||||
'@my-scope/my_package-with-wildcard-multiple' | [ref(:ppr_with_wildcard_multiples)]
|
||||
'prefix-@my-scope/any-my_package-with-wildcard-multiple-any' | [ref(:ppr_with_wildcard_multiples)]
|
||||
'****@my-scope/****my_package-with-wildcard-multiple****' | [ref(:ppr_with_wildcard_multiples)]
|
||||
'prefix-@other-scope/any-my_package-with-wildcard-multiple-any' | []
|
||||
'@my-scope/my_packagewith-wildcard-multiple' | [ref(:ppr_with_wildcard_multiples)]
|
||||
'@my-scope/any-my_package-any-with-wildcard-multiple-any' | [ref(:ppr_with_wildcard_multiples)]
|
||||
'@my-scope/****my_package****with-wildcard-multiple****' | [ref(:ppr_with_wildcard_multiples)]
|
||||
'@other-scope/any-my_package-with-wildcard-multiple-any' | []
|
||||
|
||||
# With underscore
|
||||
'@my-scope/my_package-with_____underscore' | [ref(:ppr_with_underscore)]
|
||||
'@my-scope/my_package-with_any_underscore' | []
|
||||
'@my-scope/my_package-with_____underscore' | [ref(:ppr_with_underscore)]
|
||||
'@my-scope/my_package-with_any_underscore' | []
|
||||
|
||||
'@my-scope/my_package-with-regex-characters.+' | [ref(:ppr_with_regex_characters)]
|
||||
'@my-scope/my_package-with-regex-characters.' | []
|
||||
'@my-scope/my_package-with-regex-characters' | []
|
||||
'@my-scope/my_package-with-regex-characters-any' | []
|
||||
# With regex pattern
|
||||
'@my-scope/my_package-with-regex-characters.+' | [ref(:ppr_with_regex_characters)]
|
||||
'@my-scope/my_package-with-regex-characters.' | []
|
||||
'@my-scope/my_package-with-regex-characters' | []
|
||||
'@my-scope/my_package-with-regex-characters-any' | []
|
||||
|
||||
# Special cases
|
||||
nil | []
|
||||
'' | []
|
||||
'any_package' | []
|
||||
nil | []
|
||||
'' | []
|
||||
'any_package' | []
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ RSpec.describe 'Query.project.mergeRequest.codequalityReportsComparer', feature_
|
|||
let(:codequality_reports_comparer_fields) do
|
||||
<<~QUERY
|
||||
codequalityReportsComparer {
|
||||
status
|
||||
report {
|
||||
status
|
||||
newErrors {
|
||||
|
|
@ -138,6 +139,7 @@ RSpec.describe 'Query.project.mergeRequest.codequalityReportsComparer', feature_
|
|||
expect(result).to match(
|
||||
a_hash_including(
|
||||
{
|
||||
status: 'PARSED',
|
||||
report: {
|
||||
status: 'FAILED',
|
||||
newErrors: [
|
||||
|
|
|
|||
|
|
@ -17,8 +17,11 @@ RSpec.describe 'Setting time estimate of a merge request', feature_category: :co
|
|||
|
||||
let(:extra_params) { { project_path: project.full_path } }
|
||||
let(:input_params) { input.merge(extra_params) }
|
||||
let(:mutation) { graphql_mutation(:merge_request_update, input_params, nil, ['productAnalyticsState']) }
|
||||
let(:mutation_response) { graphql_mutation_response(:merge_request_update) }
|
||||
let(:mutation) do
|
||||
# exclude codequalityReportsComparer because it's behind a feature flag
|
||||
graphql_mutation(:merge_request_update, input_params, nil, %w[productAnalyticsState codequalityReportsComparer])
|
||||
end
|
||||
|
||||
context 'when the user is not allowed to update a merge request' do
|
||||
before_all do
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ RSpec.describe 'Update of an existing merge request', feature_category: :code_re
|
|||
let(:input) { { 'iid' => merge_request.iid.to_s } }
|
||||
let(:extra_params) { { project_path: project.full_path } }
|
||||
let(:input_params) { input.merge(extra_params) }
|
||||
let(:mutation) { graphql_mutation(:merge_request_update, input_params, nil, ['productAnalyticsState']) }
|
||||
let(:mutation_response) { graphql_mutation_response(:merge_request_update) }
|
||||
let(:mutation) do
|
||||
# exclude codequalityReportsComparer because it's behind a feature flag
|
||||
graphql_mutation(:merge_request_update, input_params, nil, %w[productAnalyticsState codequalityReportsComparer])
|
||||
end
|
||||
|
||||
context 'when the user is not allowed to update the merge request' do
|
||||
it_behaves_like 'a mutation that returns a top-level access error'
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ RSpec.describe 'getting merge request information nested in a project', feature_
|
|||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
# we exclude Project.pipeline because it needs arguments,
|
||||
# codequalityReportsComparer because no pipeline exist yet
|
||||
# codequalityReportsComparer because it is behind a feature flag
|
||||
# and runners because the user is not an admin and therefore has no access
|
||||
let(:excluded) { %w[jobs pipeline runners codequalityReportsComparer] }
|
||||
let(:mr_fields) { all_graphql_fields_for('MergeRequest', excluded: excluded) }
|
||||
|
|
|
|||
|
|
@ -48,8 +48,11 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
|
|||
end
|
||||
|
||||
it_behaves_like 'a working graphql query' do
|
||||
# we exclude codequalityReportsComparer because it is behind feature flag
|
||||
let(:excluded) { %w[codequalityReportsComparer] }
|
||||
|
||||
let(:query) do
|
||||
query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 2))
|
||||
query_merge_requests(all_graphql_fields_for('MergeRequest', max_depth: 2, excluded: excluded))
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rubocop_spec_helper'
|
||||
|
||||
require_relative '../../../../rubocop/cop/style/inline_disable_annotation'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Style::InlineDisableAnnotation, feature_category: :shared do
|
||||
it 'registers an offense' do
|
||||
expect_offense(<<~RUBY)
|
||||
# some other comment
|
||||
abc = '1'
|
||||
['this', 'that'].each do |word|
|
||||
next if something? # rubocop:disable Some/Cop, Another/Cop
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
|
||||
end
|
||||
# rubocop:disable Some/Cop, Another/Cop - Bad comment
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
|
||||
# rubocop :todo Some/Cop Some other things
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
|
||||
# rubocop: disable Some/Cop, Another/Cop Some more stuff
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
|
||||
# rubocop:disable Some/Cop -- Good comment
|
||||
if blah && this # some other comment about nothing
|
||||
this.match?(/blah/) # rubocop:disable Some/Cop with a bad comment
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Inline disabling a cop needs to follow [...]
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
it 'accepts correctly formatted comment' do
|
||||
expect_no_offenses(<<~RUBY)
|
||||
# some other comment
|
||||
abc = '1'
|
||||
['this', 'that'].each do |word|
|
||||
next if something? # rubocop:disable Some/Cop, Another/Cop -- Good comment
|
||||
end
|
||||
# rubocop:disable Some/Cop, Another/Cop -- Good comment
|
||||
# rubocop :todo Some/Cop Some other things -- Good comment
|
||||
# rubocop: disable Some/Cop, Another/Cop Some more stuff -- Good comment
|
||||
# rubocop:disable Some/Cop -- Good comment
|
||||
if blah && this # some other comment about nothing
|
||||
this.match?(/blah/) # rubocop:disable Some/Cop -- Good comment
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
|
|
@ -26,34 +26,40 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
|
|||
shared_examples_for 'unlocking pipelines' do
|
||||
let(:is_tag) { target_ref.ref_path.include?(::Gitlab::Git::TAG_REF_PREFIX) }
|
||||
|
||||
let!(:other_ref_pipeline) { create_pipeline(:locked, other_ref, tag: false) }
|
||||
let!(:old_unlocked_pipeline) { create_pipeline(:unlocked, ref) }
|
||||
let!(:older_locked_pipeline_1) { create_pipeline(:locked, ref) }
|
||||
let!(:older_locked_pipeline_2) { create_pipeline(:locked, ref) }
|
||||
let!(:older_locked_pipeline_3) { create_pipeline(:locked, ref) }
|
||||
let!(:older_child_pipeline) { create_pipeline(:locked, ref, child_of: older_locked_pipeline_3) }
|
||||
let!(:pipeline) { create_pipeline(:locked, ref) }
|
||||
let!(:child_pipeline) { create_pipeline(:locked, ref, child_of: pipeline) }
|
||||
let!(:newer_pipeline) { create_pipeline(:locked, ref) }
|
||||
let!(:other_ref_pipeline) { create_pipeline(:locked, other_ref, :failed, tag: false) }
|
||||
let!(:old_unlocked_pipeline) { create_pipeline(:unlocked, ref, :failed) }
|
||||
let!(:old_locked_pipeline_1) { create_pipeline(:locked, ref, :failed) }
|
||||
let!(:old_locked_pipeline_2) { create_pipeline(:locked, ref, :success) }
|
||||
let!(:old_locked_pipeline_3) { create_pipeline(:locked, ref, :success) }
|
||||
let!(:old_locked_pipeline_3_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_3) }
|
||||
let!(:old_locked_pipeline_4) { create_pipeline(:locked, ref, :success) }
|
||||
let!(:old_locked_pipeline_4_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_4) }
|
||||
let!(:old_locked_pipeline_5) { create_pipeline(:locked, ref, :failed) }
|
||||
let!(:old_locked_pipeline_5_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_5) }
|
||||
let!(:pipeline) { create_pipeline(:locked, ref, :failed) }
|
||||
let!(:child_pipeline) { create_pipeline(:locked, ref, :failed, child_of: pipeline) }
|
||||
let!(:newer_pipeline) { create_pipeline(:locked, ref, :failed) }
|
||||
|
||||
context 'when before_pipeline is given' do
|
||||
let(:before_pipeline) { pipeline }
|
||||
|
||||
it 'only enqueues older locked pipelines within the ref' do
|
||||
it 'only enqueues old locked pipelines within the ref, excluding the last successful CI source pipeline' do
|
||||
expect { execute }
|
||||
.to change { pipeline_ids_waiting_to_be_unlocked }
|
||||
.from([])
|
||||
.to([
|
||||
older_locked_pipeline_1.id,
|
||||
older_locked_pipeline_2.id,
|
||||
older_locked_pipeline_3.id,
|
||||
older_child_pipeline.id
|
||||
old_locked_pipeline_1.id,
|
||||
old_locked_pipeline_2.id,
|
||||
old_locked_pipeline_3.id,
|
||||
old_locked_pipeline_3_child.id,
|
||||
old_locked_pipeline_5.id,
|
||||
old_locked_pipeline_5_child.id
|
||||
])
|
||||
|
||||
expect(execute).to include(
|
||||
status: :success,
|
||||
total_pending_entries: 4,
|
||||
total_new_entries: 4
|
||||
total_pending_entries: 6,
|
||||
total_new_entries: 6
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -66,10 +72,14 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
|
|||
.to change { pipeline_ids_waiting_to_be_unlocked }
|
||||
.from([])
|
||||
.to([
|
||||
older_locked_pipeline_1.id,
|
||||
older_locked_pipeline_2.id,
|
||||
older_locked_pipeline_3.id,
|
||||
older_child_pipeline.id,
|
||||
old_locked_pipeline_1.id,
|
||||
old_locked_pipeline_2.id,
|
||||
old_locked_pipeline_3.id,
|
||||
old_locked_pipeline_3_child.id,
|
||||
old_locked_pipeline_4.id,
|
||||
old_locked_pipeline_4_child.id,
|
||||
old_locked_pipeline_5.id,
|
||||
old_locked_pipeline_5_child.id,
|
||||
pipeline.id,
|
||||
child_pipeline.id,
|
||||
newer_pipeline.id
|
||||
|
|
@ -77,8 +87,8 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
|
|||
|
||||
expect(execute).to include(
|
||||
status: :success,
|
||||
total_pending_entries: 7,
|
||||
total_new_entries: 7
|
||||
total_pending_entries: 11,
|
||||
total_new_entries: 11
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -96,9 +106,9 @@ RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :cl
|
|||
it_behaves_like 'unlocking pipelines'
|
||||
end
|
||||
|
||||
def create_pipeline(type, ref, tag: is_tag, child_of: nil)
|
||||
def create_pipeline(type, ref, status, tag: is_tag, child_of: nil)
|
||||
trait = type == :locked ? :artifacts_locked : :unlocked
|
||||
create(:ci_pipeline, trait, ref: ref, tag: tag, project: project, child_of: child_of).tap do |p|
|
||||
create(:ci_pipeline, trait, status: status, ref: ref, tag: tag, project: project, child_of: child_of).tap do |p|
|
||||
if child_of
|
||||
build = create(:ci_build, pipeline: child_of)
|
||||
create(:ci_sources_pipeline, source_job: build, source_project: project, pipeline: p, project: project)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
require 'gitlab/dangerfiles/spec_helper'
|
||||
|
||||
require_relative '../../../tooling/danger/gitlab_schema_validation_suggestion'
|
||||
require_relative '../../../tooling/danger/project_helper'
|
||||
|
||||
RSpec.describe Tooling::Danger::GitlabSchemaValidationSuggestion, feature_category: :cell do
|
||||
include_context "with dangerfile"
|
||||
|
||||
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
|
||||
let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
|
||||
let(:filename) { 'db/docs/application_settings.yml' }
|
||||
let(:file_lines) do
|
||||
file_diff.map { |line| line.delete_prefix('+') }
|
||||
end
|
||||
|
||||
let(:file_diff) do
|
||||
[
|
||||
"+---",
|
||||
"+table_name: application_settings",
|
||||
"+classes:",
|
||||
"+- ApplicationSetting",
|
||||
"+feature_categories:",
|
||||
"+- continuous_integration",
|
||||
"+description: GitLab application settings",
|
||||
"+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/8589b4e137f50293952923bb07e2814257d7784d",
|
||||
"+milestone: '7.7'",
|
||||
"+gitlab_schema: #{schema}"
|
||||
]
|
||||
end
|
||||
|
||||
subject(:gitlab_schema_validation) { fake_danger.new(helper: fake_helper) }
|
||||
|
||||
before do
|
||||
allow(gitlab_schema_validation).to receive(:project_helper).and_return(fake_project_helper)
|
||||
allow(gitlab_schema_validation.project_helper).to receive(:file_lines).and_return(file_lines)
|
||||
allow(gitlab_schema_validation.helper).to receive(:changed_lines).with(filename).and_return(file_diff)
|
||||
allow(gitlab_schema_validation.helper).to receive(:all_changed_files).and_return([filename])
|
||||
end
|
||||
|
||||
shared_examples_for 'does not add a comment' do
|
||||
it do
|
||||
expect(gitlab_schema_validation).not_to receive(:markdown)
|
||||
|
||||
gitlab_schema_validation.add_suggestions_on_using_clusterwide_schema
|
||||
end
|
||||
end
|
||||
|
||||
context 'for discouraging the use of gitlab_main_clusterwide schema' do
|
||||
let(:schema) { 'gitlab_main_clusterwide' }
|
||||
|
||||
context 'when the file path matches' do
|
||||
it 'adds the comment' do
|
||||
expected_comment = "\n#{described_class::SUGGESTION.chomp}"
|
||||
|
||||
expect(gitlab_schema_validation).to receive(:markdown).with(expected_comment, file: filename, line: 10)
|
||||
|
||||
gitlab_schema_validation.add_suggestions_on_using_clusterwide_schema
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the file path does not match' do
|
||||
let(:filename) { 'some_path/application_settings.yml' }
|
||||
|
||||
it_behaves_like 'does not add a comment'
|
||||
end
|
||||
|
||||
context 'for EE' do
|
||||
let(:filename) { 'ee/db/docs/application_settings.yml' }
|
||||
|
||||
it_behaves_like 'does not add a comment'
|
||||
end
|
||||
|
||||
context 'for a deleted table' do
|
||||
let(:filename) { 'db/docs/deleted_tables/application_settings.yml' }
|
||||
|
||||
it_behaves_like 'does not add a comment'
|
||||
end
|
||||
end
|
||||
|
||||
context 'on removing the gitlab_main_clusterwide schema' do
|
||||
let(:file_diff) do
|
||||
[
|
||||
"+---",
|
||||
"+table_name: application_settings",
|
||||
"+classes:",
|
||||
"+- ApplicationSetting",
|
||||
"+feature_categories:",
|
||||
"+- continuous_integration",
|
||||
"+description: GitLab application settings",
|
||||
"+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/8589b4e137f50293952923bb07e2814257d7784d",
|
||||
"+milestone: '7.7'",
|
||||
"-gitlab_schema: gitlab_main_clusterwide",
|
||||
"+gitlab_schema: gitlab_main_cell"
|
||||
]
|
||||
end
|
||||
|
||||
it_behaves_like 'does not add a comment'
|
||||
end
|
||||
|
||||
context 'when a different schema is added' do
|
||||
let(:schema) { 'gitlab_main' }
|
||||
|
||||
it_behaves_like 'does not add a comment'
|
||||
end
|
||||
end
|
||||
|
|
@ -31,7 +31,7 @@ RSpec.describe 'ci/status/_icon' do
|
|||
end
|
||||
|
||||
it 'contains build status text' do
|
||||
expect(rendered).to have_css('.ci-status-icon.ci-status-icon-success')
|
||||
expect(rendered).to have_css('[data-testid="status_success_borderless-icon"]')
|
||||
end
|
||||
|
||||
it 'does not contain links' do
|
||||
|
|
@ -59,7 +59,7 @@ RSpec.describe 'ci/status/_icon' do
|
|||
end
|
||||
|
||||
it 'contains valid commit status text' do
|
||||
expect(rendered).to have_css('.ci-status-icon.ci-status-icon-running')
|
||||
expect(rendered).to have_css('[data-testid="status_running_borderless-icon"]')
|
||||
end
|
||||
|
||||
it 'has link to external status page' do
|
||||
|
|
@ -75,7 +75,7 @@ RSpec.describe 'ci/status/_icon' do
|
|||
end
|
||||
|
||||
it 'contains valid commit status text' do
|
||||
expect(rendered).to have_css('.ci-status-icon.ci-status-icon-canceled')
|
||||
expect(rendered).to have_css('[data-testid="status_canceled_borderless-icon"]')
|
||||
end
|
||||
|
||||
it 'has link to external status page' do
|
||||
|
|
|
|||
|
|
@ -185,6 +185,11 @@ RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do
|
|||
context 'when the user is not allowed to do anything' do
|
||||
let(:user) { create(:user, :external) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
|
||||
|
||||
before do
|
||||
allow(user).to receive(:can?).and_call_original
|
||||
allow(user).to receive(:can?).with(:create_organization).and_return(false)
|
||||
end
|
||||
|
||||
it 'is nil' do
|
||||
# We have to use `view.render` because `render` causes issues
|
||||
# https://github.com/rails/rails/issues/41320
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ RSpec.describe 'projects/issues/_related_branches' do
|
|||
expect(rendered).to have_text('other')
|
||||
expect(rendered).to have_link(href: 'link-to-feature')
|
||||
expect(rendered).to have_link(href: 'link-to-other')
|
||||
expect(rendered).to have_css('.ci-status-icon')
|
||||
expect(rendered).to have_css('[data-testid="ci-icon"]')
|
||||
expect(rendered).to have_css('.related-branch-info')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ RSpec.describe 'projects/tags/index.html.haml' do
|
|||
|
||||
render
|
||||
|
||||
expect(page.find('.tags .content-list li', text: tag)).to have_css '.gl-badge .ci-status-icon-success'
|
||||
expect(page.find('.tags .content-list li', text: tag)).to have_css '[data-testid="status_success_borderless-icon"]'
|
||||
expect(page.all('.tags .content-list li')).to all(have_css('svg.s16'))
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ RSpec.describe Ci::Refs::UnlockPreviousPipelinesWorker, :unlock_pipelines, :clea
|
|||
create(
|
||||
:ci_pipeline,
|
||||
:with_persisted_artifacts,
|
||||
:artifacts_locked,
|
||||
ref: older_pipeline.ref,
|
||||
tag: older_pipeline.tag,
|
||||
project: older_pipeline.project
|
||||
|
|
@ -25,12 +26,30 @@ RSpec.describe Ci::Refs::UnlockPreviousPipelinesWorker, :unlock_pipelines, :clea
|
|||
|
||||
describe '#perform' do
|
||||
it 'executes a service' do
|
||||
ci_ref = pipeline.ci_ref
|
||||
expect(ci_ref).to receive(:last_unlockable_ci_source_pipeline).and_return(pipeline)
|
||||
|
||||
expect(Ci::Ref).to receive(:find_by_id).with(pipeline.ci_ref.id).and_return(ci_ref)
|
||||
|
||||
expect_next_instance_of(Ci::Refs::EnqueuePipelinesToUnlockService) do |instance|
|
||||
expect(instance).to receive(:execute).and_call_original
|
||||
expect(instance).to receive(:execute).with(ci_ref, before_pipeline: pipeline).and_call_original
|
||||
end
|
||||
|
||||
worker.perform(pipeline.ci_ref.id)
|
||||
end
|
||||
|
||||
context 'when ref has no pipelines locked' do
|
||||
before do
|
||||
older_pipeline.update!(locked: :unlocked)
|
||||
pipeline.update!(locked: :unlocked)
|
||||
end
|
||||
|
||||
it 'does nothing' do
|
||||
expect(Ci::Refs::EnqueuePipelinesToUnlockService).not_to receive(:new)
|
||||
|
||||
worker.perform(pipeline.ci_ref.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'an idempotent worker' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'suggestion'
|
||||
|
||||
module Tooling
|
||||
module Danger
|
||||
module GitlabSchemaValidationSuggestion
|
||||
include ::Tooling::Danger::Suggestor
|
||||
|
||||
MATCH = %r{gitlab_schema: gitlab_main_clusterwide}
|
||||
REPLACEMENT = nil
|
||||
DB_DOCS_PATH = %r{\Adb/docs/[^/]+\.ya?ml\z}
|
||||
|
||||
SUGGESTION = <<~MESSAGE_MARKDOWN
|
||||
:warning: You have added `gitlab_main_clusterwide` as the schema for this table. We expect most tables to use the
|
||||
`gitlab_main_cell` schema instead, as using the clusterwide schema can have significant scaling implications.
|
||||
|
||||
Please see the [guidelines on choosing gitlab schema](https://docs.gitlab.com/ee/development/database/multiple_databases.html#guidelines-on-choosing-between-gitlab_main_cell-and-gitlab_main_clusterwide-schema) for more information.
|
||||
|
||||
Please consult with ~"group::tenant scale" if you believe that the clusterwide schema is the best fit for this table.
|
||||
MESSAGE_MARKDOWN
|
||||
|
||||
def add_suggestions_on_using_clusterwide_schema
|
||||
helper.all_changed_files.grep(DB_DOCS_PATH).each do |filename|
|
||||
add_suggestion(
|
||||
filename: filename,
|
||||
regex: MATCH,
|
||||
replacement: REPLACEMENT,
|
||||
comment_text: SUGGESTION
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue