Commit 45240ef2 authored by Yorick Peterse's avatar Yorick Peterse

Merge canonical master into security master

This needed to be done manually due to merge conflicts in the Gemfile.
parents 78e34e45 95a1c067
......@@ -382,7 +382,6 @@ Performance/DeleteSuffix:
- 'app/workers/concerns/application_worker.rb'
- 'ee/app/models/geo/upload_registry.rb'
- 'ee/app/workers/geo/file_download_dispatch_worker/attachment_job_finder.rb'
- 'lib/sentry/client/issue.rb'
# Offense count: 13
# Cop supports --auto-correct.
......@@ -1042,4 +1041,3 @@ Style/StringLiteralsInInterpolation:
# IgnoredMethods: respond_to, define_method
Style/SymbolProc:
Enabled: false
55bed7acf3bb27ab627272d903d99573c5f009e7
cdb02af5b322de1f4091b39c349579b2e335b914
......@@ -313,7 +313,7 @@ gem 'pg_query', '~> 1.3.0'
gem 'premailer-rails', '~> 1.10.3'
# LabKit: Tracing and Correlation
gem 'gitlab-labkit', '0.14.0'
gem 'gitlab-labkit', '~> 0.16.0'
# Thrift is a dependency of gitlab-labkit, we want a version higher than 0.14.0
# because of https://gitlab.com/gitlab-org/gitlab/-/issues/321900
gem 'thrift', '>= 0.14.0'
......
......@@ -446,19 +446,18 @@ GEM
fog-json (~> 1.2.0)
mime-types
ms_rest_azure (~> 0.12.0)
gitlab-labkit (0.14.0)
gitlab-labkit (0.16.0)
actionpack (>= 5.0.0, < 7.0.0)
activesupport (>= 5.0.0, < 7.0.0)
gitlab-pg_query (~> 1.3)
grpc (~> 1.19)
jaeger-client (~> 1.1)
opentracing (~> 0.4)
pg_query (~> 1.3)
redis (> 3.0.0, < 5.0.0)
gitlab-license (1.3.1)
gitlab-mail_room (0.0.8)
gitlab-markup (1.7.1)
gitlab-net-dns (0.9.1)
gitlab-pg_query (1.3.1)
gitlab-pry-byebug (3.9.0)
byebug (~> 11.0)
pry (~> 0.13.0)
......@@ -1416,7 +1415,7 @@ DEPENDENCIES
gitlab-chronic (~> 0.10.5)
gitlab-experiment (~> 0.4.12)
gitlab-fog-azure-rm (~> 1.0.1)
gitlab-labkit (= 0.14.0)
gitlab-labkit (~> 0.16.0)
gitlab-license (~> 1.3)
gitlab-mail_room (~> 0.0.8)
gitlab-markup (~> 1.7.1)
......
......@@ -42,6 +42,7 @@ export default {
"AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear.",
),
unassigned: __('Unassigned'),
closed: __('closed'),
},
fields: [
{
......@@ -75,7 +76,7 @@ export default {
{
key: 'issue',
label: s__('AlertManagement|Incident'),
thClass: 'gl-w-12 gl-pointer-events-none',
thClass: 'gl-w-15p gl-pointer-events-none',
tdClass,
},
{
......@@ -221,8 +222,11 @@ export default {
hasAssignees(assignees) {
return Boolean(assignees.nodes?.length);
},
getIssueLink(item) {
return joinPaths('/', this.projectPath, '-', 'issues', item.issueIid);
getIssueMeta({ issue: { iid, state } }) {
return {
state: state === 'closed' ? `(${this.$options.i18n.closed})` : '',
link: joinPaths('/', this.projectPath, '-', 'issues', iid),
};
},
tbodyTrClass(item) {
return {
......@@ -343,8 +347,14 @@ export default {
</template>
<template #cell(issue)="{ item }">
<gl-link v-if="item.issueIid" data-testid="issueField" :href="getIssueLink(item)">
#{{ item.issueIid }}
<gl-link
v-if="item.issue"
v-gl-tooltip
:title="item.issue.title"
data-testid="issueField"
:href="getIssueMeta(item).link"
>
#{{ item.issue.iid }} {{ getIssueMeta(item).state }}
</gl-link>
<div v-else data-testid="issueField">{{ s__('AlertManagement|None') }}</div>
</template>
......
......@@ -107,8 +107,8 @@ export default Vue.extend({
closeSidebar() {
this.detail.issue = {};
},
setAssignees(data) {
boardsStore.detail.issue.setAssignees(data.issueSetAssignees.issue.assignees.nodes);
setAssignees(assignees) {
boardsStore.detail.issue.setAssignees(assignees);
},
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
......
......@@ -5,7 +5,11 @@ fragment AlertListItem on AlertManagementAlert {
status
startedAt
eventCount
issueIid
issue {
iid
state
title
}
assignees {
nodes {
name
......
#import "../fragments/user.fragment.graphql"
query usersSearch($search: String!, $fullPath: ID!) {
issuable: project(fullPath: $fullPath) {
workspace: project(fullPath: $fullPath) {
users: projectMembers(search: $search) {
nodes {
user {
......
......@@ -13,7 +13,7 @@ export default {
return {
text: __('Save'),
attributes: [
{ variant: 'success' },
{ variant: 'confirm' },
{ category: 'primary' },
{ disabled: this.isDisabled },
],
......
......@@ -138,7 +138,7 @@ export default {
<gl-button
v-gl-modal.confirmSaveIntegration
category="primary"
variant="success"
variant="confirm"
:loading="isSaving"
:disabled="isDisabled"
data-qa-selector="save_changes_button"
......@@ -162,6 +162,8 @@ export default {
<gl-button
v-if="propsSource.canTest"
category="secondary"
variant="confirm"
:loading="isTesting"
:disabled="isDisabled"
:href="propsSource.testPath"
......@@ -174,7 +176,7 @@ export default {
<gl-button
v-gl-modal.confirmResetIntegration
category="secondary"
variant="default"
variant="confirm"
:loading="isResetting"
:disabled="isDisabled"
data-testid="reset-button"
......@@ -184,9 +186,7 @@ export default {
<reset-confirmation-modal @reset="onResetClick" />
</template>
<gl-button class="btn-cancel" :href="propsSource.cancelPath">{{
__('Cancel')
}}</gl-button>
<gl-button :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
</div>
</div>
</div>
......
......@@ -19,7 +19,7 @@ export default {
computed: {
fullPath() {
if (this.noteableData.web_url) {
return this.noteableData.web_url.split('/-/')[0].substring(1);
return this.noteableData.web_url.split('/-/')[0].substring(1).replace('groups/', '');
}
return null;
},
......@@ -28,7 +28,7 @@ export default {
},
},
created() {
if (this.issuableType !== IssuableType.Issue) {
if (this.issuableType !== IssuableType.Issue && this.issuableType !== IssuableType.Epic) {
return;
}
......
......@@ -38,6 +38,11 @@ export default {
required: false,
default: false,
},
isMergeTrain: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
......@@ -126,6 +131,21 @@ export default {
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
<template v-if="isMergeTrain">
<li class="gl-new-dropdown-divider" role="presentation">
<hr role="separator" aria-orientation="horizontal" class="dropdown-divider" />
</li>
<li>
<div
class="gl-display-flex gl-align-items-center"
data-testid="warning-message-merge-trains"
>
<div class="menu-item gl-font-sm gl-text-gray-300!">
{{ s__('Pipeline|Merge train pipeline jobs can not be retried') }}
</div>
</div>
</li>
</template>
</ul>
</gl-dropdown>
</template>
......@@ -15,13 +15,12 @@ import { IssuableType } from '~/issue_show/constants';
import { __, n__ } from '~/locale';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { assigneesQueries } from '~/sidebar/constants';
import { assigneesQueries, ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
export const assigneesWidget = Vue.observable({
updateAssignees: null,
});
export default {
i18n: {
unassigned: __('Unassigned'),
......@@ -88,10 +87,10 @@ export default {
return this.queryVariables;
},
update(data) {
return data.issuable || data.project?.issuable;
return data.workspace?.issuable;
},
result({ data }) {
const issuable = data.issuable || data.project?.issuable;
const issuable = data.workspace?.issuable;
if (issuable) {
this.selected = this.moveCurrentUserToStart(cloneDeep(issuable.assignees.nodes));
}
......@@ -109,7 +108,7 @@ export default {
};
},
update(data) {
const searchResults = data.issuable?.users?.nodes.map(({ user }) => user) || [];
const searchResults = data.workspace?.users?.nodes.map(({ user }) => user) || [];
const mergedSearchResults = this.participants.reduce((acc, current) => {
if (
!acc.some((user) => current.username === user.username) &&
......@@ -121,7 +120,7 @@ export default {
}, searchResults);
return mergedSearchResults;
},
debounce: 250,
debounce: ASSIGNEES_DEBOUNCE_DELAY,
skip() {
return this.isSearchEmpty;
},
......@@ -229,7 +228,7 @@ export default {
},
})
.then(({ data }) => {
this.$emit('assignees-updated', data);
this.$emit('assignees-updated', data.issuableSetAssignees.issuable.assignees.nodes);
return data;
})
.catch(() => {
......@@ -378,7 +377,7 @@ export default {
<template v-if="showCurrentUser">
<gl-dropdown-divider />
<gl-dropdown-item
data-testid="unselected-participant"
data-testid="current-user"
@click.stop="selectAssignee(currentUser)"
>
<gl-avatar-link>
......@@ -409,7 +408,7 @@ export default {
/>
</gl-avatar-link>
</gl-dropdown-item>
<gl-dropdown-item v-if="noUsersFound && !isSearching">
<gl-dropdown-item v-if="noUsersFound && !isSearching" data-testid="empty-results">
{{ __('No matching results') }}
</gl-dropdown-item>
</template>
......
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { mapState } from 'vuex';
import { __, sprintf } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import EditForm from './edit_form.vue';
export default {
components: {
EditForm,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
fullPath: {
required: true,
type: String,
},
isEditable: {
required: true,
type: Boolean,
},
issuableType: {
required: false,
type: String,
default: 'issue',
},
},
data() {
return {
edit: false,
};
},
computed: {
...mapState({
confidential: ({ noteableData, confidential }) => {
if (noteableData) {
return noteableData.confidential;
}
return Boolean(confidential);
},
}),
confidentialityIcon() {
return this.confidential ? 'eye-slash' : 'eye';
},
tooltipLabel() {
return this.confidential ? __('Confidential') : __('Not confidential');
},
confidentialText() {
return sprintf(__('This %{issuableType} is confidential'), {
issuableType: this.issuableType,
});
},
},
created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm);
},
beforeDestroy() {
eventHub.$off('closeConfidentialityForm', this.toggleForm);
},
methods: {
toggleForm() {
this.edit = !this.edit;
},
},
};
</script>
<template>
<div class="block issuable-sidebar-item confidentiality">
<div
ref="collapseIcon"
v-gl-tooltip.viewport.left
:title="tooltipLabel"
class="sidebar-collapsed-icon"
@click="toggleForm"
>
<gl-icon :name="confidentialityIcon" />
</div>
<div class="title hide-collapsed">
{{ __('Confidentiality') }}
<a
v-if="isEditable"
ref="editLink"
class="float-right confidential-edit"
href="#"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="confidentiality"
@click.prevent="toggleForm"
>{{ __('Edit') }}</a
>
</div>
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="edit"
:confidential="confidential"
:full-path="fullPath"
:issuable-type="issuableType"
/>
<div v-if="!confidential" class="no-value sidebar-item-value" data-testid="not-confidential">
<gl-icon :size="16" name="eye" class="sidebar-item-icon inline" />
{{ __('Not confidential') }}
</div>
<div v-else class="value sidebar-item-value hide-collapsed">
<gl-icon :size="16" name="eye-slash" class="sidebar-item-icon inline is-active" />
{{ confidentialText }}
</div>
</div>
</div>
</template>
<script>
import { GlSprintf } from '@gitlab/ui';
import { __ } from '../../../locale';
import editFormButtons from './edit_form_buttons.vue';
export default {
components: {
editFormButtons,
GlSprintf,
},
props: {
confidential: {
required: true,
type: Boolean,
},
fullPath: {
required: true,
type: String,
},
issuableType: {
required: true,
type: String,
},
},
computed: {
confidentialityOnWarning() {
return __(
'You are going to turn on the confidentiality. This means that only team members with %{strongStart}at least Reporter access%{strongEnd} are able to see and leave comments on the %{issuableType}.',
);
},
confidentialityOffWarning() {
return __(
'You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}.',
);
},
},
};
</script>
<template>
<div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<div>
<p v-if="!confidential">
<gl-sprintf :message="confidentialityOnWarning">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
<template #issuableType>{{ issuableType }}</template>
</gl-sprintf>
</p>
<p v-else>
<gl-sprintf :message="confidentialityOffWarning">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
<template #issuableType>{{ issuableType }}</template>
</gl-sprintf>
</p>
<edit-form-buttons :full-path="fullPath" :confidential="confidential" />
</div>
</div>
</div>
</template>
<script>
import { GlButton } from '@gitlab/ui';
import $ from 'jquery';
import { mapActions } from 'vuex';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
import eventHub from '../../event_hub';
export default {
components: {
GlButton,
},
props: {
fullPath: {
required: true,
type: String,
},
confidential: {
required: true,
type: Boolean,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
toggleButtonText() {
if (this.isLoading) {
return __('Applying');
}
return this.confidential ? __('Turn Off') : __('Turn On');
},
},
methods: {
...mapActions(['updateConfidentialityOnIssuable']),
closeForm() {
eventHub.$emit('closeConfidentialityForm');
$(this.$el).trigger('hidden.gl.dropdown');
},
submitForm() {
this.isLoading = true;
const confidential = !this.confidential;
this.updateConfidentialityOnIssuable({ confidential, fullPath: this.fullPath })
.then(() => {
eventHub.$emit('updateIssuableConfidentiality', confidential);
})
.catch((err) => {
Flash(
err || __('Something went wrong trying to change the confidentiality of this issue'),
);
})
.finally(() => {
this.closeForm();
this.isLoading = false;
});
},
},
};
</script>
<template>
<div class="sidebar-item-warning-message-actions">
<gl-button class="gl-mr-3" @click="closeForm">
{{ __('Cancel') }}
</gl-button>
<gl-button
category="secondary"
variant="warning"
:disabled="isLoading"
:loading="isLoading"
data-testid="confidential-toggle"
@click.prevent="submitForm"
>
{{ toggleButtonText }}
</gl-button>
</div>
</template>
mutation updateIssueConfidential($input: IssueSetConfidentialInput!) {
issueSetConfidential(input: $input) {
issue {
confidential
}
errors
}
}
......@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: true,
},
issuableType: {
type: String,
required: true,
},
},
computed: {
confidentialText() {
......@@ -35,7 +39,13 @@ export default {
<template>
<div>
<div v-gl-tooltip.viewport.left :title="tooltipLabel" class="sidebar-collapsed-icon">
<div
v-gl-tooltip.viewport.left
:title="tooltipLabel"
class="sidebar-collapsed-icon"
data-testid="sidebar-collapsed-icon"
@click="$emit('expandSidebar')"
>
<gl-icon
:size="16"
:name="confidentialIcon"
......
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { __, sprintf } from '~/locale';
import { confidentialityQueries } from '~/sidebar/constants';
......@@ -45,6 +46,15 @@ export default {
? this.$options.i18n.confidentialityOffWarning
: this.$options.i18n.confidentialityOnWarning;
},
workspacePath() {
return this.issuableType === IssuableType.Issue
? {
projectPath: this.fullPath,
}
: {
groupPath: this.fullPath,
};
},
},
methods: {
submitForm() {
......@@ -54,7 +64,7 @@ export default {
mutation: confidentialityQueries[this.issuableType].mutation,
variables: {
input: {
projectPath: this.fullPath,
...this.workspacePath,
iid: this.iid,
confidential: !this.confidential,
},
......
......@@ -47,12 +47,15 @@ export default {
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
iid: String(this.iid),
};
},
update(data) {
return data.workspace?.issuable?.confidential || false;
},
result({ data }) {
this.$emit('confidentialityUpdated', data.workspace?.issuable?.confidential);
},
error() {
createFlash({
message: sprintf(
......@@ -80,6 +83,7 @@ export default {
closeForm() {
this.$refs.editable.collapse();
this.$el.dispatchEvent(hideDropdownEvent);
this.$emit('closeForm');
},
// synchronizing the quick action with the sidebar widget
// this is a temporary solution until we have confidentiality real-time updates
......@@ -101,6 +105,10 @@ export default {
data,
});
},
expandSidebar() {
this.$refs.editable.expand();
this.$emit('expandSidebar');
},
},
};
</script>
......@@ -115,11 +123,16 @@ export default {
>
<template #collapsed>
<div>
<sidebar-confidentiality-content v-if="!isLoading" :confidential="confidential" />
<sidebar-confidentiality-content
v-if="!isLoading"
:confidential="confidential"
:issuable-type="issuableType"
@expandSidebar="expandSidebar"
/>
</div>
</template>
<template #default>
<sidebar-confidentiality-content :confidential="confidential" />
<sidebar-confidentiality-content :confidential="confidential" :issuable-type="issuableType" />
<sidebar-confidentiality-form
:confidential="confidential"
:issuable-type="issuableType"
......
......@@ -87,7 +87,7 @@ export default {
<gl-button
v-if="canUpdate"
variant="link"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto js-sidebar-dropdown-toggle hide-collapsed"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed"
data-testid="edit-button"
:data-track-event="tracking.event"
:data-track-label="tracking.label"
......
import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import updateEpicMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql';
import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
import updateMergeRequestParticipantsMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql';
export const ASSIGNEES_DEBOUNCE_DELAY = 250;
export const assigneesQueries = {
[IssuableType.Issue]: {
query: getIssueParticipants,
......@@ -22,4 +26,8 @@ export const confidentialityQueries = {
query: issueConfidentialQuery,
mutation: updateIssueConfidentialMutation,
},
[IssuableType.Epic]: {
query: epicConfidentialQuery,
mutation: updateEpicMutation,
},
};
query epicConfidential($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
issuable: epic(iid: $iid) {
__typename
id
confidential
}
}
}
mutation updateEpic($input: UpdateEpicInput!) {
issuableSetConfidential: updateEpic(input: $input) {
issuable: epic {
id
confidential
}
errors
}
}
......@@ -15,6 +15,7 @@ import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_a
import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import { MT_MERGE_STRATEGY } from '../constants';
export default {
name: 'MRWidgetPipeline',
......@@ -80,6 +81,11 @@ export default {
type: String,
required: true,
},
mergeStrategy: {
type: String,
required: false,
default: '',
},
},
computed: {
hasPipeline() {
......@@ -130,6 +136,9 @@ export default {
this.buildsWithCoverage.length,
);
},
isMergeTrain() {
return this.mergeStrategy === MT_MERGE_STRATEGY;
},
},
errorText: s__(
'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.',
......@@ -249,7 +258,7 @@ export default {
class="stage-container dropdown mr-widget-pipeline-stages"
data-testid="widget-mini-pipeline-graph"
>
<pipeline-stage :stage="stage" />
<pipeline-stage :stage="stage" :is-merge-train="isMergeTrain" />
</div>
</template>
</span>
......
......@@ -4,6 +4,7 @@ import { isNumber } from 'lodash';
import { sanitize } from '~/lib/dompurify';
import { n__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MergeRequestStore from '../stores/mr_widget_store';
import ArtifactsApp from './artifacts_list_app.vue';
import MrCollapsibleExtension from './mr_collapsible_extension.vue';
import MrWidgetContainer from './mr_widget_container.vue';
......@@ -84,6 +85,15 @@ export default {
this.deployments.length,
);
},
preferredAutoMergeStrategy() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return MergeRequestStore.getPreferredAutoMergeStrategy(
this.mr.availableAutoMergeStrategies,
);
}
return this.mr.preferredAutoMergeStrategy;
},
},
};
</script>
......@@ -100,6 +110,7 @@ export default {
:source-branch-link="branchLink"
:mr-troubleshooting-docs-path="mr.mrTroubleshootingDocsPath"
:ci-troubleshooting-docs-path="mr.ciTroubleshootingDocsPath"
:merge-strategy="preferredAutoMergeStrategy"
/>
<template #footer>
<div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts">
......
......@@ -268,10 +268,10 @@ export default {
</span>
</div>
<gl-button
v-if="alert.issueIid"
v-if="alert.issue"
class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-incident-button"
data-testid="viewIncidentBtn"
:href="incidentPath(alert.issueIid)"
:href="incidentPath(alert.issue.iid)"
category="primary"
variant="success"
>
......
#import "~/graphql_shared/fragments/user.fragment.graphql"
query issueParticipants($fullPath: ID!, $iid: String!) {
project(fullPath: $fullPath) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: issue(iid: $iid) {
__typename
id
participants {
nodes {
......
#import "~/graphql_shared/fragments/user.fragment.graphql"
query getMrParticipants($fullPath: ID!, $iid: String!) {
project(fullPath: $fullPath) {
workspace: project(fullPath: $fullPath) {
issuable: mergeRequest(iid: $iid) {
id
participants {
......
#import "~/graphql_shared/fragments/user.fragment.graphql"
mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) {
issueSetAssignees(
issuableSetAssignees: issueSetAssignees(
input: { iid: $iid, assigneeUsernames: $assigneeUsernames, projectPath: $fullPath }
) {
issue {
issuable: issue {
id
assignees {
nodes {
......
......@@ -15,19 +15,45 @@ module MergeRequests
# Returns a Hash that maps a commit ID to the oldest merge request that
# introduced that commit.
def execute(commits)
mapping = {}
shas = commits.map(&:id)
# To include merge requests by the commit SHA, we don't need to go through
# any diff rows.
#
# We can't squeeze all this into a single query, as the diff based data
# relies on a GROUP BY. On the other hand, retrieving MRs by their merge
# SHAs separately is much easier, and plenty fast.
@project
.merge_requests
.preload_target_project
.by_merge_commit_sha(shas)
.each do |mr|
# Merge SHAs can't be in the merge request itself. It _is_ possible a
# newer merge request includes the merge commit, but in that case we
# still want the oldest merge request.
mapping[mr.merge_commit_sha] = mr
end
remaining = shas - mapping.keys
return mapping if remaining.empty?
id_rows = MergeRequestDiffCommit
.oldest_merge_request_id_per_commit(@project.id, commits.map(&:id))
.oldest_merge_request_id_per_commit(@project.id, remaining)
mrs = MergeRequest
.preload_target_project
.id_in(id_rows.map { |r| r[:merge_request_id] })
.index_by(&:id)
id_rows.each_with_object({}) do |row, hash|
id_rows.each do |row|
if (mr = mrs[row[:merge_request_id]])
hash[row[:sha]] = mr
mapping[row[:sha]] = mr
end
end
mapping
end
end
end
......@@ -43,7 +43,8 @@ module Resolvers
def preloads
{
assignees: [:assignees],
notes: [:ordered_notes, { ordered_notes: [:system_note_metadata, :project, :noteable] }]
notes: [:ordered_notes, { ordered_notes: [:system_note_metadata, :project, :noteable] }],
issue: [:issue]
}
end
end
......
......@@ -10,7 +10,7 @@ module Resolvers
def resolve(**args)
scope = super
if only_count_is_selected_with_merged_at_filter?(args) && Feature.enabled?(:optimized_merge_request_count_with_merged_at_filter, default_enabled: :yaml)
if only_count_is_selected_with_merged_at_filter?(args)
MergeRequest::MetricsFinder
.new(current_user, args.merge(target_project: project))
.execute
......
......@@ -20,8 +20,14 @@ module Types
field :issue_iid,
GraphQL::ID_TYPE,
null: true,
deprecated: { reason: 'Use issue field', milestone: '13.10' },
description: 'Internal ID of the GitLab issue attached to the alert.'
field :issue,
Types::IssueType,
null: true,
description: 'Issue attached to the alert.'
field :title,
GraphQL::STRING_TYPE,
null: true,
......
......@@ -77,7 +77,7 @@ module ErrorTracking
def sentry_client
strong_memoize(:sentry_client) do
Sentry::Client.new(api_url, token)
ErrorTracking::SentryClient.new(api_url, token)
end
end
......@@ -168,13 +168,13 @@ module ErrorTracking
def handle_exceptions
yield
rescue Sentry::Client::Error => e
rescue ErrorTracking::SentryClient::Error => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE }
rescue Sentry::Client::MissingKeysError => e
rescue ErrorTracking::SentryClient::MissingKeysError => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_MISSING_KEYS }
rescue Sentry::Client::ResponseInvalidSizeError => e
rescue ErrorTracking::SentryClient::ResponseInvalidSizeError => e
{ error: e.message, error_type: SENTRY_API_ERROR_INVALID_SIZE }
rescue Sentry::Client::BadRequestError => e
rescue ErrorTracking::SentryClient::BadRequestError => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_BAD_REQUEST }
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e)
......
......@@ -191,12 +191,8 @@ class MergeRequest < ApplicationRecord
end
state_machine :merge_status, initial: :unchecked do
event :mark_as_preparing do
transition unchecked: :preparing
end
event :mark_as_unchecked do
transition [:preparing, :can_be_merged, :checking] => :unchecked
transition [:can_be_merged, :checking] => :unchecked
transition [:cannot_be_merged, :cannot_be_merged_rechecking] => :cannot_be_merged_recheck
end
......@@ -241,7 +237,7 @@ class MergeRequest < ApplicationRecord
# Returns current merge_status except it returns `cannot_be_merged_rechecking` as `checking`
# to avoid exposing unnecessary internal state
def public_merge_status
cannot_be_merged_rechecking? || preparing? ? 'checking' : merge_status
cannot_be_merged_rechecking? ? 'checking' : merge_status
end
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?]
......@@ -1058,8 +1054,6 @@ class MergeRequest < ApplicationRecord
end
def mergeable?(skip_ci_check: false, skip_discussions_check: false)
return false if preparing?
return false unless mergeable_state?(skip_ci_check: skip_ci_check,
skip_discussions_check: skip_discussions_check)
......
......@@ -2,9 +2,20 @@
module Boards
class UpdateService < Boards::BaseService
PERMITTED_PARAMS = %i(name hide_backlog_list hide_closed_list).freeze
def execute(board)
filter_params
board.update(params)
end
def filter_params
params.slice!(*permitted_params)
end
def permitted_params
PERMITTED_PARAMS
end
end
end
......
......@@ -3,12 +3,13 @@
module Issuable
module Clone
class BaseService < IssuableBaseService
attr_reader :original_entity, :new_entity
attr_reader :original_entity, :new_entity, :target_project
alias_method :old_project, :project
def execute(original_entity, new_project = nil)
def execute(original_entity, target_project = nil)
@original_entity = original_entity
@target_project = target_project
# Using transaction because of a high resources footprint
# on rewriting notes (unfolding references)
......@@ -77,6 +78,12 @@ module Issuable
new_entity.project.group
end
end
def relative_position
return if original_entity.project.root_ancestor.id != target_project.root_ancestor.id
original_entity.relative_position
end
end
end
end
......
......@@ -47,6 +47,7 @@ module Issues
new_params = {
id: nil,
iid: nil,
relative_position: relative_position,
project: target_project,
author: current_user,
assignee_ids: original_entity.assignee_ids
......
......@@ -48,13 +48,14 @@ module Issues
def create_new_entity
new_params = {
id: nil,
iid: nil,
project: target_project,
author: original_entity.author,
assignee_ids: original_entity.assignee_ids,
moved_issue: true
}
id: nil,
iid: nil,
relative_position: relative_position,
project: target_project,
author: original_entity.author,
assignee_ids: original_entity.assignee_ids,
moved_issue: true
}
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
......
......@@ -2,112 +2,97 @@
module Members
class InviteService < Members::BaseService
DEFAULT_LIMIT = 100
BlankEmailsError = Class.new(StandardError)
TooManyEmailsError = Class.new(StandardError)
attr_reader :errors
def initialize(*args)
super
def initialize(current_user, params)
@current_user, @params = current_user, params.dup
@errors = {}
@emails = params[:email]&.split(',')&.uniq&.flatten
end
def execute(source)
return error(s_('Email cannot be blank')) if params[:email].blank?
validate_emails!
emails = params[:email].split(',').uniq.flatten
return error(s_("Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if
user_limit && emails.size > user_limit
emails.each do |email|
next if existing_member?(source, email)
next if existing_invite?(source, email)
next if existing_request?(source, email)
if existing_user?(email)
add_existing_user_as_member(current_user, source, params, email)
next
end
invite_new_member_and_user(current_user, source, params, email)
end
return success unless errors.any?
error(errors)
@source = source
emails.each(&method(:process_email))
result
rescue BlankEmailsError, TooManyEmailsError => e
error(e.message)
end
private
def invite_new_member_and_user(current_user, source, params, email)
new_member = (source.class.name + 'Member').constantize.create(source_id: source.id,
user_id: nil,
access_level: params[:access_level],
invite_email: email,
created_by_id: current_user.id,
expires_at: params[:expires_at])
unless new_member.valid? && new_member.persisted?
errors[params[:email]] = new_member.errors.full_messages.to_sentence
end
end
attr_reader :source, :errors, :emails
def add_existing_user_as_member(current_user, source, params, email)
new_member = create_member(current_user, existing_user(email), source, params.merge({ invite_email: email }))
def validate_emails!
raise BlankEmailsError, s_('AddMember|Email cannot be blank') if emails.blank?
unless new_member.valid? && new_member.persisted?
errors[email] = new_member.errors.full_messages.to_sentence
if user_limit && emails.size > user_limit
raise TooManyEmailsError, s_("AddMember|Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }
end
end
def create_member(current_user, user, source, params)
source.add_user(user, params[:access_level], current_user: current_user, expires_at: params[:expires_at])
def user_limit
limit = params.fetch(:limit, Members::CreateService::DEFAULT_LIMIT)
limit < 0 ? nil : limit
end
def user_limit
limit = params.fetch(:limit, DEFAULT_LIMIT)
def process_email(email)
return if existing_member?(email)
return if existing_invite?(email)
return if existing_request?(email)
limit && limit < 0 ? nil : limit
add_member(email)
end
def existing_member?(source, email)
def existing_member?(email)
existing_member = source.members.with_user_by_email(email).exists?
if existing_member
errors[email] = "Already a member of #{source.name}"
errors[email] = s_("AddMember|Already a member of %{source_name}") % { source_name: source.name }
return true
end
false
end
def existing_invite?(source, email)
def existing_invite?(email)
existing_invite = source.members.search_invite_email(email).exists?
if existing_invite
errors[email] = "Member already invited to #{source.name}"
errors[email] = s_("AddMember|Member already invited to %{source_name}") % { source_name: source.name }
return true
end
false
end
def existing_request?(source, email)
def existing_request?(email)
existing_request = source.requesters.with_user_by_email(email).exists?
if existing_request
errors[email] = "Member cannot be invited because they already requested to join #{source.name}"
errors[email] = s_("AddMember|Member cannot be invited because they already requested to join %{source_name}") % { source_name: source.name }
return true
end
false
end
def existing_user(email)
User.find_by_email(email)
def add_member(email)
new_member = source.add_user(email, params[:access_level], current_user: current_user, expires_at: params[:expires_at])
errors[email] = new_member.errors.full_messages.to_sentence if new_member.invalid?
end
def existing_user?(email)
existing_user(email).present?
def result
if errors.any?
error(errors)
else
success
end
end
end
end
......@@ -3,13 +3,6 @@
module MergeRequests
class AfterCreateService < MergeRequests::BaseService
def execute(merge_request)
prepare_merge_request(merge_request)
merge_request.mark_as_unchecked! if merge_request.preparing?
end
private
def prepare_merge_request(merge_request)
event_service.open_mr(merge_request, current_user)
merge_request_activity_counter.track_create_mr_action(user: current_user)
notification_service.new_merge_request(merge_request, current_user)
......
......@@ -14,8 +14,6 @@ module MergeRequests
end
def after_create(issuable)
issuable.mark_as_preparing
# Add new items to MergeRequests::AfterCreateService if they can
# be performed in Sidekiq
NewMergeRequestWorker.perform_async(issuable.id, current_user.id)
......
......@@ -26,7 +26,7 @@ class ErrorTrackingIssueLinkWorker # rubocop:disable Scalability/IdempotentWorke
logger.info("Linking Sentry issue #{sentry_issue_id} to GitLab issue #{issue.id}")
sentry_client.create_issue_link(integration_id, sentry_issue_id, issue)
rescue Sentry::Client::Error => e
rescue ErrorTracking::SentryClient::Error => e
logger.info("Failed to link Sentry issue #{sentry_issue_id} to GitLab issue #{issue.id} with error: #{e.message}")
end
end
......@@ -63,7 +63,7 @@ class ErrorTrackingIssueLinkWorker # rubocop:disable Scalability/IdempotentWorke
sentry_client
.repos(organization_slug)
.find { |repo| repo.project_id == issue.project_id && repo.status == 'active' }
rescue Sentry::Client::Error => e
rescue ErrorTracking::SentryClient::Error => e
logger.info("Unable to retrieve Sentry repo for organization #{organization_slug}, id #{sentry_issue_id}, with error: #{e.message}")
nil
......
---
title: 'Incident management: add issue state to alerts table'
merge_request: 55185
author:
type: added
---
title: Implement new preparing internal merge_status
merge_request: 54900
author:
type: other
---
title: Add setting to control Rails.application.config.hosts
merge_request: 55491
author:
type: added
---
title: Include MRs for merge commits for changelogs
merge_request: 55371
author:
type: fixed
---
title: Handle relative position on issue move or clone
merge_request: 55555
author:
type: fixed
---
title: Group integration settings buttons to the left
merge_request: 55139
author:
type: changed
---
title: Remove the optimized_merge_request_count_with_merged_at_filter feature flag
merge_request: 55600
author:
type: other
---
title: Improve Marginalia comments for API
merge_request: 55564
author:
type: changed
---
name: optimized_merge_request_count_with_merged_at_filter
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52113
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299347
milestone: '13.9'
type: development
group: group::optimize
default_enabled: true
......@@ -73,6 +73,8 @@ production: &base
worker_src: "'self' blob:"
report_uri:
allowed_hosts: []
# Trusted Proxies
# Customize if you have GitLab behind a reverse proxy which is running on a different machine.
# Add the IP address for your reverse proxy to the list, otherwise users will appear signed in from that address.
......
......@@ -13,7 +13,7 @@ require 'marginalia'
# matching against the raw SQL, and prepending the comment prevents color
# coding from working in the development log.
Marginalia::Comment.prepend_comment = true if Rails.env.production?
Marginalia::Comment.components = [:application, :controller, :action, :correlation_id, :jid, :job_class]
Marginalia::Comment.components = [:application, :controller, :action, :correlation_id, :jid, :job_class, :endpoint_id]
# As mentioned in https://github.com/basecamp/marginalia/pull/93/files,
# adding :line has some overhead because a regexp on the backtrace has
......
......@@ -210,6 +210,7 @@ Settings.gitlab['domain_allowlist'] ||= []
Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values
Settings.gitlab['trusted_proxies'] ||= []
Settings.gitlab['content_security_policy'] ||= Gitlab::ContentSecurityPolicy::ConfigLoader.default_settings_hash
Settings.gitlab['allowed_hosts'] ||= []
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil?
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
......
......@@ -2,6 +2,11 @@
# This file requires config/initializers/1_settings.rb
if Gitlab.config.gitlab.allowed_hosts.present?
Rails.application.config.hosts << Gitlab.config.gitlab.host << 'unix'
Rails.application.config.hosts += Gitlab.config.gitlab.allowed_hosts
end
if Rails.env.development?
Rails.application.config.hosts += [Gitlab.config.gitlab.host, 'unix', 'host.docker.internal']
......
......@@ -85890,4 +85890,4 @@
]
}
}
}
\ No newline at end of file
}
......@@ -427,7 +427,8 @@ Describes an alert from the project's Alert Management.
| `eventCount` | Int | Number of events of this alert. |
| `hosts` | String! => Array | List of hosts the alert came from. |
| `iid` | ID! | Internal ID of the alert. |
| `issueIid` | ID | Internal ID of the GitLab issue attached to the alert. |
| `issue` | Issue | Issue attached to the alert. |
| `issueIid` **{warning-solid}** | ID | **Deprecated:** Use issue field. Deprecated in 13.10. |
| `metricsDashboardUrl` | String | URL for metrics embed for the alert. |
| `monitoringTool` | String | Monitoring tool the alert came from. |
| `notes` | NoteConnection! | All notes on this noteable. |
......@@ -757,6 +758,16 @@ Autogenerated return type of BoardListUpdateLimitMetrics.
| `commit` | Commit | Commit for the branch. |
| `name` | String! | Name of the branch. |
### BulkFindOrCreateDevopsAdoptionSegmentsPayload
Autogenerated return type of BulkFindOrCreateDevopsAdoptionSegments.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `segments` | DevopsAdoptionSegment! => Array | Created segments after mutation. |
### BurnupChartDailyTotals
Represents the total number of issues and their weights for a particular day.
......@@ -1906,6 +1917,7 @@ Represents an epic board.
| `hideBacklogList` | Boolean | Whether or not backlog list is hidden. |
| `hideClosedList` | Boolean | Whether or not closed list is hidden. |
| `id` | BoardsEpicBoardID! | Global ID of the epic board. |
| `labels` | LabelConnection | Labels of the board. |
| `lists` | EpicListConnection | Epic board lists. |
| `name` | String | Name of the epic board. |
| `webPath` | String! | Web path of the epic board. |
......@@ -1931,6 +1943,16 @@ Autogenerated return type of EpicBoardListCreate.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `list` | EpicList | Epic list in the epic board. |
### EpicBoardUpdatePayload
Autogenerated return type of EpicBoardUpdate.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `epicBoard` | EpicBoard | The updated epic board. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### EpicDescendantCount
Counts of descendent epics.
......@@ -2715,6 +2737,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `reference` | String! | Internal reference of the merge request. Returned in shortened format by default. |
| `reviewers` | UserConnection | Users from whom a review has been requested. |
| `securityAutoFix` | Boolean | Indicates if the merge request is created by @GitLab-Security-Bot. |
| `securityReportsUpToDateOnTargetBranch` | Boolean! | Indicates if the target branch security reports are out of date. |
| `shouldBeRebased` | Boolean! | Indicates if the merge request will be rebased. |
| `shouldRemoveSourceBranch` | Boolean | Indicates if the source branch of the merge request will be deleted after merge. |
| `sourceBranch` | String! | Source branch of the merge request. |
......
......@@ -49,7 +49,6 @@ GET /projects/:id/vulnerability_findings?scope=all
GET /projects/:id/vulnerability_findings?scope=dismissed
GET /projects/:id/vulnerability_findings?severity=high
GET /projects/:id/vulnerability_findings?confidence=unknown,experimental
GET /projects/:id/vulnerability_findings?scanner=bandit,find_sec_bugs
GET /projects/:id/vulnerability_findings?pipeline_id=42
```
......@@ -63,7 +62,6 @@ Beginning with GitLab 12.9, the `undefined` severity and confidence level is no
| `scope` | string | no | Returns vulnerability findings for the given scope: `all` or `dismissed`. Defaults to `dismissed`. |
| `severity` | string array | no | Returns vulnerability findings belonging to specified severity level: `info`, `unknown`, `low`, `medium`, `high`, or `critical`. Defaults to all. |
| `confidence` | string array | no | Returns vulnerability findings belonging to specified confidence level: `ignore`, `unknown`, `experimental`, `low`, `medium`, `high`, or `confirmed`. Defaults to all. |
| `scanner` | string array | no | Returns vulnerability findings detected by specified scanner.
| `pipeline_id` | integer/string | no | Returns vulnerability findings belonging to specified pipeline. |
```shell
......
......@@ -7,32 +7,16 @@ type: concepts, howto
# Use Docker to build Docker images
You can use GitLab CI/CD with Docker to build and test Docker images.
For example, you might want to:
1. Create a Docker image of your application.
1. Run tests against the image.
1. Push the image to a remote registry.
1. Use the image to deploy your application to a server.
Or, if your application already has a `Dockerfile`, you can
use it to create and test an image:
```shell
docker build -t my-image dockerfiles/
docker run my-image /script/to/run/tests
docker tag my-image my-registry:5000/my-image
docker push my-registry:5000/my-image
```
You can use GitLab CI/CD with Docker to create Docker images.
For example, you can create a Docker image of your application,
test it, and publish it to a container registry.
To run Docker commands in your CI/CD jobs, you must configure
GitLab Runner to support `docker` commands.
## Enable Docker commands in your CI/CD jobs
There are three ways to enable the use of `docker build` and `docker run`
during jobs, each with their own tradeoffs. You can use:
To enable Docker commands for your CI/CD jobs, you can use:
- [The shell executor](#use-the-shell-executor)
- [The Docker executor with the Docker image (Docker-in-Docker)](#use-the-docker-executor-with-the-docker-image-docker-in-docker)
......@@ -47,12 +31,9 @@ to learn more about how these runners are configured.
### Use the shell executor
One way to configure GitLab Runner for `docker` support is to use the
`shell` executor.
After you register a runner and select the `shell` executor,
your job scripts are executed as the `gitlab-runner` user.
This user needs permission to run Docker commands.
You can include Docker commands in your CI/CD jobs if your runner is configured to
use the `shell` executor. The `gitlab-runner` user runs the Docker commands, but
needs permission to run them.
1. [Install](https://gitlab.com/gitlab-org/gitlab-runner/#installation) GitLab Runner.
1. [Register](https://docs.gitlab.com/runner/register/) a runner.
......@@ -100,9 +81,11 @@ Learn more about the [security of the `docker` group](https://blog.zopyx.com/on-
### Use the Docker executor with the Docker image (Docker-in-Docker)
Another way to configure GitLab Runner for `docker` support is to
register a runner with the Docker executor and use the [Docker image](https://hub.docker.com/_/docker/)
to run your job scripts. This configuration is referred to as "Docker-in-Docker."
You can use "Docker-in-Docker" to run commands in your CI/CD jobs:
- Register a runner that uses the Docker executor.
- Use the [Docker image](https://hub.docker.com/_/docker/) provided by Docker to
run the jobs that need Docker commands.
The Docker image has all of the `docker` tools installed
and can run the job script in context of the image in privileged mode.
......@@ -111,14 +94,18 @@ The `docker-compose` command is not available in this configuration by default.
To use `docker-compose` in your job scripts, follow the `docker-compose`
[installation instructions](https://docs.docker.com/compose/install/).
An example project that uses this approach can be found here: <https://gitlab.com/gitlab-examples/docker>.
WARNING:
When you enable `--docker-privileged`, you are effectively disabling all of
the security mechanisms of containers and exposing your host to privilege
escalation which can lead to container breakout. For more information, check
escalation. Doing this can lead to container breakout. For more information, check
out the official Docker documentation on
[runtime privilege and Linux capabilities](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities).
Docker-in-Docker works well, and is the recommended configuration, but it is
#### Limitations of Docker-in-Docker
Docker-in-Docker is the recommended configuration, but it is
not without its own challenges:
- When using Docker-in-Docker, each job is in a clean environment without the past
......@@ -144,8 +131,6 @@ not without its own challenges:
- docker run -v "$MOUNT_POINT:/mnt" my-docker-image
```
An example project using this approach can be found here: <https://gitlab.com/gitlab-examples/docker>.
In the examples below, we are using Docker images tags to specify a
specific version, such as `docker:19.03.12`. If tags like `docker:stable`
are used, you have no control over what version is used. This can lead to
......@@ -373,9 +358,8 @@ build:
### Use Docker socket binding
Another way to configure GitLab Runner for `docker` support is to
bind-mount `/var/run/docker.sock` into the
container so that Docker is available in the context of the image.
To use Docker commands in your CI/CD jobs, you can bind-mount `/var/run/docker.sock` into the
container. Docker is then available in the context of the image.
NOTE:
If you bind the Docker socket and you are
......@@ -478,13 +462,10 @@ services:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27173) in GitLab Runner 13.6.
If you are an administrator of GitLab Runner and you have the `dind`
service defined for the [Docker
executor](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersdockerservices-section),
or the [Kubernetes
executor](https://docs.gitlab.com/runner/executors/kubernetes.html#using-services)
you can specify the `command` to configure the registry mirror for the
Docker daemon.
If you are a GitLab Runner administrator, you can specify the `command` to configure the registry mirror
for the Docker daemon. The `dind` service must be defined for the
[Docker](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersdockerservices-section)
or [Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes.html#using-services).
Docker:
......@@ -516,11 +497,10 @@ Kubernetes:
##### Docker executor inside GitLab Runner configuration
If you are an administrator of GitLab Runner and you want to use
the mirror for every `dind` service, update the
If you are a GitLab Runner administrator, you can use
the mirror for every `dind` service. Update the
[configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
to specify a [volume
mount](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#volumes-in-the-runnersdocker-section).
to specify a [volume mount](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#volumes-in-the-runnersdocker-section).
For example, if you have a `/opt/docker/daemon.json` file with the following
content:
......@@ -552,11 +532,10 @@ picked up by the `dind` service.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3223) in GitLab Runner 13.6.
If you are an administrator of GitLab Runner and you want to use
the mirror for every `dind` service, update the
If you are a GitLab Runner administrator, you can use
the mirror for every `dind` service. Update the
[configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
to specify a [ConfigMap volume
mount](https://docs.gitlab.com/runner/executors/kubernetes.html#using-volumes).
to specify a [ConfigMap volume mount](https://docs.gitlab.com/runner/executors/kubernetes.html#using-volumes).
For example, if you have a `/tmp/daemon.json` file with the following
content:
......@@ -602,7 +581,7 @@ The configuration is picked up by the `dind` service.
When you use Docker-in-Docker, the [normal authentication
methods](using_docker_images.html#define-an-image-from-a-private-container-registry)
won't work because a fresh Docker daemon is started with the service.
don't work because a fresh Docker daemon is started with the service.
### Option 1: Run `docker login`
......@@ -634,14 +613,14 @@ empty or remove it.
If you are an administrator for GitLab Runner, you can mount a file
with the authentication configuration to `~/.docker/config.json`.
Then every job that the runner picks up will be authenticated already. If you
Then every job that the runner picks up is authenticated already. If you
are using the official `docker:19.03.13` image, the home directory is
under `/root`.
If you mount the configuration file, any `docker` command
that modifies the `~/.docker/config.json` (for example, `docker login`)
fails, because the file is mounted as read-only. Do not change it from
read-only, because other problems will occur.
read-only, because problems occur.
Here is an example of `/opt/.docker/config.json` that follows the
[`DOCKER_AUTH_CONFIG`](using_docker_images.md#determining-your-docker_auth_config-data)
......@@ -743,8 +722,8 @@ build:
When using Docker-in-Docker, Docker downloads all layers of your image every
time you create a build. Recent versions of Docker (Docker 1.13 and above) can
use a pre-existing image as a cache during the `docker build` step, considerably
speeding up the build process.
use a pre-existing image as a cache during the `docker build` step. This considerably
speeds up the build process.
### How Docker caching works
......@@ -754,8 +733,8 @@ any changes. Change in one layer causes all subsequent layers to be recreated.
You can specify a tagged image to be used as a cache source for the `docker build`
command by using the `--cache-from` argument. Multiple images can be specified
as a cache source by using multiple `--cache-from` arguments. Keep in mind that
any image that's used with the `--cache-from` argument must first be pulled
as a cache source by using multiple `--cache-from` arguments. Any image that's used
with the `--cache-from` argument must first be pulled
(using `docker pull`) before it can be used as a cache source.
### Using Docker caching
......
......@@ -38,7 +38,7 @@ each node should have:
- [Memory](https://www.elastic.co/guide/en/elasticsearch/guide/current/hardware.html#_memory): 8 GiB (minimum).
- [CPU](https://www.elastic.co/guide/en/elasticsearch/guide/current/hardware.html#_cpus): Modern processor with multiple cores.
- [Storage](https://www.elastic.co/guide/en/elasticsearch/guide/current/hardware.html#_disks): Use SSD storage. You will need enough storage for 50% of the total size of your Git repositories.
- [Storage](https://www.elastic.co/guide/en/elasticsearch/guide/current/hardware.html#_disks): Use SSD storage. The total storage size of all Elasticsearch nodes is about 50% of the total size of your Git repositories. It includes one primary and one replica.
A few notes on CPU and storage:
......
---
stage: Enablement
group: Distribution
group: Geo
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
---
......
......@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference
---
# Upgrading deployments for newer Auto Deploy dependencies (Auto Deploy template, auto-deploy-image and auto-deploy-app chart)
# Upgrading deployments for newer Auto Deploy dependencies
[Auto Deploy](stages.md#auto-deploy) is a feature that deploys your application to a Kubernetes cluster.
It consists of several dependencies:
......
......@@ -99,6 +99,13 @@ are allowed to expire.
This setting takes precedence over the [project level setting](../../../ci/pipelines/job_artifacts.md#keep-artifacts-from-most-recent-successful-jobs).
If disabled at the instance level, you cannot enable this per-project.
To disable the setting:
1. Go to **Admin Area > Settings > CI/CD**.
1. Expand **Continuous Integration and Deployment**.
1. Clear the **Keep the latest artifacts for all jobs in the latest successful pipelines** checkbox.
1. Click **Save changes**
When you disable the feature, the latest artifacts do not immediately expire.
A new pipeline must run before the latest artifacts can expire and be deleted.
......
......@@ -63,7 +63,7 @@ export default {
} = await this.$apollo.mutate({
mutation: deleteDevopsAdoptionSegmentMutation,
variables: {
id,
id: [id],
},
update(store) {
deleteSegmentFromCache(store, id);
......
mutation($id: AnalyticsDevopsAdoptionSegmentID!) {
mutation($id: [AnalyticsDevopsAdoptionSegmentID!]!) {
deleteDevopsAdoptionSegment(input: { id: $id }) {
errors
}
......
......@@ -30,9 +30,6 @@ export default {
'sidebarCollapsed',
]),
...mapGetters(['isUserSignedIn']),
sidebarStatusClass() {
return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
},
},
};
</script>
......
......@@ -4,7 +4,7 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import AncestorsTree from 'ee/sidebar/components/ancestors_tree/ancestors_tree.vue';
import notesEventHub from '~/notes/event_hub';
import ConfidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarParticipants from '~/sidebar/components/participants/participants.vue';
import sidebarEventHub from '~/sidebar/event_hub';
import SidebarDatePickerCollapsed from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
......@@ -28,7 +28,12 @@ export default {
AncestorsTree,
SidebarParticipants,
SidebarSubscription,
ConfidentialIssueSidebar,
SidebarConfidentialityWidget,
},
data() {
return {
sidebarExpandedOnClick: false,
};
},
computed: {
...mapState([
......@@ -80,6 +85,7 @@ export default {
'toggleStartDateType',
'toggleDueDateType',
'saveDate',
'updateConfidentialityOnIssuable',
]),
getDateFromMilestonesTooltip(dateType) {
return epicUtils.getDateFromMilestonesTooltip({
......@@ -129,6 +135,15 @@ export default {
updateEpicConfidentiality(confidential) {
notesEventHub.$emit('notesApp.updateIssuableConfidentiality', confidential);
},
handleSidebarToggle() {
if (this.sidebarCollapsed) {
this.sidebarExpandedOnClick = true;
this.toggleSidebar({ sidebarCollapsed: true });
} else if (this.sidebarExpandedOnClick) {
this.sidebarExpandedOnClick = false;
this.toggleSidebar({ sidebarCollapsed: false });
}
},
},
};
</script>
......@@ -209,13 +224,12 @@ export default {
<div v-if="allowSubEpics" class="block ancestors">
<ancestors-tree :ancestors="ancestors" :is-fetching="false" data-testid="ancestors" />
</div>
<confidential-issue-sidebar
:is-editable="canUpdate"
:full-path="fullPath"
<sidebar-confidentiality-widget
issuable-type="epic"
@closeForm="handleSidebarToggle"
@expandSidebar="handleSidebarToggle"
@confidentialityUpdated="updateConfidentialityOnIssuable($event)"
/>
<div class="block participants">
<sidebar-participants
:participants="participants"
......
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions } from 'vuex';
import { parseIssuableData } from '~/issue_show/utils/parse_data';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { defaultClient } from '~/sidebar/graphql';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
import EpicApp from './components/epic_app.vue';
import createStore from './store';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient,
});
export default () => {
const el = document.getElementById('epic-app-root');
......@@ -31,8 +38,14 @@ export default () => {
return new Vue({
el,
apolloProvider,
store,
components: { EpicApp },
provide: {
canUpdate: epicData.canUpdate,
fullPath: epicData.fullPath,
iid: epicMeta.epicIid,
},
created() {
this.setEpicMeta({
...epicMeta,
......
......@@ -195,34 +195,8 @@ export const saveDate = ({ state, dispatch }, { dateType, dateTypeIsFixed, newDa
});
};
export const updateConfidentialityOnIssuable = ({ state, commit }, { confidential }) => {
const updateEpicInput = {
iid: `${state.epicIid}`,
groupPath: state.fullPath,
confidential,
};
return epicUtils.gqClient
.mutate({
mutation: updateEpic,
variables: {
updateEpicInput,
},
})
.then(({ data }) => {
if (!data?.updateEpic?.errors.length) {
commit(types.SET_EPIC_CONFIDENTIAL, confidential);
} else {
const errMsg =
data?.updateEpic?.errors[0]?.replace(/Confidential /, '') ||
s__('Epics|Unable to perform this action');
throw errMsg;
}
})
.catch((error) => {
flash(error);
throw error;
});
export const updateConfidentialityOnIssuable = ({ commit }, confidential) => {
commit(types.SET_EPIC_CONFIDENTIAL, confidential);
};
/**
......
<script>
import { GlFormGroup, GlFormInput, GlFormCheckbox } from '@gitlab/ui';
import { initFormField } from 'ee/security_configuration/utils';
import { __ } from '~/locale';
import validation from '~/vue_shared/directives/validation';
export default {
......@@ -13,7 +14,7 @@ export default {
validation: validation(),
},
props: {
fields: {
value: {
type: Object,
required: false,
default: () => ({}),
......@@ -26,32 +27,41 @@ export default {
},
data() {
const {
authEnabled,
authenticationUrl,
userName,
enabled,
url,
username,
password,
// default to commonly used names for `userName` and `password` fields in authentcation forms
userNameFormField = 'username',
passwordFormField = 'password',
} = this.fields;
// default to commonly used names for `username` and `password` fields in authentcation forms
usernameField = 'username',
passwordField = 'password',
} = this.value.fields;
const isEditMode = Object.keys(this.value.fields).length > 0;
return {
form: {
state: false,
fields: {
authEnabled: initFormField({ value: authEnabled, skipValidation: true }),
authenticationUrl: initFormField({ value: authenticationUrl }),
userName: initFormField({ value: userName }),
password: initFormField({ value: password }),
userNameFormField: initFormField({ value: userNameFormField }),
passwordFormField: initFormField({ value: passwordFormField }),
enabled: initFormField({ value: enabled, skipValidation: true }),
url: initFormField({ value: url }),
username: initFormField({ value: username }),
password: isEditMode
? initFormField({ value: password, required: false, skipValidation: true })
: initFormField({ value: password }),
usernameField: initFormField({ value: usernameField }),
passwordField: initFormField({ value: passwordField }),
},
},
isEditMode,
isSensitiveFieldRequired: !isEditMode,
};
},
computed: {
showValidationOrInEditMode() {
return this.showValidation || Object.keys(this.fields).length > 0;
return this.showValidation || this.isEditMode;
},
sensitiveFieldPlaceholder() {
return this.isEditMode ? __('[Unchanged]') : '';
},
},
watch: {
......@@ -68,41 +78,41 @@ export default {
<template>
<section>
<gl-form-group :label="s__('DastProfiles|Authentication')">
<gl-form-checkbox v-model="form.fields.authEnabled.value">{{
<gl-form-checkbox v-model="form.fields.enabled.value" data-testid="auth-enable-checkbox">{{
s__('DastProfiles|Enable Authentication')
}}</gl-form-checkbox>
</gl-form-group>
<div v-if="form.fields.authEnabled.value" data-testid="auth-form">
<div v-if="form.fields.enabled.value" data-testid="auth-form">
<div class="row">
<gl-form-group
:label="s__('DastProfiles|Authentication URL')"
:invalid-feedback="form.fields.authenticationUrl.feedback"
:invalid-feedback="form.fields.url.feedback"
class="col-md-6"
>
<gl-form-input
v-model="form.fields.authenticationUrl.value"
v-model="form.fields.url.value"
v-validation:[showValidationOrInEditMode]
name="authenticationUrl"
name="url"
type="url"
required
:state="form.fields.authenticationUrl.state"
:state="form.fields.url.state"
/>
</gl-form-group>
</div>
<div class="row">
<gl-form-group
:label="s__('DastProfiles|Username')"
:invalid-feedback="form.fields.userName.feedback"
:invalid-feedback="form.fields.username.feedback"
class="col-md-6"
>
<gl-form-input
v-model="form.fields.userName.value"
v-model="form.fields.username.value"
v-validation:[showValidationOrInEditMode]
autocomplete="off"
name="userName"
name="username"
type="text"
required
:state="form.fields.userName.state"
:state="form.fields.username.state"
/>
</gl-form-group>
<gl-form-group
......@@ -116,7 +126,8 @@ export default {
autocomplete="off"
name="password"
type="password"
required
:placeholder="sensitiveFieldPlaceholder"
:required="isSensitiveFieldRequired"
:state="form.fields.password.state"
/>
</gl-form-group>
......@@ -124,30 +135,30 @@ export default {
<div class="row">
<gl-form-group
:label="s__('DastProfiles|Username form field')"
:invalid-feedback="form.fields.userNameFormField.feedback"
:invalid-feedback="form.fields.usernameField.feedback"
class="col-md-6"
>
<gl-form-input
v-model="form.fields.userNameFormField.value"
v-model="form.fields.usernameField.value"
v-validation:[showValidationOrInEditMode]
name="userNameFormField"
name="usernameField"
type="text"
required
:state="form.fields.userNameFormField.state"
:state="form.fields.usernameField.state"
/>
</gl-form-group>
<gl-form-group
:label="s__('DastProfiles|Password form field')"
:invalid-feedback="form.fields.passwordFormField.feedback"
:invalid-feedback="form.fields.passwordField.feedback"
class="col-md-6"
>
<gl-form-input
v-model="form.fields.passwordFormField.value"
v-model="form.fields.passwordField.value"
v-validation:[showValidationOrInEditMode]
name="passwordFormField"
name="passwordField"
type="text"
required
:state="form.fields.passwordFormField.state"
:state="form.fields.passwordField.state"
/>
</gl-form-group>
</div>
......
......@@ -56,7 +56,7 @@ export default {
},
},
data() {
const { name = '', targetUrl = '', excludedUrls = '', requestHeaders = '' } =
const { name = '', targetUrl = '', excludedUrls = '', requestHeaders = '', auth = {} } =
this.siteProfile || {};
const form = {
......@@ -76,7 +76,7 @@ export default {
return {
form,
authSection: {},
authSection: { fields: auth },
initialFormValues: serializeFormObject(form.fields),
isLoading: false,
hasAlert: false,
......@@ -126,7 +126,7 @@ export default {
onSubmit() {
const isAuthEnabled =
this.glFeatures.securityDastSiteProfilesAdditionalFields &&
this.authSection.fields.authEnabled.value;
this.authSection.fields.enabled.value;
this.form.showValidation = true;
......@@ -143,7 +143,7 @@ export default {
fullPath: this.fullPath,
...(this.isEdit ? { id: this.siteProfile.id } : {}),
...serializeFormObject(this.form.fields),
...(isAuthEnabled ? serializeFormObject(this.authSection.fields) : {}),
auth: isAuthEnabled ? serializeFormObject(this.authSection.fields) : {},
},
};
......
<script>
import { GlAlert, GlButton, GlIcon, GlLink } from '@gitlab/ui';
import { GlAlert, GlButton, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { cloneDeep } from 'lodash';
import DynamicFields from 'ee/security_configuration/components/dynamic_fields.vue';
......@@ -22,6 +22,7 @@ export default {
GlButton,
GlIcon,
GlLink,
GlSprintf,
},
inject: {
createSastMergeRequestPath: {
......@@ -55,6 +56,7 @@ export default {
analyzersConfiguration: cloneDeep(this.sastCiConfiguration.analyzers.nodes),
hasSubmissionError: false,
isSubmitting: false,
showAnalyzersTip: false,
};
},
computed: {
......@@ -62,6 +64,9 @@ export default {
return this.analyzersConfiguration.length > 0;
},
},
beforeMount() {
this.shouldRenderAnalyzersTip();
},
methods: {
onSubmit() {
this.isSubmitting = true;
......@@ -100,7 +105,20 @@ export default {
analyzers: this.analyzersConfiguration.map(toSastCiConfigurationAnalyzerEntityInput),
};
},
shouldRenderAnalyzersTip() {
this.analyzersConfiguration.some((analyzer) => {
if (analyzer.enabled === false && this.showAnalyzersTip === false) {
this.showAnalyzersTip = true;
return true;
}
return false;
});
},
onAnalyzerChange(name, updatedAnalyzer) {
// show AnalyzersTip when Analyzer was unchecked
if (updatedAnalyzer.enabled === false && this.showAnalyzersTip === false) {
this.showAnalyzersTip = true;
}
const index = this.analyzersConfiguration.findIndex((analyzer) => analyzer.name === name);
if (index === -1) {
return;
......@@ -108,6 +126,9 @@ export default {
this.analyzersConfiguration.splice(index, 1, updatedAnalyzer);
},
dismissAnalyzersTip() {
this.showAnalyzersTip = false;
},
},
i18n: {
submissionError: s__(
......@@ -122,6 +143,10 @@ export default {
cover all languages across your project, and only run if the language is
detected in the Merge Request.`,
),
analyzersTipHeading: s__('We recommend leaving all SAST analyzers enabled'),
analyzersTipBody: s__(
'Keeping all SAST analyzers enabled future-proofs the project in case new languages are added later on. Determining which analyzers apply is a process that consumes minimal resources and adds minimal time to the pipeline. Leaving all SAST analyzers enabled ensures maximum coverage.',
),
},
};
</script>
......@@ -157,13 +182,27 @@ export default {
:entity="analyzer"
@input="onAnalyzerChange(analyzer.name, $event)"
/>
<gl-alert
v-if="showAnalyzersTip"
data-testid="analyzers-section-tip"
:title="$options.i18n.analyzersTipHeading"
variant="tip"
@dismiss="dismissAnalyzersTip"
>
<gl-sprintf :message="$options.i18n.analyzersTipBody" />
</gl-alert>
</expandable-section>
<hr v-else />
<gl-alert v-if="hasSubmissionError" class="gl-mb-5" variant="danger" :dismissible="false">{{
$options.i18n.submissionError
}}</gl-alert>
<gl-alert
v-if="hasSubmissionError"
data-testid="analyzers-error-alert"
class="gl-mb-5"
variant="danger"
:dismissible="false"
>{{ $options.i18n.submissionError }}</gl-alert
>
<div class="gl-display-flex">
<gl-button
......
......@@ -8,7 +8,6 @@ class TrialsController < ApplicationController
before_action :check_if_gl_com_or_dev
before_action :authenticate_user!
before_action :find_or_create_namespace, only: :apply
before_action :record_user_for_group_only_trials_experiment, only: :select
feature_category :purchase
......@@ -121,10 +120,6 @@ class TrialsController < ApplicationController
group
end
def record_user_for_group_only_trials_experiment
record_experiment_user(:group_only_trials)
end
def remove_known_trial_form_fields_context
{
first_name_present: current_user.first_name.present?,
......
......@@ -5,7 +5,7 @@ module EE
module Boards
module Create
extend ActiveSupport::Concern
include Mutations::Boards::ScopedBoardMutation
prepend ::Mutations::Boards::ScopedBoardMutation
prepended do
include Mutations::Boards::ScopedBoardArguments
......
......@@ -10,7 +10,6 @@ module EE
argument :assignee_id,
::Types::GlobalIDType[::User],
required: false,
loads: ::Types::UserType,
description: 'The ID of user to be assigned to the board.'
# Cannot pre-load ::Types::MilestoneType because we are also assigning values like:
......
# frozen_string_literal: true
module EE
module Mutations
module Boards
module ScopedBoardMutation
extend ::Gitlab::Utils::Override
override :resolve
def resolve(**args)
parsed_params = parse_arguments(args)
super(**parsed_params)
end
def ready?(**args)
if args.slice(*mutually_exclusive_args).size > 1
arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
raise ::Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required"
end
super
end
private
def parse_arguments(args = {})
if args[:assignee_id]
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
args[:assignee_id] = ::Types::GlobalIDType[::User].coerce_isolated_input(args[:assignee_id])
args[:assignee_id] = args[:assignee_id].model_id
end
if args[:milestone_id]
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
args[:milestone_id] = ::Types::GlobalIDType[::Milestone].coerce_isolated_input(args[:milestone_id])
args[:milestone_id] = args[:milestone_id].model_id
end
args[:label_ids] &&= args[:label_ids].map do |label_id|
::GitlabSchema.parse_gid(label_id, expected_type: ::Label).model_id
end
# we need this because we also pass `gid://gitlab/Iteration/-4` or `gid://gitlab/Iteration/-4`
# as `iteration_id` when we scope board to `Iteration::Predefined::Current` or `Iteration::Predefined::None`
args[:iteration_id] = args[:iteration_id].model_id if args[:iteration_id]
args
end
def mutually_exclusive_args
[:labels, :label_ids]
end
end
end
end
end
......@@ -5,7 +5,7 @@ module EE
module Boards
module Update
extend ActiveSupport::Concern
include Mutations::Boards::ScopedBoardMutation
prepend ::Mutations::Boards::ScopedBoardMutation
prepended do
include Mutations::Boards::ScopedBoardArguments
......
......@@ -19,6 +19,10 @@ module EE
null: false, calls_gitaly: true,
method: :has_security_reports?,
description: 'Indicates if the source branch has any security reports.'
field :security_reports_up_to_date_on_target_branch, GraphQL::BOOLEAN_TYPE,
null: false, calls_gitaly: true,
method: :security_reports_up_to_date?,
description: 'Indicates if the target branch security reports are out of date.'
end
def merge_trains_count
......
......@@ -37,6 +37,7 @@ module EE
mount_mutation ::Mutations::Boards::Update
mount_mutation ::Mutations::Boards::UpdateEpicUserPreferences
mount_mutation ::Mutations::Boards::EpicBoards::Create
mount_mutation ::Mutations::Boards::EpicBoards::Update
mount_mutation ::Mutations::Boards::EpicLists::Create
mount_mutation ::Mutations::Boards::Lists::UpdateLimitMetrics
mount_mutation ::Mutations::InstanceSecurityDashboard::AddProject
......@@ -58,6 +59,7 @@ module EE
mount_mutation ::Mutations::Namespaces::IncreaseStorageTemporarily
mount_mutation ::Mutations::QualityManagement::TestCases::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::BulkFindOrCreate
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Update
......
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreate < BaseMutation
include Mixins::CommonMethods
graphql_name 'BulkFindOrCreateDevopsAdoptionSegments'
argument :namespace_ids, [::Types::GlobalIDType[::Namespace]],
required: true,
description: 'List of Namespace IDs for the segments.'
field :segments,
[::Types::Admin::Analytics::DevopsAdoption::SegmentType],
null: true,
description: 'Created segments after mutation.'
def resolve(namespace_ids:, **)
namespaces = GlobalID::Locator.locate_many(namespace_ids)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkFindOrCreateService
.new(current_user: current_user, params: { namespaces: namespaces })
segments = service.execute.payload.fetch(:segments)
{
segments: segments.select(&:persisted?),
errors: segments.sum { |segment| errors_on_object(segment) }
}
end
end
end
end
end
end
end
end
......@@ -22,11 +22,14 @@ module Mutations
def resolve(namespace_id:, **)
namespace = namespace_id.find
response = ::Analytics::DevopsAdoption::Segments::CreateService
.new(current_user: current_user, params: { namespace: namespace })
.execute
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::CreateService
.new(current_user: current_user, params: { namespace: namespace })
resolve_segment(response)
response = service.execute
resolve_segment(response)
end
end
end
end
......
......@@ -10,16 +10,23 @@ module Mutations
graphql_name 'DeleteDevopsAdoptionSegment'
argument :id, ::Types::GlobalIDType[::Analytics::DevopsAdoption::Segment],
argument :id, [::Types::GlobalIDType[::Analytics::DevopsAdoption::Segment]],
required: true,
description: "ID of the segment."
description: "One or many IDs of the segments to delete."
def resolve(id:, **)
response = ::Analytics::DevopsAdoption::Segments::DeleteService
.new(segment: id.find, current_user: current_user)
.execute
segments = GlobalID::Locator.locate_many(id)
{ errors: errors_on_object(response.payload[:segment]) }
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkDeleteService
.new(segments: segments, current_user: current_user)
response = service.execute
errors = response.payload[:segments].sum { |segment| errors_on_object(segment) }
{ errors: errors }
end
end
end
end
......
......@@ -13,11 +13,7 @@ module Mutations
def ready?(**args)
unless License.feature_available?(:instance_level_devops_adoption)
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, FEATURE_UNAVAILABLE_MESSAGE
end
unless current_user&.admin?
raise Gitlab::Graphql::Errors::ResourceNotAvailable, ADMIN_MESSAGE
raise_resource_not_available_error!(FEATURE_UNAVAILABLE_MESSAGE)
end
super
......@@ -33,6 +29,16 @@ module Mutations
errors: errors_on_object(segment)
}
end
def with_authorization_handler
yield
rescue ::Analytics::DevopsAdoption::Segments::AuthorizationError => e
handle_unauthorized!(e)
end
def handle_unauthorized!(_exception)
raise_resource_not_available_error!(ADMIN_MESSAGE)
end
end
end
end
......
# frozen_string_literal: true
module Mutations
module Boards
module EpicBoards
class Update < ::Mutations::BaseMutation
include Mutations::Boards::CommonMutationArguments
prepend Mutations::Boards::ScopedBoardMutation
graphql_name 'EpicBoardUpdate'
authorize :admin_epic_board
argument :id,
::Types::GlobalIDType[::Boards::EpicBoard],
required: true,
description: 'The epic board global ID.'
argument :labels, [GraphQL::STRING_TYPE],
required: false,
description: 'Labels to be added to the board.'
argument :label_ids, [::Types::GlobalIDType[::Label]],
required: false,
description: 'The IDs of labels to be added to the board.'
field :epic_board,
Types::Boards::EpicBoardType,
null: true,
description: 'The updated epic board.'
def resolve(**args)
board = authorized_find!(id: args[:id])
unless Feature.enabled?(:epic_boards, board.resource_parent)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'epic_boards feature is disabled'
end
::Boards::EpicBoards::UpdateService.new(board.resource_parent, current_user, args).execute(board)
{
epic_board: board.reset,
errors: errors_on_object(board)
}
end
private
def find_object(id:)
GitlabSchema.find_by_gid(id)
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module Boards
module ScopedBoardMutation
extend ActiveSupport::Concern
def resolve(**args)
parsed_params = parse_arguments(args)
super(**parsed_params)
end
def ready?(**args)
if args.slice(*mutually_exclusive_args).size > 1
arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
raise ::Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required"
end
super
end
private
def parse_arguments(args = {})
if args[:assignee_id]
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
args[:assignee_id] = ::Types::GlobalIDType[::User].coerce_isolated_input(args[:assignee_id])
args[:assignee_id] = args[:assignee_id].model_id
end
if args[:milestone_id]
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
args[:milestone_id] = ::Types::GlobalIDType[::Milestone].coerce_isolated_input(args[:milestone_id])
args[:milestone_id] = args[:milestone_id].model_id
end
args[:label_ids] &&= args[:label_ids].map do |label_id|
::GitlabSchema.parse_gid(label_id, expected_type: ::Label).model_id
end
# we need this because we also pass `gid://gitlab/Iteration/-4` or `gid://gitlab/Iteration/-4`
# as `iteration_id` when we scope board to `Iteration::Predefined::Current` or `Iteration::Predefined::None`
args[:iteration_id] = args[:iteration_id].model_id if args[:iteration_id]
args
end
def mutually_exclusive_args
[:labels, :label_ids]
end
end
end
end
......@@ -23,6 +23,9 @@ module Types
field :hide_closed_list, type: GraphQL::BOOLEAN_TYPE, null: true,
description: 'Whether or not closed list is hidden.'
field :labels, ::Types::LabelType.connection_type, null: true,
description: 'Labels of the board.'
field :lists,
Types::Boards::EpicListType.connection_type,
null: true,
......
......@@ -24,11 +24,7 @@ module EE
end
def trial_selection_intro_text
if any_trial_user_namespaces? && any_trial_group_namespaces?
s_('Trials|You can apply your trial to a new group, an existing group, or your personal account.')
elsif any_trial_user_namespaces?
s_('Trials|You can apply your trial to a new group or your personal account.')
elsif any_trial_group_namespaces?
if any_trial_group_namespaces?
s_('Trials|You can apply your trial to a new group or an existing group.')
else
s_('Trials|Create a new group to start your GitLab Ultimate trial.')
......@@ -36,14 +32,13 @@ module EE
end
def show_trial_namespace_select?
any_trial_group_namespaces? || any_trial_user_namespaces?
any_trial_group_namespaces?
end
def namespace_options_for_select(selected = nil)
grouped_options = {
'New' => [[_('Create group'), 0]],
'Groups' => trial_group_namespaces.map { |n| [n.name, n.id] },
'Users' => trial_user_namespaces.map { |n| [n.name, n.id] }
'Groups' => trial_group_namespaces.map { |n| [n.name, n.id] }
}
grouped_options_for_select(grouped_options, selected, prompt: _('Please select'))
......@@ -65,21 +60,8 @@ module EE
end
end
def trial_user_namespaces
return [] if experiment_enabled?(:group_only_trials)
strong_memoize(:trial_user_namespaces) do
user_namespace = current_user.namespace
user_namespace.eligible_for_trial? ? [user_namespace] : []
end
end
def any_trial_group_namespaces?
trial_group_namespaces.any?
end
def any_trial_user_namespaces?
trial_user_namespaces.any?
end
end
end
......@@ -6,6 +6,7 @@ module Boards
has_many :epic_board_labels, foreign_key: :epic_board_id, inverse_of: :epic_board
has_many :epic_board_positions, foreign_key: :epic_board_id, inverse_of: :epic_board
has_many :epic_lists, -> { ordered }, foreign_key: :epic_board_id, inverse_of: :epic_board
has_many :labels, through: :epic_board_labels
validates :name, length: { maximum: 255 }, presence: true
......@@ -59,14 +60,6 @@ module Boards
nil
end
def label_ids
[]
end
def labels
[]
end
def weight
nil
end
......
......@@ -270,6 +270,10 @@ module EE
end
end
def security_reports_up_to_date?
project.security_reports_up_to_date_for_ref?(target_branch)
end
private
def has_approved_license_check?
......
......@@ -301,6 +301,10 @@ module EE
all_pipelines.newest_first(ref: default_branch).with_reports(reports).take
end
def security_reports_up_to_date_for_ref?(ref)
latest_pipeline_with_security_reports(only_successful: true) == ci_pipelines.newest_first(ref: ref).take
end
def ensure_external_webhook_token
return if external_webhook_token.present?
......
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
class AuthorizationError < StandardError
attr_reader :service
def initialize(service, *args)
@service = service
super(*args)
end
end
end
end
end
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
class BulkDeleteService
include CommonMethods
def initialize(segments:, current_user:)
@segments = segments
@current_user = current_user
end
def execute
authorize!
result = nil
ActiveRecord::Base.transaction do
segments.each do |segment|
response = delete_segment(segment)
if response.error?
result = ServiceResponse.error(message: response.message, payload: response_payload)
raise ActiveRecord::Rollback
end
end
result = ServiceResponse.success(payload: response_payload)
end
result
end
private
attr_reader :segments, :current_user
def response_payload
{ segments: segments }
end
def delete_segment(segment)
DeleteService.new(current_user: current_user, segment: segment).execute
end
end
end
end
end
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreateService
include CommonMethods
def initialize(params: {}, current_user:)
@params = params
@current_user = current_user
end
def execute
authorize!
segments = params[:namespaces].map do |namespace|
response = FindOrCreateService
.new(current_user: current_user, params: { namespace: namespace })
.execute
response.payload[:segment]
end
ServiceResponse.success(payload: { segments: segments })
end
private
attr_reader :params, :current_user
end
end
end
end
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
module CommonMethods
include Gitlab::Allowable
def authorize!
unless can?(current_user, :manage_devops_adoption_segments, :global)
raise AuthorizationError.new(self, 'Forbidden')
end
end
end
end
end
end
......@@ -4,7 +4,7 @@ module Analytics
module DevopsAdoption
module Segments
class CreateService
include Gitlab::Allowable
include CommonMethods
def initialize(segment: Analytics::DevopsAdoption::Segment.new, params: {}, current_user:)
@segment = segment
......@@ -13,9 +13,7 @@ module Analytics
end
def execute
unless can?(current_user, :manage_devops_adoption_segments, :global)
return ServiceResponse.error(message: 'Forbidden', payload: response_payload)
end
authorize!
segment.assign_attributes(attributes)
......
......@@ -4,7 +4,7 @@ module Analytics
module DevopsAdoption
module Segments
class DeleteService
include Gitlab::Allowable
include CommonMethods
def initialize(segment:, current_user:)
@segment = segment
......@@ -12,12 +12,11 @@ module Analytics
end
def execute
unless can?(current_user, :manage_devops_adoption_segments, :global)
return ServiceResponse.error(message: 'Forbidden', payload: response_payload)
end
authorize!
begin
segment.destroy!
ServiceResponse.success(payload: response_payload)
rescue ActiveRecord::RecordNotDestroyed
ServiceResponse.error(message: 'Devops Adoption Segment deletion error', payload: response_payload)
......
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
class FindOrCreateService
include CommonMethods
def initialize(params: {}, current_user:)
@params = params
@current_user = current_user
end
def execute
authorize!
segment = Analytics::DevopsAdoption::Segment.find_by_namespace_id(namespace_id)
if segment
ServiceResponse.success(payload: { segment: segment })
else
CreateService.new(current_user: current_user, params: params).execute
end
end
private
attr_reader :params, :current_user
def namespace_id
params.fetch(:namespace_id, params[:namespace]&.id)
end
end
end
end
end
# frozen_string_literal: true
module Boards
module EpicBoards
class UpdateService < Boards::UpdateService
extend ::Gitlab::Utils::Override
override :permitted_params
def permitted_params
permitted = PERMITTED_PARAMS
if parent.feature_available?(:scoped_issue_board)
permitted += %i(labels label_ids)
end
permitted
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment