Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-07-04 09:11:02 +00:00
parent 1816056039
commit 5f95234f7b
64 changed files with 562 additions and 150 deletions

View File

@ -2369,11 +2369,9 @@
- <<: *if-merge-request-labels-run-review-app
- <<: *if-dot-com-gitlab-org-merge-request
changes: *ci-review-patterns
allow_failure: true
- <<: *if-dot-com-gitlab-org-merge-request
changes: *frontend-build-patterns
variables: *review-change-pattern
allow_failure: true
- <<: *if-dot-com-gitlab-org-merge-request
changes: *controllers-patterns
variables: *review-change-pattern
@ -2391,7 +2389,6 @@
allow_failure: true
- <<: *if-dot-com-gitlab-org-merge-request
changes: *qa-patterns
allow_failure: true
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-patterns
when: manual

View File

@ -78,6 +78,9 @@ export default {
},
computed: {
...mapState(['isShowingLabels', 'allowSubEpics']),
isLoading() {
return this.item.isLoading || this.item.iid === '-1';
},
cappedAssignees() {
// e.g. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
@ -243,7 +246,7 @@ export default {
<a
:href="item.path || item.webUrl || ''"
:title="item.title"
:class="{ 'gl-text-gray-400!': item.isLoading }"
:class="{ 'gl-text-gray-400!': isLoading }"
class="js-no-trigger gl-text-body gl-hover-text-gray-900"
@mousemove.stop
>{{ item.title }}</a
@ -272,9 +275,9 @@ export default {
<div
class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden"
>
<gl-loading-icon v-if="item.isLoading" size="lg" class="gl-mt-5" />
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-5" />
<span
v-if="item.referencePath"
v-if="item.referencePath && !isLoading"
class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3 gl-font-sm gl-text-secondary"
:class="{ 'gl-font-base': isEpicBoard }"
>

View File

@ -22,6 +22,7 @@ import {
removeItemFromList,
updateEpicsCount,
updateIssueCountAndWeight,
setError,
} from '../graphql/cache_updates';
import { shouldCloneCard, moveItemVariables } from '../boards_util';
import eventHub from '../eventhub';
@ -33,7 +34,7 @@ export default {
name: 'BoardList',
i18n: {
loading: __('Loading'),
loadingMoreboardItems: __('Loading more'),
loadingMoreBoardItems: __('Loading more'),
showingAllIssues: __('Showing all issues'),
showingAllEpics: __('Showing all epics'),
},
@ -83,6 +84,7 @@ export default {
isLoadingMore: false,
toListId: null,
toList: {},
addItemToListInProgress: false,
};
},
apollo: {
@ -213,7 +215,8 @@ export default {
return !this.disabled;
},
treeRootWrapper() {
return this.canMoveIssue && !this.listsFlags[this.list.id]?.addItemToListInProgress
return this.canMoveIssue &&
(!this.listsFlags[this.list.id]?.addItemToListInProgress || this.addItemToListInProgress)
? Draggable
: 'ul';
},
@ -468,14 +471,14 @@ export default {
this.updateCountAndWeight({ fromListId, toListId, issuable, cache });
},
updateCountAndWeight({ fromListId, toListId, issuable, isAddingIssue, cache }) {
updateCountAndWeight({ fromListId, toListId, issuable, isAddingItem, cache }) {
if (!this.isEpicBoard) {
updateIssueCountAndWeight({
fromListId,
toListId,
filterParams: this.filterParams,
issuable,
shouldClone: isAddingIssue || this.shouldCloneCard,
shouldClone: isAddingItem || this.shouldCloneCard,
cache,
});
} else {
@ -486,7 +489,7 @@ export default {
fromListId,
filterParams,
issuable,
shouldClone: this.shouldCloneCard,
shouldClone: isAddingItem || this.shouldCloneCard,
cache,
});
}
@ -538,6 +541,59 @@ export default {
},
});
},
async addListItem(input) {
this.toggleForm();
this.addItemToListInProgress = true;
try {
await this.$apollo.mutate({
mutation: listIssuablesQueries[this.issuableType].createMutation,
variables: {
input: this.isEpicBoard ? input : { ...input, moveAfterId: this.boardListItems[0]?.id },
withColor: this.isEpicBoard && this.glFeatures.epicColorHighlight,
},
update: (cache, { data: { createIssuable } }) => {
const { issuable } = createIssuable;
addItemToList({
query: listIssuablesQueries[this.issuableType].query,
variables: { ...this.listQueryVariables, id: this.currentList.id },
issuable,
newIndex: 0,
boardType: this.boardType,
issuableType: this.issuableType,
cache,
});
this.updateCountAndWeight({
fromListId: null,
toListId: this.list.id,
issuable,
isAddingItem: true,
cache,
});
},
optimisticResponse: {
createIssuable: {
errors: [],
issuable: {
...listIssuablesQueries[this.issuableType].optimisticResponse,
title: input.title,
},
},
},
});
} catch (error) {
setError({
message: sprintf(
__('An error occurred while creating the %{issuableType}. Please try again.'),
{
issuableType: this.isEpicBoard ? 'epic' : 'issue',
},
),
error,
});
} finally {
this.addItemToListInProgress = false;
}
},
},
};
</script>
@ -556,8 +612,18 @@ export default {
>
<gl-loading-icon size="sm" />
</div>
<board-new-issue v-if="issueCreateFormVisible" :list="list" />
<board-new-epic v-if="epicCreateFormVisible" :list="list" />
<board-new-issue
v-if="issueCreateFormVisible"
:list="list"
:board-id="boardId"
@addNewIssue="addListItem"
/>
<board-new-epic
v-if="epicCreateFormVisible"
:list="list"
:board-id="boardId"
@addNewEpic="addListItem"
/>
<component
:is="treeRootWrapper"
v-show="!loading"
@ -610,7 +676,7 @@ export default {
<gl-loading-icon
v-if="loadingMore"
size="sm"
:label="$options.i18n.loadingMoreboardItems"
:label="$options.i18n.loadingMoreBoardItems"
/>
<span v-if="showingAllItems">{{ showingAllItemsText }}</span>
<span v-else>{{ paginatedIssueText }}</span>

View File

@ -1,33 +1,72 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import { s__ } from '~/locale';
import { getMilestone, formatIssueInput, getBoardQuery } from 'ee_else_ce/boards/boards_util';
import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
import { toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
import { setError } from '../graphql/cache_updates';
import BoardNewItem from './board_new_item.vue';
import ProjectSelect from './project_select.vue';
export default {
name: 'BoardNewIssue',
i18n: {
errorFetchingBoard: s__('Boards|An error occurred while fetching board. Please try again.'),
},
components: {
BoardNewItem,
ProjectSelect,
},
mixins: [BoardNewIssueMixin],
inject: ['fullPath', 'isGroupBoard'],
inject: ['boardType', 'groupId', 'fullPath', 'isGroupBoard', 'isEpicBoard', 'isApolloBoard'],
props: {
list: {
type: Object,
required: true,
},
boardId: {
type: String,
required: true,
},
},
data() {
return {
selectedProject: {},
board: {},
};
},
apollo: {
board: {
query() {
return getBoardQuery(this.boardType, this.isEpicBoard);
},
variables() {
return {
fullPath: this.fullPath,
boardId: this.boardId,
};
},
skip() {
return !this.isApolloBoard;
},
update(data) {
const { board } = data.workspace;
return {
...board,
labels: board.labels?.nodes,
};
},
error(error) {
setError({
error,
message: this.$options.i18n.errorFetchingBoard,
});
},
},
},
computed: {
...mapGetters(['getBoardItemsByList']),
formEventPrefix() {
@ -46,8 +85,20 @@ export default {
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
const milestone = getMilestone(this.list);
const firstItemId = this.getBoardItemsByList(this.list.id)[0]?.id;
if (this.isApolloBoard) {
return this.addNewIssueToList({
issueInput: {
title,
labelIds: labels?.map((l) => l.id),
assigneeIds: assignees?.map((a) => a?.id),
milestoneId: milestone?.id,
projectPath: this.projectPath,
},
});
}
const firstItemId = this.getBoardItemsByList(this.list.id)[0]?.id;
return this.addListNewIssue({
list: this.list,
issueInput: {
@ -62,6 +113,22 @@ export default {
this.cancel();
});
},
addNewIssueToList({ issueInput }) {
const { labels, assignee, milestone, weight } = this.board;
const config = {
labels,
assigneeId: assignee?.id || null,
milestoneId: milestone?.id || null,
weight,
};
const input = formatIssueInput(issueInput, config);
if (!this.isGroupBoard) {
input.projectPath = this.fullPath;
}
this.$emit('addNewIssue', input);
},
cancel() {
eventHub.$emit(`${this.formEventPrefix}${this.list.id}`);
},

View File

@ -1,6 +1,7 @@
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import { TYPE_EPIC, TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import { s__, __ } from '~/locale';
import { TYPENAME_ISSUE } from '~/graphql_shared/constants';
import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
import createBoardListMutation from './graphql/board_list_create.mutation.graphql';
@ -11,6 +12,7 @@ import toggleListCollapsedMutation from './graphql/client/board_toggle_collapsed
import issueSetSubscriptionMutation from './graphql/issue_set_subscription.mutation.graphql';
import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql';
import issueMoveListMutation from './graphql/issue_move_list.mutation.graphql';
import issueCreateMutation from './graphql/issue_create.mutation.graphql';
import groupBoardQuery from './graphql/group_board.query.graphql';
import projectBoardQuery from './graphql/project_board.query.graphql';
import listIssuesQuery from './graphql/lists_issues.query.graphql';
@ -126,6 +128,30 @@ export const listIssuablesQueries = {
[TYPE_ISSUE]: {
query: listIssuesQuery,
moveMutation: issueMoveListMutation,
createMutation: issueCreateMutation,
optimisticResponse: {
assignees: { nodes: [], __typename: 'UserCoreConnection' },
confidential: false,
dueDate: null,
emailsDisabled: false,
hidden: false,
humanTimeEstimate: null,
humanTotalTimeSpent: null,
id: 'gid://gitlab/Issue/-1',
iid: '-1',
labels: { nodes: [], __typename: 'LabelConnection' },
milestone: null,
referencePath: '',
relativePosition: null,
severity: 'UNKNOWN',
timeEstimate: 0,
title: '',
totalTimeSpent: 0,
type: 'ISSUE',
webUrl: '',
weight: null,
__typename: TYPENAME_ISSUE,
},
},
};

View File

@ -1,8 +1,8 @@
#import "ee_else_ce/boards/graphql/issue.fragment.graphql"
mutation CreateIssue($input: CreateIssueInput!) {
createIssue(input: $input) {
issue {
createIssuable: createIssue(input: $input) {
issuable: issue {
...Issue
}
errors

View File

@ -743,11 +743,11 @@ export default {
},
})
.then(({ data }) => {
if (data.createIssue.errors.length) {
if (data.createIssuable.errors.length) {
throw new Error();
}
const rawIssue = data.createIssue?.issue;
const rawIssue = data.createIssuable?.issuable;
const formattedIssue = formatIssue(rawIssue);
dispatch('removeListItem', { listId: list.id, itemId: placeholderId });
dispatch('addListItem', { list, item: formattedIssue, position: 0 });

View File

@ -16,7 +16,7 @@ module MembershipActions
member_data = if member.expires?
{
expires_soon: member.expires_soon?,
expires_at_formatted: member.expires_at.to_time.in_time_zone.to_s(:medium)
expires_at_formatted: member.expires_at.to_time.in_time_zone.to_fs(:medium)
}
else
{}

View File

@ -1,22 +0,0 @@
# frozen_string_literal: true
class EmptyRepoUploadExperiment < ApplicationExperiment
include ProjectCommitCount
TRACKING_START_DATE = DateTime.parse('2021/4/20')
INITIAL_COMMIT_COUNT = 1
def track_initial_write
return unless should_track? # early return if we don't need to ask for commit counts
return unless context.project.created_at > TRACKING_START_DATE # early return for older projects
return unless commit_count == INITIAL_COMMIT_COUNT
track(:initial_write, project: context.project)
end
private
def commit_count
commit_count_for(context.project, max_count: INITIAL_COMMIT_COUNT, experiment: name)
end
end

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
class ForceCompanyTrialExperiment < ApplicationExperiment
exclude :setup_for_personal
private
def setup_for_personal
!context.user.setup_for_company
end
end

View File

@ -1,9 +0,0 @@
# frozen_string_literal: true
class LoggedOutMarketingHeaderExperiment < ApplicationExperiment
# These default behaviors are overriden in ApplicationHelper and header
# template partial
control {}
candidate {}
variant(:trial_focused) {}
end

View File

@ -17,7 +17,7 @@ module Mutations
description: 'Whether the integration is receiving alerts.'
argument :api_url, GraphQL::Types::String,
required: true,
required: false,
description: 'Endpoint at which Prometheus can be queried.'
def resolve(args)

View File

@ -50,7 +50,7 @@ module IssuablesHelper
def due_date_with_remaining_days(due_date, start_date = nil)
return unless due_date
"#{due_date.to_s(:medium)} (#{remaining_days_in_words(due_date, start_date)})"
"#{due_date.to_fs(:medium)} (#{remaining_days_in_words(due_date, start_date)})"
end
def multi_label_name(current_labels, default_label)

View File

@ -109,7 +109,7 @@ module TimeboxesHelper
content = [
title,
"<br />",
date.to_s(:medium),
date.to_fs(:medium),
"(#{time_ago} #{state})"
].join(" ")
@ -172,7 +172,7 @@ module TimeboxesHelper
def milestone_tooltip_due_date(milestone)
if milestone.due_date
"#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone.due_date, milestone.start_date)})"
"#{milestone.due_date.to_fs(:medium)} (#{remaining_days_in_words(milestone.due_date, milestone.start_date)})"
else
_('Milestone')
end

View File

@ -90,9 +90,9 @@ module Milestoneish
def expires_at
if due_date
if due_date.past?
"expired on #{due_date.to_s(:medium)}"
"expired on #{due_date.to_fs(:medium)}"
else
"expires on #{due_date.to_s(:medium)}"
"expires on #{due_date.to_fs(:medium)}"
end
end
end

View File

@ -351,7 +351,7 @@ class Deployment < ApplicationRecord
end
def formatted_deployment_time
deployed_at&.to_time&.in_time_zone&.to_s(:medium)
deployed_at&.to_time&.in_time_zone&.to_fs(:medium)
end
def deployed_by

View File

@ -15,7 +15,7 @@ module Integrations
title: 'API URL',
placeholder: -> { s_('PrometheusService|https://prometheus.example.com/') },
help: -> { s_('PrometheusService|The Prometheus API base URL.') },
required: true
required: false
field :google_iap_audience_client_id,
title: 'Google IAP Audience Client ID',
@ -34,8 +34,8 @@ module Integrations
# to allow localhost URLs when the following conditions are true:
# 1. api_url is the internal Prometheus URL.
with_options presence: true do
validates :api_url, public_url: true, if: ->(object) { object.manual_configuration? && !object.allow_local_api_url? }
validates :api_url, url: true, if: ->(object) { object.manual_configuration? && object.allow_local_api_url? }
validates :api_url, public_url: true, if: ->(object) { object.api_url.present? && object.manual_configuration? && !object.allow_local_api_url? }
validates :api_url, url: true, if: ->(object) { object.api_url.present? && object.manual_configuration? && object.allow_local_api_url? }
end
before_save :synchronize_service_state

View File

@ -43,7 +43,7 @@
%li
%span.light= _('Created on:')
%strong
= @group.created_at.to_s(:medium)
= @group.created_at.to_fs(:medium)
%li
%span.light= _('ID:')

View File

@ -60,7 +60,7 @@
%span.light
= _('Created on:')
%strong
= @project.created_at.to_s(:medium)
= @project.created_at.to_fs(:medium)
%li{ class: 'gl-px-5!' }
%span.light
@ -158,10 +158,10 @@
= _("This repository has never been checked.")
- elsif @project.last_repository_check_failed?
- failed_message = _("This repository was last checked %{last_check_timestamp}. The check %{strong_start}failed.%{strong_end} See the 'repocheck.log' file for error messages.")
- failed_message = failed_message % { last_check_timestamp: @project.last_repository_check_at.to_s(:medium), strong_start: "<strong class='cred'>", strong_end: "</strong>" }
- failed_message = failed_message % { last_check_timestamp: @project.last_repository_check_at.to_fs(:medium), strong_start: "<strong class='cred'>", strong_end: "</strong>" }
= failed_message.html_safe
- else
= _("This repository was last checked %{last_check_timestamp}. The check passed.") % { last_check_timestamp: @project.last_repository_check_at.to_s(:medium) }
= _("This repository was last checked %{last_check_timestamp}. The check passed.") % { last_check_timestamp: @project.last_repository_check_at.to_fs(:medium) }
= link_to sprite_icon('question-o'), help_page_path('administration/repository_checks')

View File

@ -5,7 +5,7 @@
%ul.content-list
%li
%span.light= _('Member since')
%strong= user.created_at.to_s(:medium)
%strong= user.created_at.to_fs(:medium)
- unless user.public_email.blank?
%li
%span.light= _('E-mail:')

View File

@ -86,12 +86,12 @@
%li
%span.light= _('Member since:')
%strong
= @user.created_at.to_s(:medium)
= @user.created_at.to_fs(:medium)
- if @user.confirmed_at
%li
%span.light= _('Confirmed at:')
%strong
= @user.confirmed_at.to_s(:medium)
= @user.confirmed_at.to_fs(:medium)
- else
%li
%span.ligh= _('Confirmed:')
@ -106,7 +106,7 @@
%li
%span.light= _('Current sign-in at:')
%strong
= @user.current_sign_in_at&.to_s(:medium) || _('never')
= @user.current_sign_in_at&.to_fs(:medium) || _('never')
%li
%span.light= _('Last sign-in IP:')
@ -116,7 +116,7 @@
%li
%span.light= _('Last sign-in at:')
%strong
= @user.last_sign_in_at&.to_s(:medium) || _('never')
= @user.last_sign_in_at&.to_fs(:medium) || _('never')
%li
%span.light= _('Sign-in count:')

View File

@ -5,7 +5,7 @@
%p
= assignees_label(@issue)
%p
= sprintf(s_('Notify|This issue is due on: %{issue_due_date}'), { issue_due_date: @issue.due_date.to_s(:medium) }).html_safe
= sprintf(s_('Notify|This issue is due on: %{issue_due_date}'), { issue_due_date: @issue.due_date.to_fs(:medium) }).html_safe
- if @issue.description
.md

View File

@ -14,7 +14,7 @@
%strong= ssh_key_usage_types.invert[@key.usage_type]
%li
%span.light= _('Created on:')
%strong= @key.created_at.to_s(:medium)
%strong= @key.created_at.to_fs(:medium)
%li
%span.light= _('Expires:')
%strong= @key.expires_at.try(:to_s, :medium) || _('Never')

View File

@ -101,7 +101,7 @@
- else
%span.gl-text-gray-500
= _("no name set")
%td= registration[:created_at].to_date.to_s(:medium)
%td= registration[:created_at].to_date.to_fs(:medium)
%td
= render Pajamas::ButtonComponent.new(variant: :danger,
href: registration[:delete_path],

View File

@ -33,7 +33,7 @@
%span.issuable-due-date.d-none.d-sm-inline-block.has-tooltip{ class: "#{'cred' if issue.overdue? && !issue.closed?}", title: _('Due date') }
&nbsp;
= sprite_icon('calendar')
= issue.due_date.to_s(:medium)
= issue.due_date.to_fs(:medium)
= render_if_exists "projects/issues/issue_weight", issue: issue
= render_if_exists "projects/issues/health_status", issue: issue

View File

@ -16,7 +16,7 @@
%tr
%td= token.name
%td= token.username
%td= token.created_at.to_date.to_s(:medium)
%td= token.created_at.to_date.to_fs(:medium)
%td
- if token.expires?
%span{ class: ('text-warning' if token.expires_soon?) }

View File

@ -43,7 +43,7 @@
= _("Given access %{time_ago}").html_safe % { time_ago: time_ago_with_tooltip(member.created_at) }
%span.js-expires-in{ class: ('gl-display-none' unless member.expires?) }
&middot;
%span.js-expires-in-text{ class: "has-tooltip#{' text-warning' if member.expires_soon?}", title: (member.expires_at.to_time.in_time_zone.to_s(:medium) if member.expires?) }
%span.js-expires-in-text{ class: "has-tooltip#{' text-warning' if member.expires_soon?}", title: (member.expires_at.to_time.in_time_zone.to_fs(:medium) if member.expires?) }
- if member.expires?
- preposition = current_user.time_display_relative ? '' : 'on'
= _("Expires %{preposition} %{expires_at}").html_safe % { expires_at: time_ago_with_tooltip(member.expires_at), preposition: preposition }

View File

@ -26,7 +26,7 @@
.value
%span.value-content{ data: { qa_selector: 'start_date_content' } }
- if milestone.start_date
%span.bold= milestone.start_date.to_s(:medium)
%span.bold= milestone.start_date.to_fs(:medium)
- else
%span.no-value= s_('MilestoneSidebar|No start date')
@ -63,7 +63,7 @@
.value.hide-collapsed
%span.value-content{ data: { qa_selector: 'due_date_content' } }
- if milestone.due_date
%span.bold= milestone.due_date.to_s(:medium)
%span.bold= milestone.due_date.to_fs(:medium)
- else
%span.no-value= s_('MilestoneSidebar|No due date')
- remaining_days = remaining_days_in_words(milestone.due_date, milestone.start_date)

View File

@ -1,5 +1,5 @@
%h4.prepend-top-20
= html_escape(_("Contributions for %{calendar_date}")) % { calendar_date: tag.strong(@calendar_date.to_s(:medium)) }
= html_escape(_("Contributions for %{calendar_date}")) % { calendar_date: tag.strong(@calendar_date.to_fs(:medium)) }
- if @events.any?
%ul.bordered-list

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
unless experiments.class_files_removed?
msg = "This merge request removes experiment: `#{experiments.removed_experiments.join(',')}`" \
", please also remove the class file."
fail msg # rubocop:disable Style/SignalException
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
require_relative '../../tooling/danger/experiments'
module Danger
class Experiments < ::Danger::Plugin
# Put the helper code somewhere it can be tested
include Tooling::Danger::Experiments
end
end

View File

@ -0,0 +1,13 @@
- title: "Deprecate `CiRunner` GraphQL fields duplicated in `CiRunnerManager`" # (required) The name of the feature to be deprecated
announcement_milestone: "16.2" # (required) The milestone when this feature was first announced as deprecated.
announcement_date: "2023-07-22" # (required) The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
removal_date: "2024-05-22" # (required) The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # (required) If this deprecation is a breaking change, set this value to true
reporter: DarrenEastman # (required) GitLab username of the person reporting the deprecation
stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/41518 # (required) Link to the deprecation issue in GitLab
body: | # (required) Do not modify this line, instead modify the lines below.
These fields (`architectureName`, `ipAddress`, `platformName`, `revision`, `version`) are now deprecated from the [GraphQL `CiRunner`](https://docs.gitlab.com/ee/api/graphql/reference/#cirunner) type as they are duplicated with the introduction of runner managers grouped within a runner configuration.
end_of_support_milestone: "17.0" # (optional) Use "XX.YY" format. The milestone when support for this feature will end.
end_of_support_date: "2024-05-22" # (optional) The date of the milestone release when support for this feature will end.

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
class CreateFkMlCandidatesOnUserId < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
NEW_CONSTRAINT_NAME = 'fk_ml_candidates_on_user_id'
def up
add_concurrent_foreign_key(
:ml_candidates,
:users,
column: :user_id,
on_delete: :nullify,
validate: false,
name: NEW_CONSTRAINT_NAME
)
end
def down
with_lock_retries do
remove_foreign_key_if_exists(
:ml_candidates,
column: :user_id,
on_delete: :nullify,
name: NEW_CONSTRAINT_NAME
)
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class ValidateFkMlCandidatesOnUserId < Gitlab::Database::Migration[2.1]
NEW_CONSTRAINT_NAME = 'fk_ml_candidates_on_user_id'
def up
validate_foreign_key(:ml_candidates, :user_id, name: NEW_CONSTRAINT_NAME)
end
def down
# no-op
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class RemoveOldFkMlCandidatesOnUserId < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
OLD_CONSTRAINT_NAME = 'fk_rails_1b37441fe5'
def up
remove_foreign_key_if_exists(:ml_candidates, column: :user_id, name: OLD_CONSTRAINT_NAME)
end
def down
add_concurrent_foreign_key(
:ml_candidates,
:users,
column: :user_id,
validate: false,
name: OLD_CONSTRAINT_NAME
)
end
end

View File

@ -0,0 +1 @@
90b19651bc3f69a2e104a94ec4b9c4a758b4e258b49dad19b9795d1574c51946

View File

@ -0,0 +1 @@
1cca0c19cc117465e14bf52ad8aadbf61c4c6e4c3fbc17a77138dd1f40fad902

View File

@ -0,0 +1 @@
fc7195a78541583e95a007594b393d0cd67f942c275efb8a43c6953128e8b4ec

View File

@ -36336,6 +36336,9 @@ ALTER TABLE ONLY ml_candidate_metrics
ALTER TABLE ONLY ml_candidate_params
ADD CONSTRAINT fk_ml_candidate_params_on_candidate_id FOREIGN KEY (candidate_id) REFERENCES ml_candidates(id) ON DELETE CASCADE;
ALTER TABLE ONLY ml_candidates
ADD CONSTRAINT fk_ml_candidates_on_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY path_locks
ADD CONSTRAINT fk_path_locks_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
@ -36531,9 +36534,6 @@ ALTER TABLE ONLY vulnerability_user_mentions
ALTER TABLE ONLY packages_debian_file_metadata
ADD CONSTRAINT fk_rails_1ae85be112 FOREIGN KEY (package_file_id) REFERENCES packages_package_files(id) ON DELETE CASCADE;
ALTER TABLE ONLY ml_candidates
ADD CONSTRAINT fk_rails_1b37441fe5 FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE ONLY issuable_slas
ADD CONSTRAINT fk_rails_1b8768cd63 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;

View File

@ -5416,7 +5416,7 @@ Input type: `PrometheusIntegrationCreateInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationprometheusintegrationcreateactive"></a>`active` | [`Boolean!`](#boolean) | Whether the integration is receiving alerts. |
| <a id="mutationprometheusintegrationcreateapiurl"></a>`apiUrl` | [`String!`](#string) | Endpoint at which Prometheus can be queried. |
| <a id="mutationprometheusintegrationcreateapiurl"></a>`apiUrl` | [`String`](#string) | Endpoint at which Prometheus can be queried. |
| <a id="mutationprometheusintegrationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationprometheusintegrationcreateprojectpath"></a>`projectPath` | [`ID!`](#id) | Project to create the integration in. |

View File

@ -312,7 +312,7 @@ listed in the descriptions of the relevant settings.
| `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. |
| `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It automatically builds, tests, and deploys applications based on a predefined CI/CD configuration. |
| `automatic_purchased_storage_allocation` | boolean | no | Enabling this permits automatic allocation of purchased storage in a namespace. |
| `automatic_purchased_storage_allocation` | boolean | no | Enabling this permits automatic allocation of purchased storage in a namespace. Relevant only to EE distributions. |
| `bulk_import_enabled` | boolean | no | Enable migrating GitLab groups by direct transfer. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/383268) in GitLab 15.8. Setting also [available](../user/admin_area/settings/visibility_and_access_controls.md#enable-migration-of-groups-and-projects-by-direct-transfer) in the Admin Area. |
| `can_create_group` | boolean | no | Indicates whether users can create top-level groups. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367754) in GitLab 15.5. Defaults to `true`. |
| `check_namespace_plan` **(PREMIUM)** | boolean | no | Enabling this makes only licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public. |

View File

@ -30,11 +30,17 @@ Base type for issue, requirement, test case, incident and task (this list is pla
A set of predefined types for different categories of work items. Currently, the available types are:
- Issue
- Incident
- Test case
- Requirement
- Task
- [Incident](/ee/operations/incident_management/incidents.md)
- [Test case](/ee/ci/test_cases/index.md)
- [Requirement](/ee/user/project/requirements/index.md)
- [Task](/ee/user/tasks.md)
- [OKRs](/ee/user/okrs.md)
Work is underway to convert existing objects to Work Item Types or add new ones:
- [Issue](https://gitlab.com/groups/gitlab-org/-/epics/9584)
- [Epic](https://gitlab.com/groups/gitlab-org/-/epics/9290)
- [Ticket](https://gitlab.com/groups/gitlab-org/-/epics/10419)
#### Work Item properties
@ -58,7 +64,7 @@ All Work Item types share the same pool of predefined widgets and are customized
### Work Item widget types (updating)
| widget type | feature flag |
|---|---|---|
|---|---|
| assignees | |
| description | |
| hierarchy | |

View File

@ -882,3 +882,22 @@ WARNING:
If you add `CI_DEBUG_TRACE` as a local variable to runners, debug logs generate and are visible
to all users with access to job logs. The permission levels are not checked by the runner,
so you should only use the variable in GitLab itself.
## Known issues and workarounds
These are some know issues with CI/CD variables, and where applicable, known workarounds.
### "argument list too long"
This issue occurs when the combined length of all CI/CD variables defined for a job exceeds the limit imposed by the
shell where the job executes. This includes the names and values of pre-defined and user defined variables. This limit
is typically referred to as `ARG_MAX`, and is shell and operating system dependent. This issue also occurs when the
content of a single [File-type](#use-file-type-cicd-variables) variable exceeds `ARG_MAX`.
For more information, see [issue 392406](https://gitlab.com/gitlab-org/gitlab/-/issues/392406#note_1414219596).
As a workaround you can either:
- Use [File-type](#use-file-type-cicd-variables) CI/CD variables for large environment variables where possible.
- If a single large variable is larger than `ARG_MAX`, try using [Secure Files](../secure_files/index.md), or
bring the file to the job through some other mechanism.

View File

@ -1575,7 +1575,7 @@ Helpers should follow the Rails naming / namespacing convention, where
module Features
module IterationHelpers
def iteration_period(iteration)
"#{iteration.start_date.to_s(:medium)} - #{iteration.due_date.to_s(:medium)}"
"#{iteration.start_date.to_fs(:medium)} - #{iteration.due_date.to_fs(:medium)}"
end
end
end

View File

@ -90,6 +90,9 @@ For more information, see the [Atlassian documentation](https://confluence.atlas
### Use a prefix
You can define a prefix for GitLab to match Jira issue keys. For example, if your Jira issue ID is `ALPHA-1`
and you've set a `JIRA#` prefix, GitLab matches `JIRA#ALPHA-1` rather than `ALPHA-1`.
To define a prefix for Jira issue keys:
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.

View File

@ -144,6 +144,21 @@ In GitLab 11.11 the Windows Batch executor, the CMD shell was deprecated in GitL
<div class="deprecation breaking-change" data-milestone="17.0">
### Deprecate `CiRunner` GraphQL fields duplicated in `CiRunnerManager`
<div class="deprecation-notes">
- Announced in: GitLab <span class="milestone">16.2</span>
- End of Support: GitLab <span class="milestone">17.0</span>
- This is a [breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change).
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/41518).
</div>
These fields (`architectureName`, `ipAddress`, `platformName`, `revision`, `version`) are now deprecated from the [GraphQL `CiRunner`](https://docs.gitlab.com/ee/api/graphql/reference/#cirunner) type as they are duplicated with the introduction of runner managers grouped within a runner configuration.
</div>
<div class="deprecation breaking-change" data-milestone="17.0">
### Deprecate `message` field from Vulnerability Management features
<div class="deprecation-notes">

View File

@ -4857,6 +4857,9 @@ msgstr ""
msgid "An error occurred while checking group path. Please refresh and try again."
msgstr ""
msgid "An error occurred while creating the %{issuableType}. Please try again."
msgstr ""
msgid "An error occurred while decoding the file."
msgstr ""
@ -7767,6 +7770,9 @@ msgstr ""
msgid "Boards|An error occurred while creating the list. Please try again."
msgstr ""
msgid "Boards|An error occurred while fetching board. Please try again."
msgstr ""
msgid "Boards|An error occurred while fetching child groups. Please try again."
msgstr ""

View File

@ -234,7 +234,7 @@ RSpec.describe Groups::GroupMembersController do
it 'returns correct json response' do
expect(json_response).to eq({
"expires_soon" => false,
"expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium)
"expires_at_formatted" => expiry_date.to_time.in_time_zone.to_fs(:medium)
})
end
end

View File

@ -320,7 +320,7 @@ RSpec.describe Projects::ProjectMembersController do
it 'returns correct json response' do
expect(json_response).to eq({
"expires_soon" => false,
"expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium)
"expires_at_formatted" => expiry_date.to_time.in_time_zone.to_fs(:medium)
})
end
end

View File

@ -1,24 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ForceCompanyTrialExperiment, :experiment do
subject { described_class.new(current_user: user) }
let(:user) { create(:user, setup_for_company: setup_for_company) }
let(:setup_for_company) { true }
context 'when a user is setup_for_company' do
it 'is not excluded' do
expect(subject).not_to exclude(user: user)
end
end
context 'when a user is not setup_for_company' do
let(:setup_for_company) { nil }
it 'is excluded' do
expect(subject).to exclude(user: user)
end
end
end

View File

@ -159,7 +159,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
click_button 'Create issue'
page.within '.issuable-sidebar' do
expect(page).to have_content date.to_s(:medium)
expect(page).to have_content date.to_fs(:medium)
end
end
end

View File

@ -82,7 +82,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
click_button _('Save changes')
page.within '.issuable-sidebar' do
expect(page).to have_content date.to_s(:medium)
expect(page).to have_content date.to_fs(:medium)
end
end

View File

@ -39,7 +39,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js, feature_categ
wait_for_requests
assert_env_widget("Deployed to", environment.name)
expect(find('.js-deploy-time')['title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
expect(find('.js-deploy-time')['title']).to eq(deployment.created_at.to_time.in_time_zone.to_fs(:medium))
end
context 'when a user created a new merge request with the same SHA' do

View File

@ -1,26 +1,49 @@
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import BoardNewItem from '~/boards/components/board_new_item.vue';
import ProjectSelect from '~/boards/components/project_select.vue';
import eventHub from '~/boards/eventhub';
import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import { mockList, mockGroupProjects, mockIssue, mockIssue2 } from '../mock_data';
import {
mockList,
mockGroupProjects,
mockIssue,
mockIssue2,
mockProjectBoardResponse,
mockGroupBoardResponse,
} from '../mock_data';
Vue.use(Vuex);
Vue.use(VueApollo);
const addListNewIssuesSpy = jest.fn().mockResolvedValue();
const mockActions = { addListNewIssue: addListNewIssuesSpy };
const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
const mockApollo = createMockApollo([
[projectBoardQuery, projectBoardQueryHandlerSuccess],
[groupBoardQuery, groupBoardQueryHandlerSuccess],
]);
const createComponent = ({
state = {},
actions = mockActions,
getters = { getBoardItemsByList: () => () => [] },
isGroupBoard = true,
data = { selectedProject: mockGroupProjects[0] },
provide = {},
} = {}) =>
shallowMount(BoardNewIssue, {
apolloProvider: mockApollo,
store: new Vuex.Store({
state,
actions,
@ -28,6 +51,7 @@ const createComponent = ({
}),
propsData: {
list: mockList,
boardId: 'gid://gitlab/Board/1',
},
data: () => data,
provide: {
@ -36,6 +60,10 @@ const createComponent = ({
weightFeatureAvailable: false,
boardWeight: null,
isGroupBoard,
boardType: 'group',
isEpicBoard: false,
isApolloBoard: false,
...provide,
},
stubs: {
BoardNewItem,
@ -139,4 +167,33 @@ describe('Issue boards new issue form', () => {
expect(projectSelect.exists()).toBe(false);
});
});
describe('Apollo boards', () => {
it.each`
boardType | queryHandler | notCalledHandler
${WORKSPACE_GROUP} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
${WORKSPACE_PROJECT} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
`(
'fetches $boardType board and emits addNewIssue event',
async ({ boardType, queryHandler, notCalledHandler }) => {
wrapper = createComponent({
provide: {
boardType,
isProjectBoard: boardType === WORKSPACE_PROJECT,
isGroupBoard: boardType === WORKSPACE_GROUP,
isApolloBoard: true,
},
});
await nextTick();
findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' });
await nextTick();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
expect(wrapper.emitted('addNewIssue')[0][0]).toMatchObject({ title: 'Foo' });
},
);
});
});

View File

@ -1541,8 +1541,8 @@ describe('addListNewIssue', () => {
it('should add board scope to the issue being created', async () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
createIssue: {
issue: mockIssue,
createIssuable: {
issuable: mockIssue,
errors: [],
},
},
@ -1600,8 +1600,8 @@ describe('addListNewIssue', () => {
it('dispatches a correct set of mutations', () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
createIssue: {
issue: mockIssue,
createIssuable: {
issuable: mockIssue,
errors: [],
},
},

View File

@ -6,7 +6,8 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:args) { { project_path: project.full_path, active: true, api_url: 'http://prometheus.com/' } }
let(:api_url) { 'http://prometheus.com/' }
let(:args) { { project_path: project.full_path, active: true, api_url: api_url } }
specify { expect(described_class).to require_graphql_authorizations(:admin_project) }
@ -29,6 +30,14 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create do
end
end
context 'when api_url is nil' do
let(:api_url) { nil }
it 'creates the integration' do
expect { resolve }.to change(::Alerting::ProjectAlertingSetting, :count).by(1)
end
end
context 'when UpdateService responds with success' do
it 'returns the integration with no errors' do
expect(resolve).to eq(
@ -38,7 +47,7 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create do
end
it 'creates a corresponding token' do
expect { resolve }.to change(::Alerting::ProjectAlertingSetting, :count).by(1)
expect { resolve }.to change(::Integrations::Prometheus, :count).by(1)
end
end

View File

@ -4,7 +4,7 @@ require 'spec_helper'
require 'googleauth'
RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching, :snowplow do
RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching, :snowplow, feature_category: :metrics do
include PrometheusHelpers
include ReactiveCachingHelpers
@ -37,8 +37,8 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
integration.manual_configuration = true
end
it 'validates presence of api_url' do
expect(integration).to validate_presence_of(:api_url)
it 'does not validates presence of api_url' do
expect(integration).not_to validate_presence_of(:api_url)
end
end
@ -119,7 +119,7 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
context 'when configuration is not valid' do
before do
integration.api_url = nil
integration.manual_configuration = nil
end
it 'returns failure message' do

View File

@ -8,11 +8,13 @@ RSpec.describe 'Creating a new Prometheus Integration', feature_category: :incid
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:api_url) { 'https://prometheus-url.com' }
let(:variables) do
{
project_path: project.full_path,
active: false,
api_url: 'https://prometheus-url.com'
api_url: api_url
}
end
@ -56,7 +58,20 @@ RSpec.describe 'Creating a new Prometheus Integration', feature_category: :incid
expect(integration_response['apiUrl']).to eq(new_integration.api_url)
end
[:project_path, :active, :api_url].each do |argument|
context 'without api url' do
let(:api_url) { nil }
it 'creates a new integration' do
post_graphql_mutation(mutation, current_user: current_user)
integration_response = mutation_response['integration']
expect(response).to have_gitlab_http_status(:success)
expect(integration_response['apiUrl']).to be_nil
end
end
[:project_path, :active].each do |argument|
context "without required argument #{argument}" do
before do
variables.delete(argument)

View File

@ -690,7 +690,7 @@ RSpec.describe Projects::UpdateService, feature_category: :groups_and_projects d
attributes_for(
:prometheus_integration,
project: project,
properties: { api_url: nil, manual_configuration: "1" }
properties: { api_url: 'invalid-url', manual_configuration: "1" }
)
end

View File

@ -302,7 +302,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
it 'returns due_date message: Date.new(2016, 8, 28) if content contains /due 2016-08-28' do
_, _, message = service.execute(content, issuable)
expect(message).to eq("Set the due date to #{expected_date.to_s(:medium)}.")
expect(message).to eq("Set the due date to #{expected_date.to_fs(:medium)}.")
end
end

View File

@ -3,7 +3,7 @@
module Features
module IterationHelpers
def iteration_period(iteration)
"#{iteration.start_date.to_s(:medium)} - #{iteration.due_date.to_s(:medium)}"
"#{iteration.start_date.to_fs(:medium)} - #{iteration.due_date.to_fs(:medium)}"
end
end
end

View File

@ -26,7 +26,7 @@ RSpec.shared_examples 'date sidebar widget' do
wait_for_requests
expect(page).to have_content(today.to_s(:medium))
expect(page).to have_content(today.to_fs(:medium))
expect(due_date_value.text).to have_content Time.current.strftime('%b %-d, %Y')
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'gitlab-dangerfiles'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/experiments'
RSpec.describe Tooling::Danger::Experiments, feature_category: :tooling do
include_context "with dangerfile"
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
subject(:experiments) { fake_danger.new(helper: fake_helper) }
describe '#removed_experiments' do
let(:removed_experiments_yml_files) do
[
'config/feature_flags/experiment/tier_badge.yml',
'ee/config/feature_flags/experiment/direct_to_trial.yml'
]
end
let(:deleted_files) do
[
'app/models/model.rb',
'app/assets/javascripts/file.js'
] + removed_experiments_yml_files
end
it 'returns names of removed experiments' do
expect(experiments.removed_experiments).to eq(%w[tier_badge direct_to_trial])
end
end
describe '#class_files_removed?' do
let(:removed_experiments_name) { current_experiment_with_class_files_example }
context 'when yml file is deleted but not class file' do
let(:deleted_files) { ["config/feature_flags/experiment/#{removed_experiments_name}.yml"] }
it 'returns false' do
expect(experiments.class_files_removed?).to eq(false)
end
end
context 'when yml file is deleted but no corresponding class file exists' do
let(:deleted_files) { ["config/feature_flags/experiment/fake_experiment.yml"] }
it 'returns true' do
expect(experiments.class_files_removed?).to eq(true)
end
end
end
def current_experiment_with_class_files_example
path = Dir.glob("app/experiments/*.rb").last
File.basename(path).chomp('_experiment.rb')
end
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Tooling
module Danger
module Experiments
EXPERIMENTS_YML_REGEX = %r{\A(ee/)?config/feature_flags/experiment/}
CLASS_FILES_DIR = %w[app/experiments/ ee/app/experiments/].freeze
def class_files_removed?
(removed_experiments & current_experiments_with_class_files).empty?
end
def removed_experiments
yml_files_paths = helper.deleted_files
yml_files_paths.select { |path| path =~ EXPERIMENTS_YML_REGEX }.map { |path| File.basename(path).chomp('.yml') }
end
private
def current_experiments_with_class_files
experiment_names = []
CLASS_FILES_DIR.each do |directory_path|
experiment_names += Dir.glob("#{directory_path}*.rb").map do |path|
File.basename(path).chomp('_experiment.rb')
end
end
experiment_names
end
end
end
end