Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-08 09:09:56 +00:00
parent fe30598cbd
commit e612fbe905
64 changed files with 12568 additions and 132 deletions

View File

@ -112,3 +112,7 @@ overrides:
import/no-nodejs-modules: off import/no-nodejs-modules: off
filenames/match-regex: off filenames/match-regex: off
no-console: off no-console: off
- files:
- '*.stories.js'
rules:
filenames/match-regex: off

View File

@ -344,3 +344,18 @@ startup-css-check as-if-foss:
needs: needs:
- job: "compile-test-assets as-if-foss" - job: "compile-test-assets as-if-foss"
- job: "rspec frontend_fixture as-if-foss" - job: "rspec frontend_fixture as-if-foss"
compile-storybook:
extends:
- .compile-assets-base
script:
- source scripts/utils.sh
- cd storybook/
- run_timed_command "retry yarn install --frozen-lockfile"
- yarn build
artifacts:
name: storybook
expire_in: 31d
when: always
paths:
- storybook/public

View File

@ -8,12 +8,14 @@ pages:
- coverage-frontend - coverage-frontend
- karma - karma
- compile-production-assets - compile-production-assets
- compile-storybook
script: script:
- mv public/ .public/ - mv public/ .public/
- mkdir public/ - mkdir public/
- mv coverage/ public/coverage-ruby/ || true - mv coverage/ public/coverage-ruby/ || true
- mv coverage-frontend/ public/coverage-frontend/ || true - mv coverage-frontend/ public/coverage-frontend/ || true
- mv coverage-javascript/ public/coverage-javascript/ || true - mv coverage-javascript/ public/coverage-javascript/ || true
- mv storybook/public public/storybook || true
- cp .public/assets/application-*.css public/application.css || true - cp .public/assets/application-*.css public/application.css || true
- cp .public/assets/application-*.css.gz public/application.css.gz || true - cp .public/assets/application-*.css.gz public/application.css.gz || true
artifacts: artifacts:

View File

@ -1,9 +1,9 @@
<script> <script>
import { GlDrawer } from '@gitlab/ui'; import { GlDrawer } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue'; import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
@ -23,11 +23,9 @@ export default {
BoardSidebarLabelsSelect, BoardSidebarLabelsSelect,
BoardSidebarDueDate, BoardSidebarDueDate,
SidebarSubscriptionsWidget, SidebarSubscriptionsWidget,
BoardSidebarMilestoneSelect, SidebarDropdownWidget,
BoardSidebarWeightInput: () => BoardSidebarWeightInput: () =>
import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'), import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
SidebarDropdownWidget: () =>
import('ee_component/sidebar/components/sidebar_dropdown_widget.vue'),
}, },
inject: { inject: {
multipleAssigneesFeatureAvailable: { multipleAssigneesFeatureAvailable: {
@ -97,7 +95,14 @@ export default {
data-testid="sidebar-epic" data-testid="sidebar-epic"
/> />
<div> <div>
<board-sidebar-milestone-select /> <sidebar-dropdown-widget
:iid="activeBoardItem.iid"
issuable-attribute="milestone"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="projectPathForActiveIssue"
:issuable-type="issuableType"
data-testid="sidebar-milestones"
/>
<sidebar-dropdown-widget <sidebar-dropdown-widget
v-if="iterationFeatureAvailable" v-if="iterationFeatureAvailable"
:iid="activeBoardItem.iid" :iid="activeBoardItem.iid"

View File

@ -135,6 +135,13 @@ export default {
resolveWithIssuePath() { resolveWithIssuePath() {
return !this.discussionResolved ? this.discussion.resolve_with_issue_path : ''; return !this.discussionResolved ? this.discussion.resolve_with_issue_path : '';
}, },
canShowReplyActions() {
if (this.shouldRenderDiffs && !this.discussion.diff_file.diff_refs) {
return false;
}
return true;
},
}, },
created() { created() {
eventHub.$on('startReplying', this.onStartReplying); eventHub.$on('startReplying', this.onStartReplying);
@ -263,7 +270,7 @@ export default {
:draft="draftForDiscussion(discussion.reply_id)" :draft="draftForDiscussion(discussion.reply_id)"
/> />
<div <div
v-else-if="showReplies" v-else-if="canShowReplyActions && showReplies"
:class="{ 'is-replying': isReplying }" :class="{ 'is-replying': isReplying }"
class="discussion-reply-holder gl-border-t-0! clearfix" class="discussion-reply-holder gl-border-t-0! clearfix"
> >

View File

@ -26,10 +26,10 @@ const PRIVATE_VISIBILITY = 'private';
const INTERNAL_VISIBILITY = 'internal'; const INTERNAL_VISIBILITY = 'internal';
const PUBLIC_VISIBILITY = 'public'; const PUBLIC_VISIBILITY = 'public';
const ALLOWED_VISIBILITY = { const VISIBILITY_LEVEL = {
private: [PRIVATE_VISIBILITY], [PRIVATE_VISIBILITY]: 0,
internal: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY], [INTERNAL_VISIBILITY]: 10,
public: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY, PUBLIC_VISIBILITY], [PUBLIC_VISIBILITY]: 20,
}; };
const initFormField = ({ value, required = true, skipValidation = false }) => ({ const initFormField = ({ value, required = true, skipValidation = false }) => ({
@ -124,14 +124,23 @@ export default {
projectUrl() { projectUrl() {
return `${gon.gitlab_url}/`; return `${gon.gitlab_url}/`;
}, },
projectAllowedVisibility() { projectVisibilityLevel() {
return ALLOWED_VISIBILITY[this.projectVisibility]; return VISIBILITY_LEVEL[this.projectVisibility];
}, },
namespaceAllowedVisibility() { namespaceVisibilityLevel() {
return ( const visibility = this.form.fields.namespace.value?.visibility || PUBLIC_VISIBILITY;
ALLOWED_VISIBILITY[this.form.fields.namespace.value?.visibility] || return VISIBILITY_LEVEL[visibility];
ALLOWED_VISIBILITY[PUBLIC_VISIBILITY] },
); visibilityLevelCap() {
return Math.min(this.projectVisibilityLevel, this.namespaceVisibilityLevel);
},
allowedVisibilityLevels() {
return Object.entries(VISIBILITY_LEVEL).reduce((levels, [levelName, levelValue]) => {
if (levelValue <= this.visibilityLevelCap) {
levels.push(levelName);
}
return levels;
}, []);
}, },
visibilityLevels() { visibilityLevels() {
return [ return [
@ -179,11 +188,8 @@ export default {
const { data } = await axios.get(this.endpoint); const { data } = await axios.get(this.endpoint);
this.namespaces = data.namespaces; this.namespaces = data.namespaces;
}, },
isVisibilityLevelDisabled(visibilityLevel) { isVisibilityLevelDisabled(visibility) {
return !( return !this.allowedVisibilityLevels.includes(visibility);
this.projectAllowedVisibility.includes(visibilityLevel) &&
this.namespaceAllowedVisibility.includes(visibilityLevel)
);
}, },
async onSubmit() { async onSubmit() {
this.form.showValidation = true; this.form.showValidation = true;

View File

@ -0,0 +1,344 @@
<script>
import {
GlLink,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlSearchBoxByType,
GlDropdownDivider,
GlLoadingIcon,
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issue_show/constants';
import { __, s__, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import {
IssuableAttributeState,
IssuableAttributeType,
issuableAttributesQueries,
noAttributeId,
} from '../constants';
export default {
noAttributeId,
IssuableAttributeState,
issuableAttributesQueries,
i18n: {
[IssuableAttributeType.Milestone]: __('Milestone'),
none: __('None'),
},
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
SidebarEditableItem,
GlLink,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlDropdownDivider,
GlSearchBoxByType,
GlIcon,
GlLoadingIcon,
},
inject: {
isClassicSidebar: {
default: false,
},
},
props: {
issuableAttribute: {
type: String,
required: true,
validator(value) {
return [IssuableAttributeType.Milestone].includes(value);
},
},
workspacePath: {
required: true,
type: String,
},
iid: {
required: true,
type: String,
},
attrWorkspacePath: {
required: true,
type: String,
},
issuableType: {
type: String,
required: true,
validator(value) {
return value === IssuableType.Issue;
},
},
},
apollo: {
currentAttribute: {
query() {
const { current } = this.issuableAttributeQuery;
const { query } = current[this.issuableType];
return query;
},
variables() {
return {
fullPath: this.workspacePath,
iid: this.iid,
};
},
update(data) {
return data?.workspace?.issuable.attribute;
},
error(error) {
createFlash({
message: this.i18n.currentFetchError,
captureError: true,
error,
});
},
},
attributesList: {
query() {
const { list } = this.issuableAttributeQuery;
const { query } = list[this.issuableType];
return query;
},
skip() {
return !this.editing;
},
debounce: 250,
variables() {
return {
fullPath: this.attrWorkspacePath,
title: this.searchTerm,
state: this.$options.IssuableAttributeState[this.issuableAttribute],
};
},
update(data) {
if (data?.workspace) {
return data?.workspace?.attributes.nodes;
}
return [];
},
error(error) {
createFlash({ message: this.i18n.listFetchError, captureError: true, error });
},
},
},
data() {
return {
searchTerm: '',
editing: false,
updating: false,
selectedTitle: null,
currentAttribute: null,
attributesList: [],
tracking: {
label: 'right_sidebar',
event: 'click_edit_button',
property: this.issuableAttribute,
},
};
},
computed: {
issuableAttributeQuery() {
return this.$options.issuableAttributesQueries[this.issuableAttribute];
},
attributeTitle() {
return this.currentAttribute?.title || this.i18n.noAttribute;
},
attributeUrl() {
return this.currentAttribute?.webUrl;
},
dropdownText() {
return this.currentAttribute
? this.currentAttribute?.title
: this.$options.i18n[this.issuableAttribute];
},
loading() {
return this.$apollo.queries.currentAttribute.loading;
},
emptyPropsList() {
return this.attributesList.length === 0;
},
attributeTypeTitle() {
return this.$options.i18n[this.issuableAttribute];
},
i18n() {
return {
noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), {
issuableAttribute: this.issuableAttribute,
}),
assignAttribute: sprintf(s__('DropdownWidget|Assign %{issuableAttribute}'), {
issuableAttribute: this.issuableAttribute,
}),
noAttributesFound: sprintf(s__('DropdownWidget|No %{issuableAttribute} found'), {
issuableAttribute: this.issuableAttribute,
}),
updateError: sprintf(
s__(
'DropdownWidget|Failed to set %{issuableAttribute} on this %{issuableType}. Please try again.',
),
{ issuableAttribute: this.issuableAttribute, issuableType: this.issuableType },
),
listFetchError: sprintf(
s__(
'DropdownWidget|Failed to fetch the %{issuableAttribute} for this %{issuableType}. Please try again.',
),
{ issuableAttribute: this.issuableAttribute, issuableType: this.issuableType },
),
currentFetchError: sprintf(
s__(
'DropdownWidget|An error occurred while fetching the assigned %{issuableAttribute} of the selected %{issuableType}.',
),
{ issuableAttribute: this.issuableAttribute, issuableType: this.issuableType },
),
};
},
},
methods: {
updateAttribute(attributeId) {
if (this.currentAttribute === null && attributeId === null) return;
if (attributeId === this.currentAttribute?.id) return;
this.updating = true;
const selectedAttribute =
Boolean(attributeId) && this.attributesList.find((p) => p.id === attributeId);
this.selectedTitle = selectedAttribute ? selectedAttribute.title : this.$options.i18n.none;
const { current } = this.issuableAttributeQuery;
const { mutation } = current[this.issuableType];
this.$apollo
.mutate({
mutation,
variables: {
fullPath: this.workspacePath,
attributeId:
this.issuableAttribute === IssuableAttributeType.Milestone
? getIdFromGraphQLId(attributeId)
: attributeId,
iid: this.iid,
},
})
.then(({ data }) => {
if (data.issuableSetAttribute?.errors?.length) {
createFlash({
message: data.issuableSetAttribute.errors[0],
captureError: true,
error: data.issuableSetAttribute.errors[0],
});
} else {
this.$emit('attribute-updated', data);
}
})
.catch((error) => {
createFlash({ message: this.i18n.updateError, captureError: true, error });
})
.finally(() => {
this.updating = false;
this.searchTerm = '';
this.selectedTitle = null;
});
},
isAttributeChecked(attributeId = undefined) {
return (
attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId)
);
},
showDropdown() {
this.$refs.newDropdown.show();
},
handleOpen() {
this.editing = true;
this.showDropdown();
},
handleClose() {
this.editing = false;
},
setFocus() {
this.$refs.search.focusInput();
},
},
};
</script>
<template>
<sidebar-editable-item
ref="editable"
:title="attributeTypeTitle"
:data-testid="`${issuableAttribute}-edit`"
:tracking="tracking"
:loading="updating || loading"
@open="handleOpen"
@close="handleClose"
>
<template #collapsed>
<div v-if="isClassicSidebar" v-gl-tooltip class="sidebar-collapsed-icon">
<gl-icon :size="16" :aria-label="attributeTypeTitle" :name="issuableAttribute" />
<span class="collapse-truncated-title">{{ attributeTitle }}</span>
</div>
<div
:data-testid="`select-${issuableAttribute}`"
:class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'"
>
<span v-if="updating" class="gl-font-weight-bold">{{ selectedTitle }}</span>
<span v-else-if="!currentAttribute" class="gl-text-gray-500">
{{ $options.i18n.none }}
</span>
<gl-link v-else class="gl-text-gray-900! gl-font-weight-bold" :href="attributeUrl">
{{ attributeTitle }}
</gl-link>
</div>
</template>
<template #default>
<gl-dropdown
ref="newDropdown"
lazy
:header-text="i18n.assignAttribute"
:text="dropdownText"
:loading="loading"
class="gl-w-full"
@shown="setFocus"
>
<gl-search-box-by-type ref="search" v-model="searchTerm" />
<gl-dropdown-item
:data-testid="`no-${issuableAttribute}-item`"
:is-check-item="true"
:is-checked="isAttributeChecked($options.noAttributeId)"
@click="updateAttribute($options.noAttributeId)"
>
{{ i18n.noAttribute }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-loading-icon
v-if="$apollo.queries.attributesList.loading"
class="gl-py-4"
data-testid="loading-icon-dropdown"
/>
<template v-else>
<gl-dropdown-text v-if="emptyPropsList">
{{ i18n.noAttributesFound }}
</gl-dropdown-text>
<gl-dropdown-item
v-for="attrItem in attributesList"
:key="attrItem.id"
:is-check-item="true"
:is-checked="isAttributeChecked(attrItem.id)"
:data-testid="`${issuableAttribute}-items`"
@click="updateAttribute(attrItem.id)"
>
{{ attrItem.title }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>
</sidebar-editable-item>
</template>

View File

@ -29,6 +29,9 @@ import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries
import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql'; import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql';
import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql'; import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql'; import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql';
import projectIssueMilestoneMutation from './queries/project_issue_milestone.mutation.graphql';
import projectIssueMilestoneQuery from './queries/project_issue_milestone.query.graphql';
import projectMilestonesQuery from './queries/project_milestones.query.graphql';
export const ASSIGNEES_DEBOUNCE_DELAY = 250; export const ASSIGNEES_DEBOUNCE_DELAY = 250;
@ -143,3 +146,33 @@ export const timelogQueries = {
query: getMrTimelogsQuery, query: getMrTimelogsQuery,
}, },
}; };
export const noAttributeId = null;
export const issuableMilestoneQueries = {
[IssuableType.Issue]: {
query: projectIssueMilestoneQuery,
mutation: projectIssueMilestoneMutation,
},
};
export const milestonesQueries = {
[IssuableType.Issue]: {
query: projectMilestonesQuery,
},
};
export const IssuableAttributeType = {
Milestone: 'milestone',
};
export const IssuableAttributeState = {
[IssuableAttributeType.Milestone]: 'active',
};
export const issuableAttributesQueries = {
[IssuableAttributeType.Milestone]: {
current: issuableMilestoneQueries,
list: milestonesQueries,
},
};

View File

@ -0,0 +1,5 @@
fragment MilestoneFragment on Milestone {
id
title
webUrl: webPath
}

View File

@ -0,0 +1,17 @@
mutation projectIssueMilestoneMutation($fullPath: ID!, $iid: String!, $attributeId: ID) {
issuableSetAttribute: updateIssue(
input: { projectPath: $fullPath, iid: $iid, milestoneId: $attributeId }
) {
__typename
errors
issuable: issue {
__typename
id
attribute: milestone {
title
id
state
}
}
}
}

View File

@ -0,0 +1,14 @@
#import "./milestone.fragment.graphql"
query projectIssueMilestone($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: issue(iid: $iid) {
__typename
id
attribute: milestone {
...MilestoneFragment
}
}
}
}

View File

@ -0,0 +1,13 @@
#import "./milestone.fragment.graphql"
query projectMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) {
workspace: project(fullPath: $fullPath) {
__typename
attributes: milestones(searchTitle: $title, state: $state) {
nodes {
...MilestoneFragment
state
}
}
}
}

View File

@ -0,0 +1,23 @@
/* eslint-disable @gitlab/require-i18n-strings */
import TodoButton from './todo_button.vue';
export default {
component: TodoButton,
title: 'vue_shared/components/todo_button',
};
const Template = (args, { argTypes }) => ({
components: { TodoButton },
props: Object.keys(argTypes),
template: '<todo-button v-bind="$props" v-on="$props" />',
});
export const Default = Template.bind({});
Default.argTypes = {
isTodo: {
description: 'True if to-do is unresolved (i.e. not "done")',
control: { type: 'boolean' },
},
click: { action: 'clicked' },
};

View File

@ -63,6 +63,7 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
end end
def edit def edit
exclude_legacy_flags_check
end end
def update def update
@ -158,4 +159,12 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
render json: { message: messages }, render json: { message: messages },
status: status status: status
end end
def exclude_legacy_flags_check
if Feature.enabled?(:remove_legacy_flags, project, default_enabled: :yaml) &&
Feature.disabled?(:remove_legacy_flags_override, project, default_enabled: :yaml) &&
feature_flag.legacy_flag?
not_found
end
end
end end

View File

@ -1076,6 +1076,10 @@ module Ci
::Ci::PendingBuild.where(build_id: self.id) ::Ci::PendingBuild.where(build_id: self.id)
end end
def create_queuing_entry!
::Ci::PendingBuild.upsert_from_build!(self)
end
protected protected
def run_status_commit_hooks! def run_status_commit_hooks!

View File

@ -9,7 +9,7 @@ class MergeRequestDiffEntity < Grape::Entity
@merge_request_diffs = options[:merge_request_diffs] @merge_request_diffs = options[:merge_request_diffs]
diff = options[:merge_request_diff] diff = options[:merge_request_diff]
next unless diff.present? next unless @merge_request_diffs.include?(diff)
next unless @merge_request_diffs.size > 1 next unless @merge_request_diffs.size > 1
version_index(merge_request_diff) version_index(merge_request_diff)

View File

@ -287,15 +287,11 @@ module Ci
.order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_builds.id ASC') .order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_builds.id ASC')
end end
end end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def builds_for_project_runner def builds_for_project_runner
new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('id ASC') new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('id ASC')
end end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def builds_for_group_runner def builds_for_group_runner
# Workaround for weird Rails bug, that makes `runner.groups.to_sql` to return `runner_id = NULL` # Workaround for weird Rails bug, that makes `runner.groups.to_sql` to return `runner_id = NULL`
groups = ::Group.joins(:runner_namespaces).merge(runner.runner_namespaces) groups = ::Group.joins(:runner_namespaces).merge(runner.runner_namespaces)
@ -307,17 +303,23 @@ module Ci
.without_deleted .without_deleted
new_builds.where(project: projects).order('id ASC') new_builds.where(project: projects).order('id ASC')
end end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def running_builds_for_shared_runners def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.instance_type) Ci::Build.running.where(runner: Ci::Runner.instance_type)
.group(:project_id).select(:project_id, 'count(*) AS running_builds') .group(:project_id).select(:project_id, 'count(*) AS running_builds')
end end
def all_builds
if Feature.enabled?(:ci_pending_builds_queue_join, runner, default_enabled: :yaml)
Ci::Build.joins(:queuing_entry)
else
Ci::Build.all
end
end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def new_builds def new_builds
builds = Ci::Build.pending.unstarted builds = all_builds.pending.unstarted
builds = builds.ref_protected if runner.ref_protected? builds = builds.ref_protected if runner.ref_protected?
builds builds
end end

View File

@ -19,7 +19,7 @@ module Ci
raise InvalidQueueTransition unless transition.to == 'pending' raise InvalidQueueTransition unless transition.to == 'pending'
transition.within_transaction do transition.within_transaction do
result = ::Ci::PendingBuild.upsert_from_build!(build) result = build.create_queuing_entry!
unless result.empty? unless result.empty?
metrics.increment_queue_operation(:build_queue_push) metrics.increment_queue_operation(:build_queue_push)

View File

@ -9,6 +9,8 @@ module Deployments
delegate :variables, to: :deployable delegate :variables, to: :deployable
delegate :options, to: :deployable, allow_nil: true delegate :options, to: :deployable, allow_nil: true
EnvironmentUpdateFailure = Class.new(StandardError)
def initialize(deployment) def initialize(deployment)
@deployment = deployment @deployment = deployment
@deployable = deployment.deployable @deployable = deployment.deployable
@ -31,8 +33,18 @@ module Deployments
renew_deployment_tier renew_deployment_tier
environment.fire_state_event(action) environment.fire_state_event(action)
if environment.save && !environment.stopped? if environment.save
deployment.update_merge_request_metrics! deployment.update_merge_request_metrics! unless environment.stopped?
else
# If there is a validation error on environment update, such as
# the external URL is malformed, the error message is recorded for debugging purpose.
# We should surface the error message to users for letting them to take an action.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/21182.
Gitlab::ErrorTracking.track_exception(
EnvironmentUpdateFailure.new,
project_id: deployment.project_id,
environment_id: environment.id,
reason: environment.errors.full_messages.to_sentence)
end end
end end
end end

View File

@ -6,6 +6,8 @@
.form-group .form-group
= f.label :email = f.label :email
= f.email_field :email, class: "form-control gl-form-input", required: true, value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.') = f.email_field :email, class: "form-control gl-form-input", required: true, value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
.form-text.text-muted
= _('Requires your primary GitLab email address.')
.clearfix .clearfix
= f.submit _("Reset password"), class: "gl-button btn-confirm btn" = f.submit _("Reset password"), class: "gl-button btn-confirm btn"

View File

@ -8,10 +8,11 @@
.flash-container .flash-container
%table.table.table-bordered %table.table.table-bordered
%colgroup %colgroup
%col{ width: "30%" }
%col{ width: "20%" } %col{ width: "20%" }
%col{ width: "20%" } %col{ width: "20%" }
%col{ width: "20%" } %col{ width: "10%" }
%col{ width: "20%" } %col{ width: "10%" }
- if can_admin_project - if can_admin_project
%col %col
%thead %thead
@ -23,8 +24,8 @@
%th %th
= s_("ProtectedBranch|Allowed to push") = s_("ProtectedBranch|Allowed to push")
%th %th
= s_("ProtectedBranch|Allow force push") = s_("ProtectedBranch|Allowed to force push")
%span.has-tooltip{ data: { container: 'body' }, title: s_('ProtectedBranch|Allow force push for all users with push access.'), 'aria-hidden': 'true' } %span.has-tooltip{ data: { container: 'body' }, title: s_('ProtectedBranch|Allow all users with push access to force push.'), 'aria-hidden': 'true' }
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-500') = sprite_icon('question', size: 16, css_class: 'gl-text-gray-500')
= render_if_exists 'projects/protected_branches/ee/code_owner_approval_table_head' = render_if_exists 'projects/protected_branches/ee/code_owner_approval_table_head'

View File

@ -22,11 +22,13 @@
.col-md-10 .col-md-10
= yield :push_access_levels = yield :push_access_levels
.form-group.row .form-group.row
= f.label :allow_force_push, s_("ProtectedBranch|Allow force push:"), class: 'col-md-2 gl-text-left text-md-right' = f.label :allow_force_push, s_("ProtectedBranch|Allowed to force push:"), class: 'col-md-2 gl-text-left text-md-right'
.col-md-10 .col-md-10
= render "shared/buttons/project_feature_toggle", class_list: "js-force-push-toggle project-feature-toggle" = render "shared/buttons/project_feature_toggle", class_list: "js-force-push-toggle project-feature-toggle"
.form-text.gl-text-gray-600.gl-mt-0 .form-text.gl-text-gray-600.gl-mt-0
= s_("ProtectedBranch|Allow force push for all users with push access.") - force_push_docs_url = help_page_url('topics/git/git_rebase', anchor: 'force-push')
- force_push_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: force_push_docs_url }
= (s_("ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}.") % { tag_start: force_push_link_start, tag_end: '</a>' }).html_safe
= render_if_exists 'projects/protected_branches/ee/code_owner_approval_form', f: f = render_if_exists 'projects/protected_branches/ee/code_owner_approval_form', f: f
.card-footer .card-footer
= f.submit s_('ProtectedBranch|Protect'), class: 'gl-button btn btn-confirm', disabled: true, data: { qa_selector: 'protect_button' } = f.submit s_('ProtectedBranch|Protect'), class: 'gl-button btn btn-confirm', disabled: true, data: { qa_selector: 'protect_button' }

View File

@ -14,7 +14,7 @@
%ul %ul
%li Allow only users with Maintainer #{link_to "permissions", help_page_path("user/permissions")} to create new protected branches. %li Allow only users with Maintainer #{link_to "permissions", help_page_path("user/permissions")} to create new protected branches.
%li Allow only users with Maintainer permissions to push code. %li Allow only users with Maintainer permissions to push code.
%li Prevent <strong>anyone</strong> from force-pushing to the branch. %li Prevent <strong>anyone</strong> from #{link_to "force-pushing", help_page_path('topics/git/git_rebase', anchor: 'force-push')} to the branch.
%li Prevent <strong>anyone</strong> from deleting the branch. %li Prevent <strong>anyone</strong> from deleting the branch.
- if can? current_user, :admin_project, @project - if can? current_user, :admin_project, @project

View File

@ -34,4 +34,4 @@
= _('Members of %{group} can also push to this branch: %{branch}') % { group: (group_push_access_levels.size > 1 ? 'these groups' : 'this group'), branch: group_push_access_levels.map(&:humanize).to_sentence } = _('Members of %{group} can also push to this branch: %{branch}') % { group: (group_push_access_levels.size > 1 ? 'these groups' : 'this group'), branch: group_push_access_levels.map(&:humanize).to_sentence }
%td %td
= render "shared/buttons/project_feature_toggle", is_checked: protected_branch.allow_force_push, label: s_("ProtectedBranch|Toggle allow force push"), class_list: "js-force-push-toggle project-feature-toggle", data: { qa_selector: 'force_push_toggle_button', qa_branch_name: protected_branch.name } = render "shared/buttons/project_feature_toggle", is_checked: protected_branch.allow_force_push, label: s_("ProtectedBranch|Toggle allowed to force push"), class_list: "js-force-push-toggle project-feature-toggle", data: { qa_selector: 'force_push_toggle_button', qa_branch_name: protected_branch.name }

View File

@ -0,0 +1,8 @@
---
name: ci_pending_builds_queue_join
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62195
rollout_issue_url:
milestone: '13.12'
type: development
group: group::pipeline execution
default_enabled: false

View File

@ -1,7 +1,7 @@
--- ---
name: ci_pending_builds_queue_maintain name: ci_pending_builds_queue_maintain
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61581 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61581
rollout_issue_url: rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331496
milestone: '13.12' milestone: '13.12'
type: development type: development
group: group::continuous integration group: group::continuous integration

View File

@ -385,7 +385,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
# The wiki and repository routing contains wildcard characters so # The wiki and repository routing contains wildcard characters so
# its preferable to keep it below all other project routes # its preferable to keep it below all other project routes
draw :repository_scoped draw :repository
draw :wiki draw :wiki
namespace :import do namespace :import do

View File

@ -2,7 +2,7 @@
# Repository routes without /-/ scope. # Repository routes without /-/ scope.
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/28848. # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/28848.
# Do not add new routes here. Add new routes to repository_scoped.rb instead # Do not add new routes here. Add new routes to repository.rb instead
# (see https://docs.gitlab.com/ee/development/routing.html#project-routes). # (see https://docs.gitlab.com/ee/development/routing.html#project-routes).
resource :repository, only: [:create] resource :repository, only: [:create]

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
class CleanUpPendingBuildsTable < ActiveRecord::Migration[6.0]
BATCH_SIZE = 1000
disable_ddl_transaction!
def up
return unless Gitlab.dev_or_test_env? || Gitlab.com?
each_batch('ci_pending_builds', of: BATCH_SIZE) do |min, max|
execute <<~SQL
DELETE FROM ci_pending_builds
USING ci_builds
WHERE ci_builds.id = ci_pending_builds.build_id
AND ci_builds.status != 'pending'
AND ci_builds.type = 'Ci::Build'
AND ci_pending_builds.id BETWEEN #{min} AND #{max}
SQL
end
end
def down
# noop
end
private
def each_batch(table_name, scope: ->(table) { table.all }, of: 1000)
table = Class.new(ActiveRecord::Base) do
include EachBatch
self.table_name = table_name
self.inheritance_column = :_type_disabled
end
scope.call(table).each_batch(of: of) do |batch|
yield batch.pluck('MIN(id), MAX(id)').first
end
end
end

View File

@ -0,0 +1 @@
5dc1119c5efe28225bb7ac8a9ed2c4c5cfaeaff202194ed4419cfd54eaf7483d

View File

@ -1176,6 +1176,28 @@ Prints the metrics saved in `conversational_development_index_metrics`.
rake gitlab:usage_data:generate_and_send rake gitlab:usage_data:generate_and_send
``` ```
## Kubernetes integration
Find cluster:
```ruby
cluster = Clusters::Cluster.find(1)
cluster = Clusters::Cluster.find_by(name: 'cluster_name')
```
Delete cluster without associated resources:
```ruby
# Find an admin user
user = User.find_by(username: 'admin_user')
# Find the cluster with the ID
cluster = Clusters::Cluster.find(1)
# Delete the cluster
Clusters::DestroyService.new(user).execute(cluster)
```
## Elasticsearch ## Elasticsearch
### Configuration attributes ### Configuration attributes

View File

@ -204,7 +204,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, Maintainer role) | | `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, Maintainer role) |
| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, Maintainer role) | | `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, Maintainer role) |
| `unprotect_access_level` | string | no | Access levels allowed to unprotect (defaults: `40`, Maintainer role) | | `unprotect_access_level` | string | no | Access levels allowed to unprotect (defaults: `40`, Maintainer role) |
| `allow_force_push` | boolean | no | Allow force push for all users with push access. (defaults: false) | | `allow_force_push` | boolean | no | Allow all users with push access to force push. (default: `false`) |
| `allowed_to_push` | array | no | **(PREMIUM)** Array of access levels allowed to push, with each described by a hash | | `allowed_to_push` | array | no | **(PREMIUM)** Array of access levels allowed to push, with each described by a hash |
| `allowed_to_merge` | array | no | **(PREMIUM)** Array of access levels allowed to merge, with each described by a hash | | `allowed_to_merge` | array | no | **(PREMIUM)** Array of access levels allowed to merge, with each described by a hash |
| `allowed_to_unprotect` | array | no | **(PREMIUM)** Array of access levels allowed to unprotect, with each described by a hash | | `allowed_to_unprotect` | array | no | **(PREMIUM)** Array of access levels allowed to unprotect, with each described by a hash |

View File

@ -0,0 +1,50 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Storybook
The Storybook for the `gitlab-org/gitlab` project is available on our [GitLab Pages site](https://gitlab-org.gitlab.io/gitlab/storybook).
## Storybook in local development
Storybook dependencies and configuration are located under the `storybook/` directory.
To build and launch Storybook locally, in the root directory of the `gitlab` project:
1. Install Storybook dependencies:
```shell
yarn storybook:install
```
1. Build the Storybook site:
```shell
yarn storybook:start
```
## Adding components to Storybook
Stories can be added for any Vue component in the `gitlab` repository.
To add a story:
1. Create a new `.stories.js` file in the same directory as the Vue component.
The file name should have the same prefix as the Vue component.
```txt
vue_shared/
├─ components/
│ ├─ todo_button.vue
│ ├─ todo_button.stories.js
```
1. Write the story as per the [official Storybook instructions](https://storybook.js.org/docs/vue/writing-stories/introduction)
Notes:
- Specify the `title` field of the story as the component's file path from the `javascripts/` directory,
e.g. if the component is located at `app/assets/javascripts/vue_shared/components/todo_button.vue`, specify the `title` as
`vue_shared/components/To-do Button`. This will ensure the Storybook navigation maps closely to our internal directory structure.

View File

@ -17,7 +17,7 @@ as the hardware requirements that are needed to install and use GitLab.
- Ubuntu (16.04/18.04/20.04) - Ubuntu (16.04/18.04/20.04)
- Debian (9/10) - Debian (9/10)
- CentOS (7/8) - CentOS (7/8)
- openSUSE Leap (15.1/15.2) - openSUSE Leap (15.2)
- SUSE Linux Enterprise Server (12 SP2/12 SP5) - SUSE Linux Enterprise Server (12 SP2/12 SP5)
- Red Hat Enterprise Linux (please use the CentOS packages and instructions) - Red Hat Enterprise Linux (please use the CentOS packages and instructions)
- Scientific Linux (please use the CentOS packages and instructions) - Scientific Linux (please use the CentOS packages and instructions)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@ -184,17 +184,17 @@ command line or a Git client application.
WARNING: WARNING:
This feature might not be available to you. Check the **version history** note above for details. This feature might not be available to you. Check the **version history** note above for details.
You can allow force pushes to protected branches by either setting **Allow force push** You can allow [force pushes](../../topics/git/git_rebase.md#force-push) to
protected branches by either setting **Allowed to force push**
when you protect a new branch, or by configuring an already-protected branch. when you protect a new branch, or by configuring an already-protected branch.
To protect a new branch and enable Force push: To protect a new branch and enable Force push:
1. Navigate to your project's **Settings > Repository**. 1. Navigate to your project's **Settings > Repository**.
1. Expand **Protected branches**, and scroll to **Protect a branch**. 1. Expand **Protected branches**, and scroll to **Protect a branch**.
![Code Owners approval - new protected branch](img/code_owners_approval_new_protected_branch_v13_10.png)
1. Select a **Branch** or wildcard you'd like to protect. 1. Select a **Branch** or wildcard you'd like to protect.
1. Select the user levels **Allowed to merge** and **Allowed to push**. 1. Select the user levels **Allowed to merge** and **Allowed to push**.
1. To allow all users with push access to force push, toggle the **Allow force push** slider. 1. To allow all users with push access to force push, toggle the **Allowed to force push** slider.
1. To reject code pushes that change files listed in the `CODEOWNERS` file, toggle 1. To reject code pushes that change files listed in the `CODEOWNERS` file, toggle
**Require approval from code owners**. **Require approval from code owners**.
1. Click **Protect**. 1. Click **Protect**.
@ -203,8 +203,7 @@ To enable force pushes on branches already protected:
1. Navigate to your project's **Settings > Repository**. 1. Navigate to your project's **Settings > Repository**.
1. Expand **Protected branches** and scroll to **Protected branch**. 1. Expand **Protected branches** and scroll to **Protected branch**.
![Code Owners approval - branch already protected](img/code_owners_approval_protected_branch_v13_10.png) 1. Toggle the **Allowed to force push** slider for the chosen branch.
1. Toggle the **Allow force push** slider for the chosen branch.
When enabled, members who are allowed to push to this branch can also force push. When enabled, members who are allowed to push to this branch can also force push.
@ -224,15 +223,11 @@ To protect a new branch and enable Code Owner's approval:
1. Scroll down to **Protect a branch**, select a **Branch** or wildcard you'd like to protect, select who's **Allowed to merge** and **Allowed to push**, and toggle the **Require approval from code owners** slider. 1. Scroll down to **Protect a branch**, select a **Branch** or wildcard you'd like to protect, select who's **Allowed to merge** and **Allowed to push**, and toggle the **Require approval from code owners** slider.
1. Click **Protect**. 1. Click **Protect**.
![Code Owners approval - new protected branch](img/code_owners_approval_new_protected_branch_v13_10.png)
To enable Code Owner's approval to branches already protected: To enable Code Owner's approval to branches already protected:
1. Navigate to your project's **Settings > Repository** and expand **Protected branches**. 1. Navigate to your project's **Settings > Repository** and expand **Protected branches**.
1. Scroll down to **Protected branch** and toggle the **Code owner approval** slider for the chosen branch. 1. Scroll down to **Protected branch** and toggle the **Code owner approval** slider for the chosen branch.
![Code Owners approval - branch already protected](img/code_owners_approval_protected_branch_v13_10.png)
When enabled, all merge requests targeting these branches require approval When enabled, all merge requests targeting these branches require approval
by a Code Owner per matched rule before they can be merged. by a Code Owner per matched rule before they can be merged.
Additionally, direct pushes to the protected branch are denied if a rule is matched. Additionally, direct pushes to the protected branch are denied if a rule is matched.

View File

@ -90,6 +90,7 @@ module API
end end
get do get do
authorize_read_feature_flag! authorize_read_feature_flag!
exclude_legacy_flags_check!
present_entity(feature_flag) present_entity(feature_flag)
end end
@ -104,6 +105,7 @@ module API
end end
post :enable do post :enable do
not_found! unless Feature.enabled?(:feature_flag_api, user_project) not_found! unless Feature.enabled?(:feature_flag_api, user_project)
exclude_legacy_flags_check!
render_api_error!('Version 2 flags not supported', :unprocessable_entity) if new_version_flag_present? render_api_error!('Version 2 flags not supported', :unprocessable_entity) if new_version_flag_present?
result = ::FeatureFlags::EnableService result = ::FeatureFlags::EnableService
@ -127,6 +129,7 @@ module API
end end
post :disable do post :disable do
not_found! unless Feature.enabled?(:feature_flag_api, user_project) not_found! unless Feature.enabled?(:feature_flag_api, user_project)
exclude_legacy_flags_check!
render_api_error!('Version 2 flags not supported', :unprocessable_entity) if feature_flag.new_version_flag? render_api_error!('Version 2 flags not supported', :unprocessable_entity) if feature_flag.new_version_flag?
result = ::FeatureFlags::DisableService result = ::FeatureFlags::DisableService
@ -162,6 +165,7 @@ module API
end end
put do put do
authorize_update_feature_flag! authorize_update_feature_flag!
exclude_legacy_flags_check!
render_api_error!('PUT operations are not supported for legacy feature flags', :unprocessable_entity) if feature_flag.legacy_flag? render_api_error!('PUT operations are not supported for legacy feature flags', :unprocessable_entity) if feature_flag.legacy_flag?
attrs = declared_params(include_missing: false) attrs = declared_params(include_missing: false)
@ -232,6 +236,10 @@ module API
@feature_flag ||= user_project.operations_feature_flags.find_by_name!(params[:feature_flag_name]) @feature_flag ||= user_project.operations_feature_flags.find_by_name!(params[:feature_flag_name])
end end
def project
@project ||= feature_flag.project
end
def new_version_flag_present? def new_version_flag_present?
user_project.operations_feature_flags.new_version_flag.find_by_name(params[:name]).present? user_project.operations_feature_flags.new_version_flag.find_by_name(params[:name]).present?
end end
@ -245,6 +253,14 @@ module API
hash[key] = yield(hash[key]) if hash.key?(key) hash[key] = yield(hash[key]) if hash.key?(key)
hash hash
end end
def exclude_legacy_flags_check!
if Feature.enabled?(:remove_legacy_flags, project, default_enabled: :yaml) &&
Feature.disabled?(:remove_legacy_flags_override, project, default_enabled: :yaml) &&
feature_flag.legacy_flag?
not_found!
end
end
end end
end end
end end

View File

@ -26651,13 +26651,16 @@ msgstr ""
msgid "ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported." msgid "ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported."
msgstr "" msgstr ""
msgid "ProtectedBranch|Allow force push" msgid "ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}."
msgstr "" msgstr ""
msgid "ProtectedBranch|Allow force push for all users with push access." msgid "ProtectedBranch|Allow all users with push access to force push."
msgstr "" msgstr ""
msgid "ProtectedBranch|Allow force push:" msgid "ProtectedBranch|Allowed to force push"
msgstr ""
msgid "ProtectedBranch|Allowed to force push:"
msgstr "" msgstr ""
msgid "ProtectedBranch|Allowed to merge" msgid "ProtectedBranch|Allowed to merge"
@ -26702,7 +26705,7 @@ msgstr ""
msgid "ProtectedBranch|There are currently no protected branches, protect a branch with the form above." msgid "ProtectedBranch|There are currently no protected branches, protect a branch with the form above."
msgstr "" msgstr ""
msgid "ProtectedBranch|Toggle allow force push" msgid "ProtectedBranch|Toggle allowed to force push"
msgstr "" msgstr ""
msgid "ProtectedBranch|Toggle code owner approval" msgid "ProtectedBranch|Toggle code owner approval"
@ -27987,6 +27990,9 @@ msgstr[1] ""
msgid "Requires values to meet regular expression requirements." msgid "Requires values to meet regular expression requirements."
msgstr "" msgstr ""
msgid "Requires your primary GitLab email address."
msgstr ""
msgid "Resend" msgid "Resend"
msgstr "" msgstr ""

View File

@ -40,6 +40,8 @@
"markdownlint:no-trailing-spaces": "markdownlint --config doc/.markdownlint/markdownlint-no-trailing-spaces.yml", "markdownlint:no-trailing-spaces": "markdownlint --config doc/.markdownlint/markdownlint-no-trailing-spaces.yml",
"markdownlint:no-trailing-spaces:fix": "yarn run markdownlint:no-trailing-spaces --fix", "markdownlint:no-trailing-spaces:fix": "yarn run markdownlint:no-trailing-spaces --fix",
"postinstall": "node ./scripts/frontend/postinstall.js", "postinstall": "node ./scripts/frontend/postinstall.js",
"storybook:install": "yarn --cwd ./storybook install",
"storybook:start": "yarn --cwd ./storybook start",
"stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js", "stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js",
"webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js", "webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js",
"webpack-vendor": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.vendor.config.js", "webpack-vendor": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.vendor.config.js",

View File

@ -6,7 +6,7 @@ module QA
RSpec.describe 'Create' do RSpec.describe 'Create' do
context 'Gitaly' do context 'Gitaly' do
# Issue to track removal of feature flag: https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/602 # Issue to track removal of feature flag: https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/602
describe 'Distributed reads', :orchestrated, :gitaly_cluster, :skip_live_env, :requires_admin do describe 'Distributed reads', :orchestrated, :gitaly_cluster, :skip_live_env, :requires_admin, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/322814', type: :investigating } do
let(:number_of_reads_per_loop) { 9 } let(:number_of_reads_per_loop) { 9 }
let(:praefect_manager) { Service::PraefectManager.new } let(:praefect_manager) { Service::PraefectManager.new }
let(:project) do let(:project) do

View File

@ -4,7 +4,7 @@ require 'parallel'
module QA module QA
RSpec.describe 'Create' do RSpec.describe 'Create' do
context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env do context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/331989', type: :investigating } do
let(:praefect_manager) { Service::PraefectManager.new } let(:praefect_manager) { Service::PraefectManager.new }
let(:project) do let(:project) do
Resource::Project.fabricate! do |project| Resource::Project.fabricate! do |project|

View File

@ -371,6 +371,58 @@ RSpec.describe Projects::FeatureFlagsController do
end end
end end
describe 'GET edit' do
subject { get(:edit, params: params) }
context 'with legacy flags' do
let!(:feature_flag) { create(:operations_feature_flag, project: project) }
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
iid: feature_flag.iid
}
end
context 'removed' do
before do
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
end
it 'returns not found' do
is_expected.to have_gitlab_http_status(:not_found)
end
end
context 'removed' do
before do
stub_feature_flags(remove_legacy_flags: false)
end
it 'returns ok' do
is_expected.to have_gitlab_http_status(:ok)
end
end
end
context 'with new version flags' do
let!(:feature_flag) { create(:operations_feature_flag, :new_version_flag, project: project) }
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
iid: feature_flag.iid
}
end
it 'returns successfully' do
is_expected.to have_gitlab_http_status(:ok)
end
end
end
describe 'POST create.json' do describe 'POST create.json' do
subject { post(:create, params: params, format: :json) } subject { post(:create, params: params, format: :json) }

View File

@ -79,6 +79,7 @@ FactoryBot.define do
trait :pending do trait :pending do
queued_at { 'Di 29. Okt 09:50:59 CET 2013' } queued_at { 'Di 29. Okt 09:50:59 CET 2013' }
status { 'pending' } status { 'pending' }
end end
@ -286,6 +287,15 @@ FactoryBot.define do
trait :queued do trait :queued do
queued_at { Time.now } queued_at { Time.now }
after(:create) do |build|
build.create_queuing_entry!
end
end
trait :picked do
running
runner factory: :ci_runner runner factory: :ci_runner
end end

View File

@ -38,7 +38,7 @@ RSpec.describe 'Project issue boards sidebar milestones', :js do
wait_for_requests wait_for_requests
page.within('.value') do page.within('[data-testid="select-milestone"]') do
expect(page).to have_content(milestone.title) expect(page).to have_content(milestone.title)
end end
end end
@ -56,7 +56,7 @@ RSpec.describe 'Project issue boards sidebar milestones', :js do
wait_for_requests wait_for_requests
page.within('.value') do page.within('[data-testid="select-milestone"]') do
expect(page).not_to have_content(milestone.title) expect(page).not_to have_content(milestone.title)
end end
end end

View File

@ -1,11 +1,11 @@
import { GlDrawer } from '@gitlab/ui'; import { GlDrawer } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
@ -68,6 +68,9 @@ describe('BoardContentSidebar', () => {
iterations: { iterations: {
loading: false, loading: false,
}, },
attributesList: {
loading: false,
},
}, },
}, },
}, },
@ -84,38 +87,41 @@ describe('BoardContentSidebar', () => {
}); });
it('confirms we render GlDrawer', () => { it('confirms we render GlDrawer', () => {
expect(wrapper.find(GlDrawer).exists()).toBe(true); expect(wrapper.findComponent(GlDrawer).exists()).toBe(true);
}); });
it('does not render GlDrawer when isSidebarOpen is false', () => { it('does not render GlDrawer when isSidebarOpen is false', () => {
createStore({ mockGetters: { isSidebarOpen: () => false } }); createStore({ mockGetters: { isSidebarOpen: () => false } });
createComponent(); createComponent();
expect(wrapper.find(GlDrawer).exists()).toBe(false); expect(wrapper.findComponent(GlDrawer).exists()).toBe(false);
}); });
it('applies an open attribute', () => { it('applies an open attribute', () => {
expect(wrapper.find(GlDrawer).props('open')).toBe(true); expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true);
}); });
it('renders BoardSidebarLabelsSelect', () => { it('renders BoardSidebarLabelsSelect', () => {
expect(wrapper.find(BoardSidebarLabelsSelect).exists()).toBe(true); expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true);
}); });
it('renders BoardSidebarTitle', () => { it('renders BoardSidebarTitle', () => {
expect(wrapper.find(BoardSidebarTitle).exists()).toBe(true); expect(wrapper.findComponent(BoardSidebarTitle).exists()).toBe(true);
}); });
it('renders BoardSidebarDueDate', () => { it('renders BoardSidebarDueDate', () => {
expect(wrapper.find(BoardSidebarDueDate).exists()).toBe(true); expect(wrapper.findComponent(BoardSidebarDueDate).exists()).toBe(true);
}); });
it('renders BoardSidebarSubscription', () => { it('renders BoardSidebarSubscription', () => {
expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true); expect(wrapper.findComponent(SidebarSubscriptionsWidget).exists()).toBe(true);
}); });
it('renders BoardSidebarMilestoneSelect', () => { it('renders SidebarDropdownWidget for milestones', () => {
expect(wrapper.find(BoardSidebarMilestoneSelect).exists()).toBe(true); expect(wrapper.findComponent(SidebarDropdownWidget).exists()).toBe(true);
expect(wrapper.findComponent(SidebarDropdownWidget).props('issuableAttribute')).toEqual(
'milestone',
);
}); });
describe('when we emit close', () => { describe('when we emit close', () => {
@ -128,7 +134,7 @@ describe('BoardContentSidebar', () => {
}); });
it('calls toggleBoardItem with correct parameters', async () => { it('calls toggleBoardItem with correct parameters', async () => {
wrapper.find(GlDrawer).vm.$emit('close'); wrapper.findComponent(GlDrawer).vm.$emit('close');
expect(toggleBoardItem).toHaveBeenCalledTimes(1); expect(toggleBoardItem).toHaveBeenCalledTimes(1);
expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), { expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {

View File

@ -56,6 +56,18 @@ describe('noteable_discussion component', () => {
expect(wrapper.find('.discussion-header').exists()).toBe(true); expect(wrapper.find('.discussion-header').exists()).toBe(true);
}); });
it('should hide actions when diff refs do not exists', async () => {
const discussion = { ...discussionMock };
discussion.diff_file = { ...mockDiffFile, diff_refs: null };
discussion.diff_discussion = true;
discussion.expanded = false;
wrapper.setProps({ discussion });
await nextTick();
expect(wrapper.vm.canShowReplyActions).toBe(false);
});
describe('actions', () => { describe('actions', () => {
it('should toggle reply form', async () => { it('should toggle reply form', async () => {
await nextTick(); await nextTick();

View File

@ -0,0 +1,503 @@
import {
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlLink,
GlSearchBoxByType,
GlFormInput,
GlLoadingIcon,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issue_show/constants';
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { IssuableAttributeType } from '~/sidebar/constants';
import projectIssueMilestoneMutation from '~/sidebar/queries/project_issue_milestone.mutation.graphql';
import projectIssueMilestoneQuery from '~/sidebar/queries/project_issue_milestone.query.graphql';
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
import {
mockIssue,
mockProjectMilestonesResponse,
noCurrentMilestoneResponse,
mockMilestoneMutationResponse,
mockMilestone2,
emptyProjectMilestonesResponse,
} from '../mock_data';
jest.mock('~/flash');
const localVue = createLocalVue();
describe('SidebarDropdownWidget', () => {
let wrapper;
let mockApollo;
const promiseData = { issuableSetAttribute: { issue: { attribute: { id: '123' } } } };
const firstErrorMsg = 'first error';
const promiseWithErrors = {
...promiseData,
issuableSetAttribute: { ...promiseData.issuableSetAttribute, errors: [firstErrorMsg] },
};
const mutationSuccess = () => jest.fn().mockResolvedValue({ data: promiseData });
const mutationError = () =>
jest.fn().mockRejectedValue('Failed to set milestone on this issue. Please try again.');
const mutationSuccessWithErrors = () => jest.fn().mockResolvedValue({ data: promiseWithErrors });
const findGlLink = () => wrapper.findComponent(GlLink);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownText = () => wrapper.findComponent(GlDropdownText);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownItemWithText = (text) =>
findAllDropdownItems().wrappers.find((x) => x.text() === text);
const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findEditButton = () => findSidebarEditableItem().find('[data-testid="edit-button"]');
const findEditableLoadingIcon = () => findSidebarEditableItem().findComponent(GlLoadingIcon);
const findAttributeItems = () => wrapper.findByTestId('milestone-items');
const findSelectedAttribute = () => wrapper.findByTestId('select-milestone');
const findNoAttributeItem = () => wrapper.findByTestId('no-milestone-item');
const findLoadingIconDropdown = () => wrapper.findByTestId('loading-icon-dropdown');
const waitForDropdown = async () => {
// BDropdown first changes its `visible` property
// in a requestAnimationFrame callback.
// It then emits `shown` event in a watcher for `visible`
// Hence we need both of these:
await waitForPromises();
await wrapper.vm.$nextTick();
};
const waitForApollo = async () => {
jest.runOnlyPendingTimers();
await waitForPromises();
};
// Used with createComponentWithApollo which uses 'mount'
const clickEdit = async () => {
await findEditButton().trigger('click');
await waitForDropdown();
// We should wait for attributes list to be fetched.
await waitForApollo();
};
// Used with createComponent which shallow mounts components
const toggleDropdown = async () => {
wrapper.vm.$refs.editable.expand();
await waitForDropdown();
};
const createComponentWithApollo = async ({
requestHandlers = [],
projectMilestonesSpy = jest.fn().mockResolvedValue(mockProjectMilestonesResponse),
currentMilestoneSpy = jest.fn().mockResolvedValue(noCurrentMilestoneResponse),
} = {}) => {
localVue.use(VueApollo);
mockApollo = createMockApollo([
[projectMilestonesQuery, projectMilestonesSpy],
[projectIssueMilestoneQuery, currentMilestoneSpy],
...requestHandlers,
]);
wrapper = extendedWrapper(
mount(SidebarDropdownWidget, {
localVue,
provide: { canUpdate: true },
apolloProvider: mockApollo,
propsData: {
workspacePath: mockIssue.projectPath,
attrWorkspacePath: mockIssue.projectPath,
iid: mockIssue.iid,
issuableType: IssuableType.Issue,
issuableAttribute: IssuableAttributeType.Milestone,
},
attachTo: document.body,
}),
);
await waitForApollo();
};
const createComponent = ({ data = {}, mutationPromise = mutationSuccess, queries = {} } = {}) => {
wrapper = extendedWrapper(
shallowMount(SidebarDropdownWidget, {
provide: { canUpdate: true },
data() {
return data;
},
propsData: {
workspacePath: '',
attrWorkspacePath: '',
iid: '',
issuableType: IssuableType.Issue,
issuableAttribute: IssuableAttributeType.Milestone,
},
mocks: {
$apollo: {
mutate: mutationPromise(),
queries: {
currentAttribute: { loading: false },
attributesList: { loading: false },
...queries,
},
},
},
stubs: {
SidebarEditableItem,
GlSearchBoxByType,
GlDropdown,
},
}),
);
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when not editing', () => {
beforeEach(() => {
createComponent({
data: {
currentAttribute: { id: 'id', title: 'title', webUrl: 'webUrl' },
},
stubs: {
GlDropdown,
SidebarEditableItem,
},
});
});
it('shows the current attribute', () => {
expect(findSelectedAttribute().text()).toBe('title');
});
it('links to the current attribute', () => {
expect(findGlLink().attributes().href).toBe('webUrl');
});
it('does not show a loading spinner next to the heading', () => {
expect(findEditableLoadingIcon().exists()).toBe(false);
});
it('shows a loading spinner while fetching the current attribute', () => {
createComponent({
queries: {
currentAttribute: { loading: true },
},
});
expect(findEditableLoadingIcon().exists()).toBe(true);
});
it('shows the loading spinner and the title of the selected attribute while updating', () => {
createComponent({
data: {
updating: true,
selectedTitle: 'Some milestone title',
},
queries: {
currentAttribute: { loading: false },
},
});
expect(findEditableLoadingIcon().exists()).toBe(true);
expect(findSelectedAttribute().text()).toBe('Some milestone title');
});
describe('when current attribute does not exist', () => {
it('renders "None" as the selected attribute title', () => {
createComponent();
expect(findSelectedAttribute().text()).toBe('None');
});
});
});
describe('when a user can edit', () => {
describe('when user is editing', () => {
describe('when rendering the dropdown', () => {
it('shows a loading spinner while fetching a list of attributes', async () => {
createComponent({
queries: {
attributesList: { loading: true },
},
});
await toggleDropdown();
expect(findLoadingIconDropdown().exists()).toBe(true);
});
describe('GlDropdownItem with the right title and id', () => {
const id = 'id';
const title = 'title';
beforeEach(async () => {
createComponent({
data: { attributesList: [{ id, title }], currentAttribute: { id, title } },
});
await toggleDropdown();
});
it('does not show a loading spinner', () => {
expect(findLoadingIconDropdown().exists()).toBe(false);
});
it('renders title $title', () => {
expect(findDropdownItemWithText(title).exists()).toBe(true);
});
it('checks the correct dropdown item', () => {
expect(
findAllDropdownItems()
.filter((w) => w.props('isChecked') === true)
.at(0)
.text(),
).toBe(title);
});
});
describe('when no data is assigned', () => {
beforeEach(async () => {
createComponent();
await toggleDropdown();
});
it('finds GlDropdownItem with "No milestone"', () => {
expect(findNoAttributeItem().text()).toBe('No milestone');
});
it('"No milestone" is checked', () => {
expect(findNoAttributeItem().props('isChecked')).toBe(true);
});
it('does not render any dropdown item', () => {
expect(findAttributeItems().exists()).toBe(false);
});
});
describe('when clicking on dropdown item', () => {
describe('when currentAttribute is equal to attribute id', () => {
it('does not call setIssueAttribute mutation', async () => {
createComponent({
data: {
attributesList: [{ id: 'id', title: 'title' }],
currentAttribute: { id: 'id', title: 'title' },
},
});
await toggleDropdown();
findDropdownItemWithText('title').vm.$emit('click');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(0);
});
});
describe('when currentAttribute is not equal to attribute id', () => {
describe('when error', () => {
const bootstrapComponent = (mutationResp) => {
createComponent({
data: {
attributesList: [
{ id: '123', title: '123' },
{ id: 'id', title: 'title' },
],
currentAttribute: '123',
},
mutationPromise: mutationResp,
});
};
describe.each`
description | mutationResp | expectedMsg
${'top-level error'} | ${mutationError} | ${'Failed to set milestone on this issue. Please try again.'}
${'user-recoverable error'} | ${mutationSuccessWithErrors} | ${firstErrorMsg}
`(`$description`, ({ mutationResp, expectedMsg }) => {
beforeEach(async () => {
bootstrapComponent(mutationResp);
await toggleDropdown();
findDropdownItemWithText('title').vm.$emit('click');
});
it(`calls createFlash with "${expectedMsg}"`, async () => {
await wrapper.vm.$nextTick();
expect(createFlash).toHaveBeenCalledWith({
message: expectedMsg,
captureError: true,
error: expectedMsg,
});
});
});
});
});
});
});
describe('when a user is searching', () => {
describe('when search result is not found', () => {
it('renders "No milestone found"', async () => {
createComponent();
await toggleDropdown();
findSearchBox().vm.$emit('input', 'non existing milestones');
await wrapper.vm.$nextTick();
expect(findDropdownText().text()).toBe('No milestone found');
});
});
});
});
});
describe('with mock apollo', () => {
let error;
beforeEach(() => {
jest.spyOn(Sentry, 'captureException');
error = new Error('mayday');
});
describe("when issuable type is 'issue'", () => {
describe('when dropdown is expanded and user can edit', () => {
let milestoneMutationSpy;
beforeEach(async () => {
milestoneMutationSpy = jest.fn().mockResolvedValue(mockMilestoneMutationResponse);
await createComponentWithApollo({
requestHandlers: [[projectIssueMilestoneMutation, milestoneMutationSpy]],
});
await clickEdit();
});
it('renders the dropdown on clicking edit', async () => {
expect(findDropdown().isVisible()).toBe(true);
});
it('focuses on the input when dropdown is shown', async () => {
expect(document.activeElement).toEqual(wrapper.findComponent(GlFormInput).element);
});
describe('when currentAttribute is not equal to attribute id', () => {
describe('when update is successful', () => {
beforeEach(() => {
findDropdownItemWithText(mockMilestone2.title).vm.$emit('click');
});
it('calls setIssueAttribute mutation', () => {
expect(milestoneMutationSpy).toHaveBeenCalledWith({
iid: mockIssue.iid,
attributeId: getIdFromGraphQLId(mockMilestone2.id),
fullPath: mockIssue.projectPath,
});
});
it('sets the value returned from the mutation to currentAttribute', async () => {
expect(findSelectedAttribute().text()).toBe(mockMilestone2.title);
});
});
});
describe('milestones', () => {
let projectMilestonesSpy;
it('should call createFlash if milestones query fails', async () => {
await createComponentWithApollo({
projectMilestonesSpy: jest.fn().mockRejectedValue(error),
});
await clickEdit();
expect(createFlash).toHaveBeenCalledWith({
message: wrapper.vm.i18n.listFetchError,
captureError: true,
error: expect.any(Error),
});
});
it('only fetches attributes when dropdown is opened', async () => {
projectMilestonesSpy = jest.fn().mockResolvedValueOnce(emptyProjectMilestonesResponse);
await createComponentWithApollo({ projectMilestonesSpy });
expect(projectMilestonesSpy).not.toHaveBeenCalled();
await clickEdit();
expect(projectMilestonesSpy).toHaveBeenNthCalledWith(1, {
fullPath: mockIssue.projectPath,
title: '',
state: 'active',
});
});
describe('when a user is searching', () => {
const mockSearchTerm = 'foobar';
beforeEach(async () => {
projectMilestonesSpy = jest
.fn()
.mockResolvedValueOnce(emptyProjectMilestonesResponse);
await createComponentWithApollo({ projectMilestonesSpy });
await clickEdit();
});
it('sends a projectMilestones query with the entered search term "foo"', async () => {
findSearchBox().vm.$emit('input', mockSearchTerm);
await wrapper.vm.$nextTick();
// Account for debouncing
jest.runAllTimers();
expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, {
fullPath: mockIssue.projectPath,
title: mockSearchTerm,
state: 'active',
});
});
});
});
});
describe('currentAttributes', () => {
it('should call createFlash if currentAttributes query fails', async () => {
await createComponentWithApollo({
currentMilestoneSpy: jest.fn().mockRejectedValue(error),
});
expect(createFlash).toHaveBeenCalledWith({
message: wrapper.vm.i18n.currentFetchError,
captureError: true,
error: expect.any(Error),
});
});
});
});
});
});

View File

@ -513,4 +513,83 @@ export const participantsQueryResponse = {
}, },
}; };
export const mockGroupPath = 'gitlab-org';
export const mockProjectPath = `${mockGroupPath}/some-project`;
export const mockIssue = {
projectPath: mockProjectPath,
iid: '1',
groupPath: mockGroupPath,
};
export const mockIssueId = 'gid://gitlab/Issue/1';
export const mockMilestone1 = {
__typename: 'Milestone',
id: 'gid://gitlab/Milestone/1',
title: 'Foobar Milestone',
webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/1',
state: 'active',
};
export const mockMilestone2 = {
__typename: 'Milestone',
id: 'gid://gitlab/Milestone/2',
title: 'Awesome Milestone',
webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/2',
state: 'active',
};
export const mockProjectMilestonesResponse = {
data: {
workspace: {
attributes: {
nodes: [mockMilestone1, mockMilestone2],
},
__typename: 'MilestoneConnection',
},
__typename: 'Project',
},
};
export const noCurrentMilestoneResponse = {
data: {
workspace: {
issuable: { id: mockIssueId, attribute: null, __typename: 'Issue' },
__typename: 'Project',
},
},
};
export const mockMilestoneMutationResponse = {
data: {
issuableSetAttribute: {
errors: [],
issuable: {
id: 'gid://gitlab/Issue/1',
attribute: {
id: 'gid://gitlab/Milestone/2',
title: 'Awesome Milestone',
state: 'active',
__typename: 'Milestone',
},
__typename: 'Issue',
},
__typename: 'UpdateIssuePayload',
},
},
};
export const emptyProjectMilestonesResponse = {
data: {
workspace: {
attributes: {
nodes: [],
},
__typename: 'MilestoneConnection',
},
__typename: 'Project',
},
};
export default mockData; export default mockData;

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20210525075724_clean_up_pending_builds_table.rb')
RSpec.describe CleanUpPendingBuildsTable do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:queue) { table(:ci_pending_builds) }
let(:builds) { table(:ci_builds) }
before do
namespaces.create!(id: 123, name: 'sample', path: 'sample')
projects.create!(id: 123, name: 'sample', path: 'sample', namespace_id: 123)
builds.create!(id: 1, project_id: 123, status: 'pending', type: 'Ci::Build')
builds.create!(id: 2, project_id: 123, status: 'pending', type: 'GenericCommitStatus')
builds.create!(id: 3, project_id: 123, status: 'success', type: 'Ci::Bridge')
builds.create!(id: 4, project_id: 123, status: 'success', type: 'Ci::Build')
builds.create!(id: 5, project_id: 123, status: 'running', type: 'Ci::Build')
builds.create!(id: 6, project_id: 123, status: 'created', type: 'Ci::Build')
queue.create!(id: 1, project_id: 123, build_id: 1)
queue.create!(id: 2, project_id: 123, build_id: 4)
queue.create!(id: 3, project_id: 123, build_id: 5)
end
it 'removes duplicated data from pending builds table' do
migrate!
expect(queue.all.count).to eq 1
expect(queue.first.id).to eq 1
expect(builds.all.count).to eq 6
end
context 'when there are multiple batches' do
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
end
it 'iterates the data correctly' do
migrate!
expect(queue.all.count).to eq 1
end
end
end

View File

@ -354,7 +354,7 @@ RSpec.describe Ci::Build do
it 'does not push build to the queue' do it 'does not push build to the queue' do
build.enqueue build.enqueue
expect(::Ci::PendingBuild.all.count).to be_zero expect(build.queuing_entry).not_to be_present
end end
end end

View File

@ -23,7 +23,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let(:runner) { create(:ci_runner, :project, projects: [project]) } let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:job) do let(:job) do
create(:ci_build, :artifacts, :extended_options, create(:ci_build, :pending, :queued, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
end end
@ -129,7 +129,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when other projects have pending jobs' do context 'when other projects have pending jobs' do
before do before do
job.success job.success
create(:ci_build, :pending) create(:ci_build, :pending, :queued)
end end
it_behaves_like 'no jobs available' it_behaves_like 'no jobs available'
@ -239,7 +239,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end end
context 'when job is made for tag' do context 'when job is made for tag' do
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
it 'sets branch as ref_type' do it 'sets branch as ref_type' do
request_job request_job
@ -297,7 +297,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end end
context 'when job filtered by job_age' do context 'when job filtered by job_age' do
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) } let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) }
context 'job is queued less than job_age parameter' do context 'job is queued less than job_age parameter' do
let(:job_age) { 120 } let(:job_age) { 120 }
@ -359,7 +359,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end end
context 'when job is for a release' do context 'when job is for a release' do
let!(:job) { create(:ci_build, :release_options, pipeline: pipeline) } let!(:job) { create(:ci_build, :pending, :queued, :release_options, pipeline: pipeline) }
context 'when `multi_build_steps` is passed by the runner' do context 'when `multi_build_steps` is passed by the runner' do
it 'exposes release info' do it 'exposes release info' do
@ -398,7 +398,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when job is made for merge request' do context 'when job is made for merge request' do
let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) } let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) }
let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) } let!(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) }
let(:merge_request) { create(:merge_request) } let(:merge_request) { create(:merge_request) }
it 'sets branch as ref_type' do it 'sets branch as ref_type' do
@ -479,9 +479,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end end
context 'when project and pipeline have multiple jobs' do context 'when project and pipeline have multiple jobs' do
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } let!(:test_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do before do
job.success job.success
@ -531,8 +531,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end end
context 'when pipeline have jobs with artifacts' do context 'when pipeline have jobs with artifacts' do
let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:job) { create(:ci_build, :pending, :queued, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } let!(:test_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do before do
job.success job.success
@ -551,10 +551,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end end
context 'when explicit dependencies are defined' do context 'when explicit dependencies are defined' do
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) do let!(:test_job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy', create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
stage: 'deploy', stage_idx: 1, stage: 'deploy', stage_idx: 1,
options: { script: ['bash'], dependencies: [job2.name] }) options: { script: ['bash'], dependencies: [job2.name] })
end end
@ -575,10 +575,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end end
context 'when dependencies is an empty array' do context 'when dependencies is an empty array' do
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:empty_dependencies_job) do let!(:empty_dependencies_job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job', create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
stage: 'deploy', stage_idx: 1, stage: 'deploy', stage_idx: 1,
options: { script: ['bash'], dependencies: [] }) options: { script: ['bash'], dependencies: [] })
end end
@ -739,7 +739,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end end
describe 'port support' do describe 'port support' do
let(:job) { create(:ci_build, pipeline: pipeline, options: options) } let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
context 'when job image has ports' do context 'when job image has ports' do
let(:options) do let(:options) do
@ -791,7 +791,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
describe 'a job with excluded artifacts' do describe 'a job with excluded artifacts' do
context 'when excluded paths are defined' do context 'when excluded paths are defined' do
let(:job) do let(:job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'test', create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'test',
stage: 'deploy', stage_idx: 1, stage: 'deploy', stage_idx: 1,
options: { artifacts: { paths: ['abc'], exclude: ['cde'] } }) options: { artifacts: { paths: ['abc'], exclude: ['cde'] } })
end end
@ -839,7 +839,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
subject { request_job } subject { request_job }
context 'when triggered by a user' do context 'when triggered by a user' do
let(:job) { create(:ci_build, user: user, project: project) } let(:job) { create(:ci_build, :pending, :queued, user: user, project: project) }
subject { request_job(id: job.id) } subject { request_job(id: job.id) }

View File

@ -148,6 +148,18 @@ RSpec.describe API::FeatureFlags do
expect(json_response['version']).to eq('legacy_flag') expect(json_response['version']).to eq('legacy_flag')
end end
context 'without legacy flags' do
before do
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
end
it 'returns not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
it_behaves_like 'check user permission' it_behaves_like 'check user permission'
end end
@ -492,6 +504,18 @@ RSpec.describe API::FeatureFlags do
end end
it_behaves_like 'check user permission' it_behaves_like 'check user permission'
context 'without legacy flags' do
before do
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
end
it 'returns not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end end
context 'when feature flag exists already' do context 'when feature flag exists already' do
@ -537,6 +561,18 @@ RSpec.describe API::FeatureFlags do
end end
end end
end end
context 'without legacy flags' do
before do
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
end
it 'returns not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end end
context 'with a version 2 flag' do context 'with a version 2 flag' do
@ -612,6 +648,18 @@ RSpec.describe API::FeatureFlags do
}) })
end end
context 'without legacy flags' do
before do
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
end
it 'returns not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
it_behaves_like 'check user permission' it_behaves_like 'check user permission'
context 'when strategies become empty array after the removal' do context 'when strategies become empty array after the removal' do
@ -976,6 +1024,20 @@ RSpec.describe API::FeatureFlags do
expect(feature_flag.reload.strategies.first.scopes.count).to eq(0) expect(feature_flag.reload.strategies.first.scopes.count).to eq(0)
end end
end end
context 'without legacy flags' do
before do
stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false)
end
it 'returns not found' do
params = { description: 'new description' }
put api("/projects/#{project.id}/feature_flags/other_flag_name", user), params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end end
describe 'DELETE /projects/:id/feature_flags/:name' do describe 'DELETE /projects/:id/feature_flags/:name' do

View File

@ -11,7 +11,13 @@ RSpec.describe MergeRequestDiffEntity do
let(:merge_request_diff) { merge_request_diffs.first } let(:merge_request_diff) { merge_request_diffs.first }
let(:entity) do let(:entity) do
described_class.new(merge_request_diff, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs) described_class.new(
merge_request_diff,
request: request,
merge_request: merge_request,
merge_request_diff: merge_request_diff,
merge_request_diffs: merge_request_diffs
)
end end
subject { entity.as_json } subject { entity.as_json }
@ -26,6 +32,46 @@ RSpec.describe MergeRequestDiffEntity do
end end
end end
describe '#version_index' do
shared_examples 'version_index is nil' do
it 'returns nil' do
expect(subject[:version_index]).to be_nil
end
end
context 'when diff is not present' do
let(:entity) do
described_class.new(
merge_request_diff,
request: request,
merge_request: merge_request,
merge_request_diffs: merge_request_diffs
)
end
it_behaves_like 'version_index is nil'
end
context 'when diff is not included in @merge_request_diffs' do
let(:merge_request_diff) { create(:merge_request_diff) }
let(:merge_request_diff_2) { create(:merge_request_diff) }
before do
merge_request_diffs << merge_request_diff_2
end
it_behaves_like 'version_index is nil'
end
context 'when @merge_request_diffs.size <= 1' do
before do
expect(merge_request_diffs.size).to eq(1)
end
it_behaves_like 'version_index is nil'
end
end
describe '#short_commit_sha' do describe '#short_commit_sha' do
it 'returns short sha' do it 'returns short sha' do
expect(subject[:short_commit_sha]).to eq('b83d6e39') expect(subject[:short_commit_sha]).to eq('b83d6e39')

View File

@ -11,7 +11,7 @@ module Ci
let!(:shared_runner) { create(:ci_runner, :instance) } let!(:shared_runner) { create(:ci_runner, :instance) }
let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) } let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
let!(:group_runner) { create(:ci_runner, :group, groups: [group]) } let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let!(:pending_job) { create(:ci_build, pipeline: pipeline) } let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
describe '#execute' do describe '#execute' do
context 'checks database loadbalancing stickiness' do context 'checks database loadbalancing stickiness' do
@ -104,11 +104,11 @@ module Ci
let!(:project3) { create :project, shared_runners_enabled: true } let!(:project3) { create :project, shared_runners_enabled: true }
let!(:pipeline3) { create :ci_pipeline, project: project3 } let!(:pipeline3) { create :ci_pipeline, project: project3 }
let!(:build1_project1) { pending_job } let!(:build1_project1) { pending_job }
let!(:build2_project1) { FactoryBot.create :ci_build, pipeline: pipeline } let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
let!(:build3_project1) { FactoryBot.create :ci_build, pipeline: pipeline } let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
let!(:build1_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 } let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
let!(:build2_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 } let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
let!(:build1_project3) { FactoryBot.create :ci_build, pipeline: pipeline3 } let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) }
context 'when using fair scheduling' do context 'when using fair scheduling' do
context 'when all builds are pending' do context 'when all builds are pending' do
@ -255,17 +255,17 @@ module Ci
let!(:pipeline3) { create(:ci_pipeline, project: project3) } let!(:pipeline3) { create(:ci_pipeline, project: project3) }
let!(:build1_project1) { pending_job } let!(:build1_project1) { pending_job }
let!(:build2_project1) { create(:ci_build, pipeline: pipeline) } let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) }
let!(:build3_project1) { create(:ci_build, pipeline: pipeline) } let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) }
let!(:build1_project2) { create(:ci_build, pipeline: pipeline2) } let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
let!(:build2_project2) { create(:ci_build, pipeline: pipeline2) } let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
let!(:build1_project3) { create(:ci_build, pipeline: pipeline3) } let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) }
# these shouldn't influence the scheduling # these shouldn't influence the scheduling
let!(:unrelated_group) { create(:group) } let!(:unrelated_group) { create(:group) }
let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) } let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) }
let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) } let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) }
let!(:build1_unrelated_project) { create(:ci_build, pipeline: unrelated_pipeline) } let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) }
let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) } let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
it 'does not consider builds from other group runners' do it 'does not consider builds from other group runners' do
@ -346,7 +346,7 @@ module Ci
subject { described_class.new(specific_runner).execute } subject { described_class.new(specific_runner).execute }
context 'with multiple builds are in queue' do context 'with multiple builds are in queue' do
let!(:other_build) { create :ci_build, pipeline: pipeline } let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
before do before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner) allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
@ -387,7 +387,7 @@ module Ci
let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) } let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
context 'when a job is protected' do context 'when a job is protected' do
let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) } let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
it 'picks the job' do it 'picks the job' do
expect(execute(specific_runner)).to eq(pending_job) expect(execute(specific_runner)).to eq(pending_job)
@ -395,7 +395,7 @@ module Ci
end end
context 'when a job is unprotected' do context 'when a job is unprotected' do
let!(:pending_job) { create(:ci_build, pipeline: pipeline) } let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
it 'picks the job' do it 'picks the job' do
expect(execute(specific_runner)).to eq(pending_job) expect(execute(specific_runner)).to eq(pending_job)
@ -403,7 +403,7 @@ module Ci
end end
context 'when protected attribute of a job is nil' do context 'when protected attribute of a job is nil' do
let!(:pending_job) { create(:ci_build, pipeline: pipeline) } let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
before do before do
pending_job.update_attribute(:protected, nil) pending_job.update_attribute(:protected, nil)
@ -419,7 +419,7 @@ module Ci
let!(:specific_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) } let!(:specific_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }
context 'when a job is protected' do context 'when a job is protected' do
let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) } let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
it 'picks the job' do it 'picks the job' do
expect(execute(specific_runner)).to eq(pending_job) expect(execute(specific_runner)).to eq(pending_job)
@ -427,7 +427,7 @@ module Ci
end end
context 'when a job is unprotected' do context 'when a job is unprotected' do
let!(:pending_job) { create(:ci_build, pipeline: pipeline) } let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
it 'does not pick the job' do it 'does not pick the job' do
expect(execute(specific_runner)).to be_nil expect(execute(specific_runner)).to be_nil
@ -435,7 +435,7 @@ module Ci
end end
context 'when protected attribute of a job is nil' do context 'when protected attribute of a job is nil' do
let!(:pending_job) { create(:ci_build, pipeline: pipeline) } let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
before do before do
pending_job.update_attribute(:protected, nil) pending_job.update_attribute(:protected, nil)
@ -449,7 +449,7 @@ module Ci
context 'runner feature set is verified' do context 'runner feature set is verified' do
let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } } let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } }
let!(:pending_job) { create(:ci_build, :pending, pipeline: pipeline, options: options) } let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
subject { execute(specific_runner, params) } subject { execute(specific_runner, params) }
@ -485,7 +485,7 @@ module Ci
shared_examples 'validation is active' do shared_examples 'validation is active' do
context 'when depended job has not been completed yet' do context 'when depended job has not been completed yet' do
let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
it { expect(subject).to eq(pending_job) } it { expect(subject).to eq(pending_job) }
end end
@ -522,7 +522,7 @@ module Ci
shared_examples 'validation is not active' do shared_examples 'validation is not active' do
context 'when depended job has not been completed yet' do context 'when depended job has not been completed yet' do
let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
it { expect(subject).to eq(pending_job) } it { expect(subject).to eq(pending_job) }
end end
@ -547,7 +547,7 @@ module Ci
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) } let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
let!(:pending_job) do let!(:pending_job) do
create(:ci_build, :pending, create(:ci_build, :pending, :queued,
pipeline: pipeline, stage_idx: 1, pipeline: pipeline, stage_idx: 1,
options: { script: ["bash"], dependencies: ['test'] }) options: { script: ["bash"], dependencies: ['test'] })
end end
@ -558,7 +558,7 @@ module Ci
end end
context 'when build is degenerated' do context 'when build is degenerated' do
let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) } let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
subject { execute(specific_runner, {}) } subject { execute(specific_runner, {}) }
@ -573,7 +573,7 @@ module Ci
context 'when build has data integrity problem' do context 'when build has data integrity problem' do
let!(:pending_job) do let!(:pending_job) do
create(:ci_build, :pending, pipeline: pipeline) create(:ci_build, :pending, :queued, pipeline: pipeline)
end end
before do before do
@ -598,7 +598,7 @@ module Ci
context 'when build fails to be run!' do context 'when build fails to be run!' do
let!(:pending_job) do let!(:pending_job) do
create(:ci_build, :pending, pipeline: pipeline) create(:ci_build, :pending, :queued, pipeline: pipeline)
end end
before do before do
@ -640,12 +640,12 @@ module Ci
context 'when only some builds can be matched by runner' do context 'when only some builds can be matched by runner' do
let!(:specific_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) } let!(:specific_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) }
let!(:pending_job) { create(:ci_build, pipeline: pipeline, tag_list: %w[matching]) } let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) }
before do before do
# create additional matching and non-matching jobs # create additional matching and non-matching jobs
create_list(:ci_build, 2, pipeline: pipeline, tag_list: %w[matching]) create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching])
create(:ci_build, pipeline: pipeline, tag_list: %w[non-matching]) create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching])
end end
it 'observes queue size of only matching jobs' do it 'observes queue size of only matching jobs' do
@ -693,7 +693,7 @@ module Ci
end end
context 'when there is another build in queue' do context 'when there is another build in queue' do
let!(:next_pending_job) { create(:ci_build, pipeline: pipeline) } let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
it 'skips this build and picks another build' do it 'skips this build and picks another build' do
expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment) expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
@ -732,6 +732,22 @@ module Ci
include_examples 'handles runner assignment' include_examples 'handles runner assignment'
end end
context 'when joining with pending builds table' do
before do
stub_feature_flags(ci_pending_builds_queue_join: true)
end
include_examples 'handles runner assignment'
end
context 'when not joining with pending builds table' do
before do
stub_feature_flags(ci_pending_builds_queue_join: false)
end
include_examples 'handles runner assignment'
end
end end
describe '#register_success' do describe '#register_success' do
@ -775,8 +791,8 @@ module Ci
end end
context 'when project already has running jobs' do context 'when project already has running jobs' do
let!(:build2) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) } let!(:build2) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }
let!(:build3) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) } let!(:build3) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }
it 'counts job queuing time histogram with expected labels' do it 'counts job queuing time histogram with expected labels' do
allow(attempt_counter).to receive(:increment) allow(attempt_counter).to receive(:increment)
@ -859,9 +875,9 @@ module Ci
end end
context 'when max queue depth is reached' do context 'when max queue depth is reached' do
let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) } let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
let!(:pending_job_2) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) } let!(:pending_job_2) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
let!(:pending_job_3) { create(:ci_build, :pending, pipeline: pipeline) } let!(:pending_job_3) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
before do before do
stub_const("#{described_class}::MAX_QUEUE_DEPTH", 2) stub_const("#{described_class}::MAX_QUEUE_DEPTH", 2)

View File

@ -66,7 +66,7 @@ RSpec.describe Ci::RetryBuildService do
let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) } let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
let_it_be(:build) do let_it_be(:build) do
create(:ci_build, :failed, :expired, :erased, :queued, :coverage, :tags, create(:ci_build, :failed, :picked, :expired, :erased, :queued, :coverage, :tags,
:allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group, :allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group,
description: 'my-job', stage: 'test', stage_id: stage.id, description: 'my-job', stage: 'test', stage_id: stage.id,
pipeline: pipeline, auto_canceled_by: another_pipeline, pipeline: pipeline, auto_canceled_by: another_pipeline,

View File

@ -95,6 +95,42 @@ RSpec.describe Deployments::UpdateEnvironmentService do
end end
end end
context 'when external URL is specified and the tier is unset' do
let(:options) { { name: 'production', url: external_url } }
before do
environment.update_columns(external_url: external_url, tier: nil)
job.update!(environment: 'production')
end
context 'when external URL is valid' do
let(:external_url) { 'https://google.com' }
it 'succeeds to update the tier automatically' do
expect { subject.execute }.to change { environment.tier }.from(nil).to('production')
end
end
context 'when external URL is invalid' do
let(:external_url) { 'google.com' }
it 'fails to update the tier due to validation error' do
expect { subject.execute }.not_to change { environment.tier }
end
it 'tracks an exception' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(an_instance_of(described_class::EnvironmentUpdateFailure),
project_id: project.id,
environment_id: environment.id,
reason: %q{External url is blocked: Only allowed schemes are http, https})
.once
subject.execute
end
end
end
context 'when variables are used' do context 'when variables are used' do
let(:options) do let(:options) do
{ name: 'review-apps/$CI_COMMIT_REF_NAME', { name: 'review-apps/$CI_COMMIT_REF_NAME',

2
storybook/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
public/

8
storybook/config/main.js Normal file
View File

@ -0,0 +1,8 @@
/* eslint-disable import/no-commonjs */
module.exports = {
stories: [
'../../app/assets/javascripts/**/*.stories.js',
'../../ee/app/assets/javascripts/**/*.stories.js',
],
addons: ['@storybook/addon-essentials', '@storybook/addon-a11y'],
};

View File

@ -0,0 +1,7 @@
const stylesheetsRequireCtx = require.context(
'../../app/assets/stylesheets',
true,
/application\.scss$/,
);
stylesheetsRequireCtx('./application.scss');

View File

@ -0,0 +1,103 @@
/* eslint-disable no-param-reassign */
const { statSync } = require('fs');
const path = require('path');
const sass = require('node-sass'); // eslint-disable-line import/no-unresolved
const { buildIncludePaths, resolveGlobUrl } = require('node-sass-magic-importer/dist/toolbox'); // eslint-disable-line import/no-unresolved
const webpack = require('webpack');
const gitlabWebpackConfig = require('../../config/webpack.config.js');
const ROOT = path.resolve(__dirname, '../../');
const TRANSPARENT_1X1_PNG =
'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==)';
const SASS_INCLUDE_PATHS = [
'app/assets/stylesheets',
'ee/app/assets/stylesheets',
'ee/app/assets/stylesheets/_ee',
'node_modules',
].map((p) => path.resolve(ROOT, p));
/**
* Custom importer for node-sass, used when LibSass encounters the `@import` directive.
* Doc source: https://github.com/sass/node-sass#importer--v200---experimental
* @param {*} url the path in import as-is, which LibSass encountered.
* @param {*} prev the previously resolved path.
* @returns {Object | null} the new import string.
*/
function sassSmartImporter(url, prev) {
const nodeSassOptions = this.options;
const includePaths = buildIncludePaths(nodeSassOptions.includePaths, prev).filter(
(includePath) => !includePath.includes('node_modules'),
);
// GitLab extensively uses glob-style import paths, but
// Sass doesn't support glob-style URLs out of the box.
// Here, we try and resolve the glob URL.
// If it resolves, we update the @import statement with the resolved path.
const filePaths = resolveGlobUrl(url, includePaths);
if (filePaths) {
const contents = filePaths
.filter((file) => statSync(file).isFile())
.map((x) => `@import '${x}';`)
.join(`\n`);
return { contents };
}
return null;
}
const sassLoaderOptions = {
functions: {
'image-url($url)': function sassImageUrlStub() {
return new sass.types.String(TRANSPARENT_1X1_PNG);
},
'asset_path($url)': function sassAssetPathStub() {
return new sass.types.String(TRANSPARENT_1X1_PNG);
},
'asset_url($url)': function sassAssetUrlStub() {
return new sass.types.String(TRANSPARENT_1X1_PNG);
},
'url($url)': function sassUrlStub() {
return new sass.types.String(TRANSPARENT_1X1_PNG);
},
},
includePaths: SASS_INCLUDE_PATHS,
importer: sassSmartImporter,
};
module.exports = function storybookWebpackConfig({ config }) {
// Add any missing extensions from the main GitLab webpack config
config.resolve.extensions = Array.from(
new Set([...config.resolve.extensions, ...gitlabWebpackConfig.resolve.extensions]),
);
// Replace any Storybook-defined CSS loaders with our custom one.
config.module.rules = [
...config.module.rules.filter((r) => !r.test.test('.css')),
{
test: /\.s?css$/,
exclude: /typescale\/\w+_demo\.scss$/, // skip typescale demo stylesheets
loaders: [
'style-loader',
'css-loader',
{
loader: 'sass-loader',
options: sassLoaderOptions,
},
],
},
];
// Silence webpack warnings about moment/pikaday not being able to resolve.
config.plugins.push(new webpack.IgnorePlugin(/moment/, /pikaday/));
// Add any missing aliases from the main GitLab webpack config
Object.assign(config.resolve.alias, gitlabWebpackConfig.resolve.alias);
// The main GitLab project aliases this `icons.svg` file to app/assets/javascripts/lib/utils/icons_path.js,
// which depends on the existence of a global `gon` variable.
// By deleting the alias, imports of this path will resolve as expected.
delete config.resolve.alias['@gitlab/svgs/dist/icons.svg'];
return config;
};

20
storybook/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "start-storybook -p 9002 -c config",
"build": "build-storybook -c config -o public"
},
"dependencies": {},
"devDependencies": {
"@storybook/addon-a11y": "^6.2.9",
"@storybook/addon-actions": "^6.2.9",
"@storybook/addon-controls": "^6.2.9",
"@storybook/addon-essentials": "^6.2.9",
"@storybook/vue": "6.2.9",
"node-sass": "^4.14.1",
"node-sass-magic-importer": "^5.3.2",
"postcss-loader": "3.0.0",
"sass-loader": "^7.1.0"
}
}

10766
storybook/yarn.lock Normal file

File diff suppressed because it is too large Load Diff