Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-11-03 09:08:44 +00:00
parent ea044b0c4c
commit b4c39709e3
72 changed files with 4629 additions and 433 deletions

View File

@ -139,6 +139,7 @@ linters:
- Style/HashSyntax
- Style/IdenticalConditionalBranches
- Style/IfInsideElse
- Style/InlineDisableAnnotation
- Style/NegatedIf
- Style/NestedTernaryOperator
- Style/RedundantInterpolation

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,4 +5,4 @@ rollout_issue_url:
milestone: '16.5'
type: ops
group: group::tenant scale
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
gitlab_schema_validation.add_suggestions_on_using_clusterwide_schema

View File

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

View File

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

View File

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

View File

@ -100,6 +100,9 @@ RSpec/FeatureCategory:
Style/HashSyntax:
Enabled: false
Style/InlineDisableAnnotation:
Enabled: false
Style/Lambda:
EnforcedStyle: literal

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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