Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4901ff1764
commit
6a38034714
|
@ -58,10 +58,13 @@ update-qa-cache:
|
|||
- tooling/bin/find_change_diffs ${CHANGES_DIFFS_DIR}
|
||||
script:
|
||||
- |
|
||||
if tooling/bin/qa/check_if_only_quarantined_specs ${CHANGES_DIFFS_DIR}; then
|
||||
exit 0
|
||||
else
|
||||
tooling/bin/qa/package_and_qa_check ${CHANGES_DIFFS_DIR} && exit_code=$?
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
./scripts/trigger-build omnibus
|
||||
elif [ $exit_code -eq 1 ]; then
|
||||
exit 1
|
||||
else
|
||||
echo "Downstream jobs will not be triggered because package_and_qa_check exited with code: $exit_code"
|
||||
fi
|
||||
# These jobs often time out, so temporarily use private runners and a long timeout: https://gitlab.com/gitlab-org/gitlab/-/issues/238563
|
||||
tags:
|
||||
|
|
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -2,6 +2,22 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 14.4.2 (2021-11-08)
|
||||
|
||||
### Fixed (3 changes)
|
||||
|
||||
- [Skip retrying for reads on connection errors if primary only](gitlab-org/gitlab@8e1976ed75bd6c606d49c83863cf46bf3c4d5070) ([merge request](gitlab-org/gitlab!73919))
|
||||
- [Fix error 500 loading branch with UTF-8 characters with performance bar](gitlab-org/gitlab@67ddc428472d57bb3d8a4a84eb0750487a175f75) ([merge request](gitlab-org/gitlab!73919))
|
||||
- [Skip st_diff callback setting on LegacyDiffNote when importing](gitlab-org/gitlab@84f5c66321473cd702b3b671584054fcf3d141ae) ([merge request](gitlab-org/gitlab!73919))
|
||||
|
||||
### Changed (1 change)
|
||||
|
||||
- [Remove skip_legacy_diff_note_callback_on_import from legacy diff note](gitlab-org/gitlab@547a2ec29ea9e9299eab727899c3d90886ffc21c) ([merge request](gitlab-org/gitlab!73919))
|
||||
|
||||
### Performance (1 change)
|
||||
|
||||
- [Prevent Sidekiq size limiter middleware from running multiple times on the same job](gitlab-org/gitlab@294c01be38d400607536fb20a2038e098c0f0e28) ([merge request](gitlab-org/gitlab!73919))
|
||||
|
||||
## 14.4.1 (2021-10-28)
|
||||
|
||||
### Security (13 changes)
|
||||
|
|
|
@ -3,7 +3,7 @@ import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
|
|||
import createFlash from '~/flash';
|
||||
import { __, s__ } from '~/locale';
|
||||
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
|
||||
import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
|
||||
import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
|
||||
const i18n = {
|
||||
|
@ -71,7 +71,7 @@ export default {
|
|||
runnerUpdate: { errors },
|
||||
},
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: runnerUpdateMutation,
|
||||
mutation: runnerActionsUpdateMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.runner.id,
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<script>
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
|
||||
import RunnerContactedStateBadge from '../runner_contacted_state_badge.vue';
|
||||
import RunnerPausedBadge from '../runner_paused_badge.vue';
|
||||
|
||||
import { I18N_LOCKED_RUNNER_DESCRIPTION, I18N_PAUSED_RUNNER_DESCRIPTION } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RunnerContactedStateBadge,
|
||||
RunnerPausedBadge,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
runner: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
paused() {
|
||||
return !this.runner.active;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
I18N_LOCKED_RUNNER_DESCRIPTION,
|
||||
I18N_PAUSED_RUNNER_DESCRIPTION,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<runner-contacted-state-badge :runner="runner" size="sm" />
|
||||
<runner-paused-badge v-if="paused" size="sm" />
|
||||
</div>
|
||||
</template>
|
|
@ -1,11 +1,21 @@
|
|||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
|
||||
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
|
||||
import RunnerName from '../runner_name.vue';
|
||||
import RunnerTypeBadge from '../runner_type_badge.vue';
|
||||
|
||||
import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
TooltipOnTruncate,
|
||||
RunnerName,
|
||||
RunnerTypeBadge,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
runner: {
|
||||
|
@ -14,10 +24,19 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
runnerType() {
|
||||
return this.runner.runnerType;
|
||||
},
|
||||
locked() {
|
||||
return this.runner.locked;
|
||||
},
|
||||
description() {
|
||||
return this.runner.description;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
I18N_LOCKED_RUNNER_DESCRIPTION,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -26,6 +45,14 @@ export default {
|
|||
<slot :runner="runner" name="runner-name">
|
||||
<runner-name :runner="runner" />
|
||||
</slot>
|
||||
|
||||
<runner-type-badge :type="runnerType" size="sm" />
|
||||
<gl-icon
|
||||
v-if="locked"
|
||||
v-gl-tooltip
|
||||
:title="$options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION"
|
||||
name="lock"
|
||||
/>
|
||||
<tooltip-on-truncate class="gl-display-block" :title="description" truncate-target="child">
|
||||
<div class="gl-text-truncate">
|
||||
{{ description }}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
<script>
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import RunnerTypeBadge from '../runner_type_badge.vue';
|
||||
import RunnerStateLockedBadge from '../runner_state_locked_badge.vue';
|
||||
import RunnerStatePausedBadge from '../runner_state_paused_badge.vue';
|
||||
import { I18N_LOCKED_RUNNER_DESCRIPTION, I18N_PAUSED_RUNNER_DESCRIPTION } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RunnerTypeBadge,
|
||||
RunnerStateLockedBadge,
|
||||
RunnerStatePausedBadge,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
runner: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
runnerType() {
|
||||
return this.runner.runnerType;
|
||||
},
|
||||
locked() {
|
||||
return this.runner.locked;
|
||||
},
|
||||
paused() {
|
||||
return !this.runner.active;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
I18N_LOCKED_RUNNER_DESCRIPTION,
|
||||
I18N_PAUSED_RUNNER_DESCRIPTION,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<runner-type-badge :type="runnerType" size="sm" />
|
||||
<runner-state-locked-badge v-if="locked" size="sm" />
|
||||
<runner-state-paused-badge v-if="paused" size="sm" />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,69 @@
|
|||
<script>
|
||||
import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { getTimeago } from '~/lib/utils/datetime_utility';
|
||||
import {
|
||||
I18N_ONLINE_RUNNER_DESCRIPTION,
|
||||
I18N_OFFLINE_RUNNER_DESCRIPTION,
|
||||
I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
|
||||
STATUS_ONLINE,
|
||||
STATUS_OFFLINE,
|
||||
STATUS_NOT_CONNECTED,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
runner: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
contactedAtTimeAgo() {
|
||||
if (this.runner.contactedAt) {
|
||||
return getTimeago().format(this.runner.contactedAt);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
badge() {
|
||||
switch (this.runner.status) {
|
||||
case STATUS_ONLINE:
|
||||
return {
|
||||
variant: 'success',
|
||||
label: s__('Runners|online'),
|
||||
tooltip: sprintf(I18N_ONLINE_RUNNER_DESCRIPTION, {
|
||||
timeAgo: this.contactedAtTimeAgo,
|
||||
}),
|
||||
};
|
||||
case STATUS_OFFLINE:
|
||||
return {
|
||||
variant: 'muted',
|
||||
label: s__('Runners|offline'),
|
||||
tooltip: sprintf(I18N_OFFLINE_RUNNER_DESCRIPTION, {
|
||||
timeAgo: this.contactedAtTimeAgo,
|
||||
}),
|
||||
};
|
||||
case STATUS_NOT_CONNECTED:
|
||||
return {
|
||||
variant: 'muted',
|
||||
label: s__('Runners|not connected'),
|
||||
tooltip: I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" :variant="badge.variant" v-bind="$attrs">
|
||||
{{ badge.label }}
|
||||
</gl-badge>
|
||||
</template>
|
|
@ -5,7 +5,7 @@ import { __, s__ } from '~/locale';
|
|||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import RunnerActionsCell from './cells/runner_actions_cell.vue';
|
||||
import RunnerSummaryCell from './cells/runner_summary_cell.vue';
|
||||
import RunnerTypeCell from './cells/runner_type_cell.vue';
|
||||
import RunnerStatusCell from './cells/runner_status_cell.vue';
|
||||
import RunnerTags from './runner_tags.vue';
|
||||
|
||||
const tableField = ({ key, label = '', width = 10 }) => {
|
||||
|
@ -36,7 +36,7 @@ export default {
|
|||
RunnerActionsCell,
|
||||
RunnerSummaryCell,
|
||||
RunnerTags,
|
||||
RunnerTypeCell,
|
||||
RunnerStatusCell,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -63,8 +63,8 @@ export default {
|
|||
},
|
||||
},
|
||||
fields: [
|
||||
tableField({ key: 'type', label: __('Type/State') }),
|
||||
tableField({ key: 'summary', label: s__('Runners|Runner'), width: 30 }),
|
||||
tableField({ key: 'status', label: s__('Runners|Status') }),
|
||||
tableField({ key: 'summary', label: s__('Runners|Runner ID'), width: 30 }),
|
||||
tableField({ key: 'version', label: __('Version') }),
|
||||
tableField({ key: 'ipAddress', label: __('IP Address') }),
|
||||
tableField({ key: 'tagList', label: __('Tags'), width: 20 }),
|
||||
|
@ -88,8 +88,8 @@ export default {
|
|||
<gl-skeleton-loader v-for="i in 4" :key="i" />
|
||||
</template>
|
||||
|
||||
<template #cell(type)="{ item }">
|
||||
<runner-type-cell :runner="item" />
|
||||
<template #cell(status)="{ item }">
|
||||
<runner-status-cell :runner="item" />
|
||||
</template>
|
||||
|
||||
<template #cell(summary)="{ item, index }">
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
<script>
|
||||
import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
i18n: {
|
||||
I18N_LOCKED_RUNNER_DESCRIPTION,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-badge
|
||||
v-gl-tooltip="$options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION"
|
||||
variant="warning"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
{{ s__('Runners|locked') }}
|
||||
</gl-badge>
|
||||
</template>
|
|
@ -9,17 +9,14 @@ const ALERT_DATA = {
|
|||
message: s__(
|
||||
'Runners|This runner is available to all groups and projects in your GitLab instance.',
|
||||
),
|
||||
variant: 'success',
|
||||
anchor: 'shared-runners',
|
||||
},
|
||||
[GROUP_TYPE]: {
|
||||
message: s__('Runners|This runner is available to all projects and subgroups in a group.'),
|
||||
variant: 'success',
|
||||
anchor: 'group-runners',
|
||||
},
|
||||
[PROJECT_TYPE]: {
|
||||
message: s__('Runners|This runner is associated with one or more projects.'),
|
||||
variant: 'info',
|
||||
anchor: 'specific-runners',
|
||||
},
|
||||
};
|
||||
|
@ -50,7 +47,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-alert v-if="alert" :variant="alert.variant" :dismissible="false">
|
||||
<gl-alert v-if="alert" variant="info" :dismissible="false">
|
||||
{{ alert.message }}
|
||||
<gl-link :href="helpHref">{{ __('Learn more.') }}</gl-link>
|
||||
</gl-alert>
|
||||
|
|
|
@ -12,17 +12,14 @@ import {
|
|||
|
||||
const BADGE_DATA = {
|
||||
[INSTANCE_TYPE]: {
|
||||
variant: 'success',
|
||||
text: s__('Runners|shared'),
|
||||
tooltip: I18N_INSTANCE_RUNNER_DESCRIPTION,
|
||||
},
|
||||
[GROUP_TYPE]: {
|
||||
variant: 'success',
|
||||
text: s__('Runners|group'),
|
||||
tooltip: I18N_GROUP_RUNNER_DESCRIPTION,
|
||||
},
|
||||
[PROJECT_TYPE]: {
|
||||
variant: 'info',
|
||||
text: s__('Runners|specific'),
|
||||
tooltip: I18N_PROJECT_RUNNER_DESCRIPTION,
|
||||
},
|
||||
|
@ -53,7 +50,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" :variant="badge.variant" v-bind="$attrs">
|
||||
<gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" variant="info" v-bind="$attrs">
|
||||
{{ badge.text }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
|
|
@ -6,11 +6,24 @@ export const GROUP_RUNNER_COUNT_LIMIT = 1000;
|
|||
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
|
||||
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
|
||||
|
||||
// Type
|
||||
export const I18N_INSTANCE_RUNNER_DESCRIPTION = s__('Runners|Available to all projects');
|
||||
export const I18N_GROUP_RUNNER_DESCRIPTION = s__(
|
||||
'Runners|Available to all projects and subgroups in the group',
|
||||
);
|
||||
export const I18N_PROJECT_RUNNER_DESCRIPTION = s__('Runners|Associated with one or more projects');
|
||||
|
||||
// Status
|
||||
export const I18N_ONLINE_RUNNER_DESCRIPTION = s__(
|
||||
'Runners|Runner is online; last contact was %{timeAgo}',
|
||||
);
|
||||
export const I18N_OFFLINE_RUNNER_DESCRIPTION = s__(
|
||||
'Runners|No recent contact from this runner; last contact was %{timeAgo}',
|
||||
);
|
||||
export const I18N_NOT_CONNECTED_RUNNER_DESCRIPTION = s__(
|
||||
'Runners|This runner has never connected to this instance',
|
||||
);
|
||||
|
||||
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects');
|
||||
export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs');
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
#import "~/runner/graphql/runner_node.fragment.graphql"
|
||||
|
||||
# Mutation for updates within the runners list via action
|
||||
# buttons (play, pause, ...), loads attributes shown in the
|
||||
# runner list.
|
||||
|
||||
mutation runnerActionsUpdate($input: RunnerUpdateInput!) {
|
||||
runnerUpdate(input: $input) {
|
||||
runner {
|
||||
...RunnerNode
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -10,4 +10,5 @@ fragment RunnerNode on CiRunner {
|
|||
locked
|
||||
tagList
|
||||
contactedAt
|
||||
status
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
#import "ee_else_ce/runner/graphql/runner_details.fragment.graphql"
|
||||
|
||||
# Mutation for updates from the runner form, loads
|
||||
# attributes shown in the runner details.
|
||||
|
||||
mutation runnerUpdate($input: RunnerUpdateInput!) {
|
||||
runnerUpdate(input: $input) {
|
||||
runner {
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Issues
|
||||
class SetCrmContacts < Base
|
||||
graphql_name 'IssueSetCrmContacts'
|
||||
|
||||
argument :crm_contact_ids,
|
||||
[::Types::GlobalIDType[::CustomerRelations::Contact]],
|
||||
required: true,
|
||||
description: 'Customer relations contact IDs to set. Replaces existing contacts by default.'
|
||||
|
||||
argument :operation_mode,
|
||||
Types::MutationOperationModeEnum,
|
||||
required: false,
|
||||
description: 'Changes the operation mode. Defaults to REPLACE.'
|
||||
|
||||
def resolve(project_path:, iid:, crm_contact_ids:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
|
||||
issue = authorized_find!(project_path: project_path, iid: iid)
|
||||
project = issue.project
|
||||
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, project.group, default_enabled: :yaml)
|
||||
|
||||
crm_contact_ids = crm_contact_ids.compact.map do |crm_contact_id|
|
||||
raise Gitlab::Graphql::Errors::ArgumentError, "Contact #{crm_contact_id} is invalid." unless crm_contact_id.respond_to?(:model_id)
|
||||
|
||||
crm_contact_id.model_id.to_i
|
||||
end
|
||||
|
||||
attribute_name = case operation_mode
|
||||
when Types::MutationOperationModeEnum.enum[:append]
|
||||
:add_crm_contact_ids
|
||||
when Types::MutationOperationModeEnum.enum[:remove]
|
||||
:remove_crm_contact_ids
|
||||
else
|
||||
:crm_contact_ids
|
||||
end
|
||||
|
||||
response = ::Issues::SetCrmContactsService.new(project: project, current_user: current_user, params: { attribute_name => crm_contact_ids })
|
||||
.execute(issue)
|
||||
|
||||
{
|
||||
issue: issue,
|
||||
errors: response.errors
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -49,6 +49,7 @@ module Types
|
|||
mount_mutation Mutations::Environments::CanaryIngress::Update
|
||||
mount_mutation Mutations::Issues::Create
|
||||
mount_mutation Mutations::Issues::SetAssignees
|
||||
mount_mutation Mutations::Issues::SetCrmContacts
|
||||
mount_mutation Mutations::Issues::SetConfidential
|
||||
mount_mutation Mutations::Issues::SetLocked
|
||||
mount_mutation Mutations::Issues::SetDueDate
|
||||
|
|
|
@ -15,6 +15,6 @@ class CustomerRelations::IssueContact < ApplicationRecord
|
|||
return unless issue&.project&.namespace_id
|
||||
return if contact.group_id == issue.project.namespace_id
|
||||
|
||||
errors.add(:base, _('The contact does not belong to the same group as the issue.'))
|
||||
errors.add(:base, _('The contact does not belong to the same group as the issue'))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -194,13 +194,8 @@ class Group < Namespace
|
|||
def ids_with_disabled_email(groups)
|
||||
inner_groups = Group.where('id = namespaces_with_emails_disabled.id')
|
||||
|
||||
inner_ancestors = if Feature.enabled?(:linear_group_ancestor_scopes, default_enabled: :yaml)
|
||||
inner_groups.self_and_ancestors
|
||||
else
|
||||
Gitlab::ObjectHierarchy.new(inner_groups).base_and_ancestors
|
||||
end
|
||||
|
||||
inner_query = inner_ancestors
|
||||
inner_query = inner_groups
|
||||
.self_and_ancestors
|
||||
.where(emails_disabled: true)
|
||||
.select('1')
|
||||
.limit(1)
|
||||
|
|
|
@ -12,6 +12,9 @@ class IssuePolicy < IssuablePolicy
|
|||
@user && IssueCollection.new([@subject]).visible_to(@user).any?
|
||||
end
|
||||
|
||||
desc "User can read contacts belonging to the issue group"
|
||||
condition(:can_read_crm_contacts, scope: :subject) { @user.can?(:read_crm_contact, @subject.project.group) }
|
||||
|
||||
desc "Issue is confidential"
|
||||
condition(:confidential, scope: :subject) { @subject.confidential? }
|
||||
|
||||
|
@ -77,6 +80,10 @@ class IssuePolicy < IssuablePolicy
|
|||
rule { ~persisted & can?(:create_issue) }.policy do
|
||||
enable :set_confidentiality
|
||||
end
|
||||
|
||||
rule { can?(:set_issue_metadata) & can_read_crm_contacts }.policy do
|
||||
enable :set_issue_crm_contacts
|
||||
end
|
||||
end
|
||||
|
||||
IssuePolicy.prepend_mod_with('IssuePolicy')
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Issues
|
||||
class SetCrmContactsService < ::BaseProjectService
|
||||
attr_accessor :issue, :errors
|
||||
|
||||
MAX_ADDITIONAL_CONTACTS = 6
|
||||
|
||||
def execute(issue)
|
||||
@issue = issue
|
||||
@errors = []
|
||||
|
||||
return error_no_permissions unless allowed?
|
||||
return error_invalid_params unless valid_params?
|
||||
|
||||
determine_changes if params[:crm_contact_ids]
|
||||
|
||||
return error_too_many if too_many?
|
||||
|
||||
add_contacts if params[:add_crm_contact_ids]
|
||||
remove_contacts if params[:remove_crm_contact_ids]
|
||||
|
||||
if issue.valid?
|
||||
ServiceResponse.success(payload: issue)
|
||||
else
|
||||
# The default error isn't very helpful: "Issue customer relations contacts is invalid"
|
||||
issue.errors.delete(:issue_customer_relations_contacts)
|
||||
issue.errors.add(:issue_customer_relations_contacts, errors.to_sentence)
|
||||
ServiceResponse.error(payload: issue, message: issue.errors.full_messages)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def determine_changes
|
||||
existing_contact_ids = issue.issue_customer_relations_contacts.map(&:contact_id)
|
||||
params[:add_crm_contact_ids] = params[:crm_contact_ids] - existing_contact_ids
|
||||
params[:remove_crm_contact_ids] = existing_contact_ids - params[:crm_contact_ids]
|
||||
end
|
||||
|
||||
def add_contacts
|
||||
params[:add_crm_contact_ids].uniq.each do |contact_id|
|
||||
issue_contact = issue.issue_customer_relations_contacts.create(contact_id: contact_id)
|
||||
|
||||
unless issue_contact.persisted?
|
||||
# The validation ensures that the id exists and the user has permission
|
||||
errors << "#{contact_id}: The resource that you are attempting to access does not exist or you don't have permission to perform this action"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def remove_contacts
|
||||
issue.issue_customer_relations_contacts
|
||||
.where(contact_id: params[:remove_crm_contact_ids]) # rubocop: disable CodeReuse/ActiveRecord
|
||||
.delete_all
|
||||
end
|
||||
|
||||
def allowed?
|
||||
current_user&.can?(:set_issue_crm_contacts, issue)
|
||||
end
|
||||
|
||||
def valid_params?
|
||||
set_present? ^ add_or_remove_present?
|
||||
end
|
||||
|
||||
def set_present?
|
||||
params[:crm_contact_ids].present?
|
||||
end
|
||||
|
||||
def add_or_remove_present?
|
||||
params[:add_crm_contact_ids].present? || params[:remove_crm_contact_ids].present?
|
||||
end
|
||||
|
||||
def too_many?
|
||||
params[:add_crm_contact_ids] && params[:add_crm_contact_ids].length > MAX_ADDITIONAL_CONTACTS
|
||||
end
|
||||
|
||||
def error_no_permissions
|
||||
ServiceResponse.error(message: ['You have insufficient permissions to set customer relations contacts for this issue'])
|
||||
end
|
||||
|
||||
def error_invalid_params
|
||||
ServiceResponse.error(message: ['You cannot combine crm_contact_ids with add_crm_contact_ids or remove_crm_contact_ids'])
|
||||
end
|
||||
|
||||
def error_too_many
|
||||
ServiceResponse.error(payload: issue, message: ["You can only add up to #{MAX_ADDITIONAL_CONTACTS} contacts at one time"])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -146,8 +146,7 @@ module Projects
|
|||
|
||||
def caching_enabled?
|
||||
container_expiration_policy &&
|
||||
older_than.present? &&
|
||||
Feature.enabled?(:container_registry_expiration_policies_caching, @project)
|
||||
older_than.present?
|
||||
end
|
||||
|
||||
def throttling_enabled?
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
.mb-3
|
||||
%ul.nav-links
|
||||
= nav_link(path: 'product_analytics#index') do
|
||||
= link_to _('Events'), project_product_analytics_path(@project)
|
||||
= nav_link(path: 'product_analytics#graphs') do
|
||||
= link_to 'Graphs', graphs_project_product_analytics_path(@project)
|
||||
= nav_link(path: 'product_analytics#test') do
|
||||
= link_to _('Test'), test_project_product_analytics_path(@project)
|
||||
= nav_link(path: 'product_analytics#setup') do
|
||||
= link_to _('Setup'), setup_project_product_analytics_path(@project)
|
||||
= gl_tabs_nav({ class: 'mb-3'}) do
|
||||
= gl_tab_link_to _('Events'), project_product_analytics_path(@project)
|
||||
= gl_tab_link_to _('Graphs'), graphs_project_product_analytics_path(@project)
|
||||
= gl_tab_link_to _('Test'), test_project_product_analytics_path(@project)
|
||||
= gl_tab_link_to _('Setup'), setup_project_product_analytics_path(@project)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: linear_group_ancestor_scopes
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70495
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341115
|
||||
milestone: '14.4'
|
||||
name: api_v3_commits_skip_diff_files
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67647
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344617
|
||||
milestone: '14.5'
|
||||
type: development
|
||||
group: group::access
|
||||
group: group::integrations
|
||||
default_enabled: false
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: ci_new_artifact_file_reader
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46552
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273755
|
||||
milestone: '13.6'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: true
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: container_registry_expiration_policies_caching
|
||||
introduced_by_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340606
|
||||
milestone: '14.3'
|
||||
type: development
|
||||
group: group::package
|
||||
default_enabled: false
|
|
@ -9,3 +9,9 @@ ActsAsTaggableOn.tags_counter = false
|
|||
# validate that counter cache is disabled
|
||||
raise "Counter cache is not disabled" if
|
||||
ActsAsTaggableOn::Tagging.reflections["tag"].options[:counter_cache]
|
||||
|
||||
# Redirects retrieve_connection to use Ci::ApplicationRecord's connection
|
||||
[::ActsAsTaggableOn::Tag, ::ActsAsTaggableOn::Tagging].each do |model|
|
||||
model.connection_specification_name = Ci::ApplicationRecord.connection_specification_name
|
||||
model.singleton_class.delegate :connection, :sticking, to: '::Ci::ApplicationRecord'
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Currently we register validator only for `dev` or `test` environment
|
||||
Gitlab::Database::QueryAnalyzer.new.hook! if Gitlab.dev_or_test_env?
|
|
@ -2820,6 +2820,28 @@ Input type: `IssueSetConfidentialInput`
|
|||
| <a id="mutationissuesetconfidentialerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationissuesetconfidentialissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. |
|
||||
|
||||
### `Mutation.issueSetCrmContacts`
|
||||
|
||||
Input type: `IssueSetCrmContactsInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationissuesetcrmcontactsclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationissuesetcrmcontactscrmcontactids"></a>`crmContactIds` | [`[CustomerRelationsContactID!]!`](#customerrelationscontactid) | Customer relations contact IDs to set. Replaces existing contacts by default. |
|
||||
| <a id="mutationissuesetcrmcontactsiid"></a>`iid` | [`String!`](#string) | IID of the issue to mutate. |
|
||||
| <a id="mutationissuesetcrmcontactsoperationmode"></a>`operationMode` | [`MutationOperationMode`](#mutationoperationmode) | Changes the operation mode. Defaults to REPLACE. |
|
||||
| <a id="mutationissuesetcrmcontactsprojectpath"></a>`projectPath` | [`ID!`](#id) | Project the issue to mutate is in. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationissuesetcrmcontactsclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationissuesetcrmcontactserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationissuesetcrmcontactsissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. |
|
||||
|
||||
### `Mutation.issueSetDueDate`
|
||||
|
||||
Input type: `IssueSetDueDateInput`
|
||||
|
|
|
@ -175,13 +175,13 @@ is similar to [`rules:` defined in jobs](#rules).
|
|||
You can use the [`workflow:rules` templates](#workflowrules-templates) to import
|
||||
a preconfigured `workflow: rules` entry.
|
||||
|
||||
`workflow: rules` accepts these keywords:
|
||||
`workflow: rules` accepts some of the same keywords as [`rules`](#rules):
|
||||
|
||||
- [`if`](#rulesif): Check this rule to determine when to run a pipeline.
|
||||
- [`when`](#when): Specify what to do when the `if` rule evaluates to true.
|
||||
- To run a pipeline, set to `always`.
|
||||
- To prevent pipelines from running, set to `never`.
|
||||
- [`variables`](#workflowrulesvariables): If not defined, uses the [variables defined elsewhere](#variables).
|
||||
- [`rules: if`](#rulesif).
|
||||
- [`rules: changes`](#ruleschanges).
|
||||
- [`rules: exists`](#rulesexists).
|
||||
- [`when`](#when), can only be `always` or `never` when used with `workflow`.
|
||||
- [`variables`](#workflowrulesvariables).
|
||||
|
||||
When no rules evaluate to true, the pipeline does not run.
|
||||
|
||||
|
|
|
@ -59,8 +59,8 @@ module API
|
|||
expose :parents do |commit|
|
||||
commit.parent_ids.map { |id| { sha: id } }
|
||||
end
|
||||
expose :files do |commit|
|
||||
commit.diffs.diff_files.flat_map do |diff|
|
||||
expose :files do |_commit, options|
|
||||
options[:diff_files].flat_map do |diff|
|
||||
additions = diff.added_lines
|
||||
deletions = diff.removed_lines
|
||||
|
||||
|
|
|
@ -20,6 +20,9 @@ module API
|
|||
# Jira Server user agent format: Jira DVCS Connector/version
|
||||
JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo'
|
||||
|
||||
GITALY_TIMEOUT_CACHE_KEY = 'api:v3:Gitaly-timeout-cache-key'
|
||||
GITALY_TIMEOUT_CACHE_EXPIRY = 1.day
|
||||
|
||||
include PaginationParams
|
||||
|
||||
feature_category :integrations
|
||||
|
@ -93,6 +96,32 @@ module API
|
|||
notes.select { |n| n.readable_by?(current_user) }
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# Returns an empty Array instead of the Commit diff files for a period
|
||||
# of time after a Gitaly timeout, to mitigate frequent Gitaly timeouts
|
||||
# for some Commit diffs.
|
||||
def diff_files(commit)
|
||||
return commit.diffs.diff_files unless Feature.enabled?(:api_v3_commits_skip_diff_files, commit.project)
|
||||
|
||||
cache_key = [
|
||||
GITALY_TIMEOUT_CACHE_KEY,
|
||||
commit.project.id,
|
||||
commit.cache_key
|
||||
].join(':')
|
||||
|
||||
return [] if Rails.cache.read(cache_key).present?
|
||||
|
||||
begin
|
||||
commit.diffs.diff_files
|
||||
rescue GRPC::DeadlineExceeded => error
|
||||
# Gitaly fails to load diffs consistently for some commits. The other information
|
||||
# is still valuable for Jira. So we skip the loading and respond with a 200 excluding diffs
|
||||
# Remove this when https://gitlab.com/gitlab-org/gitaly/-/issues/3741 is fixed.
|
||||
Rails.cache.write(cache_key, 1, expires_in: GITALY_TIMEOUT_CACHE_EXPIRY)
|
||||
Gitlab::ErrorTracking.track_exception(error)
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resource :orgs do
|
||||
|
@ -228,10 +257,9 @@ module API
|
|||
user_project = find_project_with_access(params)
|
||||
|
||||
commit = user_project.commit(params[:sha])
|
||||
|
||||
not_found! 'Commit' unless commit
|
||||
|
||||
present commit, with: ::API::Github::Entities::RepoCommit
|
||||
present commit, with: ::API::Github::Entities::RepoCommit, diff_files: diff_files(commit)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -45,14 +45,6 @@ module Gitlab
|
|||
end
|
||||
|
||||
def read_zip_file!(file_path)
|
||||
if ::Feature.enabled?(:ci_new_artifact_file_reader, job.project, default_enabled: :yaml)
|
||||
read_with_new_artifact_file_reader(file_path)
|
||||
else
|
||||
read_with_legacy_artifact_file_reader(file_path)
|
||||
end
|
||||
end
|
||||
|
||||
def read_with_new_artifact_file_reader(file_path)
|
||||
job.artifacts_file.use_open_file do |file|
|
||||
zip_file = Zip::File.new(file, false, true)
|
||||
entry = zip_file.find_entry(file_path)
|
||||
|
@ -69,25 +61,6 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def read_with_legacy_artifact_file_reader(file_path)
|
||||
job.artifacts_file.use_file do |archive_path|
|
||||
Zip::File.open(archive_path) do |zip_file|
|
||||
entry = zip_file.find_entry(file_path)
|
||||
unless entry
|
||||
raise Error, "Path `#{file_path}` does not exist inside the `#{job.name}` artifacts archive!"
|
||||
end
|
||||
|
||||
if entry.name_is_directory?
|
||||
raise Error, "Path `#{file_path}` was expected to be a file but it was a directory!"
|
||||
end
|
||||
|
||||
zip_file.get_input_stream(entry) do |is|
|
||||
is.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def max_archive_size_in_mb
|
||||
ActiveSupport::NumberHelper.number_to_human_size(MAX_ARCHIVE_SIZE)
|
||||
end
|
||||
|
|
|
@ -8,17 +8,84 @@
|
|||
# - gitlab_shared - defines a set of tables that are found on all databases (data accessed is dependent on connection)
|
||||
# - gitlab_main / gitlab_ci - defines a set of tables that can only exist on a given database
|
||||
#
|
||||
# Tables for the purpose of tests should be prefixed with `_test_my_table_name`
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module GitlabSchema
|
||||
# These tables are deleted/renamed, but still referenced by migrations.
|
||||
# This is needed for now, but should be removed in the future
|
||||
DELETED_TABLES = {
|
||||
# main tables
|
||||
'alerts_service_data' => :gitlab_main,
|
||||
'analytics_devops_adoption_segment_selections' => :gitlab_main,
|
||||
'analytics_repository_file_commits' => :gitlab_main,
|
||||
'analytics_repository_file_edits' => :gitlab_main,
|
||||
'analytics_repository_files' => :gitlab_main,
|
||||
'audit_events_archived' => :gitlab_main,
|
||||
'backup_labels' => :gitlab_main,
|
||||
'clusters_applications_fluentd' => :gitlab_main,
|
||||
'forked_project_links' => :gitlab_main,
|
||||
'issue_milestones' => :gitlab_main,
|
||||
'merge_request_milestones' => :gitlab_main,
|
||||
'namespace_onboarding_actions' => :gitlab_main,
|
||||
'services' => :gitlab_main,
|
||||
'terraform_state_registry' => :gitlab_main,
|
||||
'tmp_fingerprint_sha256_migration' => :gitlab_main, # used by lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb
|
||||
'web_hook_logs_archived' => :gitlab_main,
|
||||
'vulnerability_export_registry' => :gitlab_main,
|
||||
'vulnerability_finding_fingerprints' => :gitlab_main,
|
||||
'vulnerability_export_verification_status' => :gitlab_main,
|
||||
|
||||
# CI tables
|
||||
'ci_build_trace_sections' => :gitlab_ci,
|
||||
'ci_build_trace_section_names' => :gitlab_ci,
|
||||
'ci_daily_report_results' => :gitlab_ci,
|
||||
'ci_test_cases' => :gitlab_ci,
|
||||
'ci_test_case_failures' => :gitlab_ci,
|
||||
|
||||
# leftovers from early implementation of partitioning
|
||||
'audit_events_part_5fc467ac26' => :gitlab_main,
|
||||
'web_hook_logs_part_0c5294f417' => :gitlab_main
|
||||
}.freeze
|
||||
|
||||
def self.table_schemas(tables)
|
||||
tables.map { |table| table_schema(table) }.to_set
|
||||
end
|
||||
|
||||
def self.table_schema(name)
|
||||
schema_name, table_name = name.split('.', 2) # Strip schema name like: `public.`
|
||||
|
||||
# Most of names do not have schemas, ensure that this is table
|
||||
unless table_name
|
||||
table_name = schema_name
|
||||
schema_name = nil
|
||||
end
|
||||
|
||||
# strip partition number of a form `loose_foreign_keys_deleted_records_1`
|
||||
table_name.gsub!(/_[0-9]+$/, '')
|
||||
|
||||
# Tables that are properly mapped
|
||||
if gitlab_schema = tables_to_schema[table_name]
|
||||
return gitlab_schema
|
||||
end
|
||||
|
||||
# Tables that are deleted, but we still need to reference them
|
||||
if gitlab_schema = DELETED_TABLES[table_name]
|
||||
return gitlab_schema
|
||||
end
|
||||
|
||||
# All tables from `information_schema.` are `:gitlab_shared`
|
||||
return :gitlab_shared if schema_name == 'information_schema'
|
||||
|
||||
# All tables that start with `_test_` are shared and ignored
|
||||
return :gitlab_shared if table_name.start_with?('_test_')
|
||||
|
||||
# All `pg_` tables are marked as `shared`
|
||||
return :gitlab_shared if table_name.start_with?('pg_')
|
||||
|
||||
# When undefined it's best to return a unique name so that we don't incorrectly assume that 2 undefined schemas belong on the same database
|
||||
tables_to_schema[name] || :"undefined_#{name}"
|
||||
:"undefined_#{table_name}"
|
||||
end
|
||||
|
||||
def self.tables_to_schema
|
||||
|
|
|
@ -77,6 +77,10 @@ module Gitlab
|
|||
(@primary_model || @model).connection_specification_name
|
||||
end
|
||||
|
||||
def primary_db_config
|
||||
(@primary_model || @model).connection_db_config
|
||||
end
|
||||
|
||||
def replica_db_config
|
||||
@model.connection_db_config
|
||||
end
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
# The purpose of this class is to implement a various query analyzers based on `pg_query`
|
||||
# And process them all via `Gitlab::Database::QueryAnalyzers::*`
|
||||
class QueryAnalyzer
|
||||
ANALYZERS = [].freeze
|
||||
|
||||
Parsed = Struct.new(
|
||||
:sql, :connection, :pg
|
||||
)
|
||||
|
||||
def hook!
|
||||
@subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |event|
|
||||
process_sql(event.payload[:sql], event.payload[:connection])
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_sql(sql, connection)
|
||||
analyzers = enabled_analyzers(connection)
|
||||
return unless analyzers.any?
|
||||
|
||||
parsed = parse(sql, connection)
|
||||
return unless parsed
|
||||
|
||||
analyzers.each do |analyzer|
|
||||
analyzer.analyze(parsed)
|
||||
rescue => e # rubocop:disable Style/RescueStandardError
|
||||
# We catch all standard errors to prevent validation errors to introduce fatal errors in production
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
||||
end
|
||||
end
|
||||
|
||||
def enabled_analyzers(connection)
|
||||
ANALYZERS.select do |analyzer|
|
||||
analyzer.enabled?(connection)
|
||||
rescue StandardError => e # rubocop:disable Style/RescueStandardError
|
||||
# We catch all standard errors to prevent validation errors to introduce fatal errors in production
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
||||
end
|
||||
end
|
||||
|
||||
def parse(sql, connection)
|
||||
parsed = PgQuery.parse(sql)
|
||||
return unless parsed
|
||||
|
||||
normalized = PgQuery.normalize(sql)
|
||||
Parsed.new(normalized, connection, parsed)
|
||||
rescue PgQuery::ParseError => e
|
||||
# Ignore PgQuery parse errors (due to depth limit or other reasons)
|
||||
Gitlab::ErrorTracking.track_exception(e)
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module QueryAnalyzers
|
||||
class Base
|
||||
def self.enabled?(connection)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def self.analyze(parsed)
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16109,6 +16109,9 @@ msgstr ""
|
|||
msgid "GraphViewType|Stage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Graphs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gravatar"
|
||||
msgstr ""
|
||||
|
||||
|
@ -29691,6 +29694,9 @@ msgstr ""
|
|||
msgid "Runners|New runner, has not connected yet"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|No recent contact from this runner; last contact was %{timeAgo}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Not available to run jobs"
|
||||
msgstr ""
|
||||
|
||||
|
@ -29742,6 +29748,9 @@ msgstr ""
|
|||
msgid "Runners|Runner #%{runner_id}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Runner ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Runner assigned to project."
|
||||
msgstr ""
|
||||
|
||||
|
@ -29751,6 +29760,9 @@ msgstr ""
|
|||
msgid "Runners|Runner is online, last contact was %{runner_contact} ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Runner is online; last contact was %{timeAgo}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Runner is paused, last contact was %{runner_contact} ago"
|
||||
msgstr ""
|
||||
|
||||
|
@ -29781,12 +29793,18 @@ msgstr ""
|
|||
msgid "Runners|Something went wrong while fetching the tags suggestions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Stop the runner from accepting new jobs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|This runner has never connected to this instance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|This runner is associated with one or more projects."
|
||||
msgstr ""
|
||||
|
||||
|
@ -29853,6 +29871,15 @@ msgstr ""
|
|||
msgid "Runners|locked"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|not connected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|offline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|online"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|paused"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34136,7 +34163,7 @@ msgstr ""
|
|||
msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
|
||||
msgstr ""
|
||||
|
||||
msgid "The contact does not belong to the same group as the issue."
|
||||
msgid "The contact does not belong to the same group as the issue"
|
||||
msgstr ""
|
||||
|
||||
msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Page
|
||||
module Group
|
||||
|
@ -20,22 +19,33 @@ module QA
|
|||
|
||||
def set_allow_duplicates_disabled
|
||||
expand_content :package_registry_settings_content do
|
||||
click_element(:allow_duplicates_toggle) if duplicates_enabled?
|
||||
click_on_allow_duplicates_button if duplicates_enabled?
|
||||
end
|
||||
end
|
||||
|
||||
def set_allow_duplicates_enabled
|
||||
expand_content :package_registry_settings_content do
|
||||
click_element(:allow_duplicates_toggle) if duplicates_disabled?
|
||||
click_on_allow_duplicates_button unless duplicates_enabled?
|
||||
end
|
||||
end
|
||||
|
||||
def click_on_allow_duplicates_button
|
||||
with_allow_duplicates_button do |button|
|
||||
button.click
|
||||
end
|
||||
end
|
||||
|
||||
def duplicates_enabled?
|
||||
has_element?(:allow_duplicates_label, text: 'Allow duplicates')
|
||||
with_allow_duplicates_button do |button|
|
||||
button[:class].include?('is-checked')
|
||||
end
|
||||
end
|
||||
|
||||
def duplicates_disabled?
|
||||
has_element?(:allow_duplicates_label, text: 'Do not allow duplicates')
|
||||
def with_allow_duplicates_button
|
||||
within_element :allow_duplicates_toggle do
|
||||
toggle = find('button.gl-toggle')
|
||||
yield(toggle)
|
||||
end
|
||||
end
|
||||
|
||||
def has_dependency_proxy_enabled?
|
||||
|
|
|
@ -8,12 +8,11 @@ import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue'
|
|||
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
|
||||
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
|
||||
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
|
||||
import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
|
||||
import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
import { runnersData, runnerData } from '../../mock_data';
|
||||
import { runnersData } from '../../mock_data';
|
||||
|
||||
const mockRunner = runnersData.data.runners.nodes[0];
|
||||
const mockRunnerDetails = runnerData.data.runner;
|
||||
|
||||
const getRunnersQueryName = getRunnersQuery.definitions[0].name.value;
|
||||
const getGroupRunnersQueryName = getGroupRunnersQuery.definitions[0].name.value;
|
||||
|
@ -27,7 +26,7 @@ jest.mock('~/runner/sentry_utils');
|
|||
describe('RunnerTypeCell', () => {
|
||||
let wrapper;
|
||||
const runnerDeleteMutationHandler = jest.fn();
|
||||
const runnerUpdateMutationHandler = jest.fn();
|
||||
const runnerActionsUpdateMutationHandler = jest.fn();
|
||||
|
||||
const findEditBtn = () => wrapper.findByTestId('edit-runner');
|
||||
const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
|
||||
|
@ -46,7 +45,7 @@ describe('RunnerTypeCell', () => {
|
|||
localVue,
|
||||
apolloProvider: createMockApollo([
|
||||
[runnerDeleteMutation, runnerDeleteMutationHandler],
|
||||
[runnerUpdateMutation, runnerUpdateMutationHandler],
|
||||
[runnerActionsUpdateMutation, runnerActionsUpdateMutationHandler],
|
||||
]),
|
||||
...options,
|
||||
}),
|
||||
|
@ -62,10 +61,10 @@ describe('RunnerTypeCell', () => {
|
|||
},
|
||||
});
|
||||
|
||||
runnerUpdateMutationHandler.mockResolvedValue({
|
||||
runnerActionsUpdateMutationHandler.mockResolvedValue({
|
||||
data: {
|
||||
runnerUpdate: {
|
||||
runner: mockRunnerDetails,
|
||||
runner: mockRunner,
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
|
@ -74,7 +73,7 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
afterEach(() => {
|
||||
runnerDeleteMutationHandler.mockReset();
|
||||
runnerUpdateMutationHandler.mockReset();
|
||||
runnerActionsUpdateMutationHandler.mockReset();
|
||||
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
@ -116,12 +115,12 @@ describe('RunnerTypeCell', () => {
|
|||
|
||||
describe(`When clicking on the ${icon} button`, () => {
|
||||
it(`The apollo mutation to set active to ${newActiveValue} is called`, async () => {
|
||||
expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(0);
|
||||
expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(0);
|
||||
|
||||
await findToggleActiveBtn().vm.$emit('click');
|
||||
|
||||
expect(runnerUpdateMutationHandler).toHaveBeenCalledTimes(1);
|
||||
expect(runnerUpdateMutationHandler).toHaveBeenCalledWith({
|
||||
expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(1);
|
||||
expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: mockRunner.id,
|
||||
active: newActiveValue,
|
||||
|
@ -145,7 +144,7 @@ describe('RunnerTypeCell', () => {
|
|||
const mockErrorMsg = 'Update error!';
|
||||
|
||||
beforeEach(async () => {
|
||||
runnerUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
|
||||
runnerActionsUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
|
||||
|
||||
await findToggleActiveBtn().vm.$emit('click');
|
||||
});
|
||||
|
@ -167,10 +166,10 @@ describe('RunnerTypeCell', () => {
|
|||
const mockErrorMsg2 = 'User not allowed!';
|
||||
|
||||
beforeEach(async () => {
|
||||
runnerUpdateMutationHandler.mockResolvedValue({
|
||||
runnerActionsUpdateMutationHandler.mockResolvedValue({
|
||||
data: {
|
||||
runnerUpdate: {
|
||||
runner: runnerData.data.runner,
|
||||
runner: mockRunner,
|
||||
errors: [mockErrorMsg, mockErrorMsg2],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import { GlBadge } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue';
|
||||
import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE } from '~/runner/constants';
|
||||
|
||||
describe('RunnerTypeCell', () => {
|
||||
let wrapper;
|
||||
|
||||
const findBadgeAt = (i) => wrapper.findAllComponents(GlBadge).at(i);
|
||||
|
||||
const createComponent = ({ runner = {} } = {}) => {
|
||||
wrapper = mount(RunnerStatusCell, {
|
||||
propsData: {
|
||||
runner: {
|
||||
runnerType: INSTANCE_TYPE,
|
||||
active: true,
|
||||
status: STATUS_ONLINE,
|
||||
...runner,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('Displays online status', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.text()).toMatchInterpolatedText('online');
|
||||
expect(findBadgeAt(0).text()).toBe('online');
|
||||
});
|
||||
|
||||
it('Displays offline status', () => {
|
||||
createComponent({
|
||||
runner: {
|
||||
status: STATUS_OFFLINE,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toMatchInterpolatedText('offline');
|
||||
expect(findBadgeAt(0).text()).toBe('offline');
|
||||
});
|
||||
|
||||
it('Displays paused status', () => {
|
||||
createComponent({
|
||||
runner: {
|
||||
active: false,
|
||||
status: STATUS_ONLINE,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toMatchInterpolatedText('online paused');
|
||||
|
||||
expect(findBadgeAt(0).text()).toBe('online');
|
||||
expect(findBadgeAt(1).text()).toBe('paused');
|
||||
});
|
||||
|
||||
it('Is empty when data is missing', () => {
|
||||
createComponent({
|
||||
runner: {
|
||||
status: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toBe('');
|
||||
});
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue';
|
||||
import { INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants';
|
||||
|
||||
const mockId = '1';
|
||||
const mockShortSha = '2P6oDVDm';
|
||||
|
@ -8,13 +9,17 @@ const mockDescription = 'runner-1';
|
|||
describe('RunnerTypeCell', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (options) => {
|
||||
wrapper = mount(RunnerSummaryCell, {
|
||||
const findLockIcon = () => wrapper.findByTestId('lock-icon');
|
||||
|
||||
const createComponent = (runner, options) => {
|
||||
wrapper = mountExtended(RunnerSummaryCell, {
|
||||
propsData: {
|
||||
runner: {
|
||||
id: `gid://gitlab/Ci::Runner/${mockId}`,
|
||||
shortSha: mockShortSha,
|
||||
description: mockDescription,
|
||||
runnerType: INSTANCE_TYPE,
|
||||
...runner,
|
||||
},
|
||||
},
|
||||
...options,
|
||||
|
@ -33,6 +38,23 @@ describe('RunnerTypeCell', () => {
|
|||
expect(wrapper.text()).toContain(`#${mockId} (${mockShortSha})`);
|
||||
});
|
||||
|
||||
it('Displays the runner type', () => {
|
||||
expect(wrapper.text()).toContain('shared');
|
||||
});
|
||||
|
||||
it('Does not display the locked icon', () => {
|
||||
expect(findLockIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('Displays the locked icon for locked runners', () => {
|
||||
createComponent({
|
||||
runnerType: PROJECT_TYPE,
|
||||
locked: true,
|
||||
});
|
||||
|
||||
expect(findLockIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('Displays the runner description', () => {
|
||||
expect(wrapper.text()).toContain(mockDescription);
|
||||
});
|
||||
|
@ -40,11 +62,14 @@ describe('RunnerTypeCell', () => {
|
|||
it('Displays a custom slot', () => {
|
||||
const slotContent = 'My custom runner summary';
|
||||
|
||||
createComponent({
|
||||
createComponent(
|
||||
{},
|
||||
{
|
||||
slots: {
|
||||
'runner-name': slotContent,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toContain(slotContent);
|
||||
});
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import { GlBadge } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import RunnerTypeCell from '~/runner/components/cells/runner_type_cell.vue';
|
||||
import { INSTANCE_TYPE } from '~/runner/constants';
|
||||
|
||||
describe('RunnerTypeCell', () => {
|
||||
let wrapper;
|
||||
|
||||
const findBadges = () => wrapper.findAllComponents(GlBadge);
|
||||
|
||||
const createComponent = ({ runner = {} } = {}) => {
|
||||
wrapper = mount(RunnerTypeCell, {
|
||||
propsData: {
|
||||
runner: {
|
||||
runnerType: INSTANCE_TYPE,
|
||||
active: true,
|
||||
locked: false,
|
||||
...runner,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('Displays the runner type', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findBadges()).toHaveLength(1);
|
||||
expect(findBadges().at(0).text()).toBe('shared');
|
||||
});
|
||||
|
||||
it('Displays locked and paused states', () => {
|
||||
createComponent({
|
||||
runner: {
|
||||
active: false,
|
||||
locked: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findBadges()).toHaveLength(3);
|
||||
expect(findBadges().at(0).text()).toBe('shared');
|
||||
expect(findBadges().at(1).text()).toBe('locked');
|
||||
expect(findBadges().at(2).text()).toBe('paused');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
import { GlBadge } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import RunnerContactedStateBadge from '~/runner/components/runner_contacted_state_badge.vue';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_NOT_CONNECTED } from '~/runner/constants';
|
||||
|
||||
describe('RunnerTypeBadge', () => {
|
||||
let wrapper;
|
||||
|
||||
const findBadge = () => wrapper.findComponent(GlBadge);
|
||||
const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip');
|
||||
|
||||
const createComponent = ({ runner = {} } = {}) => {
|
||||
wrapper = shallowMount(RunnerContactedStateBadge, {
|
||||
propsData: {
|
||||
runner: {
|
||||
contactedAt: '2021-01-01T00:00:00Z',
|
||||
status: STATUS_ONLINE,
|
||||
...runner,
|
||||
},
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers('modern');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useFakeTimers('legacy');
|
||||
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders online state', () => {
|
||||
jest.setSystemTime(new Date('2021-01-01T00:01:00Z'));
|
||||
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.text()).toBe('online');
|
||||
expect(findBadge().props('variant')).toBe('success');
|
||||
expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago');
|
||||
});
|
||||
|
||||
it('renders offline state', () => {
|
||||
jest.setSystemTime(new Date('2021-01-02T00:00:00Z'));
|
||||
|
||||
createComponent({
|
||||
runner: {
|
||||
status: STATUS_OFFLINE,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toBe('offline');
|
||||
expect(findBadge().props('variant')).toBe('muted');
|
||||
expect(getTooltip().value).toBe(
|
||||
'No recent contact from this runner; last contact was 1 day ago',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders not connected state', () => {
|
||||
createComponent({
|
||||
runner: {
|
||||
contactedAt: null,
|
||||
status: STATUS_NOT_CONNECTED,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toBe('not connected');
|
||||
expect(findBadge().props('variant')).toBe('muted');
|
||||
expect(getTooltip().value).toMatch('This runner has never connected');
|
||||
});
|
||||
|
||||
it('does not fail when data is missing', () => {
|
||||
createComponent({
|
||||
runner: {
|
||||
status: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toBe('');
|
||||
});
|
||||
});
|
|
@ -42,8 +42,8 @@ describe('RunnerList', () => {
|
|||
const headerLabels = findHeaders().wrappers.map((w) => w.text());
|
||||
|
||||
expect(headerLabels).toEqual([
|
||||
'Type/State',
|
||||
'Runner',
|
||||
'Status',
|
||||
'Runner ID',
|
||||
'Version',
|
||||
'IP Address',
|
||||
'Tags',
|
||||
|
@ -62,7 +62,7 @@ describe('RunnerList', () => {
|
|||
const { id, description, version, ipAddress, shortSha } = mockRunners[0];
|
||||
|
||||
// Badges
|
||||
expect(findCell({ fieldKey: 'type' }).text()).toMatchInterpolatedText('specific paused');
|
||||
expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('not connected paused');
|
||||
|
||||
// Runner summary
|
||||
expect(findCell({ fieldKey: 'summary' }).text()).toContain(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { GlBadge } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import RunnerStatePausedBadge from '~/runner/components/runner_state_paused_badge.vue';
|
||||
import RunnerStatePausedBadge from '~/runner/components/runner_paused_badge.vue';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
|
||||
describe('RunnerTypeBadge', () => {
|
|
@ -1,45 +0,0 @@
|
|||
import { GlBadge } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import RunnerStateLockedBadge from '~/runner/components/runner_state_locked_badge.vue';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
|
||||
describe('RunnerTypeBadge', () => {
|
||||
let wrapper;
|
||||
|
||||
const findBadge = () => wrapper.findComponent(GlBadge);
|
||||
const getTooltip = () => getBinding(findBadge().element, 'gl-tooltip');
|
||||
|
||||
const createComponent = ({ props = {} } = {}) => {
|
||||
wrapper = shallowMount(RunnerStateLockedBadge, {
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders locked state', () => {
|
||||
expect(wrapper.text()).toBe('locked');
|
||||
expect(findBadge().props('variant')).toBe('warning');
|
||||
});
|
||||
|
||||
it('renders tooltip', () => {
|
||||
expect(getTooltip().value).toBeDefined();
|
||||
});
|
||||
|
||||
it('passes arbitrary attributes to the badge', () => {
|
||||
createComponent({ props: { size: 'sm' } });
|
||||
|
||||
expect(findBadge().props('size')).toBe('sm');
|
||||
});
|
||||
});
|
|
@ -23,11 +23,11 @@ describe('RunnerTypeAlert', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
type | exampleText | anchor | variant
|
||||
${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'} | ${'success'}
|
||||
${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'} | ${'success'}
|
||||
${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'} | ${'info'}
|
||||
`('When it is an $type level runner', ({ type, exampleText, anchor, variant }) => {
|
||||
type | exampleText | anchor
|
||||
${INSTANCE_TYPE} | ${'This runner is available to all groups and projects'} | ${'#shared-runners'}
|
||||
${GROUP_TYPE} | ${'This runner is available to all projects and subgroups in a group'} | ${'#group-runners'}
|
||||
${PROJECT_TYPE} | ${'This runner is associated with one or more projects'} | ${'#specific-runners'}
|
||||
`('When it is an $type level runner', ({ type, exampleText, anchor }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { type } });
|
||||
});
|
||||
|
@ -36,8 +36,8 @@ describe('RunnerTypeAlert', () => {
|
|||
expect(wrapper.text()).toMatch(exampleText);
|
||||
});
|
||||
|
||||
it(`Shows a ${variant} variant`, () => {
|
||||
expect(findAlert().props('variant')).toBe(variant);
|
||||
it(`Shows an "info" variant`, () => {
|
||||
expect(findAlert().props('variant')).toBe('info');
|
||||
});
|
||||
|
||||
it(`Links to anchor "${anchor}"`, () => {
|
||||
|
|
|
@ -26,18 +26,18 @@ describe('RunnerTypeBadge', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
type | text | variant
|
||||
${INSTANCE_TYPE} | ${'shared'} | ${'success'}
|
||||
${GROUP_TYPE} | ${'group'} | ${'success'}
|
||||
${PROJECT_TYPE} | ${'specific'} | ${'info'}
|
||||
`('displays $type runner', ({ type, text, variant }) => {
|
||||
type | text
|
||||
${INSTANCE_TYPE} | ${'shared'}
|
||||
${GROUP_TYPE} | ${'group'}
|
||||
${PROJECT_TYPE} | ${'specific'}
|
||||
`('displays $type runner', ({ type, text }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { type } });
|
||||
});
|
||||
|
||||
it(`as "${text}" with a ${variant} variant`, () => {
|
||||
it(`as "${text}" with an "info" variant`, () => {
|
||||
expect(findBadge().text()).toBe(text);
|
||||
expect(findBadge().props('variant')).toBe(variant);
|
||||
expect(findBadge().props('variant')).toBe('info');
|
||||
});
|
||||
|
||||
it('with a tooltip', () => {
|
||||
|
|
|
@ -18,17 +18,6 @@ RSpec.describe Gitlab::Ci::ArtifactFileReader do
|
|||
expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom')
|
||||
end
|
||||
|
||||
context 'when FF ci_new_artifact_file_reader is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_new_artifact_file_reader: false)
|
||||
end
|
||||
|
||||
it 'returns the content at the path' do
|
||||
is_expected.to be_present
|
||||
expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when path does not exist' do
|
||||
let(:path) { 'file/does/not/exist.txt' }
|
||||
let(:expected_error) do
|
||||
|
|
|
@ -35,4 +35,24 @@ RSpec.describe Gitlab::Database::GitlabSchema do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.table_schema' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:name, :classification) do
|
||||
'ci_builds' | :gitlab_ci
|
||||
'my_schema.ci_builds' | :gitlab_ci
|
||||
'information_schema.columns' | :gitlab_shared
|
||||
'audit_events_part_5fc467ac26' | :gitlab_main
|
||||
'_test_my_table' | :gitlab_shared
|
||||
'pg_attribute' | :gitlab_shared
|
||||
'my_other_table' | :undefined_my_other_table
|
||||
end
|
||||
|
||||
with_them do
|
||||
subject { described_class.table_schema(name) }
|
||||
|
||||
it { is_expected.to eq(classification) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::QueryAnalyzer do
|
||||
let(:analyzer) { double(:query_analyzer) }
|
||||
|
||||
before do
|
||||
stub_const('Gitlab::Database::QueryAnalyzer::ANALYZERS', [analyzer])
|
||||
end
|
||||
|
||||
context 'the hook is enabled by default in specs' do
|
||||
it 'does process queries and gets normalized SQL' do
|
||||
expect(analyzer).to receive(:enabled?).and_return(true)
|
||||
expect(analyzer).to receive(:analyze) do |parsed|
|
||||
expect(parsed.sql).to include("SELECT $1 FROM projects")
|
||||
expect(parsed.pg.tables).to eq(%w[projects])
|
||||
end
|
||||
|
||||
Project.connection.execute("SELECT 1 FROM projects")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#process_sql' do
|
||||
it 'does not analyze query if not enabled' do
|
||||
expect(analyzer).to receive(:enabled?).and_return(false)
|
||||
expect(analyzer).not_to receive(:analyze)
|
||||
|
||||
process_sql("SELECT 1 FROM projects")
|
||||
end
|
||||
|
||||
it 'does analyze query if enabled' do
|
||||
expect(analyzer).to receive(:enabled?).and_return(true)
|
||||
expect(analyzer).to receive(:analyze) do |parsed|
|
||||
expect(parsed.sql).to eq("SELECT $1 FROM projects")
|
||||
expect(parsed.pg.tables).to eq(%w[projects])
|
||||
end
|
||||
|
||||
process_sql("SELECT 1 FROM projects")
|
||||
end
|
||||
|
||||
it 'does track exception if query cannot be parsed' do
|
||||
expect(analyzer).to receive(:enabled?).and_return(true)
|
||||
expect(analyzer).not_to receive(:analyze)
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception)
|
||||
|
||||
expect { process_sql("invalid query") }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'does track exception if analyzer raises exception on enabled?' do
|
||||
expect(analyzer).to receive(:enabled?).and_raise('exception')
|
||||
expect(analyzer).not_to receive(:analyze)
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
|
||||
expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'does track exception if analyzer raises exception on analyze' do
|
||||
expect(analyzer).to receive(:enabled?).and_return(true)
|
||||
expect(analyzer).to receive(:analyze).and_raise('exception')
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||
|
||||
expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error
|
||||
end
|
||||
|
||||
def process_sql(sql)
|
||||
ApplicationRecord.connection.load_balancer.read_write do |connection|
|
||||
described_class.new.send(:process_sql, sql, connection)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ActsAsTaggableOn::Tag do
|
||||
it 'has the same connection as Ci::ApplicationRecord' do
|
||||
query = 'select current_database()'
|
||||
|
||||
expect(described_class.connection.execute(query).first).to eq(Ci::ApplicationRecord.connection.execute(query).first)
|
||||
expect(described_class.retrieve_connection.execute(query).first).to eq(Ci::ApplicationRecord.retrieve_connection.execute(query).first)
|
||||
end
|
||||
|
||||
it 'has the same sticking as Ci::ApplicationRecord' do
|
||||
expect(described_class.sticking).to eq(Ci::ApplicationRecord.sticking)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ActsAsTaggableOn::Tagging do
|
||||
it 'has the same connection as Ci::ApplicationRecord' do
|
||||
query = 'select current_database()'
|
||||
|
||||
expect(described_class.connection.execute(query).first).to eq(Ci::ApplicationRecord.connection.execute(query).first)
|
||||
expect(described_class.retrieve_connection.execute(query).first).to eq(Ci::ApplicationRecord.retrieve_connection.execute(query).first)
|
||||
end
|
||||
|
||||
it 'has the same sticking as Ci::ApplicationRecord' do
|
||||
expect(described_class.sticking).to eq(Ci::ApplicationRecord.sticking)
|
||||
end
|
||||
end
|
|
@ -2648,14 +2648,6 @@ RSpec.describe Group do
|
|||
end
|
||||
|
||||
it_behaves_like 'returns namespaces with disabled email'
|
||||
|
||||
context 'when feature flag :linear_group_ancestor_scopes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(linear_group_ancestor_scopes: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'returns namespaces with disabled email'
|
||||
end
|
||||
end
|
||||
|
||||
describe '.timelogs' do
|
||||
|
|
|
@ -284,7 +284,7 @@ RSpec.describe Namespace do
|
|||
end
|
||||
end
|
||||
|
||||
context 'creating a default Namespace' do
|
||||
context 'creating a Namespace with nil type' do
|
||||
let(:namespace_type) { nil }
|
||||
|
||||
it 'is the correct type of namespace' do
|
||||
|
@ -295,7 +295,7 @@ RSpec.describe Namespace do
|
|||
end
|
||||
|
||||
context 'creating an unknown Namespace type' do
|
||||
let(:namespace_type) { 'One' }
|
||||
let(:namespace_type) { 'nonsense' }
|
||||
|
||||
it 'creates a default Namespace' do
|
||||
expect(Namespace.find(namespace.id)).to be_a(Namespace)
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Setting issues crm contacts' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:contacts) { create_list(:contact, 4, group: group) }
|
||||
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
let(:operation_mode) { Types::MutationOperationModeEnum.default_mode }
|
||||
let(:crm_contact_ids) { [global_id_of(contacts[1]), global_id_of(contacts[2])] }
|
||||
let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
|
||||
|
||||
let(:mutation) do
|
||||
variables = {
|
||||
project_path: issue.project.full_path,
|
||||
iid: issue.iid.to_s,
|
||||
operation_mode: operation_mode,
|
||||
crm_contact_ids: crm_contact_ids
|
||||
}
|
||||
|
||||
graphql_mutation(:issue_set_crm_contacts, variables,
|
||||
<<-QL.strip_heredoc
|
||||
clientMutationId
|
||||
errors
|
||||
issue {
|
||||
customerRelationsContacts {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
QL
|
||||
)
|
||||
end
|
||||
|
||||
def mutation_response
|
||||
graphql_mutation_response(:issue_set_crm_contacts)
|
||||
end
|
||||
|
||||
before do
|
||||
create(:issue_customer_relations_contact, issue: issue, contact: contacts[0])
|
||||
create(:issue_customer_relations_contact, issue: issue, contact: contacts[1])
|
||||
end
|
||||
|
||||
context 'when the user has no permission' do
|
||||
it 'returns expected error' do
|
||||
error = Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_errors).to include(a_hash_including('message' => error))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user has permission' do
|
||||
before do
|
||||
group.add_reporter(user)
|
||||
end
|
||||
|
||||
context 'when the feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(customer_relations: false)
|
||||
end
|
||||
|
||||
it 'raises expected error' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_errors).to include(a_hash_including('message' => 'Feature disabled'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'replace' do
|
||||
it 'updates the issue with correct contacts' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
|
||||
.to match_array([global_id_of(contacts[1]), global_id_of(contacts[2])])
|
||||
end
|
||||
end
|
||||
|
||||
context 'append' do
|
||||
let(:crm_contact_ids) { [global_id_of(contacts[3])] }
|
||||
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
|
||||
.to match_array([global_id_of(contacts[0]), global_id_of(contacts[1]), global_id_of(contacts[3])])
|
||||
end
|
||||
end
|
||||
|
||||
context 'remove' do
|
||||
let(:crm_contact_ids) { [global_id_of(contacts[0])] }
|
||||
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id))
|
||||
.to match_array([global_id_of(contacts[1])])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the contact does not exist' do
|
||||
let(:crm_contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
|
||||
|
||||
it 'returns expected error' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_data_at(:issue_set_crm_contacts, :errors))
|
||||
.to match_array(["Issue customer relations contacts #{non_existing_record_id}: #{does_not_exist_or_no_permission}"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the contact belongs to a different group' do
|
||||
let(:group2) { create(:group) }
|
||||
let(:contact) { create(:contact, group: group2) }
|
||||
let(:crm_contact_ids) { [global_id_of(contact)] }
|
||||
|
||||
before do
|
||||
group2.add_reporter(user)
|
||||
end
|
||||
|
||||
it 'returns expected error' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_data_at(:issue_set_crm_contacts, :errors))
|
||||
.to match_array(["Issue customer relations contacts #{contact.id}: #{does_not_exist_or_no_permission}"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when attempting to add more than 6' do
|
||||
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] }
|
||||
let(:gid) { global_id_of(contacts[0]) }
|
||||
let(:crm_contact_ids) { [gid, gid, gid, gid, gid, gid, gid] }
|
||||
|
||||
it 'returns expected error' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_data_at(:issue_set_crm_contacts, :errors))
|
||||
.to match_array(["You can only add up to 6 contacts at one time"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when trying to remove non-existent contact' do
|
||||
let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] }
|
||||
let(:crm_contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
|
||||
|
||||
it 'raises expected error' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_data_at(:issue_set_crm_contacts, :errors)).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ RSpec.describe API::V3::Github do
|
|||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:unauthorized_user) { create(:user) }
|
||||
let_it_be(:admin) { create(:user, :admin) }
|
||||
let_it_be(:project) { create(:project, :repository, creator: user) }
|
||||
let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
|
@ -506,11 +506,18 @@ RSpec.describe API::V3::Github do
|
|||
|
||||
describe 'GET /repos/:namespace/:project/commits/:sha' do
|
||||
let(:commit) { project.repository.commit }
|
||||
let(:commit_id) { commit.id }
|
||||
|
||||
def call_api(commit_id: commit.id)
|
||||
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
|
||||
end
|
||||
|
||||
def response_diff_files(response)
|
||||
Gitlab::Json.parse(response.body)['files']
|
||||
end
|
||||
|
||||
context 'authenticated' do
|
||||
it 'returns commit with github format' do
|
||||
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
|
||||
it 'returns commit with github format', :aggregate_failures do
|
||||
call_api
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to match_response_schema('entities/github/commit')
|
||||
|
@ -519,36 +526,130 @@ RSpec.describe API::V3::Github do
|
|||
it 'returns 200 when project path include a dot' do
|
||||
project.update!(path: 'foo.bar')
|
||||
|
||||
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
|
||||
call_api
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
context 'when namespace path includes a dot' do
|
||||
let(:group) { create(:group, path: 'foo.bar') }
|
||||
let(:project) { create(:project, :repository, group: group) }
|
||||
|
||||
it 'returns 200 when namespace path include a dot' do
|
||||
group = create(:group, path: 'foo.bar')
|
||||
project = create(:project, :repository, group: group)
|
||||
project.add_reporter(user)
|
||||
|
||||
jira_get v3_api("/repos/#{group.path}/#{project.path}/commits/#{commit_id}", user)
|
||||
call_api
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the Gitaly `CommitDiff` RPC times out', :use_clean_rails_memory_store_caching do
|
||||
let(:commit_diff_args) { [project.repository_storage, :diff_service, :commit_diff, any_args] }
|
||||
|
||||
before do
|
||||
allow(Gitlab::GitalyClient).to receive(:call)
|
||||
.and_call_original
|
||||
end
|
||||
|
||||
it 'handles the error, logs it, and returns empty diff files', :aggregate_failures do
|
||||
allow(Gitlab::GitalyClient).to receive(:call)
|
||||
.with(*commit_diff_args)
|
||||
.and_raise(GRPC::DeadlineExceeded)
|
||||
|
||||
expect(Gitlab::ErrorTracking)
|
||||
.to receive(:track_exception)
|
||||
.with an_instance_of(GRPC::DeadlineExceeded)
|
||||
|
||||
call_api
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response_diff_files(response)).to be_blank
|
||||
end
|
||||
|
||||
it 'does not handle the error when feature flag is disabled', :aggregate_failures do
|
||||
stub_feature_flags(api_v3_commits_skip_diff_files: false)
|
||||
|
||||
allow(Gitlab::GitalyClient).to receive(:call)
|
||||
.with(*commit_diff_args)
|
||||
.and_raise(GRPC::DeadlineExceeded)
|
||||
|
||||
call_api
|
||||
|
||||
expect(response).to have_gitlab_http_status(:error)
|
||||
end
|
||||
|
||||
it 'only calls Gitaly once for all attempts within a period of time', :aggregate_failures do
|
||||
expect(Gitlab::GitalyClient).to receive(:call)
|
||||
.with(*commit_diff_args)
|
||||
.once # <- once
|
||||
.and_raise(GRPC::DeadlineExceeded)
|
||||
|
||||
3.times do
|
||||
call_api
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response_diff_files(response)).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
it 'calls Gitaly again after a period of time', :aggregate_failures do
|
||||
expect(Gitlab::GitalyClient).to receive(:call)
|
||||
.with(*commit_diff_args)
|
||||
.twice # <- twice
|
||||
.and_raise(GRPC::DeadlineExceeded)
|
||||
|
||||
call_api
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response_diff_files(response)).to be_blank
|
||||
|
||||
travel_to((described_class::GITALY_TIMEOUT_CACHE_EXPIRY + 1.second).from_now) do
|
||||
call_api
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response_diff_files(response)).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses a unique cache key, allowing other calls to succeed' do
|
||||
cache_key = [described_class::GITALY_TIMEOUT_CACHE_KEY, project.id, commit.cache_key].join(':')
|
||||
Rails.cache.write(cache_key, 1)
|
||||
|
||||
expect(Gitlab::GitalyClient).to receive(:call)
|
||||
.with(*commit_diff_args)
|
||||
.once # <- once
|
||||
|
||||
call_api
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response_diff_files(response)).to be_blank
|
||||
|
||||
call_api(commit_id: commit.parent.id)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response_diff_files(response).length).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthenticated' do
|
||||
let(:user) { nil }
|
||||
|
||||
it 'returns 401' do
|
||||
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", nil)
|
||||
call_api
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized' do
|
||||
it 'returns 404 when lower access level' do
|
||||
project.add_guest(unauthorized_user)
|
||||
let(:user) { unauthorized_user }
|
||||
|
||||
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}",
|
||||
unauthorized_user)
|
||||
it 'returns 404 when lower access level' do
|
||||
project.add_guest(user)
|
||||
|
||||
call_api
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Issues::SetCrmContactsService do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:contacts) { create_list(:contact, 4, group: group) }
|
||||
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }
|
||||
|
||||
before do
|
||||
create(:issue_customer_relations_contact, issue: issue, contact: contacts[0])
|
||||
create(:issue_customer_relations_contact, issue: issue, contact: contacts[1])
|
||||
end
|
||||
|
||||
subject(:set_crm_contacts) do
|
||||
described_class.new(project: project, current_user: user, params: params).execute(issue)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'when the user has no permission' do
|
||||
let(:params) { { crm_contact_ids: [contacts[1].id, contacts[2].id] } }
|
||||
|
||||
it 'returns expected error response' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.message).to match_array(['You have insufficient permissions to set customer relations contacts for this issue'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has permission' do
|
||||
before do
|
||||
group.add_reporter(user)
|
||||
end
|
||||
|
||||
context 'when the contact does not exist' do
|
||||
let(:params) { { crm_contact_ids: [non_existing_record_id] } }
|
||||
|
||||
it 'returns expected error response' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.message).to match_array(["Issue customer relations contacts #{non_existing_record_id}: #{does_not_exist_or_no_permission}"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the contact belongs to a different group' do
|
||||
let(:group2) { create(:group) }
|
||||
let(:contact) { create(:contact, group: group2) }
|
||||
let(:params) { { crm_contact_ids: [contact.id] } }
|
||||
|
||||
before do
|
||||
group2.add_reporter(user)
|
||||
end
|
||||
|
||||
it 'returns expected error response' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.message).to match_array(["Issue customer relations contacts #{contact.id}: #{does_not_exist_or_no_permission}"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'replace' do
|
||||
let(:params) { { crm_contact_ids: [contacts[1].id, contacts[2].id] } }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_success
|
||||
expect(issue.customer_relations_contacts).to match_array([contacts[1], contacts[2]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'add' do
|
||||
let(:params) { { add_crm_contact_ids: [contacts[3].id] } }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_success
|
||||
expect(issue.customer_relations_contacts).to match_array([contacts[0], contacts[1], contacts[3]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'remove' do
|
||||
let(:params) { { remove_crm_contact_ids: [contacts[0].id] } }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_success
|
||||
expect(issue.customer_relations_contacts).to match_array([contacts[1]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when attempting to add more than 6' do
|
||||
let(:id) { contacts[0].id }
|
||||
let(:params) { { add_crm_contact_ids: [id, id, id, id, id, id, id] } }
|
||||
|
||||
it 'returns expected error message' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.message).to match_array(['You can only add up to 6 contacts at one time'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when trying to remove non-existent contact' do
|
||||
let(:params) { { remove_crm_contact_ids: [non_existing_record_id] } }
|
||||
|
||||
it 'returns expected error message' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_success
|
||||
expect(response.message).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when combining params' do
|
||||
let(:error_invalid_params) { 'You cannot combine crm_contact_ids with add_crm_contact_ids or remove_crm_contact_ids' }
|
||||
|
||||
context 'add and remove' do
|
||||
let(:params) { { remove_crm_contact_ids: [contacts[1].id], add_crm_contact_ids: [contacts[3].id] } }
|
||||
|
||||
it 'updates the issue with correct contacts' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_success
|
||||
expect(issue.customer_relations_contacts).to match_array([contacts[0], contacts[3]])
|
||||
end
|
||||
end
|
||||
|
||||
context 'replace and remove' do
|
||||
let(:params) { { crm_contact_ids: [contacts[3].id], remove_crm_contact_ids: [contacts[0].id] } }
|
||||
|
||||
it 'returns expected error response' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.message).to match_array([error_invalid_params])
|
||||
end
|
||||
end
|
||||
|
||||
context 'replace and add' do
|
||||
let(:params) { { crm_contact_ids: [contacts[3].id], add_crm_contact_ids: [contacts[1].id] } }
|
||||
|
||||
it 'returns expected error response' do
|
||||
response = set_crm_contacts
|
||||
|
||||
expect(response).to be_error
|
||||
expect(response.message).to match_array([error_invalid_params])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -41,7 +41,6 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
|
|||
describe '#execute' do
|
||||
subject { service.execute }
|
||||
|
||||
shared_examples 'reading and removing tags' do |caching_enabled: true|
|
||||
context 'when no params are specified' do
|
||||
let(:params) { {} }
|
||||
|
||||
|
@ -271,7 +270,7 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
|
|||
it 'succeeds without a user' do
|
||||
expect_delete(%w(Bb Ba C), container_expiration_policy: true)
|
||||
|
||||
caching_enabled ? expect_caching : expect_no_caching
|
||||
expect_caching
|
||||
|
||||
is_expected.to eq(expected_service_response(deleted: %w(Bb Ba C), before_delete_size: 3))
|
||||
end
|
||||
|
@ -354,9 +353,8 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
|
|||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'caching' do
|
||||
context 'caching', :freeze_time do
|
||||
let(:params) do
|
||||
{
|
||||
'name_regex_delete' => '.*',
|
||||
|
@ -381,17 +379,12 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
|
|||
|
||||
before do
|
||||
expect_delete(%w(Bb Ba C), container_expiration_policy: true)
|
||||
travel_to(Time.zone.local(2021, 9, 2, 12, 0, 0))
|
||||
# We froze time so we need to set the created_at stubs again
|
||||
stub_digest_config('sha256:configA', 1.hour.ago)
|
||||
stub_digest_config('sha256:configB', 5.days.ago)
|
||||
stub_digest_config('sha256:configC', 1.month.ago)
|
||||
end
|
||||
|
||||
after do
|
||||
travel_back
|
||||
end
|
||||
|
||||
it 'caches the created_at values' do
|
||||
::Gitlab::Redis::Cache.with do |redis|
|
||||
expect_mget(redis, tags_and_created_ats.keys)
|
||||
|
@ -450,32 +443,6 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
|
|||
DateTime.rfc3339(date_time.rfc3339).rfc3339
|
||||
end
|
||||
end
|
||||
|
||||
context 'with container_registry_expiration_policies_caching enabled for the project' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_expiration_policies_caching: project)
|
||||
end
|
||||
|
||||
it_behaves_like 'reading and removing tags', caching_enabled: true
|
||||
end
|
||||
|
||||
context 'with container_registry_expiration_policies_caching disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_expiration_policies_caching: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'reading and removing tags', caching_enabled: false
|
||||
end
|
||||
|
||||
context 'with container_registry_expiration_policies_caching not enabled for the project' do
|
||||
let_it_be(:another_project) { create(:project) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(container_registry_expiration_policies_caching: another_project)
|
||||
end
|
||||
|
||||
it_behaves_like 'reading and removing tags', caching_enabled: false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -5,11 +5,22 @@ require 'gitlab'
|
|||
require 'pathname'
|
||||
|
||||
# This script saves the diffs of changes in an MR to the directory specified as the first argument
|
||||
#
|
||||
# It exits with a success code if diffs are found and saved, or if there are no changes, including if the script runs in
|
||||
# a pipeline that is not for a merge request.
|
||||
|
||||
gitlab_token = ENV.fetch('PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE')
|
||||
gitlab_endpoint = ENV.fetch('CI_API_V4_URL')
|
||||
mr_project_path = ENV.fetch('CI_MERGE_REQUEST_PROJECT_PATH')
|
||||
mr_iid = ENV.fetch('CI_MERGE_REQUEST_IID')
|
||||
mr_project_path = ENV['CI_MERGE_REQUEST_PROJECT_PATH']
|
||||
mr_iid = ENV['CI_MERGE_REQUEST_IID']
|
||||
|
||||
puts "CI_MERGE_REQUEST_PROJECT_PATH is missing." if mr_project_path.to_s.empty?
|
||||
puts "CI_MERGE_REQUEST_IID is missing." if mr_iid.to_s.empty?
|
||||
|
||||
unless mr_project_path && mr_iid
|
||||
puts "Exiting as this does not appear to be a merge request pipeline."
|
||||
exit
|
||||
end
|
||||
|
||||
abort("ERROR: Please specify a directory to write MR diffs into.") if ARGV.empty?
|
||||
output_diffs_dir = Pathname.new(ARGV.shift).expand_path
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'pathname'
|
||||
|
||||
# This script assumes the first argument is a directory of files containing diffs of changes from an MR. It exits with a
|
||||
# success code if all diffs add a line that quarantines a test. If any diffs are not specs, or they are specs that don't
|
||||
# quarantine a test, it exits with code 1 to indicate failure (i.e., there was _not_ only quarantined specs).
|
||||
|
||||
abort("ERROR: Please specify the directory containing MR diffs.") if ARGV.empty?
|
||||
diffs_dir = Pathname.new(ARGV.shift).expand_path
|
||||
|
||||
diffs_dir.glob('**/*').each do |path|
|
||||
next if path.directory?
|
||||
|
||||
exit 1 unless path.to_s.end_with?('_spec.rb.diff')
|
||||
exit 1 unless path.read.match?(/^\+.*, quarantine:/)
|
||||
end
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'pathname'
|
||||
|
||||
# This script checks if the package-and-qa job should trigger downstream pipelines to run the QA suite.
|
||||
#
|
||||
# It assumes the first argument is a directory of files containing diffs of changes from an MR
|
||||
# (e.g., created by tooling/bin/find_change_diffs). It exits with a success code if there are no diffs, or if the diffs
|
||||
# are suitable to run QA tests.
|
||||
#
|
||||
# The script will abort (exit code 1) if the argument is missing.
|
||||
#
|
||||
# The following condition will result in a failure code (2), indicating that package-and-qa should not run:
|
||||
#
|
||||
# - If the changes only include tests being put in quarantine
|
||||
|
||||
abort("ERROR: Please specify the directory containing MR diffs.") if ARGV.empty?
|
||||
diffs_dir = Pathname.new(ARGV.shift).expand_path
|
||||
|
||||
# Run package-and-qa if there are no diffs. E.g., in scheduled pipelines
|
||||
exit 0 if diffs_dir.glob('**/*').empty?
|
||||
|
||||
files_count = 0
|
||||
specs_count = 0
|
||||
quarantine_specs_count = 0
|
||||
|
||||
diffs_dir.glob('**/*').each do |path|
|
||||
next if path.directory?
|
||||
|
||||
files_count += 1
|
||||
next unless path.to_s.end_with?('_spec.rb.diff')
|
||||
|
||||
specs_count += 1
|
||||
quarantine_specs_count += 1 if path.read.match?(/^\+.*, quarantine:/)
|
||||
end
|
||||
|
||||
# Run package-and-qa if there are no specs. E.g., when the MR changes QA framework files.
|
||||
exit 0 if specs_count == 0
|
||||
|
||||
# Skip package-and-qa if there are only specs being put in quarantine.
|
||||
exit 2 if quarantine_specs_count == specs_count && quarantine_specs_count == files_count
|
||||
|
||||
# Run package-and-qa under any other circumstances. E.g., if there are specs being put in quarantine but there are also
|
||||
# other changes that might need to be tested.
|
Loading…
Reference in New Issue