Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
85a825bbbf
commit
d03aeb1110
|
@ -1 +1 @@
|
|||
518670d57d1a6527aaf46b5b9bf5cb00f2e8f11b
|
||||
f87bc1e983d11788fdbce953dced45ec5554af23
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
<script>
|
||||
import { GlButton, GlFormCheckbox, GlKeysetPagination } from '@gitlab/ui';
|
||||
import { filter } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'RegistryList',
|
||||
components: {
|
||||
GlButton,
|
||||
GlFormCheckbox,
|
||||
GlKeysetPagination,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
hiddenDelete: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
pagination: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
idProperty: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'id',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedReferences: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showPagination() {
|
||||
return this.pagination.hasPreviousPage || this.pagination.hasNextPage;
|
||||
},
|
||||
disableDeleteButton() {
|
||||
return this.isLoading || filter(this.selectedReferences).length === 0;
|
||||
},
|
||||
selectedItems() {
|
||||
return this.items.filter(this.isSelected);
|
||||
},
|
||||
selectAll: {
|
||||
get() {
|
||||
return this.items.every(this.isSelected);
|
||||
},
|
||||
set(value) {
|
||||
this.items.forEach((item) => {
|
||||
const id = item[this.idProperty];
|
||||
this.$set(this.selectedReferences, id, value);
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectItem(item) {
|
||||
const id = item[this.idProperty];
|
||||
this.$set(this.selectedReferences, id, !this.selectedReferences[id]);
|
||||
},
|
||||
isSelected(item) {
|
||||
const id = item[this.idProperty];
|
||||
return this.selectedReferences[id];
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
deleteSelected: __('Delete Selected'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="gl-display-flex gl-justify-content-space-between gl-mb-3">
|
||||
<gl-form-checkbox v-if="!hiddenDelete" v-model="selectAll" class="gl-ml-2">
|
||||
<span class="gl-font-weight-bold">{{ title }}</span>
|
||||
</gl-form-checkbox>
|
||||
|
||||
<gl-button
|
||||
v-if="!hiddenDelete"
|
||||
:disabled="disableDeleteButton"
|
||||
category="secondary"
|
||||
variant="danger"
|
||||
@click="$emit('delete', selectedItems)"
|
||||
>
|
||||
{{ $options.i18n.deleteSelected }}
|
||||
</gl-button>
|
||||
</div>
|
||||
|
||||
<div v-for="(item, index) in items" :key="index">
|
||||
<slot
|
||||
:select-item="selectItem"
|
||||
:is-selected="isSelected"
|
||||
:item="item"
|
||||
:first="index === 0"
|
||||
></slot>
|
||||
</div>
|
||||
|
||||
<div class="gl-display-flex gl-justify-content-center">
|
||||
<gl-keyset-pagination
|
||||
v-if="showPagination"
|
||||
v-bind="pagination"
|
||||
class="gl-mt-3"
|
||||
@prev="$emit('prev-page')"
|
||||
@next="$emit('next-page')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -10,6 +10,7 @@ module Ci
|
|||
include Presentable
|
||||
include Importable
|
||||
include Ci::HasRef
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
BuildArchivedError = Class.new(StandardError)
|
||||
|
||||
|
@ -723,6 +724,14 @@ module Ci
|
|||
self.token && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
|
||||
end
|
||||
|
||||
# acts_as_taggable uses this method create/remove tags with contexts
|
||||
# defined by taggings and to get those contexts it executes a query.
|
||||
# We don't use any other contexts except `tags`, so we don't need it.
|
||||
override :custom_contexts
|
||||
def custom_contexts
|
||||
[]
|
||||
end
|
||||
|
||||
def tag_list
|
||||
if tags.loaded?
|
||||
tags.map(&:name)
|
||||
|
|
|
@ -217,6 +217,10 @@ class CommitStatus < Ci::ApplicationRecord
|
|||
false
|
||||
end
|
||||
|
||||
def self.bulk_insert_tags!(statuses, tag_list_by_build)
|
||||
Gitlab::Ci::Tags::BulkInsert.new(statuses, tag_list_by_build).insert!
|
||||
end
|
||||
|
||||
def locking_enabled?
|
||||
will_save_change_to_status?
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- expanded = integration_expanded?('snowplow_')
|
||||
%section.settings.as-snowplow.no-animate#js-snowplow-settings{ class: ('expanded' if expanded) }
|
||||
%section.settings.as-snowplow.no-animate#js-snowplow-settings{ class: ('expanded' if expanded), data: { qa_selector: 'snowplow_settings_content' } }
|
||||
.settings-header
|
||||
%h4
|
||||
= _('Snowplow')
|
||||
|
@ -15,7 +15,7 @@
|
|||
%fieldset
|
||||
.form-group
|
||||
.form-check
|
||||
= f.check_box :snowplow_enabled, class: 'form-check-input'
|
||||
= f.check_box :snowplow_enabled, class: 'form-check-input', data: { qa_selector: 'snowplow_enabled_checkbox' }
|
||||
= f.label :snowplow_enabled, _('Enable Snowplow tracking'), class: 'form-check-label'
|
||||
.form-group
|
||||
= f.label :snowplow_collector_hostname, _('Collector hostname'), class: 'label-light'
|
||||
|
@ -33,4 +33,4 @@
|
|||
.form-text.text-muted
|
||||
= _('The Snowplow cookie domain.')
|
||||
|
||||
= f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
|
||||
= f.submit _('Save changes'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'save_changes_button' }
|
||||
|
|
|
@ -116,7 +116,7 @@
|
|||
%h5= _('Private profile')
|
||||
.checkbox-icon-inline-wrapper
|
||||
- private_profile_label = capture do
|
||||
= s_("Profiles|Don't display activity-related personal information on your profiles")
|
||||
= s_("Profiles|Don't display activity-related personal information on your profile")
|
||||
= f.check_box :private_profile, label: private_profile_label, inline: true, wrapper_class: 'mr-0'
|
||||
= link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'make-your-user-profile-page-private')
|
||||
%h5= s_("Profiles|Private contributions")
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
.gl-alert.gl-alert-success.gl-mb-4.gl-display-none.js-user-readme-repo
|
||||
= sprite_icon('check-circle', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
||||
.gl-alert-body
|
||||
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/profile/index', anchor: 'user-profile-readme') }
|
||||
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/profile/index', anchor: 'add-details-to-your-profile-with-a-readme') }
|
||||
= html_escape(_('%{project_path} is a project that you can use to add a README to your GitLab profile. Create a public project and initialize the repository with a README to get started. %{help_link_start}Learn more.%{help_link_end}')) % { project_path: "<strong>#{current_user.username} / #{current_user.username}</strong>".html_safe, help_link_start: help_link_start, help_link_end: '</a>'.html_safe }
|
||||
|
||||
.form-group
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_bulk_insert_tags
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73198
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346124
|
||||
milestone: '14.6'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
|
@ -7,7 +7,8 @@ product_stage: configure
|
|||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -7,7 +7,8 @@ product_stage: configure
|
|||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -7,7 +7,8 @@ product_stage: deploy
|
|||
product_group: group::5-min-app
|
||||
product_category: five_minute_production_app
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -8,7 +8,8 @@ product_stage: release
|
|||
product_group: group::release
|
||||
product_category: continuous_delivery
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -7,7 +7,8 @@ product_stage: configure
|
|||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -7,7 +7,8 @@ product_stage: configure
|
|||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -7,7 +7,8 @@ product_stage: configure
|
|||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -6,8 +6,9 @@ product_stage: ''
|
|||
product_group: ''
|
||||
product_category: ''
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone: '14.3'
|
||||
milestone_removed: '14.6'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69204
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
|
|
|
@ -7,7 +7,8 @@ product_stage: configure
|
|||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -7,7 +7,8 @@ product_stage: configure
|
|||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -7,7 +7,8 @@ product_stage: deploy
|
|||
product_group: group::5-min-app
|
||||
product_category: five_minute_production_app
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -8,7 +8,8 @@ product_stage: release
|
|||
product_group: group::release
|
||||
product_category: continuous_delivery
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -7,8 +7,9 @@ product_stage: configure
|
|||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone: '14.3'
|
||||
milestone_removed: '14.6'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69204
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
|
|
|
@ -7,7 +7,8 @@ product_stage: configure
|
|||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone_removed: '14.6'
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: RedisHLLMetric
|
||||
|
|
|
@ -6,8 +6,9 @@ product_stage: configure
|
|||
product_group: group::configure
|
||||
product_category: infrastructure_as_code
|
||||
value_type: number
|
||||
status: active
|
||||
status: removed
|
||||
milestone: '14.3'
|
||||
milestone_removed: '14.6'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69204
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MakeIterationCadencesStartDateNullable < Gitlab::Database::Migration[1.0]
|
||||
def change
|
||||
change_column_null :iterations_cadences, :start_date, true
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
9a3ba69a1df02059b240393cc381c4a5ba9db0f116818aa9f3d4f1009f055b09
|
|
@ -15439,7 +15439,7 @@ CREATE TABLE iterations_cadences (
|
|||
group_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
start_date date NOT NULL,
|
||||
start_date date,
|
||||
last_run_date date,
|
||||
duration_in_weeks integer,
|
||||
iterations_in_advance integer,
|
||||
|
|
|
@ -873,7 +873,7 @@ project.container_repositories.find_each do |repo|
|
|||
puts repo.attributes
|
||||
|
||||
# Start the tag cleanup
|
||||
puts Projects::ContainerRepository::CleanupTagsService.new(project, user, policy.attributes.except("created_at", "updated_at")).execute(repo)
|
||||
puts Projects::ContainerRepository::CleanupTagsService.new(repo, user, policy.attributes.except("created_at", "updated_at")).execute()
|
||||
end
|
||||
```
|
||||
|
||||
|
|
|
@ -26,6 +26,10 @@ To implement a new metric in Service Ping, follow these steps:
|
|||
1. [Verify your metric](#verify-your-metric)
|
||||
1. [Set up and test Service Ping locally](#set-up-and-test-service-ping-locally)
|
||||
|
||||
NOTE:
|
||||
When you add or change a Service Metric, you must migrate metrics to [instrumentation classes](metrics_instrumentation.md).
|
||||
For information about the progress on migrating Service ping metrics, see this [epic](https://gitlab.com/groups/gitlab-org/-/epics/5547).
|
||||
|
||||
## Instrumentation classes
|
||||
|
||||
We recommend you use [instrumentation classes](metrics_instrumentation.md) in `usage_data.rb` where possible.
|
||||
|
|
|
@ -100,17 +100,17 @@ When visiting the public page of a user, you can only see the projects which you
|
|||
If the [public level is restricted](../admin_area/settings/visibility_and_access_controls.md#restrict-visibility-levels),
|
||||
user profiles are only visible to signed-in users.
|
||||
|
||||
## User profile README
|
||||
## Add details to your profile with a README
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232157) in GitLab 14.5.
|
||||
|
||||
You can add a README section to your profile that can include more information and [formatting](../markdown.md) than
|
||||
your profile's bio.
|
||||
If you want to add more information to your profile page, you can create a README file. When you populate the README file with information, it's included on your profile page.
|
||||
|
||||
To add a README to your profile:
|
||||
|
||||
1. Create a new public project with the same project path as your GitLab username.
|
||||
1. Create a README file inside this project. The file can be any valid [README or index file](../project/repository/index.md#readme-and-index-files).
|
||||
1. Populate the README file with [Markdown](../markdown.md).
|
||||
|
||||
To use an existing project, [update the path](../project/settings/index.md#renaming-a-repository) of the project to match
|
||||
your username.
|
||||
|
|
|
@ -6,11 +6,17 @@ module Gitlab
|
|||
module Chain
|
||||
class Create < Chain::Base
|
||||
include Chain::Helpers
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def perform!
|
||||
logger.instrument(:pipeline_save) do
|
||||
BulkInsertableAssociations.with_bulk_insert do
|
||||
pipeline.save!
|
||||
tags = extract_tag_list_by_status
|
||||
|
||||
pipeline.transaction do
|
||||
pipeline.save!
|
||||
CommitStatus.bulk_insert_tags!(statuses, tags) if bulk_insert_tags?
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
|
@ -20,6 +26,37 @@ module Gitlab
|
|||
def break?
|
||||
!pipeline.persisted?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def statuses
|
||||
strong_memoize(:statuses) do
|
||||
pipeline.stages.flat_map(&:statuses)
|
||||
end
|
||||
end
|
||||
|
||||
# We call `job.tag_list=` to assign tags to the jobs from the
|
||||
# Chain::Seed step which uses the `@tag_list` instance variable to
|
||||
# store them on the record. We remove them here because we want to
|
||||
# bulk insert them, otherwise they would be inserted and assigned one
|
||||
# by one with callbacks. We must use `remove_instance_variable`
|
||||
# because having the instance variable defined would still run the callbacks
|
||||
def extract_tag_list_by_status
|
||||
return {} unless bulk_insert_tags?
|
||||
|
||||
statuses.each.with_object({}) do |job, acc|
|
||||
tag_list = job.clear_memoization(:tag_list)
|
||||
next unless tag_list
|
||||
|
||||
acc[job.name] = tag_list
|
||||
end
|
||||
end
|
||||
|
||||
def bulk_insert_tags?
|
||||
strong_memoize(:bulk_insert_tags) do
|
||||
::Feature.enabled?(:ci_bulk_insert_tags, project, default_enabled: :yaml)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -200,11 +200,13 @@ module Gitlab
|
|||
end
|
||||
|
||||
def runner_tags
|
||||
{ tag_list: evaluate_runner_tags }.compact
|
||||
strong_memoize(:runner_tags) do
|
||||
{ tag_list: evaluate_runner_tags }.compact
|
||||
end
|
||||
end
|
||||
|
||||
def evaluate_runner_tags
|
||||
@seed_attributes[:tag_list]&.map do |tag|
|
||||
@seed_attributes.delete(:tag_list)&.map do |tag|
|
||||
ExpandVariables.expand_existing(tag, -> { evaluate_context.variables_hash })
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Tags
|
||||
class BulkInsert
|
||||
TAGGINGS_BATCH_SIZE = 1000
|
||||
TAGS_BATCH_SIZE = 500
|
||||
|
||||
def initialize(statuses, tag_list_by_status)
|
||||
@statuses = statuses
|
||||
@tag_list_by_status = tag_list_by_status
|
||||
end
|
||||
|
||||
def insert!
|
||||
return false if tag_list_by_status.empty?
|
||||
|
||||
persist_build_tags!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :statuses, :tag_list_by_status
|
||||
|
||||
def persist_build_tags!
|
||||
all_tags = tag_list_by_status.values.flatten.uniq.reject(&:blank?)
|
||||
tag_records_by_name = create_tags(all_tags).index_by(&:name)
|
||||
taggings = build_taggings_attributes(tag_records_by_name)
|
||||
|
||||
return false if taggings.empty?
|
||||
|
||||
taggings.each_slice(TAGGINGS_BATCH_SIZE) do |taggings_slice|
|
||||
ActsAsTaggableOn::Tagging.insert_all!(taggings)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def create_tags(tags)
|
||||
existing_tag_records = ActsAsTaggableOn::Tag.where(name: tags).to_a
|
||||
missing_tags = detect_missing_tags(tags, existing_tag_records)
|
||||
return existing_tag_records if missing_tags.empty?
|
||||
|
||||
missing_tags
|
||||
.map { |tag| { name: tag } }
|
||||
.each_slice(TAGS_BATCH_SIZE) do |tags_attributes|
|
||||
ActsAsTaggableOn::Tag.insert_all!(tags_attributes)
|
||||
end
|
||||
|
||||
ActsAsTaggableOn::Tag.where(name: tags).to_a
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def build_taggings_attributes(tag_records_by_name)
|
||||
taggings = statuses.flat_map do |status|
|
||||
tag_list = tag_list_by_status[status.name]
|
||||
next unless tag_list
|
||||
|
||||
tags = tag_records_by_name.values_at(*tag_list)
|
||||
taggings_for(tags, status)
|
||||
end
|
||||
|
||||
taggings.compact!
|
||||
taggings
|
||||
end
|
||||
|
||||
def taggings_for(tags, status)
|
||||
tags.map do |tag|
|
||||
{
|
||||
tag_id: tag.id,
|
||||
taggable_type: CommitStatus.name,
|
||||
taggable_id: status.id,
|
||||
created_at: Time.current,
|
||||
context: 'tags'
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def detect_missing_tags(tags, tag_records)
|
||||
if tags.size != tag_records.size
|
||||
tags - tag_records.map(&:name)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,6 +9,7 @@ pages:
|
|||
script:
|
||||
- mkdir .public
|
||||
- cp -r * .public
|
||||
- rm -rf public
|
||||
- mv .public public
|
||||
artifacts:
|
||||
paths:
|
||||
|
|
|
@ -11174,6 +11174,9 @@ msgstr ""
|
|||
msgid "Delete Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Value Stream"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26597,7 +26600,7 @@ msgstr ""
|
|||
msgid "Profiles|Do not show on profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Don't display activity-related personal information on your profiles"
|
||||
msgid "Profiles|Don't display activity-related personal information on your profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Edit Profile"
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Flow
|
||||
module Settings
|
||||
module_function
|
||||
|
||||
def disable_snowplow
|
||||
Flow::Login.while_signed_in_as_admin do
|
||||
QA::Page::Main::Menu.perform(&:go_to_admin_area)
|
||||
QA::Page::Admin::Menu.perform(&:go_to_general_settings)
|
||||
QA::Page::Admin::Settings::Component::Snowplow.perform(&:disable_snowplow_tracking)
|
||||
end
|
||||
end
|
||||
|
||||
def enable_snowplow
|
||||
Flow::Login.while_signed_in_as_admin do
|
||||
QA::Page::Main::Menu.perform(&:go_to_admin_area)
|
||||
QA::Page::Admin::Menu.perform(&:go_to_general_settings)
|
||||
QA::Page::Admin::Settings::Component::Snowplow.perform(&:enable_snowplow_tracking)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Page
|
||||
module Admin
|
||||
module Settings
|
||||
module Component
|
||||
class Snowplow < Page::Base
|
||||
include QA::Page::Settings::Common
|
||||
|
||||
view 'app/views/admin/application_settings/_snowplow.html.haml' do
|
||||
element :snowplow_settings_content
|
||||
element :snowplow_enabled_checkbox
|
||||
element :save_changes_button
|
||||
end
|
||||
|
||||
def enable_snowplow_tracking
|
||||
expand_content(:snowplow_settings_content) do
|
||||
check_snowplow_enabled_checkbox
|
||||
click_save_changes_button
|
||||
end
|
||||
end
|
||||
|
||||
def disable_snowplow_tracking
|
||||
expand_content(:snowplow_settings_content) do
|
||||
uncheck_snowplow_enabled_checkbox
|
||||
click_save_changes_button
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_snowplow_enabled_checkbox
|
||||
check_element(:snowplow_enabled_checkbox)
|
||||
end
|
||||
|
||||
def uncheck_snowplow_enabled_checkbox
|
||||
uncheck_element(:snowplow_enabled_checkbox)
|
||||
end
|
||||
|
||||
def click_save_changes_button
|
||||
click_element :save_changes_button
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -190,7 +190,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
after :create do |project, evaluator|
|
||||
raise "Failed to create repository!" unless project.create_repository
|
||||
raise "Failed to create repository!" unless project.repository.exists? || project.create_repository
|
||||
|
||||
evaluator.files.each do |filename, content|
|
||||
project.repository.create_file(
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
import { GlButton, GlFormCheckbox, GlKeysetPagination } from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import component from '~/packages_and_registries/shared/components/registry_list.vue';
|
||||
|
||||
describe('Registry List', () => {
|
||||
let wrapper;
|
||||
|
||||
const items = [{ id: 'a' }, { id: 'b' }];
|
||||
const defaultPropsData = {
|
||||
title: 'test_title',
|
||||
items,
|
||||
};
|
||||
|
||||
const rowScopedSlot = `
|
||||
<div data-testid="scoped-slot">
|
||||
<button @click="props.selectItem(props.item)">Select</button>
|
||||
<span>{{props.first}}</span>
|
||||
<p>{{props.isSelected(props.item)}}</p>
|
||||
</div>`;
|
||||
|
||||
const mountComponent = ({ propsData = defaultPropsData } = {}) => {
|
||||
wrapper = shallowMountExtended(component, {
|
||||
propsData,
|
||||
scopedSlots: {
|
||||
default: rowScopedSlot,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findSelectAll = () => wrapper.findComponent(GlFormCheckbox);
|
||||
const findDeleteSelected = () => wrapper.findComponent(GlButton);
|
||||
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
|
||||
const findScopedSlots = () => wrapper.findAllByTestId('scoped-slot');
|
||||
const findScopedSlotSelectButton = (index) => findScopedSlots().at(index).find('button');
|
||||
const findScopedSlotFirstValue = (index) => findScopedSlots().at(index).find('span');
|
||||
const findScopedSlotIsSelectedValue = (index) => findScopedSlots().at(index).find('p');
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('header', () => {
|
||||
it('renders the title passed in the prop', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(wrapper.text()).toContain(defaultPropsData.title);
|
||||
});
|
||||
|
||||
describe('select all checkbox', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('exists', () => {
|
||||
expect(findSelectAll().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('select and unselect all', async () => {
|
||||
// no row is not selected
|
||||
items.forEach((item, index) => {
|
||||
expect(findScopedSlotIsSelectedValue(index).text()).toBe('');
|
||||
});
|
||||
|
||||
// simulate selection
|
||||
findSelectAll().vm.$emit('input', true);
|
||||
await nextTick();
|
||||
|
||||
// all rows selected
|
||||
items.forEach((item, index) => {
|
||||
expect(findScopedSlotIsSelectedValue(index).text()).toBe('true');
|
||||
});
|
||||
|
||||
// simulate de-selection
|
||||
findSelectAll().vm.$emit('input', '');
|
||||
await nextTick();
|
||||
|
||||
// no row is not selected
|
||||
items.forEach((item, index) => {
|
||||
expect(findScopedSlotIsSelectedValue(index).text()).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete button', () => {
|
||||
it('has the correct text', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findDeleteSelected().text()).toBe(component.i18n.deleteSelected);
|
||||
});
|
||||
|
||||
it('is hidden when hiddenDelete is true', () => {
|
||||
mountComponent({ propsData: { ...defaultPropsData, hiddenDelete: true } });
|
||||
|
||||
expect(findDeleteSelected().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('is disabled when isLoading is true', () => {
|
||||
mountComponent({ propsData: { ...defaultPropsData, isLoading: true } });
|
||||
|
||||
expect(findDeleteSelected().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('is disabled when no row is selected', async () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findDeleteSelected().props('disabled')).toBe(true);
|
||||
|
||||
await findScopedSlotSelectButton(0).trigger('click');
|
||||
|
||||
expect(findDeleteSelected().props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('on click emits the delete event with the selected rows', async () => {
|
||||
mountComponent();
|
||||
|
||||
await findScopedSlotSelectButton(0).trigger('click');
|
||||
|
||||
findDeleteSelected().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('delete')).toEqual([[[items[0]]]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('main area', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('renders scopedSlots based on the items props', () => {
|
||||
expect(findScopedSlots()).toHaveLength(items.length);
|
||||
});
|
||||
|
||||
it('populates the scope of the slot correctly', async () => {
|
||||
expect(findScopedSlots().at(0).exists()).toBe(true);
|
||||
|
||||
// it's the first slot
|
||||
expect(findScopedSlotFirstValue(0).text()).toBe('true');
|
||||
|
||||
// item is not selected, falsy is translated to empty string
|
||||
expect(findScopedSlotIsSelectedValue(0).text()).toBe('');
|
||||
|
||||
// find the button with the bound function
|
||||
await findScopedSlotSelectButton(0).trigger('click');
|
||||
|
||||
// the item is selected
|
||||
expect(findScopedSlotIsSelectedValue(0).text()).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('footer', () => {
|
||||
let pagination;
|
||||
|
||||
beforeEach(() => {
|
||||
pagination = { hasPreviousPage: false, hasNextPage: true };
|
||||
});
|
||||
|
||||
it('has a pagination', () => {
|
||||
mountComponent({
|
||||
propsData: { ...defaultPropsData, pagination },
|
||||
});
|
||||
|
||||
expect(findPagination().props()).toMatchObject(pagination);
|
||||
});
|
||||
|
||||
it.each`
|
||||
hasPreviousPage | hasNextPage | visible
|
||||
${true} | ${true} | ${true}
|
||||
${true} | ${false} | ${true}
|
||||
${false} | ${true} | ${true}
|
||||
${false} | ${false} | ${false}
|
||||
`(
|
||||
'when hasPreviousPage is $hasPreviousPage and hasNextPage is $hasNextPage is $visible that the pagination is shown',
|
||||
({ hasPreviousPage, hasNextPage, visible }) => {
|
||||
pagination = { hasPreviousPage, hasNextPage };
|
||||
mountComponent({
|
||||
propsData: { ...defaultPropsData, pagination },
|
||||
});
|
||||
|
||||
expect(findPagination().exists()).toBe(visible);
|
||||
},
|
||||
);
|
||||
|
||||
it('pagination emits the correct events', () => {
|
||||
mountComponent({
|
||||
propsData: { ...defaultPropsData, pagination },
|
||||
});
|
||||
|
||||
findPagination().vm.$emit('prev');
|
||||
|
||||
expect(wrapper.emitted('prev-page')).toEqual([[]]);
|
||||
|
||||
findPagination().vm.$emit('next');
|
||||
|
||||
expect(wrapper.emitted('next-page')).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -56,4 +56,74 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Create do
|
|||
.to include /Failed to persist the pipeline/
|
||||
end
|
||||
end
|
||||
|
||||
context 'tags persistence' do
|
||||
let(:stage) do
|
||||
build(:ci_stage_entity, pipeline: pipeline)
|
||||
end
|
||||
|
||||
let(:job) do
|
||||
build(:ci_build, stage: stage, pipeline: pipeline, project: project)
|
||||
end
|
||||
|
||||
let(:bridge) do
|
||||
build(:ci_bridge, stage: stage, pipeline: pipeline, project: project)
|
||||
end
|
||||
|
||||
before do
|
||||
pipeline.stages = [stage]
|
||||
stage.statuses = [job, bridge]
|
||||
end
|
||||
|
||||
context 'without tags' do
|
||||
it 'extracts an empty tag list' do
|
||||
expect(CommitStatus)
|
||||
.to receive(:bulk_insert_tags!)
|
||||
.with(stage.statuses, {})
|
||||
.and_call_original
|
||||
|
||||
step.perform!
|
||||
|
||||
expect(job.instance_variable_defined?(:@tag_list)).to be_falsey
|
||||
expect(job).to be_persisted
|
||||
expect(job.tag_list).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with tags' do
|
||||
before do
|
||||
job.tag_list = %w[tag1 tag2]
|
||||
end
|
||||
|
||||
it 'bulk inserts tags' do
|
||||
expect(CommitStatus)
|
||||
.to receive(:bulk_insert_tags!)
|
||||
.with(stage.statuses, { job.name => %w[tag1 tag2] })
|
||||
.and_call_original
|
||||
|
||||
step.perform!
|
||||
|
||||
expect(job.instance_variable_defined?(:@tag_list)).to be_falsey
|
||||
expect(job).to be_persisted
|
||||
expect(job.tag_list).to match_array(%w[tag1 tag2])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature flag is disabled' do
|
||||
before do
|
||||
job.tag_list = %w[tag1 tag2]
|
||||
stub_feature_flags(ci_bulk_insert_tags: false)
|
||||
end
|
||||
|
||||
it 'follows the old code path' do
|
||||
expect(CommitStatus).not_to receive(:bulk_insert_tags!)
|
||||
|
||||
step.perform!
|
||||
|
||||
expect(job.instance_variable_defined?(:@tag_list)).to be_truthy
|
||||
expect(job).to be_persisted
|
||||
expect(job.reload.tag_list).to match_array(%w[tag1 tag2])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Tags::BulkInsert do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let_it_be_with_refind(:job) { create(:ci_build, :unique_name, pipeline: pipeline, project: project) }
|
||||
let_it_be_with_refind(:other_job) { create(:ci_build, :unique_name, pipeline: pipeline, project: project) }
|
||||
let_it_be_with_refind(:bridge) { create(:ci_bridge, pipeline: pipeline, project: project) }
|
||||
|
||||
let(:statuses) { [job, bridge, other_job] }
|
||||
|
||||
subject(:service) { described_class.new(statuses, tags_list) }
|
||||
|
||||
describe '#insert!' do
|
||||
context 'without tags' do
|
||||
let(:tags_list) { {} }
|
||||
|
||||
it { expect(service.insert!).to be_falsey }
|
||||
end
|
||||
|
||||
context 'with tags' do
|
||||
let(:tags_list) do
|
||||
{
|
||||
job.name => %w[tag1 tag2],
|
||||
other_job.name => %w[tag2 tag3 tag4]
|
||||
}
|
||||
end
|
||||
|
||||
it 'persists tags' do
|
||||
expect(service.insert!).to be_truthy
|
||||
|
||||
expect(job.reload.tag_list).to match_array(%w[tag1 tag2])
|
||||
expect(other_job.reload.tag_list).to match_array(%w[tag2 tag3 tag4])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -958,4 +958,21 @@ RSpec.describe CommitStatus do
|
|||
expect(build_from_other_pipeline.reload).to have_attributes(retried: false, processed: false)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.bulk_insert_tags!' do
|
||||
let(:statuses) { double('statuses') }
|
||||
let(:tag_list_by_build) { double('tag list') }
|
||||
let(:inserter) { double('inserter') }
|
||||
|
||||
it 'delegates to bulk insert class' do
|
||||
expect(Gitlab::Ci::Tags::BulkInsert)
|
||||
.to receive(:new)
|
||||
.with(statuses, tag_list_by_build)
|
||||
.and_return(inserter)
|
||||
|
||||
expect(inserter).to receive(:insert!)
|
||||
|
||||
described_class.bulk_insert_tags!(statuses, tag_list_by_build)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,16 +7,15 @@ RSpec.describe Ci::CreatePipelineService do
|
|||
let_it_be(:user) { project.owner }
|
||||
|
||||
let(:ref) { 'refs/heads/master' }
|
||||
let(:source) { :push }
|
||||
let(:service) { described_class.new(project, user, { ref: ref }) }
|
||||
let(:pipeline) { service.execute(source).payload }
|
||||
let(:pipeline) { create_pipeline }
|
||||
|
||||
before do
|
||||
stub_ci_pipeline_yaml_file(config)
|
||||
stub_yaml_config(config)
|
||||
end
|
||||
|
||||
context 'with valid config' do
|
||||
let(:config) { YAML.dump({ test: { script: 'ls', tags: %w[tag1 tag2] } }) }
|
||||
let(:config) { { test: { script: 'ls', tags: %w[tag1 tag2] } } }
|
||||
|
||||
it 'creates a pipeline', :aggregate_failures do
|
||||
expect(pipeline).to be_created_successfully
|
||||
|
@ -25,8 +24,8 @@ RSpec.describe Ci::CreatePipelineService do
|
|||
end
|
||||
|
||||
context 'with too many tags' do
|
||||
let(:tags) { Array.new(50) {|i| "tag-#{i}" } }
|
||||
let(:config) { YAML.dump({ test: { script: 'ls', tags: tags } }) }
|
||||
let(:tags) { build_tag_list(label: 'custom', size: 50) }
|
||||
let(:config) { { test: { script: 'ls', tags: tags } } }
|
||||
|
||||
it 'creates a pipeline without builds', :aggregate_failures do
|
||||
expect(pipeline).not_to be_created_successfully
|
||||
|
@ -34,5 +33,167 @@ RSpec.describe Ci::CreatePipelineService do
|
|||
expect(pipeline.yaml_errors).to eq("jobs:test:tags config must be less than the limit of #{Gitlab::Ci::Config::Entry::Tags::TAGS_LIMIT} tags")
|
||||
end
|
||||
end
|
||||
|
||||
context 'tags persistence' do
|
||||
let(:config) do
|
||||
{
|
||||
build: {
|
||||
script: 'ls',
|
||||
stage: 'build',
|
||||
tags: build_tag_list(label: 'build')
|
||||
},
|
||||
test: {
|
||||
script: 'ls',
|
||||
stage: 'test',
|
||||
tags: build_tag_list(label: 'test')
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:config_without_tags) do
|
||||
config.transform_values { |job| job.except(:tags) }
|
||||
end
|
||||
|
||||
context 'with multiple tags' do
|
||||
context 'when the tags do not exist' do
|
||||
it 'does not execute N+1 queries' do
|
||||
stub_yaml_config(config_without_tags)
|
||||
|
||||
# warm up the cached objects so we get a more accurate count
|
||||
create_pipeline
|
||||
|
||||
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
||||
create_pipeline
|
||||
end
|
||||
|
||||
stub_yaml_config(config)
|
||||
|
||||
# 2 select tags.*
|
||||
# 1 insert tags
|
||||
# 1 insert taggings
|
||||
tags_queries_size = 4
|
||||
|
||||
expect { pipeline }
|
||||
.not_to exceed_all_query_limit(control)
|
||||
.with_threshold(tags_queries_size)
|
||||
|
||||
expect(pipeline).to be_created_successfully
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_bulk_insert_tags: false)
|
||||
end
|
||||
|
||||
it 'executes N+1s queries' do
|
||||
stub_yaml_config(config_without_tags)
|
||||
|
||||
# warm up the cached objects so we get a more accurate count
|
||||
create_pipeline
|
||||
|
||||
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
||||
create_pipeline
|
||||
end
|
||||
|
||||
stub_yaml_config(config)
|
||||
|
||||
expect { pipeline }
|
||||
.to exceed_all_query_limit(control)
|
||||
.with_threshold(4)
|
||||
|
||||
expect(pipeline).to be_created_successfully
|
||||
end
|
||||
end
|
||||
|
||||
context 'when tags are already persisted' do
|
||||
it 'does not execute N+1 queries' do
|
||||
# warm up the cached objects so we get a more accurate count
|
||||
# and insert the tags
|
||||
create_pipeline
|
||||
|
||||
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
|
||||
create_pipeline
|
||||
end
|
||||
|
||||
# 1 select tags.*
|
||||
# 1 insert taggings
|
||||
tags_queries_size = 2
|
||||
|
||||
expect { pipeline }
|
||||
.not_to exceed_all_query_limit(control)
|
||||
.with_threshold(tags_queries_size)
|
||||
|
||||
expect(pipeline).to be_created_successfully
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with bridge jobs' do
|
||||
let(:config) do
|
||||
{
|
||||
test_1: {
|
||||
script: 'ls',
|
||||
stage: 'test',
|
||||
tags: build_tag_list(label: 'test_1')
|
||||
},
|
||||
test_2: {
|
||||
script: 'ls',
|
||||
stage: 'test',
|
||||
tags: build_tag_list(label: '$CI_JOB_NAME')
|
||||
},
|
||||
test_3: {
|
||||
script: 'ls',
|
||||
stage: 'test',
|
||||
tags: build_tag_list(label: 'test_1') + build_tag_list(label: 'test_2')
|
||||
},
|
||||
test_4: {
|
||||
script: 'ls',
|
||||
stage: 'test'
|
||||
},
|
||||
deploy: {
|
||||
stage: 'deploy',
|
||||
trigger: 'my/project'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it do
|
||||
expect(pipeline).to be_created_successfully
|
||||
expect(pipeline.bridges.size).to eq(1)
|
||||
expect(pipeline.builds.size).to eq(4)
|
||||
|
||||
expect(tags_for('test_1'))
|
||||
.to have_attributes(count: 5)
|
||||
.and all(match(/test_1-tag-\d+/))
|
||||
|
||||
expect(tags_for('test_2'))
|
||||
.to have_attributes(count: 5)
|
||||
.and all(match(/test_2-tag-\d+/))
|
||||
|
||||
expect(tags_for('test_3'))
|
||||
.to have_attributes(count: 10)
|
||||
.and all(match(/test_[1,2]-tag-\d+/))
|
||||
|
||||
expect(tags_for('test_4')).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def tags_for(build_name)
|
||||
pipeline.builds.find_by_name(build_name).tag_list
|
||||
end
|
||||
|
||||
def stub_yaml_config(config)
|
||||
stub_ci_pipeline_yaml_file(YAML.dump(config))
|
||||
end
|
||||
|
||||
def create_pipeline
|
||||
service.execute(:push).payload
|
||||
end
|
||||
|
||||
def build_tag_list(label:, size: 5)
|
||||
Array.new(size) { |index| "#{label}-tag-#{index}" }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
- "./spec/lib/gitlab/ci/pipeline/chain/create_spec.rb"
|
||||
- "./spec/lib/gitlab/ci/pipeline/chain/seed_block_spec.rb"
|
||||
- "./spec/lib/gitlab/ci/pipeline/seed/build_spec.rb"
|
||||
- "./spec/lib/gitlab/ci/tags/bulk_insert_spec.rb"
|
||||
- "./spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb"
|
||||
- "./spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb"
|
||||
- "./spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb"
|
||||
|
|
Loading…
Reference in New Issue