Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f2fd07aa1c
commit
4938925517
|
@ -8,4 +8,6 @@
|
||||||
/vendor/
|
/vendor/
|
||||||
/sitespeed-result/
|
/sitespeed-result/
|
||||||
/fixtures/**/*.graphql
|
/fixtures/**/*.graphql
|
||||||
|
# Storybook build artifacts
|
||||||
|
/storybook/public
|
||||||
spec/fixtures/**/*.graphql
|
spec/fixtures/**/*.graphql
|
||||||
|
|
|
@ -897,6 +897,7 @@
|
||||||
rules:
|
rules:
|
||||||
- !reference [".strict-ee-only-rules", rules]
|
- !reference [".strict-ee-only-rules", rules]
|
||||||
- !reference [".frontend:rules:default-frontend-jobs-as-if-foss", rules]
|
- !reference [".frontend:rules:default-frontend-jobs-as-if-foss", rules]
|
||||||
|
- <<: *if-merge-request-labels-run-all-jest
|
||||||
- <<: *if-merge-request
|
- <<: *if-merge-request
|
||||||
changes: *frontend-patterns-for-as-if-foss
|
changes: *frontend-patterns-for-as-if-foss
|
||||||
|
|
||||||
|
|
|
@ -1817,7 +1817,6 @@ Layout/LineLength:
|
||||||
- 'ee/spec/features/groups/scim_token_spec.rb'
|
- 'ee/spec/features/groups/scim_token_spec.rb'
|
||||||
- 'ee/spec/features/groups/security/compliance_dashboards_spec.rb'
|
- 'ee/spec/features/groups/security/compliance_dashboards_spec.rb'
|
||||||
- 'ee/spec/features/groups/sso_spec.rb'
|
- 'ee/spec/features/groups/sso_spec.rb'
|
||||||
- 'ee/spec/features/groups/usage_quotas_spec.rb'
|
|
||||||
- 'ee/spec/features/integrations/jira/jira_issues_list_spec.rb'
|
- 'ee/spec/features/integrations/jira/jira_issues_list_spec.rb'
|
||||||
- 'ee/spec/features/invites_spec.rb'
|
- 'ee/spec/features/invites_spec.rb'
|
||||||
- 'ee/spec/features/issues/filtered_search/filter_issues_weight_spec.rb'
|
- 'ee/spec/features/issues/filtered_search/filter_issues_weight_spec.rb'
|
||||||
|
|
|
@ -123,10 +123,8 @@ RSpec/ContextWording:
|
||||||
- 'ee/spec/features/groups/push_rules_spec.rb'
|
- 'ee/spec/features/groups/push_rules_spec.rb'
|
||||||
- 'ee/spec/features/groups/saml_enforcement_spec.rb'
|
- 'ee/spec/features/groups/saml_enforcement_spec.rb'
|
||||||
- 'ee/spec/features/groups/saml_providers_spec.rb'
|
- 'ee/spec/features/groups/saml_providers_spec.rb'
|
||||||
- 'ee/spec/features/groups/seat_usage/seat_usage_spec.rb'
|
|
||||||
- 'ee/spec/features/groups/security/compliance_dashboards_spec.rb'
|
- 'ee/spec/features/groups/security/compliance_dashboards_spec.rb'
|
||||||
- 'ee/spec/features/groups/sso_spec.rb'
|
- 'ee/spec/features/groups/sso_spec.rb'
|
||||||
- 'ee/spec/features/groups/usage_quotas_spec.rb'
|
|
||||||
- 'ee/spec/features/groups_spec.rb'
|
- 'ee/spec/features/groups_spec.rb'
|
||||||
- 'ee/spec/features/ide/user_commits_changes_spec.rb'
|
- 'ee/spec/features/ide/user_commits_changes_spec.rb'
|
||||||
- 'ee/spec/features/ide/user_opens_ide_spec.rb'
|
- 'ee/spec/features/ide/user_opens_ide_spec.rb'
|
||||||
|
|
|
@ -4,7 +4,6 @@ RSpec/EmptyLineAfterHook:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'ee/spec/controllers/projects/integrations/zentao/issues_controller_spec.rb'
|
- 'ee/spec/controllers/projects/integrations/zentao/issues_controller_spec.rb'
|
||||||
- 'ee/spec/controllers/projects/push_rules_controller_spec.rb'
|
- 'ee/spec/controllers/projects/push_rules_controller_spec.rb'
|
||||||
- 'ee/spec/features/groups/usage_quotas_spec.rb'
|
|
||||||
- 'ee/spec/features/issues/user_bulk_edits_issues_spec.rb'
|
- 'ee/spec/features/issues/user_bulk_edits_issues_spec.rb'
|
||||||
- 'ee/spec/features/profiles/usage_quotas_spec.rb'
|
- 'ee/spec/features/profiles/usage_quotas_spec.rb'
|
||||||
- 'ee/spec/lib/ee/api/entities/user_with_admin_spec.rb'
|
- 'ee/spec/lib/ee/api/entities/user_with_admin_spec.rb'
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
1cbf6d9ce79fe9df99b545529f7b7d754baea080
|
af0cd47633f6e0a5b8ac349a2584c01164af701a
|
||||||
|
|
|
@ -67,7 +67,7 @@ export default {
|
||||||
:quick-actions-docs-path="quickActionsDocsPath"
|
:quick-actions-docs-path="quickActionsDocsPath"
|
||||||
:enable-autocomplete="enableAutocomplete"
|
:enable-autocomplete="enableAutocomplete"
|
||||||
supports-quick-actions
|
supports-quick-actions
|
||||||
init-on-autofocus
|
autofocus
|
||||||
@input="$emit('input', $event)"
|
@input="$emit('input', $event)"
|
||||||
@keydown.meta.enter="updateIssuable"
|
@keydown.meta.enter="updateIssuable"
|
||||||
@keydown.ctrl.enter="updateIssuable"
|
@keydown.ctrl.enter="updateIssuable"
|
||||||
|
|
|
@ -95,13 +95,8 @@ export default {
|
||||||
<template #right-actions>
|
<template #right-actions>
|
||||||
<slot name="commands"></slot>
|
<slot name="commands"></slot>
|
||||||
</template>
|
</template>
|
||||||
<template #metadata-count>
|
<template v-if="imagesCount" #metadata-count>
|
||||||
<metadata-item
|
<metadata-item data-testid="images-count" icon="container-image" :text="imagesCountText" />
|
||||||
v-if="imagesCount"
|
|
||||||
data-testid="images-count"
|
|
||||||
icon="container-image"
|
|
||||||
:text="imagesCountText"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template #metadata-exp-policies>
|
<template #metadata-exp-policies>
|
||||||
<metadata-item
|
<metadata-item
|
||||||
|
|
|
@ -343,7 +343,7 @@ export default {
|
||||||
:uploads-path="pageInfo.uploadsPath"
|
:uploads-path="pageInfo.uploadsPath"
|
||||||
:enable-content-editor="isMarkdownFormat"
|
:enable-content-editor="isMarkdownFormat"
|
||||||
:enable-preview="isMarkdownFormat"
|
:enable-preview="isMarkdownFormat"
|
||||||
:init-on-autofocus="pageInfo.persisted"
|
:autofocus="pageInfo.persisted"
|
||||||
:form-field-placeholder="$options.i18n.content.placeholder"
|
:form-field-placeholder="$options.i18n.content.placeholder"
|
||||||
:form-field-aria-label="$options.i18n.content.label"
|
:form-field-aria-label="$options.i18n.content.label"
|
||||||
form-field-id="wiki_content"
|
form-field-id="wiki_content"
|
||||||
|
|
|
@ -72,7 +72,7 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
initOnAutofocus: {
|
autofocus: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -87,20 +87,20 @@ export default {
|
||||||
return {
|
return {
|
||||||
editingMode: EDITING_MODE_MARKDOWN_FIELD,
|
editingMode: EDITING_MODE_MARKDOWN_FIELD,
|
||||||
switchEditingControlEnabled: true,
|
switchEditingControlEnabled: true,
|
||||||
autofocus: this.initOnAutofocus,
|
autofocused: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isContentEditorActive() {
|
isContentEditorActive() {
|
||||||
return this.enableContentEditor && this.editingMode === EDITING_MODE_CONTENT_EDITOR;
|
return this.enableContentEditor && this.editingMode === EDITING_MODE_CONTENT_EDITOR;
|
||||||
},
|
},
|
||||||
contentEditorAutofocus() {
|
contentEditorAutofocused() {
|
||||||
// Match textarea focus behavior
|
// Match textarea focus behavior
|
||||||
return this.autofocus ? 'end' : false;
|
return this.autofocus && !this.autofocused ? 'end' : false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.autofocusTextarea(this.editingMode);
|
this.autofocusTextarea();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateMarkdownFromContentEditor({ markdown }) {
|
updateMarkdownFromContentEditor({ markdown }) {
|
||||||
|
@ -120,7 +120,6 @@ export default {
|
||||||
},
|
},
|
||||||
onEditingModeChange(editingMode) {
|
onEditingModeChange(editingMode) {
|
||||||
this.notifyEditingModeChange(editingMode);
|
this.notifyEditingModeChange(editingMode);
|
||||||
this.enableAutofocus(editingMode);
|
|
||||||
},
|
},
|
||||||
onEditingModeRestored(editingMode) {
|
onEditingModeRestored(editingMode) {
|
||||||
this.notifyEditingModeChange(editingMode);
|
this.notifyEditingModeChange(editingMode);
|
||||||
|
@ -128,15 +127,15 @@ export default {
|
||||||
notifyEditingModeChange(editingMode) {
|
notifyEditingModeChange(editingMode) {
|
||||||
this.$emit(editingMode);
|
this.$emit(editingMode);
|
||||||
},
|
},
|
||||||
enableAutofocus(editingMode) {
|
autofocusTextarea() {
|
||||||
this.autofocus = true;
|
if (this.autofocus && this.editingMode === EDITING_MODE_MARKDOWN_FIELD) {
|
||||||
this.autofocusTextarea(editingMode);
|
|
||||||
},
|
|
||||||
autofocusTextarea(editingMode) {
|
|
||||||
if (this.autofocus && editingMode === EDITING_MODE_MARKDOWN_FIELD) {
|
|
||||||
this.$refs.textarea.focus();
|
this.$refs.textarea.focus();
|
||||||
|
this.setEditorAsAutofocused();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setEditorAsAutofocused() {
|
||||||
|
this.autofocused = true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
switchEditingControlOptions: [
|
switchEditingControlOptions: [
|
||||||
{ text: __('Source'), value: EDITING_MODE_MARKDOWN_FIELD },
|
{ text: __('Source'), value: EDITING_MODE_MARKDOWN_FIELD },
|
||||||
|
@ -197,7 +196,8 @@ export default {
|
||||||
:render-markdown="renderMarkdown"
|
:render-markdown="renderMarkdown"
|
||||||
:uploads-path="uploadsPath"
|
:uploads-path="uploadsPath"
|
||||||
:markdown="value"
|
:markdown="value"
|
||||||
:autofocus="contentEditorAutofocus"
|
:autofocus="contentEditorAutofocused"
|
||||||
|
@initialized="setEditorAsAutofocused"
|
||||||
@change="updateMarkdownFromContentEditor"
|
@change="updateMarkdownFromContentEditor"
|
||||||
@loading="disableSwitchEditingControl"
|
@loading="disableSwitchEditingControl"
|
||||||
@loadingSuccess="enableSwitchEditingControl"
|
@loadingSuccess="enableSwitchEditingControl"
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { __, s__ } from '~/locale';
|
||||||
import EditedAt from '~/issues/show/components/edited.vue';
|
import EditedAt from '~/issues/show/components/edited.vue';
|
||||||
import Tracking from '~/tracking';
|
import Tracking from '~/tracking';
|
||||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||||
import workItemQuery from '../graphql/work_item.query.graphql';
|
import { getWorkItemQuery } from '../utils';
|
||||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||||
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
|
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
|
||||||
|
|
||||||
|
@ -32,6 +32,15 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
fetchByIid: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
queryVariables: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
markdownDocsPath: helpPagePath('user/markdown'),
|
markdownDocsPath: helpPagePath('user/markdown'),
|
||||||
data() {
|
data() {
|
||||||
|
@ -45,11 +54,14 @@ export default {
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
workItem: {
|
workItem: {
|
||||||
query: workItemQuery,
|
query() {
|
||||||
|
return getWorkItemQuery(this.fetchByIid);
|
||||||
|
},
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return this.queryVariables;
|
||||||
id: this.workItemId,
|
},
|
||||||
};
|
update(data) {
|
||||||
|
return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
|
||||||
},
|
},
|
||||||
skip() {
|
skip() {
|
||||||
return !this.workItemId;
|
return !this.workItemId;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import {
|
import {
|
||||||
GlAlert,
|
GlAlert,
|
||||||
GlSkeletonLoader,
|
GlSkeletonLoader,
|
||||||
|
@ -11,6 +12,7 @@ import {
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg';
|
import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg';
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
|
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||||
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
|
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
|
||||||
|
@ -27,12 +29,12 @@ import {
|
||||||
WIDGET_TYPE_ITERATION,
|
WIDGET_TYPE_ITERATION,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
|
||||||
import workItemQuery from '../graphql/work_item.query.graphql';
|
|
||||||
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
|
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
|
||||||
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
|
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
|
||||||
import workItemAssigneesSubscription from '../graphql/work_item_assignees.subscription.graphql';
|
import workItemAssigneesSubscription from '../graphql/work_item_assignees.subscription.graphql';
|
||||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||||
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
|
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
|
||||||
|
import { getWorkItemQuery } from '../utils';
|
||||||
|
|
||||||
import WorkItemActions from './work_item_actions.vue';
|
import WorkItemActions from './work_item_actions.vue';
|
||||||
import WorkItemState from './work_item_state.vue';
|
import WorkItemState from './work_item_state.vue';
|
||||||
|
@ -72,6 +74,7 @@ export default {
|
||||||
WorkItemMilestone,
|
WorkItemMilestone,
|
||||||
},
|
},
|
||||||
mixins: [glFeatureFlagMixin()],
|
mixins: [glFeatureFlagMixin()],
|
||||||
|
inject: ['fullPath'],
|
||||||
props: {
|
props: {
|
||||||
isModal: {
|
isModal: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -83,6 +86,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
iid: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
workItemParentId: {
|
workItemParentId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -100,20 +108,26 @@ export default {
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
workItem: {
|
workItem: {
|
||||||
query: workItemQuery,
|
query() {
|
||||||
|
return getWorkItemQuery(this.fetchByIid);
|
||||||
|
},
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return this.queryVariables;
|
||||||
id: this.workItemId,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
skip() {
|
skip() {
|
||||||
return !this.workItemId;
|
return !this.workItemId;
|
||||||
},
|
},
|
||||||
|
update(data) {
|
||||||
|
const workItem = this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
|
||||||
|
return workItem ?? {};
|
||||||
|
},
|
||||||
error() {
|
error() {
|
||||||
this.error = this.$options.i18n.fetchError;
|
this.setEmptyState();
|
||||||
document.title = s__('404|Not found');
|
|
||||||
},
|
},
|
||||||
result() {
|
result() {
|
||||||
|
if (isEmpty(this.workItem)) {
|
||||||
|
this.setEmptyState();
|
||||||
|
}
|
||||||
if (!this.isModal && this.workItem.project) {
|
if (!this.isModal && this.workItem.project) {
|
||||||
const path = this.workItem.project?.fullPath
|
const path = this.workItem.project?.fullPath
|
||||||
? ` · ${this.workItem.project.fullPath}`
|
? ` · ${this.workItem.project.fullPath}`
|
||||||
|
@ -127,30 +141,33 @@ export default {
|
||||||
document: workItemTitleSubscription,
|
document: workItemTitleSubscription,
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
issuableId: this.workItemId,
|
issuableId: this.workItem.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
skip() {
|
||||||
|
return !this.workItem?.id;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
document: workItemDatesSubscription,
|
document: workItemDatesSubscription,
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
issuableId: this.workItemId,
|
issuableId: this.workItem.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
skip() {
|
skip() {
|
||||||
return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
|
return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE) || !this.workItem?.id;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
document: workItemAssigneesSubscription,
|
document: workItemAssigneesSubscription,
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
issuableId: this.workItemId,
|
issuableId: this.workItem.id,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
skip() {
|
skip() {
|
||||||
return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
|
return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES) || !this.workItem?.id;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -214,6 +231,19 @@ export default {
|
||||||
workItemMilestone() {
|
workItemMilestone() {
|
||||||
return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
|
return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
|
||||||
},
|
},
|
||||||
|
fetchByIid() {
|
||||||
|
return this.glFeatures.useIidInWorkItemsPath && parseBoolean(this.$route.query.iid_path);
|
||||||
|
},
|
||||||
|
queryVariables() {
|
||||||
|
return this.fetchByIid
|
||||||
|
? {
|
||||||
|
fullPath: this.fullPath,
|
||||||
|
iid: this.iid,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
id: this.workItemId,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
/** make sure that if the user has not even dismissed the alert ,
|
/** make sure that if the user has not even dismissed the alert ,
|
||||||
|
@ -231,7 +261,7 @@ export default {
|
||||||
this.updateInProgress = true;
|
this.updateInProgress = true;
|
||||||
let updateMutation = updateWorkItemMutation;
|
let updateMutation = updateWorkItemMutation;
|
||||||
let inputVariables = {
|
let inputVariables = {
|
||||||
id: this.workItemId,
|
id: this.workItem.id,
|
||||||
confidential: confidentialStatus,
|
confidential: confidentialStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -240,7 +270,7 @@ export default {
|
||||||
inputVariables = {
|
inputVariables = {
|
||||||
id: this.parentWorkItem.id,
|
id: this.parentWorkItem.id,
|
||||||
taskData: {
|
taskData: {
|
||||||
id: this.workItemId,
|
id: this.workItem.id,
|
||||||
confidential: confidentialStatus,
|
confidential: confidentialStatus,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -275,6 +305,10 @@ export default {
|
||||||
this.updateInProgress = false;
|
this.updateInProgress = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setEmptyState() {
|
||||||
|
this.error = this.$options.i18n.fetchError;
|
||||||
|
document.title = s__('404|Not found');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
WORK_ITEM_VIEWED_STORAGE_KEY,
|
WORK_ITEM_VIEWED_STORAGE_KEY,
|
||||||
};
|
};
|
||||||
|
@ -352,7 +386,7 @@ export default {
|
||||||
:can-update="canUpdate"
|
:can-update="canUpdate"
|
||||||
:is-confidential="workItem.confidential"
|
:is-confidential="workItem.confidential"
|
||||||
:is-parent-confidential="parentWorkItemConfidentiality"
|
:is-parent-confidential="parentWorkItemConfidentiality"
|
||||||
@deleteWorkItem="$emit('deleteWorkItem', workItemType)"
|
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
|
||||||
@toggleWorkItemConfidentiality="toggleConfidentiality"
|
@toggleWorkItemConfidentiality="toggleConfidentiality"
|
||||||
@error="updateError = $event"
|
@error="updateError = $event"
|
||||||
/>
|
/>
|
||||||
|
@ -406,6 +440,8 @@ export default {
|
||||||
:work-item-id="workItem.id"
|
:work-item-id="workItem.id"
|
||||||
:can-update="canUpdate"
|
:can-update="canUpdate"
|
||||||
:full-path="fullPath"
|
:full-path="fullPath"
|
||||||
|
:fetch-by-iid="fetchByIid"
|
||||||
|
:query-variables="queryVariables"
|
||||||
@error="updateError = $event"
|
@error="updateError = $event"
|
||||||
/>
|
/>
|
||||||
<work-item-due-date
|
<work-item-due-date
|
||||||
|
@ -435,6 +471,8 @@ export default {
|
||||||
:weight="workItemWeight.weight"
|
:weight="workItemWeight.weight"
|
||||||
:work-item-id="workItem.id"
|
:work-item-id="workItem.id"
|
||||||
:work-item-type="workItemType"
|
:work-item-type="workItemType"
|
||||||
|
:fetch-by-iid="fetchByIid"
|
||||||
|
:query-variables="queryVariables"
|
||||||
@error="updateError = $event"
|
@error="updateError = $event"
|
||||||
/>
|
/>
|
||||||
<template v-if="workItemsMvc2Enabled">
|
<template v-if="workItemsMvc2Enabled">
|
||||||
|
@ -445,6 +483,8 @@ export default {
|
||||||
:can-update="canUpdate"
|
:can-update="canUpdate"
|
||||||
:work-item-id="workItem.id"
|
:work-item-id="workItem.id"
|
||||||
:work-item-type="workItemType"
|
:work-item-type="workItemType"
|
||||||
|
:fetch-by-iid="fetchByIid"
|
||||||
|
:query-variables="queryVariables"
|
||||||
@error="updateError = $event"
|
@error="updateError = $event"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -452,6 +492,8 @@ export default {
|
||||||
v-if="hasDescriptionWidget"
|
v-if="hasDescriptionWidget"
|
||||||
:work-item-id="workItem.id"
|
:work-item-id="workItem.id"
|
||||||
:full-path="fullPath"
|
:full-path="fullPath"
|
||||||
|
:fetch-by-iid="fetchByIid"
|
||||||
|
:query-variables="queryVariables"
|
||||||
class="gl-pt-5"
|
class="gl-pt-5"
|
||||||
@error="updateError = $event"
|
@error="updateError = $event"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/labe
|
||||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||||
import { isScopedLabel } from '~/lib/utils/common_utils';
|
import { isScopedLabel } from '~/lib/utils/common_utils';
|
||||||
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
|
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
|
||||||
import workItemQuery from '../graphql/work_item.query.graphql';
|
import { getWorkItemQuery } from '../utils';
|
||||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -50,6 +50,15 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
fetchByIid: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
queryVariables: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -64,11 +73,14 @@ export default {
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
workItem: {
|
workItem: {
|
||||||
query: workItemQuery,
|
query() {
|
||||||
|
return getWorkItemQuery(this.fetchByIid);
|
||||||
|
},
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return this.queryVariables;
|
||||||
id: this.workItemId,
|
},
|
||||||
};
|
update(data) {
|
||||||
|
return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
|
||||||
},
|
},
|
||||||
skip() {
|
skip() {
|
||||||
return !this.workItemId;
|
return !this.workItemId;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
fragment WorkItem on WorkItem {
|
fragment WorkItem on WorkItem {
|
||||||
id
|
id
|
||||||
|
iid
|
||||||
title
|
title
|
||||||
state
|
state
|
||||||
description
|
description
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
#import "./work_item.fragment.graphql"
|
||||||
|
|
||||||
|
query workItemByIid($fullPath: ID!, $iid: String) {
|
||||||
|
workspace: project(fullPath: $fullPath) {
|
||||||
|
id
|
||||||
|
workItems(iid: $iid) {
|
||||||
|
nodes {
|
||||||
|
...WorkItem
|
||||||
|
mockWidgets @client {
|
||||||
|
... on LocalWorkItemMilestone {
|
||||||
|
type
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
expired
|
||||||
|
dueDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,10 +3,11 @@ import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
|
||||||
import { getPreferredLocales, s__ } from '~/locale';
|
import { getPreferredLocales, s__ } from '~/locale';
|
||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||||
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
|
import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
|
||||||
import workItemQuery from '../graphql/work_item.query.graphql';
|
|
||||||
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
|
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
|
||||||
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
|
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
|
||||||
|
import { getWorkItemQuery } from '../utils';
|
||||||
|
|
||||||
import ItemTitle from '../components/item_title.vue';
|
import ItemTitle from '../components/item_title.vue';
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ export default {
|
||||||
ItemTitle,
|
ItemTitle,
|
||||||
GlFormSelect,
|
GlFormSelect,
|
||||||
},
|
},
|
||||||
|
mixins: [glFeatureFlagMixin()],
|
||||||
inject: ['fullPath'],
|
inject: ['fullPath'],
|
||||||
props: {
|
props: {
|
||||||
initialTitle: {
|
initialTitle: {
|
||||||
|
@ -71,6 +73,9 @@ export default {
|
||||||
|
|
||||||
return sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemType);
|
return sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemType);
|
||||||
},
|
},
|
||||||
|
fetchByIid() {
|
||||||
|
return this.glFeatures.useIidInWorkItemsPath;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async createWorkItem() {
|
async createWorkItem() {
|
||||||
|
@ -89,28 +94,47 @@ export default {
|
||||||
workItemTypeId: this.selectedWorkItemType,
|
workItemTypeId: this.selectedWorkItemType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
update(store, { data: { workItemCreate } }) {
|
update: (store, { data: { workItemCreate } }) => {
|
||||||
const { workItem } = workItemCreate;
|
const { workItem } = workItemCreate;
|
||||||
|
const data = this.fetchByIid
|
||||||
|
? {
|
||||||
|
workspace: {
|
||||||
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
|
__typename: 'Project',
|
||||||
|
id: workItem.project.id,
|
||||||
|
workItems: {
|
||||||
|
__typename: 'WorkItemConnection',
|
||||||
|
nodes: [workItem],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: { workItem };
|
||||||
|
|
||||||
store.writeQuery({
|
store.writeQuery({
|
||||||
query: workItemQuery,
|
query: getWorkItemQuery(this.fetchByIid),
|
||||||
variables: {
|
variables: this.fetchByIid
|
||||||
|
? {
|
||||||
|
fullPath: this.fullPath,
|
||||||
|
iid: workItem.iid,
|
||||||
|
}
|
||||||
|
: {
|
||||||
id: workItem.id,
|
id: workItem.id,
|
||||||
},
|
},
|
||||||
data: {
|
data,
|
||||||
workItem,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
workItemCreate: {
|
workItemCreate: {
|
||||||
workItem: { id },
|
workItem: { id, iid },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = response;
|
} = response;
|
||||||
this.$router.push({ name: 'workItem', params: { id: `${getIdFromGraphQLId(id)}` } });
|
const routerParams = this.fetchByIid
|
||||||
|
? { name: 'workItem', params: { id: iid }, query: { iid_path: 'true' } }
|
||||||
|
: { name: 'workItem', params: { id: `${getIdFromGraphQLId(id)}` } };
|
||||||
|
this.$router.push(routerParams);
|
||||||
} catch {
|
} catch {
|
||||||
this.error = this.createErrorText;
|
this.error = this.createErrorText;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,14 +38,12 @@ export default {
|
||||||
this.ZenMode = new ZenMode();
|
this.ZenMode = new ZenMode();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
deleteWorkItem(workItemType) {
|
deleteWorkItem({ workItemType, workItemId: id }) {
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: deleteWorkItemMutation,
|
mutation: deleteWorkItemMutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: { id },
|
||||||
id: this.gid,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(({ data: { workItemDelete, errors } }) => {
|
.then(({ data: { workItemDelete, errors } }) => {
|
||||||
|
@ -72,6 +70,6 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert>
|
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert>
|
||||||
<work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem($event)" />
|
<work-item-detail :work-item-id="gid" :iid="id" @deleteWorkItem="deleteWorkItem($event)" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import workItemQuery from './graphql/work_item.query.graphql';
|
||||||
|
import workItemByIidQuery from './graphql/work_item_by_iid.query.graphql';
|
||||||
|
|
||||||
|
export function getWorkItemQuery(isFetchedByIid) {
|
||||||
|
return isFetchedByIid ? workItemByIidQuery : workItemQuery;
|
||||||
|
}
|
|
@ -3,6 +3,12 @@
|
||||||
class JiraConnect::ApplicationController < ApplicationController
|
class JiraConnect::ApplicationController < ApplicationController
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
CORS_ALLOWED_METHODS = {
|
||||||
|
'/-/jira_connect/oauth_application_id' => %i[GET OPTIONS],
|
||||||
|
'/-/jira_connect/subscriptions' => %i[GET POST OPTIONS],
|
||||||
|
'/-/jira_connect/subscriptions/*' => %i[DELETE OPTIONS]
|
||||||
|
}.freeze
|
||||||
|
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
before_action :verify_atlassian_jwt!
|
before_action :verify_atlassian_jwt!
|
||||||
|
@ -60,4 +66,25 @@ class JiraConnect::ApplicationController < ApplicationController
|
||||||
def auth_token
|
def auth_token
|
||||||
params[:jwt] || request.headers['Authorization']&.split(' ', 2)&.last
|
params[:jwt] || request.headers['Authorization']&.split(' ', 2)&.last
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cors_allowed_methods
|
||||||
|
CORS_ALLOWED_METHODS[resource]
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource
|
||||||
|
request.path.gsub(%r{/\d+$}, '/*')
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_cors_headers
|
||||||
|
return unless allow_cors_request?
|
||||||
|
|
||||||
|
response.set_header('Access-Control-Allow-Origin', Gitlab::CurrentSettings.jira_connect_proxy_url)
|
||||||
|
response.set_header('Access-Control-Allow-Methods', cors_allowed_methods.join(', '))
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_cors_request?
|
||||||
|
return false if cors_allowed_methods.nil?
|
||||||
|
|
||||||
|
!Gitlab.com? && Gitlab::CurrentSettings.jira_connect_proxy_url.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module JiraConnect
|
||||||
|
class CorsPreflightChecksController < ApplicationController
|
||||||
|
feature_category :integrations
|
||||||
|
|
||||||
|
skip_before_action :verify_atlassian_jwt!
|
||||||
|
before_action :set_cors_headers
|
||||||
|
|
||||||
|
def index
|
||||||
|
return render_404 unless allow_cors_request?
|
||||||
|
|
||||||
|
render plain: '', content_type: 'text/plain'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,11 +1,11 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module JiraConnect
|
module JiraConnect
|
||||||
class OauthApplicationIdsController < ::ApplicationController
|
class OauthApplicationIdsController < ApplicationController
|
||||||
feature_category :integrations
|
feature_category :integrations
|
||||||
|
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :verify_atlassian_jwt!
|
||||||
skip_before_action :verify_authenticity_token
|
before_action :set_cors_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
if show_application_id?
|
if show_application_id?
|
||||||
|
|
|
@ -27,6 +27,7 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
|
||||||
before_action :verify_qsh_claim!, only: :index
|
before_action :verify_qsh_claim!, only: :index
|
||||||
before_action :allow_self_managed_content_security_policy, only: :index
|
before_action :allow_self_managed_content_security_policy, only: :index
|
||||||
before_action :authenticate_user!, only: :create
|
before_action :authenticate_user!, only: :create
|
||||||
|
before_action :set_cors_headers
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@subscriptions = current_jira_installation.subscriptions.preload_namespace_route
|
@subscriptions = current_jira_installation.subscriptions.preload_namespace_route
|
||||||
|
|
|
@ -4,6 +4,7 @@ class Projects::WorkItemsController < Projects::ApplicationController
|
||||||
before_action do
|
before_action do
|
||||||
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
|
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
|
||||||
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
|
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
|
||||||
|
push_frontend_feature_flag(:use_iid_in_work_items_path, project)
|
||||||
end
|
end
|
||||||
|
|
||||||
feature_category :team_planning
|
feature_category :team_planning
|
||||||
|
|
|
@ -2789,7 +2789,7 @@ class Project < ApplicationRecord
|
||||||
return unless service_desk_enabled?
|
return unless service_desk_enabled?
|
||||||
|
|
||||||
config = Gitlab.config.incoming_email
|
config = Gitlab.config.incoming_email
|
||||||
wildcard = Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER
|
wildcard = Gitlab::Email::Common::WILDCARD_PLACEHOLDER
|
||||||
|
|
||||||
config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
|
config.address&.gsub(wildcard, "#{full_path_slug}-#{default_service_desk_suffix}")
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,6 +30,7 @@ module Ci
|
||||||
Gitlab::Ci::Pipeline::Chain::Limit::Deployments,
|
Gitlab::Ci::Pipeline::Chain::Limit::Deployments,
|
||||||
Gitlab::Ci::Pipeline::Chain::Validate::External,
|
Gitlab::Ci::Pipeline::Chain::Validate::External,
|
||||||
Gitlab::Ci::Pipeline::Chain::Populate,
|
Gitlab::Ci::Pipeline::Chain::Populate,
|
||||||
|
Gitlab::Ci::Pipeline::Chain::PopulateMetadata,
|
||||||
Gitlab::Ci::Pipeline::Chain::StopDryRun,
|
Gitlab::Ci::Pipeline::Chain::StopDryRun,
|
||||||
Gitlab::Ci::Pipeline::Chain::EnsureEnvironments,
|
Gitlab::Ci::Pipeline::Chain::EnsureEnvironments,
|
||||||
Gitlab::Ci::Pipeline::Chain::EnsureResourceGroups,
|
Gitlab::Ci::Pipeline::Chain::EnsureResourceGroups,
|
||||||
|
|
|
@ -422,22 +422,15 @@ module Gitlab
|
||||||
allow do
|
allow do
|
||||||
origins '*'
|
origins '*'
|
||||||
resource oauth_path,
|
resource oauth_path,
|
||||||
headers: %w(Authorization),
|
# These headers are added as defaults to axios.
|
||||||
|
# See: https://gitlab.com/gitlab-org/gitlab/-/blob/dd1e70d3676891025534dc4a1e89ca9383178fe7/app/assets/javascripts/lib/utils/axios_utils.js#L8)
|
||||||
|
# It's added to declare that this is a XHR request and add the CSRF token without which Rails may reject the request from the frontend.
|
||||||
|
headers: %w(Authorization X-CSRF-Token X-Requested-With),
|
||||||
credentials: false,
|
credentials: false,
|
||||||
methods: %i(post options)
|
methods: %i(post options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Cross-origin requests must be enabled to fetch the self-managed application oauth application ID
|
|
||||||
# for the GitLab for Jira app.
|
|
||||||
allow do
|
|
||||||
origins '*'
|
|
||||||
resource '/-/jira_connect/oauth_application_id',
|
|
||||||
headers: :any,
|
|
||||||
methods: %i(get options),
|
|
||||||
credentials: false
|
|
||||||
end
|
|
||||||
|
|
||||||
# These are routes from doorkeeper-openid_connect:
|
# These are routes from doorkeeper-openid_connect:
|
||||||
# https://github.com/doorkeeper-gem/doorkeeper-openid_connect#routes
|
# https://github.com/doorkeeper-gem/doorkeeper-openid_connect#routes
|
||||||
allow do
|
allow do
|
||||||
|
|
|
@ -19,6 +19,8 @@ metadata:
|
||||||
description: Operations related to access requests
|
description: Operations related to access requests
|
||||||
- name: cluster_agents
|
- name: cluster_agents
|
||||||
description: Operations related to the GitLab agent for Kubernetes
|
description: Operations related to the GitLab agent for Kubernetes
|
||||||
|
- name: ci_resource_groups
|
||||||
|
description: Operations to manage job concurrency with resource groups
|
||||||
- name: deploy_keys
|
- name: deploy_keys
|
||||||
description: Operations related to deploy keys
|
description: Operations related to deploy keys
|
||||||
- name: deploy_tokens
|
- name: deploy_tokens
|
||||||
|
|
|
@ -55,7 +55,10 @@ InitializerConnections.with_disabled_database_connections do
|
||||||
match '/oauth/token' => 'oauth/tokens#create', via: :options
|
match '/oauth/token' => 'oauth/tokens#create', via: :options
|
||||||
match '/oauth/revoke' => 'oauth/tokens#revoke', via: :options
|
match '/oauth/revoke' => 'oauth/tokens#revoke', via: :options
|
||||||
|
|
||||||
match '/-/jira_connect/oauth_application_id' => 'jira_connect/oauth_application_ids#show', via: :options
|
match '/-/jira_connect/oauth_application_id' => 'jira_connect/cors_preflight_checks#index', via: :options
|
||||||
|
match '/-/jira_connect/subscriptions' => 'jira_connect/cors_preflight_checks#index', via: :options
|
||||||
|
match '/-/jira_connect/subscriptions/:id' => 'jira_connect/cors_preflight_checks#index', via: :options
|
||||||
|
match '/-/jira_connect/installations' => 'jira_connect/cors_preflight_checks#index', via: :options
|
||||||
|
|
||||||
# Sign up
|
# Sign up
|
||||||
scope path: '/users/sign_up', module: :registrations, as: :users_sign_up do
|
scope path: '/users/sign_up', module: :registrations, as: :users_sign_up do
|
||||||
|
|
|
@ -13,11 +13,11 @@
|
||||||
- name: "merged_by API field" # The name of the feature to be deprecated
|
- name: "merged_by API field" # The name of the feature to be deprecated
|
||||||
announcement_milestone: "14.7" # The milestone when this feature was first announced as deprecated.
|
announcement_milestone: "14.7" # The milestone when this feature was first announced as deprecated.
|
||||||
announcement_date: "2022-01-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
|
announcement_date: "2022-01-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
|
||||||
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
|
removal_milestone: "16.0" # The milestone when this feature is planned to be removed
|
||||||
removal_date: "2022-05-22" # the date of the milestone release when this feature is planned to be removed
|
removal_date: "2023-05-22" # the date of the milestone release when this feature is planned to be removed
|
||||||
breaking_change: true # If this deprecation is a breaking change, set this value to true
|
breaking_change: true # If this deprecation is a breaking change, set this value to true
|
||||||
body: | # Do not modify this line, instead modify the lines below.
|
body: | # Do not modify this line, instead modify the lines below.
|
||||||
The `merged_by` field in the [merge request API](https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests) is being deprecated and will be removed in GitLab 15.0. This field is being replaced with the `merge_user` field (already present in GraphQL) which more correctly identifies who merged a merge request when performing actions (merge when pipeline succeeds, add to merge train) other than a simple merge.
|
The `merged_by` field in the [merge request API](https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests) has been deprecated in favor of the `merge_user` field which more correctly identifies who merged a merge request when performing actions (merge when pipeline succeeds, add to merge train) other than a simple merge. API users are encouraged to use the new `merge_user` field instead. The `merged_by` field will be removed in v5 of the GitLab REST API.
|
||||||
# The following items are not published on the docs page, but may be used in the future.
|
# The following items are not published on the docs page, but may be used in the future.
|
||||||
stage: create # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
|
stage: create # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
|
||||||
tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
|
tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveTmpIndexMembersOnIdWhereNamespaceIdNull < Gitlab::Database::Migration[2.0]
|
||||||
|
INDEX_NAME = 'tmp_index_members_on_id_where_namespace_id_null'
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
remove_concurrent_index_by_name :members, INDEX_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
add_concurrent_index :members, :id, name: INDEX_NAME, where: 'member_namespace_id IS NULL'
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,77 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MigrateSidekiqQueuedJobs < Gitlab::Database::Migration[2.0]
|
||||||
|
class SidekiqMigrateJobs
|
||||||
|
LOG_FREQUENCY_QUEUES = 10
|
||||||
|
|
||||||
|
attr_reader :logger, :mappings
|
||||||
|
|
||||||
|
# mappings is a hash of WorkerClassName => target_queue_name
|
||||||
|
def initialize(mappings, logger: nil)
|
||||||
|
@mappings = mappings
|
||||||
|
@logger = logger
|
||||||
|
end
|
||||||
|
|
||||||
|
# Migrates jobs from queues that are outside the mappings
|
||||||
|
# rubocop: disable Cop/SidekiqRedisCall
|
||||||
|
def migrate_queues
|
||||||
|
routing_rules_queues = mappings.values.uniq
|
||||||
|
logger&.info("List of queues based on routing rules: #{routing_rules_queues}")
|
||||||
|
Sidekiq.redis do |conn|
|
||||||
|
# Redis 6 supports conn.scan_each(match: "queue:*", type: 'list')
|
||||||
|
conn.scan_each(match: "queue:*") do |key|
|
||||||
|
# Redis 5 compatibility
|
||||||
|
next unless conn.type(key) == 'list'
|
||||||
|
|
||||||
|
queue_from = key.split(':', 2).last
|
||||||
|
next if routing_rules_queues.include?(queue_from)
|
||||||
|
|
||||||
|
logger&.info("Migrating #{queue_from} queue")
|
||||||
|
|
||||||
|
migrated = 0
|
||||||
|
while queue_length(queue_from) > 0
|
||||||
|
begin
|
||||||
|
if migrated >= 0 && migrated % LOG_FREQUENCY_QUEUES == 0
|
||||||
|
logger&.info("Migrating from #{queue_from}. Total: #{queue_length(queue_from)}. Migrated: #{migrated}.")
|
||||||
|
end
|
||||||
|
|
||||||
|
job = conn.rpop "queue:#{queue_from}"
|
||||||
|
job_hash = Sidekiq.load_json job
|
||||||
|
next unless mappings.has_key?(job_hash['class'])
|
||||||
|
|
||||||
|
destination_queue = mappings[job_hash['class']]
|
||||||
|
job_hash['queue'] = destination_queue
|
||||||
|
conn.lpush("queue:#{destination_queue}", Sidekiq.dump_json(job_hash))
|
||||||
|
migrated += 1
|
||||||
|
rescue JSON::ParserError
|
||||||
|
logger&.error("Unmarshal JSON payload from SidekiqMigrateJobs failed. Job: #{job}")
|
||||||
|
next
|
||||||
|
end
|
||||||
|
end
|
||||||
|
logger&.info("Finished migrating #{queue_from} queue")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def queue_length(queue_name)
|
||||||
|
Sidekiq.redis do |conn|
|
||||||
|
conn.llen("queue:#{queue_name}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop: enable Cop/SidekiqRedisCall
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
return if Gitlab.com?
|
||||||
|
|
||||||
|
mappings = Gitlab::SidekiqConfig.worker_queue_mappings
|
||||||
|
logger = ::Gitlab::BackgroundMigration::Logger.build
|
||||||
|
SidekiqMigrateJobs.new(mappings, logger: logger).migrate_queues
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# no-op
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RecreateAsyncTrigramIndexForVulnerabilityReadsContainerImages < Gitlab::Database::Migration[2.0]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
INDEX_NAME = 'index_vulnerability_reads_on_location_image_trigram'
|
||||||
|
REPORT_TYPES = { container_scanning: 2, cluster_image_scanning: 7 }.freeze
|
||||||
|
|
||||||
|
def up
|
||||||
|
remove_concurrent_index_by_name :vulnerability_reads, INDEX_NAME
|
||||||
|
|
||||||
|
prepare_async_index :vulnerability_reads, :location_image,
|
||||||
|
name: INDEX_NAME,
|
||||||
|
using: :gin, opclass: { location_image: :gin_trgm_ops },
|
||||||
|
where: "report_type = ANY (ARRAY[#{REPORT_TYPES.values.join(', ')}]) AND location_image IS NOT NULL"
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
unprepare_async_index :vulnerability_reads, :location_image, name: INDEX_NAME
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
90794c6a9b8b9e08e8b0898e55bc581b8411fd0e85a17fefa916213d82e98099
|
|
@ -0,0 +1 @@
|
||||||
|
662c4df2d65a9259e2eafc11e828ffc15765b92fe3a5291ff869129aaf7bb1c0
|
|
@ -0,0 +1 @@
|
||||||
|
1d7912409bb5afc7de82b7507fb2aeb164253c70a58eaf88d502513577bad979
|
|
@ -31204,8 +31204,6 @@ CREATE INDEX tmp_index_for_project_namespace_id_migration_on_routes ON routes US
|
||||||
|
|
||||||
CREATE INDEX tmp_index_issues_on_issue_type_and_id ON issues USING btree (issue_type, id);
|
CREATE INDEX tmp_index_issues_on_issue_type_and_id ON issues USING btree (issue_type, id);
|
||||||
|
|
||||||
CREATE INDEX tmp_index_members_on_id_where_namespace_id_null ON members USING btree (id) WHERE (member_namespace_id IS NULL);
|
|
||||||
|
|
||||||
CREATE INDEX tmp_index_members_on_state ON members USING btree (state) WHERE (state = 2);
|
CREATE INDEX tmp_index_members_on_state ON members USING btree (state) WHERE (state = 2);
|
||||||
|
|
||||||
CREATE INDEX tmp_index_migrated_container_registries ON container_repositories USING btree (project_id) WHERE ((migration_state = 'import_done'::text) OR (created_at >= '2022-01-23 00:00:00'::timestamp without time zone));
|
CREATE INDEX tmp_index_migrated_container_registries ON container_repositories USING btree (project_id) WHERE ((migration_state = 'import_done'::text) OR (created_at >= '2022-01-23 00:00:00'::timestamp without time zone));
|
||||||
|
|
|
@ -198,7 +198,8 @@ docker login gitlab.example.com:5050
|
||||||
When the Registry is configured to use its own domain, you need a TLS
|
When the Registry is configured to use its own domain, you need a TLS
|
||||||
certificate for that specific domain (for example, `registry.example.com`). You might need
|
certificate for that specific domain (for example, `registry.example.com`). You might need
|
||||||
a wildcard certificate if hosted under a subdomain of your existing GitLab
|
a wildcard certificate if hosted under a subdomain of your existing GitLab
|
||||||
domain, for example, `registry.gitlab.example.com`.
|
domain. For example, `*.gitlab.example.com`, is a wildcard that matches `registry.gitlab.example.com`,
|
||||||
|
and is distinct from `*.example.com`.
|
||||||
|
|
||||||
As well as manually generated SSL certificates (explained here), certificates automatically
|
As well as manually generated SSL certificates (explained here), certificates automatically
|
||||||
generated by Let's Encrypt are also [supported in Omnibus installs](https://docs.gitlab.com/omnibus/settings/ssl.html).
|
generated by Let's Encrypt are also [supported in Omnibus installs](https://docs.gitlab.com/omnibus/settings/ssl.html).
|
||||||
|
|
|
@ -260,6 +260,7 @@ control over how the Pages daemon runs and serves content in your environment.
|
||||||
| `gitlab_id` | The OAuth application public ID. Leave blank to automatically fill when Pages authenticates with GitLab. |
|
| `gitlab_id` | The OAuth application public ID. Leave blank to automatically fill when Pages authenticates with GitLab. |
|
||||||
| `gitlab_secret` | The OAuth application secret. Leave blank to automatically fill when Pages authenticates with GitLab. |
|
| `gitlab_secret` | The OAuth application secret. Leave blank to automatically fill when Pages authenticates with GitLab. |
|
||||||
| `auth_scope` | The OAuth application scope to use for authentication. Must match GitLab Pages OAuth application settings. Leave blank to use `api` scope by default. |
|
| `auth_scope` | The OAuth application scope to use for authentication. Must match GitLab Pages OAuth application settings. Leave blank to use `api` scope by default. |
|
||||||
|
| `auth_cookie_session_timeout` | Authentication cookie session timeout in seconds (default: 600s). A value of `0` means the cookie is deleted after the browser session ends. |
|
||||||
| `gitlab_server` | Server to use for authentication when access control is enabled; defaults to GitLab `external_url`. |
|
| `gitlab_server` | Server to use for authentication when access control is enabled; defaults to GitLab `external_url`. |
|
||||||
| `headers` | Specify any additional http headers that should be sent to the client with each response. Multiple headers can be given as an array, header and value as one string, for example `['my-header: myvalue', 'my-other-header: my-other-value']` |
|
| `headers` | Specify any additional http headers that should be sent to the client with each response. Multiple headers can be given as an array, header and value as one string, for example `['my-header: myvalue', 'my-other-header: my-other-value']` |
|
||||||
| `enable_disk` | Allows the GitLab Pages daemon to serve content from disk. Shall be disabled if shared disk storage isn't available. |
|
| `enable_disk` | Allows the GitLab Pages daemon to serve content from disk. Shall be disabled if shared disk storage isn't available. |
|
||||||
|
|
|
@ -43,35 +43,657 @@ components:
|
||||||
paths:
|
paths:
|
||||||
# METADATA
|
# METADATA
|
||||||
/v4/metadata:
|
/v4/metadata:
|
||||||
$ref: 'v4/metadata.yaml'
|
$ref: '#/metadata'
|
||||||
|
|
||||||
# VERSION
|
# VERSION
|
||||||
/v4/version:
|
/v4/version:
|
||||||
$ref: 'v4/version.yaml'
|
$ref: '#/version'
|
||||||
|
|
||||||
# ACCESS REQUESTS (PROJECTS)
|
# ACCESS REQUESTS (PROJECTS)
|
||||||
/v4/projects/{id}/access_requests:
|
/v4/projects/{id}/access_requests:
|
||||||
$ref: 'v4/access_requests.yaml#/accessRequestsProjects'
|
$ref: '#/accessRequestsProjects'
|
||||||
|
|
||||||
/v4/projects/{id}/access_requests/{user_id}/approve:
|
/v4/projects/{id}/access_requests/{user_id}/approve:
|
||||||
$ref: 'v4/access_requests.yaml#/accessRequestsProjectsApprove'
|
$ref: '#/accessRequestsProjectsApprove'
|
||||||
|
|
||||||
/v4/projects/{id}/access_requests/{user_id}:
|
/v4/projects/{id}/access_requests/{user_id}:
|
||||||
$ref: 'v4/access_requests.yaml#/accessRequestsProjectsDeny'
|
$ref: '#/accessRequestsProjectsDeny'
|
||||||
|
|
||||||
# ACCESS REQUESTS (GROUPS)
|
# ACCESS REQUESTS (GROUPS)
|
||||||
/v4/groups/{id}/access_requests:
|
/v4/groups/{id}/access_requests:
|
||||||
$ref: 'v4/access_requests.yaml#/accessRequestsGroups'
|
$ref: '#/accessRequestsGroups'
|
||||||
|
|
||||||
/v4/groups/{id}/access_requests/{user_id}/approve:
|
/v4/groups/{id}/access_requests/{user_id}/approve:
|
||||||
$ref: 'v4/access_requests.yaml#/accessRequestsGroupsApprove'
|
$ref: '#/accessRequestsGroupsApprove'
|
||||||
|
|
||||||
/v4/groups/{id}/access_requests/{user_id}:
|
/v4/groups/{id}/access_requests/{user_id}:
|
||||||
$ref: 'v4/access_requests.yaml#/accessRequestsGroupsDeny'
|
$ref: '#/accessRequestsGroupsDeny'
|
||||||
|
|
||||||
# ACCESS REQUESTS (PROJECTS)
|
# ACCESS REQUESTS (PROJECTS)
|
||||||
/v4/projects/{id}/access_tokens:
|
/v4/projects/{id}/access_tokens:
|
||||||
$ref: 'v4/access_tokens.yaml#/accessTokens'
|
$ref: '#/accessTokens'
|
||||||
|
|
||||||
/v4/projects/{id}/access_tokens/{token_id}:
|
/v4/projects/{id}/access_tokens/{token_id}:
|
||||||
$ref: 'v4/access_tokens.yaml#/accessTokensRevoke'
|
$ref: '#/accessTokensRevoke'
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- metadata
|
||||||
|
summary: 'Retrieve metadata information for this GitLab instance.'
|
||||||
|
operationId: 'getMetadata'
|
||||||
|
responses:
|
||||||
|
'401':
|
||||||
|
description: 'unauthorized operation'
|
||||||
|
'200':
|
||||||
|
description: 'successful operation'
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
title: 'MetadataResponse'
|
||||||
|
type: 'object'
|
||||||
|
properties:
|
||||||
|
version:
|
||||||
|
type: 'string'
|
||||||
|
revision:
|
||||||
|
type: 'string'
|
||||||
|
kas:
|
||||||
|
type: 'object'
|
||||||
|
properties:
|
||||||
|
enabled:
|
||||||
|
type: 'boolean'
|
||||||
|
externalUrl:
|
||||||
|
type: 'string'
|
||||||
|
nullable: true
|
||||||
|
version:
|
||||||
|
type: 'string'
|
||||||
|
nullable: true
|
||||||
|
examples:
|
||||||
|
Example:
|
||||||
|
value:
|
||||||
|
version: '15.0-pre'
|
||||||
|
revision: 'c401a659d0c'
|
||||||
|
kas:
|
||||||
|
enabled: true
|
||||||
|
externalUrl: 'grpc://gitlab.example.com:8150'
|
||||||
|
version: '15.0.0'
|
||||||
|
|
||||||
|
version:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- version
|
||||||
|
summary: 'Retrieve version information for this GitLab instance.'
|
||||||
|
operationId: 'getVersion'
|
||||||
|
responses:
|
||||||
|
'401':
|
||||||
|
description: 'unauthorized operation'
|
||||||
|
'200':
|
||||||
|
description: 'successful operation'
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
title: 'VersionResponse'
|
||||||
|
type: 'object'
|
||||||
|
properties:
|
||||||
|
version:
|
||||||
|
type: 'string'
|
||||||
|
revision:
|
||||||
|
type: 'string'
|
||||||
|
examples:
|
||||||
|
Example:
|
||||||
|
value:
|
||||||
|
version: '13.3.0-pre'
|
||||||
|
revision: 'f2b05afebb0'
|
||||||
|
|
||||||
|
#/v4/projects/{id}/access_requests
|
||||||
|
accessRequestsProjects:
|
||||||
|
get:
|
||||||
|
description: Lists access requests for a project
|
||||||
|
summary: List access requests for a project
|
||||||
|
operationId: accessRequestsProjects_get
|
||||||
|
tags:
|
||||||
|
- access_requests
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The ID or URL-encoded path of the project owned by the authenticated user.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
responses:
|
||||||
|
'401':
|
||||||
|
description: Unauthorized operation
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
title: ProjectAccessResponse
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
usename:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
requested_at:
|
||||||
|
type: string
|
||||||
|
example:
|
||||||
|
- 'id': 1
|
||||||
|
'username': 'raymond_smith'
|
||||||
|
'name': 'Raymond Smith'
|
||||||
|
'state': 'active'
|
||||||
|
'created_at': '2012-10-22T14:13:35Z'
|
||||||
|
'requested_at': '2012-10-22T14:13:35Z'
|
||||||
|
- 'id': 2
|
||||||
|
'username': 'john_doe'
|
||||||
|
'name': 'John Doe'
|
||||||
|
'state': 'active'
|
||||||
|
'created_at': '2012-10-22T14:13:35Z'
|
||||||
|
'requested_at': '2012-10-22T14:13:35Z'
|
||||||
|
post:
|
||||||
|
description: Requests access for the authenticated user to a project
|
||||||
|
summary: Requests access for the authenticated user to a project
|
||||||
|
operationId: accessRequestsProjects_post
|
||||||
|
tags:
|
||||||
|
- access_requests
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The ID or URL-encoded path of the project owned by the authenticated user.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
responses:
|
||||||
|
'401':
|
||||||
|
description: Unauthorized operation
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
title: ProjectAccessRequest
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
usename:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
requested_at:
|
||||||
|
type: string
|
||||||
|
example:
|
||||||
|
'id': 1
|
||||||
|
'username': 'raymond_smith'
|
||||||
|
'name': 'Raymond Smith'
|
||||||
|
'state': 'active'
|
||||||
|
'created_at': '2012-10-22T14:13:35Z'
|
||||||
|
'requested_at': '2012-10-22T14:13:35Z'
|
||||||
|
|
||||||
|
#/v4/projects/{id}/access_requests/{user_id}/approve
|
||||||
|
accessRequestsProjectsApprove:
|
||||||
|
put:
|
||||||
|
description: Approves access for the authenticated user to a project
|
||||||
|
summary: Approves access for the authenticated user to a project
|
||||||
|
operationId: accessRequestsProjectsApprove_put
|
||||||
|
tags:
|
||||||
|
- access_requests
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The ID or URL-encoded path of the project owned by the authenticated user.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
- name: user_id
|
||||||
|
in: path
|
||||||
|
description: The userID of the access requester
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- name: access_level
|
||||||
|
in: query
|
||||||
|
description: A valid project access level. 0 = no access , 10 = guest, 20 = reporter, 30 = developer, 40 = Maintainer. Default is 30.'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
enum: [0, 10, 20, 30, 40]
|
||||||
|
default: 30
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
'401':
|
||||||
|
description: Unauthorized operation
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
title: ProjectAccessApprove
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
usename:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
access_level:
|
||||||
|
type: integer
|
||||||
|
example:
|
||||||
|
'id': 1
|
||||||
|
'username': 'raymond_smith'
|
||||||
|
'name': 'Raymond Smith'
|
||||||
|
'state': 'active'
|
||||||
|
'created_at': '2012-10-22T14:13:35Z'
|
||||||
|
'access_level': 20
|
||||||
|
|
||||||
|
#/v4/projects/{id}/access_requests/{user_id}
|
||||||
|
accessRequestsProjectsDeny:
|
||||||
|
delete:
|
||||||
|
description: Denies a project access request for the given user
|
||||||
|
summary: Denies a project access request for the given user
|
||||||
|
operationId: accessRequestProjectsDeny_delete
|
||||||
|
tags:
|
||||||
|
- access_requests
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The ID or URL-encoded path of the project owned by the authenticated user.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
- name: user_id
|
||||||
|
in: path
|
||||||
|
description: The user ID of the access requester
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses: # Does anything go here? Markdown doc does not list a response.
|
||||||
|
'401':
|
||||||
|
description: Unauthorized operation
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
|
||||||
|
#/v4/groups/{id}/access_requests
|
||||||
|
accessRequestsGroups:
|
||||||
|
get:
|
||||||
|
description: List access requests for a group
|
||||||
|
summary: List access requests for a group
|
||||||
|
operationId: accessRequestsGroups_get
|
||||||
|
tags:
|
||||||
|
- access_requests
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The ID or URL-encoded path of the group owned by the authenticated user.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
responses:
|
||||||
|
'401':
|
||||||
|
description: Unauthorized operation
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
title: GroupAccessResponse
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
usename:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
requested_at:
|
||||||
|
type: string
|
||||||
|
example:
|
||||||
|
- 'id': 1
|
||||||
|
'username': 'raymond_smith'
|
||||||
|
'name': 'Raymond Smith'
|
||||||
|
'state': 'active'
|
||||||
|
'created_at': '2012-10-22T14:13:35Z'
|
||||||
|
'requested_at': '2012-10-22T14:13:35Z'
|
||||||
|
- 'id': 2
|
||||||
|
'username': 'john_doe'
|
||||||
|
'name': 'John Doe'
|
||||||
|
'state': 'active'
|
||||||
|
'created_at': '2012-10-22T14:13:35Z'
|
||||||
|
'requested_at': '2012-10-22T14:13:35Z'
|
||||||
|
post:
|
||||||
|
description: Requests access for the authenticated user to a group
|
||||||
|
summary: Requests access for the authenticated user to a group
|
||||||
|
operationId: accessRequestsGroups_post
|
||||||
|
tags:
|
||||||
|
- access_requests
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The ID or URL-encoded path of the group owned by the authenticated user.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
responses:
|
||||||
|
'401':
|
||||||
|
description: Unauthorized operation
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
title: GroupAccessRequest
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
usename:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
requested_at:
|
||||||
|
type: string
|
||||||
|
example:
|
||||||
|
'id': 1
|
||||||
|
'username': 'raymond_smith'
|
||||||
|
'name': 'Raymond Smith'
|
||||||
|
'state': 'active'
|
||||||
|
'created_at': '2012-10-22T14:13:35Z'
|
||||||
|
'requested_at': '2012-10-22T14:13:35Z'
|
||||||
|
|
||||||
|
#/v4/groups/{id}/access_requests/{user_id}/approve
|
||||||
|
accessRequestsGroupsApprove:
|
||||||
|
put:
|
||||||
|
description: Approves access for the authenticated user to a group
|
||||||
|
summary: Approves access for the authenticated user to a group
|
||||||
|
operationId: accessRequestsGroupsApprove_put
|
||||||
|
tags:
|
||||||
|
- access_requests
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The ID or URL-encoded path of the group owned by the authenticated user.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
- name: user_id
|
||||||
|
in: path
|
||||||
|
description: The userID of the access requester
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- name: access_level
|
||||||
|
in: query
|
||||||
|
description: A valid group access level. 0 = no access , 10 = Guest, 20 = Reporter, 30 = Developer, 40 = Maintainer, 50 = Owner. Default is 30.
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
enum: [0, 10, 20, 30, 40, 50]
|
||||||
|
default: 30
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
'401':
|
||||||
|
description: Unauthorized operation
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
title: GroupAccessApprove
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
usename:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
access_level:
|
||||||
|
type: integer
|
||||||
|
example:
|
||||||
|
'id': 1
|
||||||
|
'username': 'raymond_smith'
|
||||||
|
'name': 'Raymond Smith'
|
||||||
|
'state': 'active'
|
||||||
|
'created_at': '2012-10-22T14:13:35Z'
|
||||||
|
'access_level': 20
|
||||||
|
|
||||||
|
#/v4/groups/{id}/access_requests/{user_id}
|
||||||
|
accessRequestsGroupsDeny:
|
||||||
|
delete:
|
||||||
|
description: Denies a group access request for the given user
|
||||||
|
summary: Denies a group access request for the given user
|
||||||
|
operationId: accessRequestsGroupsDeny_delete
|
||||||
|
tags:
|
||||||
|
- access_requests
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The ID or URL-encoded path of the group owned by the authenticated user.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
- name: user_id
|
||||||
|
in: path
|
||||||
|
description: The userID of the access requester
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses: # Does anything go here? Markdown doc does not list a response.
|
||||||
|
'401':
|
||||||
|
description: Unauthorized operation
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
#/v4/projects/{id}/access_tokens
|
||||||
|
accessTokens:
|
||||||
|
get:
|
||||||
|
description: Lists access tokens for a project
|
||||||
|
summary: List access tokens for a project
|
||||||
|
operationId: accessTokens_get
|
||||||
|
tags:
|
||||||
|
- access_tokens
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The ID or URL-encoded path of the project
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
responses:
|
||||||
|
'404':
|
||||||
|
description: Not Found
|
||||||
|
'401':
|
||||||
|
description: Unauthorized operation
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
title: AccessTokenList
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
user_id:
|
||||||
|
type: integer
|
||||||
|
scopes:
|
||||||
|
type: array
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
expires_at:
|
||||||
|
type: date
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
active:
|
||||||
|
type: boolean
|
||||||
|
created_at:
|
||||||
|
type: date
|
||||||
|
revoked:
|
||||||
|
type: boolean
|
||||||
|
example:
|
||||||
|
'user_id': 141
|
||||||
|
'scopes': ['api']
|
||||||
|
'name': 'token'
|
||||||
|
'expires_at': '2022-01-31'
|
||||||
|
'id': 42
|
||||||
|
'active': true
|
||||||
|
'created_at': '2021-01-20T14:13:35Z'
|
||||||
|
'revoked': false
|
||||||
|
post:
|
||||||
|
description: Creates an access token for a project
|
||||||
|
summary: Creates an access token for a project
|
||||||
|
operationId: accessTokens_post
|
||||||
|
tags:
|
||||||
|
- access_tokens
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The ID or URL-encoded path of the project
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
- name: name
|
||||||
|
in: query
|
||||||
|
description: The name of the project access token
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: scopes
|
||||||
|
in: query
|
||||||
|
description: Defines read and write permissions for the token
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
[
|
||||||
|
'api',
|
||||||
|
'read_api',
|
||||||
|
'read_registry',
|
||||||
|
'write_registry',
|
||||||
|
'read_repository',
|
||||||
|
'write_repository',
|
||||||
|
]
|
||||||
|
- name: expires_at
|
||||||
|
in: query
|
||||||
|
description: Date when the token expires. Time of day is Midnight UTC of that date.
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: date
|
||||||
|
responses:
|
||||||
|
'404':
|
||||||
|
description: Not Found
|
||||||
|
'401':
|
||||||
|
description: Unauthorized operation
|
||||||
|
'200':
|
||||||
|
description: Successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
title: AccessTokenList
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
user_id:
|
||||||
|
type: integer
|
||||||
|
scopes:
|
||||||
|
type: array
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
expires_at:
|
||||||
|
type: date
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
active:
|
||||||
|
type: boolean
|
||||||
|
created_at:
|
||||||
|
type: date
|
||||||
|
revoked:
|
||||||
|
type: boolean
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
example:
|
||||||
|
'user_id': 166
|
||||||
|
'scopes': ['api', 'read_repository']
|
||||||
|
'name': 'test'
|
||||||
|
'expires_at': '2022-01-31'
|
||||||
|
'id': 58
|
||||||
|
'active': true
|
||||||
|
'created_at': '2021-01-20T14:13:35Z'
|
||||||
|
'revoked': false
|
||||||
|
'token': 'D4y...Wzr'
|
||||||
|
|
||||||
|
#/v4/projects/{id}/access_tokens/{token_id}
|
||||||
|
accessTokensRevoke:
|
||||||
|
delete:
|
||||||
|
description: Revokes an access token
|
||||||
|
summary: Revokes an access token
|
||||||
|
operationId: accessTokens_delete
|
||||||
|
tags:
|
||||||
|
- access_tokens
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
description: The ID or URL-encoded path of the project
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
- name: token_id
|
||||||
|
in: path
|
||||||
|
description: The ID of the project access token
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
responses:
|
||||||
|
'400':
|
||||||
|
description: Bad Request
|
||||||
|
'404':
|
||||||
|
description: Not Found
|
||||||
|
'204':
|
||||||
|
description: No content if successfully revoked
|
||||||
|
|
|
@ -1,381 +0,0 @@
|
||||||
# Markdown documentation: https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/api/access_requests.md
|
|
||||||
|
|
||||||
#/v4/projects/{id}/access_requests
|
|
||||||
accessRequestsProjects:
|
|
||||||
get:
|
|
||||||
description: Lists access requests for a project
|
|
||||||
summary: List access requests for a project
|
|
||||||
operationId: accessRequestsProjects_get
|
|
||||||
tags:
|
|
||||||
- access_requests
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
description: The ID or URL-encoded path of the project owned by the authenticated user.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
responses:
|
|
||||||
'401':
|
|
||||||
description: Unauthorized operation
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
title: ProjectAccessResponse
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
usename:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
state:
|
|
||||||
type: string
|
|
||||||
created_at:
|
|
||||||
type: string
|
|
||||||
requested_at:
|
|
||||||
type: string
|
|
||||||
example:
|
|
||||||
- "id": 1
|
|
||||||
"username": "raymond_smith"
|
|
||||||
"name": "Raymond Smith"
|
|
||||||
"state": "active"
|
|
||||||
"created_at": "2012-10-22T14:13:35Z"
|
|
||||||
"requested_at": "2012-10-22T14:13:35Z"
|
|
||||||
- "id": 2
|
|
||||||
"username": "john_doe"
|
|
||||||
"name": "John Doe"
|
|
||||||
"state": "active"
|
|
||||||
"created_at": "2012-10-22T14:13:35Z"
|
|
||||||
"requested_at": "2012-10-22T14:13:35Z"
|
|
||||||
post:
|
|
||||||
description: Requests access for the authenticated user to a project
|
|
||||||
summary: Requests access for the authenticated user to a project
|
|
||||||
operationId: accessRequestsProjects_post
|
|
||||||
tags:
|
|
||||||
- access_requests
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
description: The ID or URL-encoded path of the project owned by the authenticated user.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
responses:
|
|
||||||
'401':
|
|
||||||
description: Unauthorized operation
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
title: ProjectAccessRequest
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
usename:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
state:
|
|
||||||
type: string
|
|
||||||
created_at:
|
|
||||||
type: string
|
|
||||||
requested_at:
|
|
||||||
type: string
|
|
||||||
example:
|
|
||||||
"id": 1
|
|
||||||
"username": "raymond_smith"
|
|
||||||
"name": "Raymond Smith"
|
|
||||||
"state": "active"
|
|
||||||
"created_at": "2012-10-22T14:13:35Z"
|
|
||||||
"requested_at": "2012-10-22T14:13:35Z"
|
|
||||||
|
|
||||||
#/v4/projects/{id}/access_requests/{user_id}/approve
|
|
||||||
accessRequestsProjectsApprove:
|
|
||||||
put:
|
|
||||||
description: Approves access for the authenticated user to a project
|
|
||||||
summary: Approves access for the authenticated user to a project
|
|
||||||
operationId: accessRequestsProjectsApprove_put
|
|
||||||
tags:
|
|
||||||
- access_requests
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
description: The ID or URL-encoded path of the project owned by the authenticated user.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
description: The userID of the access requester
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
- name: access_level
|
|
||||||
in: query
|
|
||||||
description: A valid project access level. 0 = no access , 10 = guest, 20 = reporter, 30 = developer, 40 = Maintainer. Default is 30.'
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
enum: [0, 10, 20, 30, 40]
|
|
||||||
default: 30
|
|
||||||
type: integer
|
|
||||||
responses:
|
|
||||||
'401':
|
|
||||||
description: Unauthorized operation
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
title: ProjectAccessApprove
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
usename:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
state:
|
|
||||||
type: string
|
|
||||||
created_at:
|
|
||||||
type: string
|
|
||||||
access_level:
|
|
||||||
type: integer
|
|
||||||
example:
|
|
||||||
"id": 1
|
|
||||||
"username": "raymond_smith"
|
|
||||||
"name": "Raymond Smith"
|
|
||||||
"state": "active"
|
|
||||||
"created_at": "2012-10-22T14:13:35Z"
|
|
||||||
"access_level": 20
|
|
||||||
|
|
||||||
#/v4/projects/{id}/access_requests/{user_id}
|
|
||||||
accessRequestsProjectsDeny:
|
|
||||||
delete:
|
|
||||||
description: Denies a project access request for the given user
|
|
||||||
summary: Denies a project access request for the given user
|
|
||||||
operationId: accessRequestProjectsDeny_delete
|
|
||||||
tags:
|
|
||||||
- access_requests
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
description: The ID or URL-encoded path of the project owned by the authenticated user.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
description: The user ID of the access requester
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
responses: # Does anything go here? Markdown doc does not list a response.
|
|
||||||
'401':
|
|
||||||
description: Unauthorized operation
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
|
|
||||||
#/v4/groups/{id}/access_requests
|
|
||||||
accessRequestsGroups:
|
|
||||||
get:
|
|
||||||
description: List access requests for a group
|
|
||||||
summary: List access requests for a group
|
|
||||||
operationId: accessRequestsGroups_get
|
|
||||||
tags:
|
|
||||||
- access_requests
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
description: The ID or URL-encoded path of the group owned by the authenticated user.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
responses:
|
|
||||||
'401':
|
|
||||||
description: Unauthorized operation
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
title: GroupAccessResponse
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
usename:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
state:
|
|
||||||
type: string
|
|
||||||
created_at:
|
|
||||||
type: string
|
|
||||||
requested_at:
|
|
||||||
type: string
|
|
||||||
example:
|
|
||||||
- "id": 1
|
|
||||||
"username": "raymond_smith"
|
|
||||||
"name": "Raymond Smith"
|
|
||||||
"state": "active"
|
|
||||||
"created_at": "2012-10-22T14:13:35Z"
|
|
||||||
"requested_at": "2012-10-22T14:13:35Z"
|
|
||||||
- "id": 2
|
|
||||||
"username": "john_doe"
|
|
||||||
"name": "John Doe"
|
|
||||||
"state": "active"
|
|
||||||
"created_at": "2012-10-22T14:13:35Z"
|
|
||||||
"requested_at": "2012-10-22T14:13:35Z"
|
|
||||||
post:
|
|
||||||
description: Requests access for the authenticated user to a group
|
|
||||||
summary: Requests access for the authenticated user to a group
|
|
||||||
operationId: accessRequestsGroups_post
|
|
||||||
tags:
|
|
||||||
- access_requests
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
description: The ID or URL-encoded path of the group owned by the authenticated user.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
responses:
|
|
||||||
'401':
|
|
||||||
description: Unauthorized operation
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
title: GroupAccessRequest
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
usename:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
state:
|
|
||||||
type: string
|
|
||||||
created_at:
|
|
||||||
type: string
|
|
||||||
requested_at:
|
|
||||||
type: string
|
|
||||||
example:
|
|
||||||
"id": 1
|
|
||||||
"username": "raymond_smith"
|
|
||||||
"name": "Raymond Smith"
|
|
||||||
"state": "active"
|
|
||||||
"created_at": "2012-10-22T14:13:35Z"
|
|
||||||
"requested_at": "2012-10-22T14:13:35Z"
|
|
||||||
|
|
||||||
#/v4/groups/{id}/access_requests/{user_id}/approve
|
|
||||||
accessRequestsGroupsApprove:
|
|
||||||
put:
|
|
||||||
description: Approves access for the authenticated user to a group
|
|
||||||
summary: Approves access for the authenticated user to a group
|
|
||||||
operationId: accessRequestsGroupsApprove_put
|
|
||||||
tags:
|
|
||||||
- access_requests
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
description: The ID or URL-encoded path of the group owned by the authenticated user.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
description: The userID of the access requester
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
- name: access_level
|
|
||||||
in: query
|
|
||||||
description: A valid group access level. 0 = no access , 10 = Guest, 20 = Reporter, 30 = Developer, 40 = Maintainer, 50 = Owner. Default is 30.
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
enum: [0, 10, 20, 30, 40, 50]
|
|
||||||
default: 30
|
|
||||||
type: integer
|
|
||||||
responses:
|
|
||||||
'401':
|
|
||||||
description: Unauthorized operation
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
title: GroupAccessApprove
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
usename:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
state:
|
|
||||||
type: string
|
|
||||||
created_at:
|
|
||||||
type: string
|
|
||||||
access_level:
|
|
||||||
type: integer
|
|
||||||
example:
|
|
||||||
"id": 1
|
|
||||||
"username": "raymond_smith"
|
|
||||||
"name": "Raymond Smith"
|
|
||||||
"state": "active"
|
|
||||||
"created_at": "2012-10-22T14:13:35Z"
|
|
||||||
"access_level": 20
|
|
||||||
|
|
||||||
#/v4/groups/{id}/access_requests/{user_id}
|
|
||||||
accessRequestsGroupsDeny:
|
|
||||||
delete:
|
|
||||||
description: Denies a group access request for the given user
|
|
||||||
summary: Denies a group access request for the given user
|
|
||||||
operationId: accessRequestsGroupsDeny_delete
|
|
||||||
tags:
|
|
||||||
- access_requests
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
description: The ID or URL-encoded path of the group owned by the authenticated user.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
- name: user_id
|
|
||||||
in: path
|
|
||||||
description: The userID of the access requester
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
responses: # Does anything go here? Markdown doc does not list a response.
|
|
||||||
'401':
|
|
||||||
description: Unauthorized operation
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
|
@ -1,170 +0,0 @@
|
||||||
# Markdown documentation: https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/api/resource_access_tokens.md
|
|
||||||
|
|
||||||
#/v4/projects/{id}/access_tokens
|
|
||||||
accessTokens:
|
|
||||||
get:
|
|
||||||
description: Lists access tokens for a project
|
|
||||||
summary: List access tokens for a project
|
|
||||||
operationId: accessTokens_get
|
|
||||||
tags:
|
|
||||||
- access_tokens
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
description: The ID or URL-encoded path of the project
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
responses:
|
|
||||||
'404':
|
|
||||||
description: Not Found
|
|
||||||
'401':
|
|
||||||
description: Unauthorized operation
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
title: AccessTokenList
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
user_id:
|
|
||||||
type: integer
|
|
||||||
scopes:
|
|
||||||
type: array
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
expires_at:
|
|
||||||
type: date
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
active:
|
|
||||||
type: boolean
|
|
||||||
created_at:
|
|
||||||
type: date
|
|
||||||
revoked:
|
|
||||||
type: boolean
|
|
||||||
example:
|
|
||||||
"user_id": 141
|
|
||||||
"scopes" : ["api"]
|
|
||||||
"name": "token"
|
|
||||||
"expires_at": "2022-01-31"
|
|
||||||
"id": 42
|
|
||||||
"active": true
|
|
||||||
"created_at": "2021-01-20T14:13:35Z"
|
|
||||||
"revoked" : false
|
|
||||||
post:
|
|
||||||
description: Creates an access token for a project
|
|
||||||
summary: Creates an access token for a project
|
|
||||||
operationId: accessTokens_post
|
|
||||||
tags:
|
|
||||||
- access_tokens
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
description: The ID or URL-encoded path of the project
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
- name: name
|
|
||||||
in: query
|
|
||||||
description: The name of the project access token
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: scopes
|
|
||||||
in: query
|
|
||||||
description: Defines read and write permissions for the token
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
enum: ["api", "read_api", "read_registry", "write_registry", "read_repository", "write_repository"]
|
|
||||||
- name: expires_at
|
|
||||||
in: query
|
|
||||||
description: Date when the token expires. Time of day is Midnight UTC of that date.
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
type: date
|
|
||||||
responses:
|
|
||||||
'404':
|
|
||||||
description: Not Found
|
|
||||||
'401':
|
|
||||||
description: Unauthorized operation
|
|
||||||
'200':
|
|
||||||
description: Successful operation
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
title: AccessTokenList
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
user_id:
|
|
||||||
type: integer
|
|
||||||
scopes:
|
|
||||||
type: array
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
expires_at:
|
|
||||||
type: date
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
active:
|
|
||||||
type: boolean
|
|
||||||
created_at:
|
|
||||||
type: date
|
|
||||||
revoked:
|
|
||||||
type: boolean
|
|
||||||
token:
|
|
||||||
type: string
|
|
||||||
example:
|
|
||||||
"user_id": 166
|
|
||||||
"scopes" : [
|
|
||||||
"api",
|
|
||||||
"read_repository"
|
|
||||||
]
|
|
||||||
"name": "test"
|
|
||||||
"expires_at": "2022-01-31"
|
|
||||||
"id": 58
|
|
||||||
"active": true
|
|
||||||
"created_at": "2021-01-20T14:13:35Z"
|
|
||||||
"revoked" : false
|
|
||||||
"token" : "D4y...Wzr"
|
|
||||||
|
|
||||||
#/v4/projects/{id}/access_tokens/{token_id}
|
|
||||||
accessTokensRevoke:
|
|
||||||
delete:
|
|
||||||
description: Revokes an access token
|
|
||||||
summary: Revokes an access token
|
|
||||||
operationId: accessTokens_delete
|
|
||||||
tags:
|
|
||||||
- access_tokens
|
|
||||||
parameters:
|
|
||||||
- name: id
|
|
||||||
in: path
|
|
||||||
description: The ID or URL-encoded path of the project
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
- name: token_id
|
|
||||||
in: path
|
|
||||||
description: The ID of the project access token
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
oneOf:
|
|
||||||
- type: integer
|
|
||||||
- type: string
|
|
||||||
responses:
|
|
||||||
'400':
|
|
||||||
description: Bad Request
|
|
||||||
'404':
|
|
||||||
description: Not Found
|
|
||||||
'204':
|
|
||||||
description: No content if successfully revoked
|
|
|
@ -1,43 +0,0 @@
|
||||||
# Markdown documentation: https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/api/metadata.md
|
|
||||||
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- metadata
|
|
||||||
summary: "Retrieve metadata information for this GitLab instance."
|
|
||||||
operationId: "getMetadata"
|
|
||||||
responses:
|
|
||||||
"401":
|
|
||||||
description: "unauthorized operation"
|
|
||||||
"200":
|
|
||||||
description: "successful operation"
|
|
||||||
content:
|
|
||||||
"application/json":
|
|
||||||
schema:
|
|
||||||
title: "MetadataResponse"
|
|
||||||
type: "object"
|
|
||||||
properties:
|
|
||||||
version:
|
|
||||||
type: "string"
|
|
||||||
revision:
|
|
||||||
type: "string"
|
|
||||||
kas:
|
|
||||||
type: "object"
|
|
||||||
properties:
|
|
||||||
enabled:
|
|
||||||
type: "boolean"
|
|
||||||
externalUrl:
|
|
||||||
type: "string"
|
|
||||||
nullable: true
|
|
||||||
version:
|
|
||||||
type: "string"
|
|
||||||
nullable: true
|
|
||||||
examples:
|
|
||||||
Example:
|
|
||||||
value:
|
|
||||||
version: "15.0-pre"
|
|
||||||
revision: "c401a659d0c"
|
|
||||||
kas:
|
|
||||||
enabled: true
|
|
||||||
externalUrl: "grpc://gitlab.example.com:8150"
|
|
||||||
version: "15.0.0"
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
# Markdown documentation: https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/api/version.md
|
|
||||||
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- version
|
|
||||||
summary: "Retrieve version information for this GitLab instance."
|
|
||||||
operationId: "getVersion"
|
|
||||||
responses:
|
|
||||||
"401":
|
|
||||||
description: "unauthorized operation"
|
|
||||||
"200":
|
|
||||||
description: "successful operation"
|
|
||||||
content:
|
|
||||||
"application/json":
|
|
||||||
schema:
|
|
||||||
title: "VersionResponse"
|
|
||||||
type: "object"
|
|
||||||
properties:
|
|
||||||
version:
|
|
||||||
type: "string"
|
|
||||||
revision:
|
|
||||||
type: "string"
|
|
||||||
examples:
|
|
||||||
Example:
|
|
||||||
value:
|
|
||||||
version: "13.3.0-pre"
|
|
||||||
revision: "f2b05afebb0"
|
|
||||||
|
|
|
@ -1707,17 +1707,17 @@ only supported report file in 15.0, but this is the first step towards GitLab su
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="deprecation removal-150 breaking-change">
|
<div class="deprecation removal-160 breaking-change">
|
||||||
|
|
||||||
### merged_by API field
|
### merged_by API field
|
||||||
|
|
||||||
Planned removal: GitLab <span class="removal-milestone">15.0</span> (2022-05-22)
|
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)
|
||||||
|
|
||||||
WARNING:
|
WARNING:
|
||||||
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
|
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
|
||||||
Review the details carefully before upgrading.
|
Review the details carefully before upgrading.
|
||||||
|
|
||||||
The `merged_by` field in the [merge request API](https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests) is being deprecated and will be removed in GitLab 15.0. This field is being replaced with the `merge_user` field (already present in GraphQL) which more correctly identifies who merged a merge request when performing actions (merge when pipeline succeeds, add to merge train) other than a simple merge.
|
The `merged_by` field in the [merge request API](https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests) has been deprecated in favor of the `merge_user` field which more correctly identifies who merged a merge request when performing actions (merge when pipeline succeeds, add to merge train) other than a simple merge. API users are encouraged to use the new `merge_user` field instead. The `merged_by` field will be removed in v5 of the GitLab REST API.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -144,7 +144,7 @@ If you edit an existing comment to add a user mention that wasn't there before,
|
||||||
- Creates a to-do item for the mentioned user.
|
- Creates a to-do item for the mentioned user.
|
||||||
- Does not send a notification email.
|
- Does not send a notification email.
|
||||||
|
|
||||||
## Prevent comments by locking an issue
|
## Prevent comments by locking the discussion
|
||||||
|
|
||||||
You can prevent public comments in an issue or merge request.
|
You can prevent public comments in an issue or merge request.
|
||||||
When you do, only project members can add and edit comments.
|
When you do, only project members can add and edit comments.
|
||||||
|
@ -154,6 +154,8 @@ Prerequisite:
|
||||||
- In merge requests, you must have at least the Developer role.
|
- In merge requests, you must have at least the Developer role.
|
||||||
- In issues, you must have at least the Reporter role.
|
- In issues, you must have at least the Reporter role.
|
||||||
|
|
||||||
|
To lock an issue or merge request:
|
||||||
|
|
||||||
1. On the right sidebar, next to **Lock issue** or **Lock merge request**, select **Edit**.
|
1. On the right sidebar, next to **Lock issue** or **Lock merge request**, select **Edit**.
|
||||||
1. On the confirmation dialog, select **Lock**.
|
1. On the confirmation dialog, select **Lock**.
|
||||||
|
|
||||||
|
@ -161,6 +163,9 @@ Notes are added to the page details.
|
||||||
|
|
||||||
If an issue or merge request is locked and closed, you cannot reopen it.
|
If an issue or merge request is locked and closed, you cannot reopen it.
|
||||||
|
|
||||||
|
<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
|
||||||
|
If you don't see this action on the right sidebar, your project or instance might have [moved sidebar actions](../project/merge_requests/index.md#move-sidebar-actions) enabled.
|
||||||
|
|
||||||
## Add an internal note
|
## Add an internal note
|
||||||
|
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207473) in GitLab 13.9 [with a flag](../../administration/feature_flags.md) named `confidential_notes`. Disabled by default.
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207473) in GitLab 13.9 [with a flag](../../administration/feature_flags.md) named `confidential_notes`. Disabled by default.
|
||||||
|
|
|
@ -228,20 +228,20 @@ to change their user notification settings to **Watch** instead.
|
||||||
|
|
||||||
### Edit notification settings for issues, merge requests, and epics
|
### Edit notification settings for issues, merge requests, and epics
|
||||||
|
|
||||||
To enable notifications on a specific issue, merge request, or epic, you must turn on the
|
To toggle notifications on an issue, merge request, or epic: on the right sidebar, turn on or off the **Notifications** toggle.
|
||||||
**Notifications** toggle in the right sidebar.
|
|
||||||
|
|
||||||
- To subscribe, **turn on** if you are not a participant in the discussion, but want to receive
|
When you **turn on** notifications, you start receiving notifications on each update, even if you
|
||||||
notifications on each update.
|
haven't participated in the discussion.
|
||||||
|
When you turn notifications on in an epic, you aren't automatically subscribed to the issues linked
|
||||||
|
to the epic.
|
||||||
|
|
||||||
When you turn notifications on in an epic, you aren't automatically subscribed to the issues linked
|
When you **turn off** notifications, you stop receiving notifications for updates.
|
||||||
to the epic.
|
Turning this toggle off only unsubscribes you from updates related to this issue, merge request, or epic.
|
||||||
|
Learn how to [opt out of all emails from GitLab](#opt-out-of-all-gitlab-emails).
|
||||||
|
|
||||||
- To unsubscribe, **turn off** if you are receiving notifications for updates but no longer want to
|
<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
|
||||||
receive them.
|
If you don't see this action on the right sidebar, your project or instance may have
|
||||||
|
enabled a feature flag for [moved sidebar actions](../project/merge_requests/index.md#move-sidebar-actions).
|
||||||
Turning this toggle off only unsubscribes you from updates related to this issue, merge request, or epic.
|
|
||||||
Learn how to [opt out of all emails from GitLab](#opt-out-of-all-gitlab-emails).
|
|
||||||
|
|
||||||
### Notification events on issues, merge requests, and epics
|
### Notification events on issues, merge requests, and epics
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ To add a user to a project:
|
||||||
1. Select **Invite members**.
|
1. Select **Invite members**.
|
||||||
1. Enter an email address and select a [role](../../permissions.md).
|
1. Enter an email address and select a [role](../../permissions.md).
|
||||||
1. Optional. Select an **Access expiration date**.
|
1. Optional. Select an **Access expiration date**.
|
||||||
On that date, the user can no longer access the project.
|
From that date onwards, the user can no longer access the project.
|
||||||
1. Select **Invite**.
|
1. Select **Invite**.
|
||||||
|
|
||||||
If the user has a GitLab account, they are added to the members list.
|
If the user has a GitLab account, they are added to the members list.
|
||||||
|
@ -97,19 +97,20 @@ Each user's access is based on:
|
||||||
- The role they're assigned in the group.
|
- The role they're assigned in the group.
|
||||||
- The maximum role you choose when you invite the group.
|
- The maximum role you choose when you invite the group.
|
||||||
|
|
||||||
Prerequisite:
|
Prerequisites:
|
||||||
|
|
||||||
- You must have the Maintainer or Owner role.
|
- You must have the Maintainer or Owner role.
|
||||||
- Sharing the project with other groups must not be [prevented](../../group/access_and_permissions.md#prevent-a-project-from-being-shared-with-groups).
|
- Sharing the project with other groups must not be [prevented](../../group/access_and_permissions.md#prevent-a-project-from-being-shared-with-groups).
|
||||||
|
|
||||||
To add groups to a project:
|
To add a group to a project:
|
||||||
|
|
||||||
1. On the top bar, select **Main menu > Projects** and find your project.
|
1. On the top bar, select **Main menu > Projects** and find your project.
|
||||||
1. On the left sidebar, select **Project information > Members**.
|
1. On the left sidebar, select **Project information > Members**.
|
||||||
1. Select **Invite a group**.
|
1. Select **Invite a group**.
|
||||||
1. Select a group.
|
1. Select a group.
|
||||||
1. Select the highest [role](../../permissions.md) for users in the group.
|
1. Select the highest [role](../../permissions.md) for users in the group.
|
||||||
1. Optional. Select an **Access expiration date**. On that date, the group can no longer access the project.
|
1. Optional. Select an **Access expiration date**.
|
||||||
|
From that date onwards, the group can no longer access the project.
|
||||||
1. Select **Invite**.
|
1. Select **Invite**.
|
||||||
|
|
||||||
The members of the group are not displayed on the **Members** tab.
|
The members of the group are not displayed on the **Members** tab.
|
||||||
|
|
|
@ -250,6 +250,28 @@ This feature works only when a merge request is merged. Selecting **Remove sourc
|
||||||
after merging does not retarget open merge requests. This improvement is
|
after merging does not retarget open merge requests. This improvement is
|
||||||
[proposed as a follow-up](https://gitlab.com/gitlab-org/gitlab/-/issues/321559).
|
[proposed as a follow-up](https://gitlab.com/gitlab-org/gitlab/-/issues/321559).
|
||||||
|
|
||||||
|
## Move sidebar actions
|
||||||
|
|
||||||
|
<!-- When the `moved_mr_sidebar` feature flag is removed, delete this topic and update the steps for these actions
|
||||||
|
like in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87727/diffs?diff_id=522279685#5d9afba799c4af9920dab533571d7abb8b9e9163 -->
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85584) in GitLab 14.10 [with a flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`. Disabled by default.
|
||||||
|
|
||||||
|
FLAG:
|
||||||
|
On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`.
|
||||||
|
On GitLab.com, this feature is not available.
|
||||||
|
|
||||||
|
When this feature flag is enabled, you can find the following actions in
|
||||||
|
**Merge request actions** (**{ellipsis_v}**) on the top right:
|
||||||
|
|
||||||
|
- The [notifications](../../profile/notifications.md#edit-notification-settings-for-issues-merge-requests-and-epics) toggle
|
||||||
|
- Mark merge request as ready or [draft](../merge_requests/drafts.md)
|
||||||
|
- Close merge request
|
||||||
|
- [Lock discussion](../../discussions/index.md#prevent-comments-by-locking-the-discussion)
|
||||||
|
- Copy reference
|
||||||
|
|
||||||
|
When this feature flag is disabled, these actions are in the right sidebar.
|
||||||
|
|
||||||
## Merge request workflows
|
## Merge request workflows
|
||||||
|
|
||||||
For a software developer working in a team:
|
For a software developer working in a team:
|
||||||
|
|
|
@ -173,6 +173,7 @@ module API
|
||||||
mount ::API::AccessRequests
|
mount ::API::AccessRequests
|
||||||
mount ::API::Appearance
|
mount ::API::Appearance
|
||||||
mount ::API::BulkImports
|
mount ::API::BulkImports
|
||||||
|
mount ::API::Ci::ResourceGroups
|
||||||
mount ::API::Ci::Runner
|
mount ::API::Ci::Runner
|
||||||
mount ::API::Ci::Runners
|
mount ::API::Ci::Runners
|
||||||
mount ::API::Clusters::Agents
|
mount ::API::Clusters::Agents
|
||||||
|
@ -226,7 +227,6 @@ module API
|
||||||
mount ::API::Ci::Jobs
|
mount ::API::Ci::Jobs
|
||||||
mount ::API::Ci::PipelineSchedules
|
mount ::API::Ci::PipelineSchedules
|
||||||
mount ::API::Ci::Pipelines
|
mount ::API::Ci::Pipelines
|
||||||
mount ::API::Ci::ResourceGroups
|
|
||||||
mount ::API::Ci::SecureFiles
|
mount ::API::Ci::SecureFiles
|
||||||
mount ::API::Ci::Triggers
|
mount ::API::Ci::Triggers
|
||||||
mount ::API::Ci::Variables
|
mount ::API::Ci::Variables
|
||||||
|
|
|
@ -5,17 +5,27 @@ module API
|
||||||
class ResourceGroups < ::API::Base
|
class ResourceGroups < ::API::Base
|
||||||
include PaginationParams
|
include PaginationParams
|
||||||
|
|
||||||
|
ci_resource_groups_tags = %w[ci_resource_groups]
|
||||||
|
|
||||||
before { authenticate! }
|
before { authenticate! }
|
||||||
|
|
||||||
feature_category :continuous_delivery
|
feature_category :continuous_delivery
|
||||||
urgency :low
|
urgency :low
|
||||||
|
|
||||||
params do
|
params do
|
||||||
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
|
requires :id,
|
||||||
|
types: [String, Integer],
|
||||||
|
desc: 'The ID or URL-encoded path of the project owned by the authenticated user'
|
||||||
end
|
end
|
||||||
resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||||
desc 'Get all resource groups for this project' do
|
desc 'Get all resource groups for a project' do
|
||||||
success Entities::Ci::ResourceGroup
|
success Entities::Ci::ResourceGroup
|
||||||
|
failure [
|
||||||
|
{ code: 401, message: 'Unauthorized' },
|
||||||
|
{ code: 404, message: 'Not found' }
|
||||||
|
]
|
||||||
|
is_array true
|
||||||
|
tags ci_resource_groups_tags
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
use :pagination
|
use :pagination
|
||||||
|
@ -26,8 +36,13 @@ module API
|
||||||
present paginate(user_project.resource_groups), with: Entities::Ci::ResourceGroup
|
present paginate(user_project.resource_groups), with: Entities::Ci::ResourceGroup
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Get a single resource group' do
|
desc 'Get a specific resource group' do
|
||||||
success Entities::Ci::ResourceGroup
|
success Entities::Ci::ResourceGroup
|
||||||
|
failure [
|
||||||
|
{ code: 401, message: 'Unauthorized' },
|
||||||
|
{ code: 404, message: 'Not found' }
|
||||||
|
]
|
||||||
|
tags ci_resource_groups_tags
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
requires :key, type: String, desc: 'The key of the resource group'
|
requires :key, type: String, desc: 'The key of the resource group'
|
||||||
|
@ -38,8 +53,14 @@ module API
|
||||||
present resource_group, with: Entities::Ci::ResourceGroup
|
present resource_group, with: Entities::Ci::ResourceGroup
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'List upcoming jobs of a resource group' do
|
desc 'List upcoming jobs for a specific resource group' do
|
||||||
success Entities::Ci::JobBasic
|
success Entities::Ci::JobBasic
|
||||||
|
failure [
|
||||||
|
{ code: 401, message: 'Unauthorized' },
|
||||||
|
{ code: 404, message: 'Not found' }
|
||||||
|
]
|
||||||
|
is_array true
|
||||||
|
tags ci_resource_groups_tags
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
requires :key, type: String, desc: 'The key of the resource group'
|
requires :key, type: String, desc: 'The key of the resource group'
|
||||||
|
@ -57,12 +78,22 @@ module API
|
||||||
present paginate(upcoming_processables), with: Entities::Ci::JobBasic
|
present paginate(upcoming_processables), with: Entities::Ci::JobBasic
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Edit a resource group' do
|
desc 'Edit an existing resource group' do
|
||||||
|
detail "Updates an existing resource group's properties."
|
||||||
success Entities::Ci::ResourceGroup
|
success Entities::Ci::ResourceGroup
|
||||||
|
failure [
|
||||||
|
{ code: 400, message: 'Bad request' },
|
||||||
|
{ code: 401, message: 'Unauthorized' },
|
||||||
|
{ code: 404, message: 'Not found' }
|
||||||
|
]
|
||||||
|
tags ci_resource_groups_tags
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
requires :key, type: String, desc: 'The key of the resource group'
|
requires :key, type: String, desc: 'The key of the resource group'
|
||||||
optional :process_mode, type: String, desc: 'The process mode',
|
|
||||||
|
optional :process_mode,
|
||||||
|
type: String,
|
||||||
|
desc: 'The process mode of the resource group',
|
||||||
values: ::Ci::ResourceGroup.process_modes.keys
|
values: ::Ci::ResourceGroup.process_modes.keys
|
||||||
end
|
end
|
||||||
put ':id/resource_groups/:key' do
|
put ':id/resource_groups/:key' do
|
||||||
|
|
|
@ -4,16 +4,26 @@ module API
|
||||||
module Entities
|
module Entities
|
||||||
module Ci
|
module Ci
|
||||||
class JobBasic < Grape::Entity
|
class JobBasic < Grape::Entity
|
||||||
expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure
|
expose :id, documentation: { type: 'integer', example: 1 }
|
||||||
expose :created_at, :started_at, :finished_at
|
expose :status, documentation: { type: 'string', example: 'waiting_for_resource' }
|
||||||
|
expose :stage, documentation: { type: 'string', example: 'deploy' }
|
||||||
|
expose :name, documentation: { type: 'string', example: 'deploy_to_production' }
|
||||||
|
expose :ref, documentation: { type: 'string', example: 'main' }
|
||||||
|
expose :tag, documentation: { type: 'boolean' }
|
||||||
|
expose :coverage, documentation: { type: 'number', format: 'float', example: 0.90 }
|
||||||
|
expose :allow_failure, documentation: { type: 'boolean' }
|
||||||
|
expose :created_at, documentation: { type: 'dateTime', example: '2015-12-24T15:51:21.880Z' }
|
||||||
|
expose :started_at, documentation: { type: 'dateTime', example: '2015-12-24T17:54:30.733Z' }
|
||||||
|
expose :finished_at, documentation: { type: 'dateTime', example: '2015-12-24T17:54:31.198Z' }
|
||||||
expose :duration,
|
expose :duration,
|
||||||
documentation: { type: 'number', format: 'float', desc: 'Time spent running' }
|
documentation: { type: 'number', format: 'float', desc: 'Time spent running', example: 0.465 }
|
||||||
expose :queued_duration,
|
expose :queued_duration,
|
||||||
documentation: { type: 'number', format: 'float', desc: 'Time spent enqueued' }
|
documentation: { type: 'number', format: 'float', desc: 'Time spent enqueued', example: 0.123 }
|
||||||
expose :user, with: ::API::Entities::User
|
expose :user, with: ::API::Entities::User
|
||||||
expose :commit, with: ::API::Entities::Commit
|
expose :commit, with: ::API::Entities::Commit
|
||||||
expose :pipeline, with: ::API::Entities::Ci::PipelineBasic
|
expose :pipeline, with: ::API::Entities::Ci::PipelineBasic
|
||||||
expose :failure_reason, if: -> (job) { job.failed? }
|
expose :failure_reason,
|
||||||
|
documentation: { type: 'string', example: 'script_failure' }, if: -> (job) { job.failed? }
|
||||||
|
|
||||||
expose :web_url do |job, _options|
|
expose :web_url do |job, _options|
|
||||||
Gitlab::Routing.url_helpers.project_job_url(job.project, job)
|
Gitlab::Routing.url_helpers.project_job_url(job.project, job)
|
||||||
|
|
|
@ -4,7 +4,11 @@ module API
|
||||||
module Entities
|
module Entities
|
||||||
module Ci
|
module Ci
|
||||||
class ResourceGroup < Grape::Entity
|
class ResourceGroup < Grape::Entity
|
||||||
expose :id, :key, :process_mode, :created_at, :updated_at
|
expose :id, documentation: { type: 'integer', example: 1 }
|
||||||
|
expose :key, documentation: { type: 'string', example: 'production' }
|
||||||
|
expose :process_mode, documentation: { type: 'string', example: 'unordered' }
|
||||||
|
expose :created_at, documentation: { type: 'dateTime', example: '2021-09-01T08:04:59.650Z' }
|
||||||
|
expose :updated_at, documentation: { type: 'dateTime', example: '2021-09-01T08:04:59.650Z' }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -47,12 +47,9 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate!
|
def validate!
|
||||||
context.logger.instrument(:config_file_validation) do
|
|
||||||
validate_execution_time!
|
|
||||||
validate_location!
|
validate_location!
|
||||||
validate_content! if errors.none?
|
fetch_and_validate_content! if valid?
|
||||||
validate_hash! if errors.none?
|
load_and_validate_expanded_hash! if valid?
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def metadata
|
def metadata
|
||||||
|
@ -72,11 +69,41 @@ module Gitlab
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def expanded_content_hash
|
def validate_location!
|
||||||
return unless content_hash
|
if invalid_location_type?
|
||||||
|
errors.push("Included file `#{masked_location}` needs to be a string")
|
||||||
|
elsif invalid_extension?
|
||||||
|
errors.push("Included file `#{masked_location}` does not have YAML extension!")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
strong_memoize(:expanded_content_yaml) do
|
def fetch_and_validate_content!
|
||||||
expand_includes(content_hash)
|
context.logger.instrument(:config_file_fetch_content) do
|
||||||
|
content # calling the method fetches then memoizes the result
|
||||||
|
end
|
||||||
|
|
||||||
|
return if errors.any?
|
||||||
|
|
||||||
|
context.logger.instrument(:config_file_validate_content) do
|
||||||
|
validate_content!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_and_validate_expanded_hash!
|
||||||
|
context.logger.instrument(:config_file_fetch_content_hash) do
|
||||||
|
content_hash # calling the method loads then memoizes the result
|
||||||
|
end
|
||||||
|
|
||||||
|
context.logger.instrument(:config_file_expand_content_includes) do
|
||||||
|
expanded_content_hash # calling the method expands then memoizes the result
|
||||||
|
end
|
||||||
|
|
||||||
|
validate_hash!
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_content!
|
||||||
|
if content.blank?
|
||||||
|
errors.push("Included file `#{masked_location}` is empty or does not exist!")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -88,21 +115,11 @@ module Gitlab
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_execution_time!
|
def expanded_content_hash
|
||||||
context.check_execution_time!
|
return unless content_hash
|
||||||
end
|
|
||||||
|
|
||||||
def validate_location!
|
strong_memoize(:expanded_content_yaml) do
|
||||||
if invalid_location_type?
|
expand_includes(content_hash)
|
||||||
errors.push("Included file `#{masked_location}` needs to be a string")
|
|
||||||
elsif invalid_extension?
|
|
||||||
errors.push("Included file `#{masked_location}` does not have YAML extension!")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_content!
|
|
||||||
if content.blank?
|
|
||||||
errors.push("Included file `#{masked_location}` is empty or does not exist!")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -127,6 +127,7 @@ module Gitlab
|
||||||
|
|
||||||
def verify!(location_object)
|
def verify!(location_object)
|
||||||
verify_max_includes!
|
verify_max_includes!
|
||||||
|
verify_execution_time!
|
||||||
location_object.validate!
|
location_object.validate!
|
||||||
expandset.add(location_object)
|
expandset.add(location_object)
|
||||||
end
|
end
|
||||||
|
@ -137,6 +138,10 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def verify_execution_time!
|
||||||
|
context.check_execution_time!
|
||||||
|
end
|
||||||
|
|
||||||
def expand_variables(data)
|
def expand_variables(data)
|
||||||
logger.instrument(:config_mapper_variables) do
|
logger.instrument(:config_mapper_variables) do
|
||||||
expand_variables_without_instrumentation(data)
|
expand_variables_without_instrumentation(data)
|
||||||
|
|
|
@ -25,8 +25,6 @@ module Gitlab
|
||||||
return error('Failed to build the pipeline!')
|
return error('Failed to build the pipeline!')
|
||||||
end
|
end
|
||||||
|
|
||||||
set_pipeline_name
|
|
||||||
|
|
||||||
raise Populate::PopulateError if pipeline.persisted?
|
raise Populate::PopulateError if pipeline.persisted?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -36,21 +34,6 @@ module Gitlab
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pipeline_name
|
|
||||||
return if Feature.disabled?(:pipeline_name, pipeline.project) ||
|
|
||||||
@command.yaml_processor_result.workflow_name.blank?
|
|
||||||
|
|
||||||
name = @command.yaml_processor_result.workflow_name
|
|
||||||
name = ExpandVariables.expand(name, -> { global_context.variables.sort_and_expand_all })
|
|
||||||
|
|
||||||
pipeline.build_pipeline_metadata(project: pipeline.project, name: name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def global_context
|
|
||||||
Gitlab::Ci::Build::Context::Global.new(
|
|
||||||
pipeline, yaml_variables: @command.pipeline_seed.root_variables)
|
|
||||||
end
|
|
||||||
|
|
||||||
def stage_names
|
def stage_names
|
||||||
# We filter out `.pre/.post` stages, as they alone are not considered
|
# We filter out `.pre/.post` stages, as they alone are not considered
|
||||||
# a complete pipeline:
|
# a complete pipeline:
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Ci
|
||||||
|
module Pipeline
|
||||||
|
module Chain
|
||||||
|
class PopulateMetadata < Chain::Base
|
||||||
|
include Chain::Helpers
|
||||||
|
|
||||||
|
def perform!
|
||||||
|
set_pipeline_name
|
||||||
|
return if pipeline.pipeline_metadata.nil? || pipeline.pipeline_metadata.valid?
|
||||||
|
|
||||||
|
message = pipeline.pipeline_metadata.errors.full_messages.join(', ')
|
||||||
|
error("Failed to build pipeline metadata! #{message}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def break?
|
||||||
|
pipeline.pipeline_metadata&.errors&.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_pipeline_name
|
||||||
|
return if Feature.disabled?(:pipeline_name, pipeline.project) ||
|
||||||
|
@command.yaml_processor_result.workflow_name.blank?
|
||||||
|
|
||||||
|
name = @command.yaml_processor_result.workflow_name
|
||||||
|
name = ExpandVariables.expand(name, -> { global_context.variables.sort_and_expand_all })
|
||||||
|
|
||||||
|
pipeline.build_pipeline_metadata(project: pipeline.project, name: name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def global_context
|
||||||
|
Gitlab::Ci::Build::Context::Global.new(
|
||||||
|
pipeline, yaml_variables: @command.pipeline_seed.root_variables)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,59 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Email
|
||||||
|
# Contains common methods which must be present in all email classes
|
||||||
|
module Common
|
||||||
|
UNSUBSCRIBE_SUFFIX = '-unsubscribe'
|
||||||
|
UNSUBSCRIBE_SUFFIX_LEGACY = '+unsubscribe'
|
||||||
|
WILDCARD_PLACEHOLDER = '%{key}'
|
||||||
|
|
||||||
|
# This can be overridden for a custom config
|
||||||
|
def config
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def incoming_email_config
|
||||||
|
Gitlab.config.incoming_email
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
!!config&.enabled && config.address.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def supports_wildcard?
|
||||||
|
config_address = incoming_email_config.address
|
||||||
|
|
||||||
|
config_address.present? && config_address.include?(WILDCARD_PLACEHOLDER)
|
||||||
|
end
|
||||||
|
|
||||||
|
def supports_issue_creation?
|
||||||
|
enabled? && supports_wildcard?
|
||||||
|
end
|
||||||
|
|
||||||
|
def reply_address(key)
|
||||||
|
incoming_email_config.address.sub(WILDCARD_PLACEHOLDER, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
# example: incoming+1234567890abcdef1234567890abcdef-unsubscribe@incoming.gitlab.com
|
||||||
|
def unsubscribe_address(key)
|
||||||
|
incoming_email_config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def key_from_address(address, wildcard_address: nil)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def key_from_fallback_message_id(mail_id)
|
||||||
|
message_id_regexp = /\Areply-(.+)@#{Gitlab.config.gitlab.host}\z/
|
||||||
|
|
||||||
|
mail_id[message_id_regexp, 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def scan_fallback_references(references)
|
||||||
|
# It's looking for each <...>
|
||||||
|
references.scan(/(?!<)[^<>]+(?=>)/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -73,7 +73,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_handle_legacy_format?
|
def can_handle_legacy_format?
|
||||||
project_path && !incoming_email_token.include?('+') && !mail_key.include?(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY)
|
project_path && !incoming_email_token.include?('+') && !mail_key.include?(Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,8 +12,8 @@ module Gitlab
|
||||||
delegate :project, to: :sent_notification, allow_nil: true
|
delegate :project, to: :sent_notification, allow_nil: true
|
||||||
|
|
||||||
HANDLER_REGEX_FOR = -> (suffix) { /\A(?<reply_token>\w+)#{Regexp.escape(suffix)}\z/ }.freeze
|
HANDLER_REGEX_FOR = -> (suffix) { /\A(?<reply_token>\w+)#{Regexp.escape(suffix)}\z/ }.freeze
|
||||||
HANDLER_REGEX = HANDLER_REGEX_FOR.call(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX).freeze
|
HANDLER_REGEX = HANDLER_REGEX_FOR.call(Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX).freeze
|
||||||
HANDLER_REGEX_LEGACY = HANDLER_REGEX_FOR.call(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY).freeze
|
HANDLER_REGEX_LEGACY = HANDLER_REGEX_FOR.call(Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY).freeze
|
||||||
|
|
||||||
def initialize(mail, mail_key)
|
def initialize(mail, mail_key)
|
||||||
super(mail, mail_key)
|
super(mail, mail_key)
|
||||||
|
|
|
@ -2,30 +2,11 @@
|
||||||
|
|
||||||
module Gitlab
|
module Gitlab
|
||||||
module IncomingEmail
|
module IncomingEmail
|
||||||
UNSUBSCRIBE_SUFFIX = '-unsubscribe'
|
|
||||||
UNSUBSCRIBE_SUFFIX_LEGACY = '+unsubscribe'
|
|
||||||
WILDCARD_PLACEHOLDER = '%{key}'
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def enabled?
|
include Gitlab::Email::Common
|
||||||
config.enabled && config.address.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def supports_wildcard?
|
def config
|
||||||
config.address.present? && config.address.include?(WILDCARD_PLACEHOLDER)
|
incoming_email_config
|
||||||
end
|
|
||||||
|
|
||||||
def supports_issue_creation?
|
|
||||||
enabled? && supports_wildcard?
|
|
||||||
end
|
|
||||||
|
|
||||||
def reply_address(key)
|
|
||||||
config.address.sub(WILDCARD_PLACEHOLDER, key)
|
|
||||||
end
|
|
||||||
|
|
||||||
# example: incoming+1234567890abcdef1234567890abcdef-unsubscribe@incoming.gitlab.com
|
|
||||||
def unsubscribe_address(key)
|
|
||||||
config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def key_from_address(address, wildcard_address: nil)
|
def key_from_address(address, wildcard_address: nil)
|
||||||
|
@ -39,21 +20,6 @@ module Gitlab
|
||||||
match[1]
|
match[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
def key_from_fallback_message_id(mail_id)
|
|
||||||
message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/
|
|
||||||
|
|
||||||
mail_id[message_id_regexp, 1]
|
|
||||||
end
|
|
||||||
|
|
||||||
def scan_fallback_references(references)
|
|
||||||
# It's looking for each <...>
|
|
||||||
references.scan(/(?!<)[^<>]+(?=>)/)
|
|
||||||
end
|
|
||||||
|
|
||||||
def config
|
|
||||||
Gitlab.config.incoming_email
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def address_regex(wildcard_address)
|
def address_regex(wildcard_address)
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
module Gitlab
|
module Gitlab
|
||||||
module ServiceDeskEmail
|
module ServiceDeskEmail
|
||||||
class << self
|
class << self
|
||||||
def enabled?
|
include Gitlab::Email::Common
|
||||||
!!config&.enabled && config&.address.present?
|
|
||||||
|
def config
|
||||||
|
Gitlab.config.service_desk_email
|
||||||
end
|
end
|
||||||
|
|
||||||
def key_from_address(address)
|
def key_from_address(address)
|
||||||
|
@ -14,20 +16,10 @@ module Gitlab
|
||||||
Gitlab::IncomingEmail.key_from_address(address, wildcard_address: wildcard_address)
|
Gitlab::IncomingEmail.key_from_address(address, wildcard_address: wildcard_address)
|
||||||
end
|
end
|
||||||
|
|
||||||
def config
|
|
||||||
Gitlab.config.service_desk_email
|
|
||||||
end
|
|
||||||
|
|
||||||
def address_for_key(key)
|
def address_for_key(key)
|
||||||
return if config.address.blank?
|
return if config.address.blank?
|
||||||
|
|
||||||
config.address.sub(Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER, key)
|
config.address.sub(WILDCARD_PLACEHOLDER, key)
|
||||||
end
|
|
||||||
|
|
||||||
def key_from_fallback_message_id(mail_id)
|
|
||||||
message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/
|
|
||||||
|
|
||||||
mail_id[message_id_regexp, 1]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
module Gitlab
|
module Gitlab
|
||||||
class SidekiqMigrateJobs
|
class SidekiqMigrateJobs
|
||||||
LOG_FREQUENCY = 1_000
|
LOG_FREQUENCY = 1_000
|
||||||
|
LOG_FREQUENCY_QUEUES = 10
|
||||||
|
|
||||||
attr_reader :logger, :mappings
|
attr_reader :logger, :mappings
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ module Gitlab
|
||||||
migrated = 0
|
migrated = 0
|
||||||
while queue_length(queue_from) > 0
|
while queue_length(queue_from) > 0
|
||||||
begin
|
begin
|
||||||
if migrated >= 0 && migrated % LOG_FREQUENCY == 0
|
if migrated >= 0 && migrated % LOG_FREQUENCY_QUEUES == 0
|
||||||
logger&.info("Migrating from #{queue_from}. Total: #{queue_length(queue_from)}. Migrated: #{migrated}.")
|
logger&.info("Migrating from #{queue_from}. Total: #{queue_length(queue_from)}. Migrated: #{migrated}.")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ function retrieve_tests_metadata() {
|
||||||
|
|
||||||
if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then
|
if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then
|
||||||
curl --location -o "${FLAKY_RSPEC_SUITE_REPORT_PATH}" "https://gitlab-org.gitlab.io/gitlab/${FLAKY_RSPEC_SUITE_REPORT_PATH}" ||
|
curl --location -o "${FLAKY_RSPEC_SUITE_REPORT_PATH}" "https://gitlab-org.gitlab.io/gitlab/${FLAKY_RSPEC_SUITE_REPORT_PATH}" ||
|
||||||
curl --location -o "${FLAKY_RSPEC_SUITE_REPORT_PATH}" "https://gitlab-org.gitlab.io/gitlab/rspec_flaky/report-suite.json" || # temporary back-compat
|
|
||||||
echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
|
echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
@ -35,13 +34,7 @@ function retrieve_tests_metadata() {
|
||||||
|
|
||||||
if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then
|
if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then
|
||||||
scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ||
|
scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ||
|
||||||
scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "rspec_flaky/report-suite.json" || # temporary back-compat
|
|
||||||
echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
|
echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
|
||||||
|
|
||||||
# temporary back-compat
|
|
||||||
if [[ -f "rspec_flaky/report-suite.json" ]]; then
|
|
||||||
mv "rspec_flaky/report-suite.json" "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "test_metadata_job_id couldn't be found!"
|
echo "test_metadata_job_id couldn't be found!"
|
||||||
|
|
|
@ -101,6 +101,35 @@ RSpec.describe "Issues > User edits issue", :js do
|
||||||
visit project_issue_path(project, issue)
|
visit project_issue_path(project, issue)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'edit description' do
|
||||||
|
def click_edit_issue_description
|
||||||
|
click_on 'Edit title and description'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'places focus on the web editor' do
|
||||||
|
toggle_editing_mode_selector = '[data-testid="toggle-editing-mode-button"] label'
|
||||||
|
content_editor_focused_selector = '[data-testid="content-editor"].is-focused'
|
||||||
|
markdown_field_focused_selector = 'textarea:focus'
|
||||||
|
click_edit_issue_description
|
||||||
|
|
||||||
|
expect(page).to have_selector(markdown_field_focused_selector)
|
||||||
|
|
||||||
|
find(toggle_editing_mode_selector, text: 'Rich text').click
|
||||||
|
|
||||||
|
expect(page).not_to have_selector(content_editor_focused_selector)
|
||||||
|
|
||||||
|
refresh
|
||||||
|
|
||||||
|
click_edit_issue_description
|
||||||
|
|
||||||
|
expect(page).to have_selector(content_editor_focused_selector)
|
||||||
|
|
||||||
|
find(toggle_editing_mode_selector, text: 'Source').click
|
||||||
|
|
||||||
|
expect(page).not_to have_selector(markdown_field_focused_selector)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'update labels' do
|
describe 'update labels' do
|
||||||
it 'will not send ajax request when no data is changed' do
|
it 'will not send ajax request when no data is changed' do
|
||||||
page.within '.labels' do
|
page.within '.labels' do
|
||||||
|
|
|
@ -86,7 +86,7 @@ describe('Description field component', () => {
|
||||||
renderMarkdownPath: '/',
|
renderMarkdownPath: '/',
|
||||||
markdownDocsPath: '/',
|
markdownDocsPath: '/',
|
||||||
quickActionsDocsPath: expect.any(String),
|
quickActionsDocsPath: expect.any(String),
|
||||||
initOnAutofocus: true,
|
autofocus: true,
|
||||||
supportsQuickActions: true,
|
supportsQuickActions: true,
|
||||||
enableAutocomplete: true,
|
enableAutocomplete: true,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -58,6 +58,12 @@ describe('registry_header', () => {
|
||||||
|
|
||||||
describe('sub header parts', () => {
|
describe('sub header parts', () => {
|
||||||
describe('images count', () => {
|
describe('images count', () => {
|
||||||
|
it('does not exist', async () => {
|
||||||
|
await mountComponent({ imagesCount: 0 });
|
||||||
|
|
||||||
|
expect(findImagesCountSubHeader().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('exists', async () => {
|
it('exists', async () => {
|
||||||
await mountComponent({ imagesCount: 1 });
|
await mountComponent({ imagesCount: 1 });
|
||||||
|
|
||||||
|
|
|
@ -116,7 +116,7 @@ describe('WikiForm', () => {
|
||||||
renderMarkdownPath: pageInfoPersisted.markdownPreviewPath,
|
renderMarkdownPath: pageInfoPersisted.markdownPreviewPath,
|
||||||
markdownDocsPath: pageInfoPersisted.markdownHelpPath,
|
markdownDocsPath: pageInfoPersisted.markdownHelpPath,
|
||||||
uploadsPath: pageInfoPersisted.uploadsPath,
|
uploadsPath: pageInfoPersisted.uploadsPath,
|
||||||
initOnAutofocus: pageInfoPersisted.persisted,
|
autofocus: pageInfoPersisted.persisted,
|
||||||
formFieldId: 'wiki_content',
|
formFieldId: 'wiki_content',
|
||||||
formFieldName: 'wiki[content]',
|
formFieldName: 'wiki[content]',
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -27,7 +27,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
|
||||||
const formFieldAriaLabel = 'Edit your content';
|
const formFieldAriaLabel = 'Edit your content';
|
||||||
let mock;
|
let mock;
|
||||||
|
|
||||||
const buildWrapper = ({ propsData = {}, attachTo } = {}) => {
|
const buildWrapper = ({ propsData = {}, attachTo, stubs = {} } = {}) => {
|
||||||
wrapper = mountExtended(MarkdownEditor, {
|
wrapper = mountExtended(MarkdownEditor, {
|
||||||
attachTo,
|
attachTo,
|
||||||
propsData: {
|
propsData: {
|
||||||
|
@ -45,6 +45,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
BubbleMenu: stubComponent(BubbleMenu),
|
BubbleMenu: stubComponent(BubbleMenu),
|
||||||
|
...stubs,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -138,9 +139,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
|
||||||
expect(wrapper.emitted('input')).toEqual([[newValue]]);
|
expect(wrapper.emitted('input')).toEqual([[newValue]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when initOnAutofocus is true', () => {
|
describe('when autofocus is true', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
buildWrapper({ attachTo: document.body, propsData: { initOnAutofocus: true } });
|
buildWrapper({ attachTo: document.body, propsData: { autofocus: true } });
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
});
|
});
|
||||||
|
@ -171,7 +172,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
|
||||||
renderMarkdown: expect.any(Function),
|
renderMarkdown: expect.any(Function),
|
||||||
uploadsPath: window.uploads_path,
|
uploadsPath: window.uploads_path,
|
||||||
markdown: value,
|
markdown: value,
|
||||||
autofocus: 'end',
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -204,10 +204,12 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
|
||||||
findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
|
findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when initOnAutofocus is true', () => {
|
describe('when autofocus is true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
buildWrapper({ propsData: { initOnAutofocus: true } });
|
buildWrapper({
|
||||||
findLocalStorageSync().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
|
propsData: { autofocus: true },
|
||||||
|
stubs: { ContentEditor: stubComponent(ContentEditor) },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the content editor autofocus property to end', () => {
|
it('sets the content editor autofocus property to end', () => {
|
||||||
|
@ -247,19 +249,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
|
||||||
it('updates localStorage value', () => {
|
it('updates localStorage value', () => {
|
||||||
expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_MARKDOWN_FIELD);
|
expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_MARKDOWN_FIELD);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the textarea as the activeElement in the document', async () => {
|
|
||||||
// The component should be rebuilt to attach it to the document body
|
|
||||||
buildWrapper({ attachTo: document.body });
|
|
||||||
await findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
|
|
||||||
|
|
||||||
expect(findContentEditor().exists()).toBe(true);
|
|
||||||
|
|
||||||
await findSegmentedControl().vm.$emit('input', EDITING_MODE_MARKDOWN_FIELD);
|
|
||||||
await findSegmentedControl().vm.$emit('change', EDITING_MODE_MARKDOWN_FIELD);
|
|
||||||
|
|
||||||
expect(document.activeElement).toBe(findTextarea().element);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when content editor emits loading event', () => {
|
describe('when content editor emits loading event', () => {
|
||||||
|
|
|
@ -12,10 +12,12 @@ import WorkItemDescription from '~/work_items/components/work_item_description.v
|
||||||
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||||
|
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||||
import {
|
import {
|
||||||
updateWorkItemMutationResponse,
|
updateWorkItemMutationResponse,
|
||||||
workItemResponseFactory,
|
workItemResponseFactory,
|
||||||
workItemQueryResponse,
|
workItemQueryResponse,
|
||||||
|
projectWorkItemResponse,
|
||||||
} from '../mock_data';
|
} from '../mock_data';
|
||||||
|
|
||||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
|
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
|
||||||
|
@ -29,6 +31,8 @@ describe('WorkItemDescription', () => {
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
|
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
|
||||||
|
const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
|
||||||
|
let workItemResponseHandler;
|
||||||
|
|
||||||
const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
|
const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
|
||||||
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
|
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
|
||||||
|
@ -44,18 +48,24 @@ describe('WorkItemDescription', () => {
|
||||||
canUpdate = true,
|
canUpdate = true,
|
||||||
workItemResponse = workItemResponseFactory({ canUpdate }),
|
workItemResponse = workItemResponseFactory({ canUpdate }),
|
||||||
isEditing = false,
|
isEditing = false,
|
||||||
|
fetchByIid = false,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
|
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
|
||||||
|
|
||||||
const { id } = workItemQueryResponse.data.workItem;
|
const { id } = workItemQueryResponse.data.workItem;
|
||||||
wrapper = shallowMount(WorkItemDescription, {
|
wrapper = shallowMount(WorkItemDescription, {
|
||||||
apolloProvider: createMockApollo([
|
apolloProvider: createMockApollo([
|
||||||
[workItemQuery, workItemResponseHandler],
|
[workItemQuery, workItemResponseHandler],
|
||||||
[updateWorkItemMutation, mutationHandler],
|
[updateWorkItemMutation, mutationHandler],
|
||||||
|
[workItemByIidQuery, workItemByIidResponseHandler],
|
||||||
]),
|
]),
|
||||||
propsData: {
|
propsData: {
|
||||||
workItemId: id,
|
workItemId: id,
|
||||||
fullPath: 'test-project-path',
|
fullPath: 'test-project-path',
|
||||||
|
queryVariables: {
|
||||||
|
id: workItemId,
|
||||||
|
},
|
||||||
|
fetchByIid,
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
MarkdownField,
|
MarkdownField,
|
||||||
|
@ -242,4 +252,20 @@ describe('WorkItemDescription', () => {
|
||||||
expect(updateDraft).toHaveBeenCalled();
|
expect(updateDraft).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
|
||||||
|
createComponent({ fetchByIid: false });
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(workItemResponseHandler).toHaveBeenCalled();
|
||||||
|
expect(workItemByIidResponseHandler).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the IID work item query when when `fetchByIid` prop is true', async () => {
|
||||||
|
createComponent({ fetchByIid: true });
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(workItemResponseHandler).not.toHaveBeenCalled();
|
||||||
|
expect(workItemByIidResponseHandler).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -86,6 +86,7 @@ describe('WorkItemDetailModal component', () => {
|
||||||
isModal: true,
|
isModal: true,
|
||||||
workItemId: defaultPropsData.workItemId,
|
workItemId: defaultPropsData.workItemId,
|
||||||
workItemParentId: defaultPropsData.issueGid,
|
workItemParentId: defaultPropsData.issueGid,
|
||||||
|
iid: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
|
||||||
import WorkItemInformation from '~/work_items/components/work_item_information.vue';
|
import WorkItemInformation from '~/work_items/components/work_item_information.vue';
|
||||||
import { i18n } from '~/work_items/constants';
|
import { i18n } from '~/work_items/constants';
|
||||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||||
|
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||||
import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
|
import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
|
||||||
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
|
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
|
||||||
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
|
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
|
||||||
|
@ -37,6 +38,7 @@ import {
|
||||||
workItemResponseFactory,
|
workItemResponseFactory,
|
||||||
workItemTitleSubscriptionResponse,
|
workItemTitleSubscriptionResponse,
|
||||||
workItemAssigneesSubscriptionResponse,
|
workItemAssigneesSubscriptionResponse,
|
||||||
|
projectWorkItemResponse,
|
||||||
} from '../mock_data';
|
} from '../mock_data';
|
||||||
|
|
||||||
describe('WorkItemDetail component', () => {
|
describe('WorkItemDetail component', () => {
|
||||||
|
@ -52,6 +54,7 @@ describe('WorkItemDetail component', () => {
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
});
|
});
|
||||||
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
|
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
|
||||||
|
const successByIidHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
|
||||||
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
|
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
|
||||||
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
|
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
|
||||||
const assigneesSubscriptionHandler = jest
|
const assigneesSubscriptionHandler = jest
|
||||||
|
@ -87,12 +90,15 @@ describe('WorkItemDetail component', () => {
|
||||||
error = undefined,
|
error = undefined,
|
||||||
includeWidgets = false,
|
includeWidgets = false,
|
||||||
workItemsMvc2Enabled = false,
|
workItemsMvc2Enabled = false,
|
||||||
|
fetchByIid = false,
|
||||||
|
iidPathQueryParam = undefined,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const handlers = [
|
const handlers = [
|
||||||
[workItemQuery, handler],
|
[workItemQuery, handler],
|
||||||
[workItemTitleSubscription, subscriptionHandler],
|
[workItemTitleSubscription, subscriptionHandler],
|
||||||
[workItemDatesSubscription, datesSubscriptionHandler],
|
[workItemDatesSubscription, datesSubscriptionHandler],
|
||||||
[workItemAssigneesSubscription, assigneesSubscriptionHandler],
|
[workItemAssigneesSubscription, assigneesSubscriptionHandler],
|
||||||
|
[workItemByIidQuery, successByIidHandler],
|
||||||
confidentialityMock,
|
confidentialityMock,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -104,7 +110,7 @@ describe('WorkItemDetail component', () => {
|
||||||
typePolicies: includeWidgets ? config.cacheConfig.typePolicies : {},
|
typePolicies: includeWidgets ? config.cacheConfig.typePolicies : {},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
propsData: { isModal, workItemId },
|
propsData: { isModal, workItemId, iid: '1' },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
updateInProgress,
|
updateInProgress,
|
||||||
|
@ -114,15 +120,24 @@ describe('WorkItemDetail component', () => {
|
||||||
provide: {
|
provide: {
|
||||||
glFeatures: {
|
glFeatures: {
|
||||||
workItemsMvc2: workItemsMvc2Enabled,
|
workItemsMvc2: workItemsMvc2Enabled,
|
||||||
|
useIidInWorkItemsPath: fetchByIid,
|
||||||
},
|
},
|
||||||
hasIssueWeightsFeature: true,
|
hasIssueWeightsFeature: true,
|
||||||
hasIterationsFeature: true,
|
hasIterationsFeature: true,
|
||||||
projectNamespace: 'namespace',
|
projectNamespace: 'namespace',
|
||||||
|
fullPath: 'group/project',
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
WorkItemWeight: true,
|
WorkItemWeight: true,
|
||||||
WorkItemIteration: true,
|
WorkItemIteration: true,
|
||||||
},
|
},
|
||||||
|
mocks: {
|
||||||
|
$route: {
|
||||||
|
query: {
|
||||||
|
iid_path: iidPathQueryParam,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -421,8 +436,9 @@ describe('WorkItemDetail component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('subscriptions', () => {
|
describe('subscriptions', () => {
|
||||||
it('calls the title subscription', () => {
|
it('calls the title subscription', async () => {
|
||||||
createComponent();
|
createComponent();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
expect(titleSubscriptionHandler).toHaveBeenCalledWith({
|
expect(titleSubscriptionHandler).toHaveBeenCalledWith({
|
||||||
issuableId: workItemQueryResponse.data.workItem.id,
|
issuableId: workItemQueryResponse.data.workItem.id,
|
||||||
|
@ -571,4 +587,35 @@ describe('WorkItemDetail component', () => {
|
||||||
expect(findWorkItemInformationAlert().exists()).toBe(false);
|
expect(findWorkItemInformationAlert().exists()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is false', async () => {
|
||||||
|
createComponent();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(successHandler).toHaveBeenCalledWith({
|
||||||
|
id: workItemQueryResponse.data.workItem.id,
|
||||||
|
});
|
||||||
|
expect(successByIidHandler).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is true but there is no `iid_path` parameter in URL', async () => {
|
||||||
|
createComponent({ fetchByIid: true });
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(successHandler).toHaveBeenCalledWith({
|
||||||
|
id: workItemQueryResponse.data.workItem.id,
|
||||||
|
});
|
||||||
|
expect(successByIidHandler).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present', async () => {
|
||||||
|
createComponent({ fetchByIid: true, iidPathQueryParam: 'true' });
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(successHandler).not.toHaveBeenCalled();
|
||||||
|
expect(successByIidHandler).toHaveBeenCalledWith({
|
||||||
|
fullPath: 'group/project',
|
||||||
|
iid: '1',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widg
|
||||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||||
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
|
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
|
||||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||||
|
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||||
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
|
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
|
||||||
import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constants';
|
import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constants';
|
||||||
import {
|
import {
|
||||||
|
@ -18,6 +19,7 @@ import {
|
||||||
workItemResponseFactory,
|
workItemResponseFactory,
|
||||||
updateWorkItemMutationResponse,
|
updateWorkItemMutationResponse,
|
||||||
workItemLabelsSubscriptionResponse,
|
workItemLabelsSubscriptionResponse,
|
||||||
|
projectWorkItemResponse,
|
||||||
} from '../mock_data';
|
} from '../mock_data';
|
||||||
|
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
|
@ -33,6 +35,7 @@ describe('WorkItemLabels component', () => {
|
||||||
const findLabelsTitle = () => wrapper.findByTestId('labels-title');
|
const findLabelsTitle = () => wrapper.findByTestId('labels-title');
|
||||||
|
|
||||||
const workItemQuerySuccess = jest.fn().mockResolvedValue(workItemQueryResponse);
|
const workItemQuerySuccess = jest.fn().mockResolvedValue(workItemQueryResponse);
|
||||||
|
const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
|
||||||
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
|
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
|
||||||
const successUpdateWorkItemMutationHandler = jest
|
const successUpdateWorkItemMutationHandler = jest
|
||||||
.fn()
|
.fn()
|
||||||
|
@ -45,12 +48,14 @@ describe('WorkItemLabels component', () => {
|
||||||
workItemQueryHandler = workItemQuerySuccess,
|
workItemQueryHandler = workItemQuerySuccess,
|
||||||
searchQueryHandler = successSearchQueryHandler,
|
searchQueryHandler = successSearchQueryHandler,
|
||||||
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
|
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
|
||||||
|
fetchByIid = false,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const apolloProvider = createMockApollo([
|
const apolloProvider = createMockApollo([
|
||||||
[workItemQuery, workItemQueryHandler],
|
[workItemQuery, workItemQueryHandler],
|
||||||
[labelSearchQuery, searchQueryHandler],
|
[labelSearchQuery, searchQueryHandler],
|
||||||
[updateWorkItemMutation, updateWorkItemMutationHandler],
|
[updateWorkItemMutation, updateWorkItemMutationHandler],
|
||||||
[workItemLabelsSubscription, subscriptionHandler],
|
[workItemLabelsSubscription, subscriptionHandler],
|
||||||
|
[workItemByIidQuery, workItemByIidResponseHandler],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
wrapper = mountExtended(WorkItemLabels, {
|
wrapper = mountExtended(WorkItemLabels, {
|
||||||
|
@ -58,6 +63,10 @@ describe('WorkItemLabels component', () => {
|
||||||
workItemId,
|
workItemId,
|
||||||
canUpdate,
|
canUpdate,
|
||||||
fullPath: 'test-project-path',
|
fullPath: 'test-project-path',
|
||||||
|
queryVariables: {
|
||||||
|
id: workItemId,
|
||||||
|
},
|
||||||
|
fetchByIid,
|
||||||
},
|
},
|
||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
apolloProvider,
|
apolloProvider,
|
||||||
|
@ -226,4 +235,20 @@ describe('WorkItemLabels component', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
|
||||||
|
createComponent({ fetchByIid: false });
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(workItemQuerySuccess).toHaveBeenCalled();
|
||||||
|
expect(workItemByIidResponseHandler).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the IID work item query when when `fetchByIid` prop is true', async () => {
|
||||||
|
createComponent({ fetchByIid: true });
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(workItemQuerySuccess).not.toHaveBeenCalled();
|
||||||
|
expect(workItemByIidResponseHandler).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -41,6 +41,7 @@ export const workItemQueryResponse = {
|
||||||
workItem: {
|
workItem: {
|
||||||
__typename: 'WorkItem',
|
__typename: 'WorkItem',
|
||||||
id: 'gid://gitlab/WorkItem/1',
|
id: 'gid://gitlab/WorkItem/1',
|
||||||
|
iid: '1',
|
||||||
title: 'Test',
|
title: 'Test',
|
||||||
state: 'OPEN',
|
state: 'OPEN',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
|
@ -113,6 +114,7 @@ export const updateWorkItemMutationResponse = {
|
||||||
workItem: {
|
workItem: {
|
||||||
__typename: 'WorkItem',
|
__typename: 'WorkItem',
|
||||||
id: 'gid://gitlab/WorkItem/1',
|
id: 'gid://gitlab/WorkItem/1',
|
||||||
|
iid: '1',
|
||||||
title: 'Updated title',
|
title: 'Updated title',
|
||||||
state: 'OPEN',
|
state: 'OPEN',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
|
@ -199,6 +201,7 @@ export const workItemResponseFactory = ({
|
||||||
workItem: {
|
workItem: {
|
||||||
__typename: 'WorkItem',
|
__typename: 'WorkItem',
|
||||||
id: 'gid://gitlab/WorkItem/1',
|
id: 'gid://gitlab/WorkItem/1',
|
||||||
|
iid: 1,
|
||||||
title: 'Updated title',
|
title: 'Updated title',
|
||||||
state: 'OPEN',
|
state: 'OPEN',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
|
@ -331,6 +334,7 @@ export const createWorkItemMutationResponse = {
|
||||||
workItem: {
|
workItem: {
|
||||||
__typename: 'WorkItem',
|
__typename: 'WorkItem',
|
||||||
id: 'gid://gitlab/WorkItem/1',
|
id: 'gid://gitlab/WorkItem/1',
|
||||||
|
iid: '1',
|
||||||
title: 'Updated title',
|
title: 'Updated title',
|
||||||
state: 'OPEN',
|
state: 'OPEN',
|
||||||
description: 'description',
|
description: 'description',
|
||||||
|
@ -368,6 +372,7 @@ export const createWorkItemFromTaskMutationResponse = {
|
||||||
__typename: 'WorkItem',
|
__typename: 'WorkItem',
|
||||||
description: 'New description',
|
description: 'New description',
|
||||||
id: 'gid://gitlab/WorkItem/1',
|
id: 'gid://gitlab/WorkItem/1',
|
||||||
|
iid: '1',
|
||||||
title: 'Updated title',
|
title: 'Updated title',
|
||||||
state: 'OPEN',
|
state: 'OPEN',
|
||||||
confidential: false,
|
confidential: false,
|
||||||
|
@ -405,6 +410,7 @@ export const createWorkItemFromTaskMutationResponse = {
|
||||||
newWorkItem: {
|
newWorkItem: {
|
||||||
__typename: 'WorkItem',
|
__typename: 'WorkItem',
|
||||||
id: 'gid://gitlab/WorkItem/1000000',
|
id: 'gid://gitlab/WorkItem/1000000',
|
||||||
|
iid: '100',
|
||||||
title: 'Updated title',
|
title: 'Updated title',
|
||||||
state: 'OPEN',
|
state: 'OPEN',
|
||||||
createdAt: '2022-08-03T12:41:54Z',
|
createdAt: '2022-08-03T12:41:54Z',
|
||||||
|
@ -776,6 +782,7 @@ export const changeWorkItemParentMutationResponse = {
|
||||||
},
|
},
|
||||||
description: null,
|
description: null,
|
||||||
id: 'gid://gitlab/WorkItem/2',
|
id: 'gid://gitlab/WorkItem/2',
|
||||||
|
iid: '2',
|
||||||
state: 'OPEN',
|
state: 'OPEN',
|
||||||
title: 'Foo',
|
title: 'Foo',
|
||||||
confidential: false,
|
confidential: false,
|
||||||
|
@ -1122,3 +1129,14 @@ export const projectMilestonesResponseWithNoMilestones = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const projectWorkItemResponse = {
|
||||||
|
data: {
|
||||||
|
workspace: {
|
||||||
|
id: 'gid://gitlab/Project/1',
|
||||||
|
workItems: {
|
||||||
|
nodes: [workItemQueryResponse.data.workItem],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -37,12 +37,17 @@ describe('Create work item component', () => {
|
||||||
props = {},
|
props = {},
|
||||||
queryHandler = querySuccessHandler,
|
queryHandler = querySuccessHandler,
|
||||||
mutationHandler = createWorkItemSuccessHandler,
|
mutationHandler = createWorkItemSuccessHandler,
|
||||||
|
fetchByIid = false,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
fakeApollo = createMockApollo([
|
fakeApollo = createMockApollo(
|
||||||
|
[
|
||||||
[projectWorkItemTypesQuery, queryHandler],
|
[projectWorkItemTypesQuery, queryHandler],
|
||||||
[createWorkItemMutation, mutationHandler],
|
[createWorkItemMutation, mutationHandler],
|
||||||
[createWorkItemFromTaskMutation, mutationHandler],
|
[createWorkItemFromTaskMutation, mutationHandler],
|
||||||
]);
|
],
|
||||||
|
{},
|
||||||
|
{ typePolicies: { Project: { merge: true } } },
|
||||||
|
);
|
||||||
wrapper = shallowMount(CreateWorkItem, {
|
wrapper = shallowMount(CreateWorkItem, {
|
||||||
apolloProvider: fakeApollo,
|
apolloProvider: fakeApollo,
|
||||||
data() {
|
data() {
|
||||||
|
@ -61,6 +66,9 @@ describe('Create work item component', () => {
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
fullPath: 'full-path',
|
fullPath: 'full-path',
|
||||||
|
glFeatures: {
|
||||||
|
useIidInWorkItemsPath: fetchByIid,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -99,7 +107,12 @@ describe('Create work item component', () => {
|
||||||
wrapper.find('form').trigger('submit');
|
wrapper.find('form').trigger('submit');
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(wrapper.vm.$router.push).toHaveBeenCalled();
|
expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
|
||||||
|
name: 'workItem',
|
||||||
|
params: {
|
||||||
|
id: '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds right margin for create button', () => {
|
it('adds right margin for create button', () => {
|
||||||
|
@ -197,4 +210,18 @@ describe('Create work item component', () => {
|
||||||
'Something went wrong when creating work item. Please try again.',
|
'Something went wrong when creating work item. Please try again.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('performs a correct redirect when `useIidInWorkItemsPath` feature flag is enabled', async () => {
|
||||||
|
createComponent({ fetchByIid: true });
|
||||||
|
findTitleInput().vm.$emit('title-input', 'Test title');
|
||||||
|
|
||||||
|
wrapper.find('form').trigger('submit');
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
|
||||||
|
name: 'workItem',
|
||||||
|
params: { id: '1' },
|
||||||
|
query: { iid_path: 'true' },
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -55,6 +55,7 @@ describe('Work items root component', () => {
|
||||||
isModal: false,
|
isModal: false,
|
||||||
workItemId: 'gid://gitlab/WorkItem/1',
|
workItemId: 'gid://gitlab/WorkItem/1',
|
||||||
workItemParentId: null,
|
workItemParentId: null,
|
||||||
|
iid: '1',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -65,11 +66,15 @@ describe('Work items root component', () => {
|
||||||
deleteWorkItemHandler,
|
deleteWorkItemHandler,
|
||||||
});
|
});
|
||||||
|
|
||||||
findWorkItemDetail().vm.$emit('deleteWorkItem');
|
findWorkItemDetail().vm.$emit('deleteWorkItem', { workItemType: 'task', workItemId: '1' });
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(deleteWorkItemHandler).toHaveBeenCalled();
|
expect(deleteWorkItemHandler).toHaveBeenCalledWith({
|
||||||
|
input: {
|
||||||
|
id: '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
expect(mockToastShow).toHaveBeenCalled();
|
expect(mockToastShow).toHaveBeenCalled();
|
||||||
expect(visitUrl).toHaveBeenCalledWith(issuesListPath);
|
expect(visitUrl).toHaveBeenCalledWith(issuesListPath);
|
||||||
});
|
});
|
||||||
|
@ -81,7 +86,7 @@ describe('Work items root component', () => {
|
||||||
deleteWorkItemHandler,
|
deleteWorkItemHandler,
|
||||||
});
|
});
|
||||||
|
|
||||||
findWorkItemDetail().vm.$emit('deleteWorkItem');
|
findWorkItemDetail().vm.$emit('deleteWorkItem', { workItemType: 'task', workItemId: '1' });
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata do
|
||||||
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
let(:pipeline) do
|
||||||
|
build(:ci_pipeline, project: project, ref: 'master', user: user)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:command) do
|
||||||
|
Gitlab::Ci::Pipeline::Chain::Command.new(
|
||||||
|
project: project,
|
||||||
|
current_user: user,
|
||||||
|
origin_ref: 'master')
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:dependencies) do
|
||||||
|
[
|
||||||
|
Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command),
|
||||||
|
Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command),
|
||||||
|
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules.new(pipeline, command),
|
||||||
|
Gitlab::Ci::Pipeline::Chain::SeedBlock.new(pipeline, command),
|
||||||
|
Gitlab::Ci::Pipeline::Chain::Seed.new(pipeline, command),
|
||||||
|
Gitlab::Ci::Pipeline::Chain::Populate.new(pipeline, command)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:step) { described_class.new(pipeline, command) }
|
||||||
|
|
||||||
|
let(:config) do
|
||||||
|
{ rspec: { script: 'rspec' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_chain
|
||||||
|
dependencies.map(&:perform!)
|
||||||
|
step.perform!
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_ci_pipeline_yaml_file(YAML.dump(config))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with pipeline name' do
|
||||||
|
let(:config) do
|
||||||
|
{ workflow: { name: ' Pipeline name ' }, rspec: { script: 'rspec' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not break the chain' do
|
||||||
|
run_chain
|
||||||
|
|
||||||
|
expect(step.break?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with feature flag disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(pipeline_name: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not build pipeline_metadata' do
|
||||||
|
run_chain
|
||||||
|
|
||||||
|
expect(pipeline.pipeline_metadata).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with feature flag enabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(pipeline_name: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'builds pipeline_metadata' do
|
||||||
|
run_chain
|
||||||
|
|
||||||
|
expect(pipeline.pipeline_metadata.name).to eq('Pipeline name')
|
||||||
|
expect(pipeline.pipeline_metadata.project).to eq(pipeline.project)
|
||||||
|
expect(pipeline.pipeline_metadata).not_to be_persisted
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with empty name' do
|
||||||
|
let(:config) do
|
||||||
|
{ workflow: { name: ' ' }, rspec: { script: 'rspec' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'strips whitespace from name' do
|
||||||
|
run_chain
|
||||||
|
|
||||||
|
expect(pipeline.pipeline_metadata).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with variables' do
|
||||||
|
let(:config) do
|
||||||
|
{
|
||||||
|
variables: { ROOT_VAR: 'value $WORKFLOW_VAR1' },
|
||||||
|
workflow: {
|
||||||
|
name: 'Pipeline $ROOT_VAR $WORKFLOW_VAR2 $UNKNOWN_VAR',
|
||||||
|
rules: [{ variables: { WORKFLOW_VAR1: 'value1', WORKFLOW_VAR2: 'value2' } }]
|
||||||
|
},
|
||||||
|
rspec: { script: 'rspec' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'substitutes variables' do
|
||||||
|
run_chain
|
||||||
|
|
||||||
|
expect(pipeline.pipeline_metadata.name).to eq('Pipeline value value1 value2 ')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid name' do
|
||||||
|
let(:config) do
|
||||||
|
{
|
||||||
|
variables: { ROOT_VAR: 'a' * 256 },
|
||||||
|
workflow: {
|
||||||
|
name: 'Pipeline $ROOT_VAR'
|
||||||
|
},
|
||||||
|
rspec: { script: 'rspec' }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns error and breaks chain' do
|
||||||
|
ret = run_chain
|
||||||
|
|
||||||
|
expect(ret)
|
||||||
|
.to match_array(["Failed to build pipeline metadata! Name is too long (maximum is 255 characters)"])
|
||||||
|
expect(pipeline.pipeline_metadata.errors.full_messages)
|
||||||
|
.to match_array(['Name is too long (maximum is 255 characters)'])
|
||||||
|
expect(step.break?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -236,66 +236,4 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with pipeline name' do
|
|
||||||
let(:config) do
|
|
||||||
{ workflow: { name: ' Pipeline name ' }, rspec: { script: 'rspec' } }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with feature flag disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(pipeline_name: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not build pipeline_metadata' do
|
|
||||||
run_chain
|
|
||||||
|
|
||||||
expect(pipeline.pipeline_metadata).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with feature flag enabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(pipeline_name: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'builds pipeline_metadata' do
|
|
||||||
run_chain
|
|
||||||
|
|
||||||
expect(pipeline.pipeline_metadata.name).to eq('Pipeline name')
|
|
||||||
expect(pipeline.pipeline_metadata.project).to eq(pipeline.project)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with empty name' do
|
|
||||||
let(:config) do
|
|
||||||
{ workflow: { name: ' ' }, rspec: { script: 'rspec' } }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'strips whitespace from name' do
|
|
||||||
run_chain
|
|
||||||
|
|
||||||
expect(pipeline.pipeline_metadata).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with variables' do
|
|
||||||
let(:config) do
|
|
||||||
{
|
|
||||||
variables: { ROOT_VAR: 'value $WORKFLOW_VAR1' },
|
|
||||||
workflow: {
|
|
||||||
name: 'Pipeline $ROOT_VAR $WORKFLOW_VAR2 $UNKNOWN_VAR',
|
|
||||||
rules: [{ variables: { WORKFLOW_VAR1: 'value1', WORKFLOW_VAR2: 'value2' } }]
|
|
||||||
},
|
|
||||||
rspec: { script: 'rspec' }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'substitutes variables' do
|
|
||||||
run_chain
|
|
||||||
|
|
||||||
expect(pipeline.pipeline_metadata.name).to eq('Pipeline value value1 value2 ')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do
|
||||||
stub_config_setting(host: 'localhost')
|
stub_config_setting(host: 'localhost')
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX}") }
|
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX}") }
|
||||||
let(:project) { create(:project, :public) }
|
let(:project) { create(:project, :public) }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:noteable) { create(:issue, project: project) }
|
let(:noteable) { create(:issue, project: project) }
|
||||||
|
@ -21,19 +21,19 @@ RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do
|
||||||
let(:mail) { Mail::Message.new(email_raw) }
|
let(:mail) { Mail::Message.new(email_raw) }
|
||||||
|
|
||||||
it "matches the new format" do
|
it "matches the new format" do
|
||||||
handler = described_class.new(mail, "#{mail_key}#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX}")
|
handler = described_class.new(mail, "#{mail_key}#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX}")
|
||||||
|
|
||||||
expect(handler.can_handle?).to be_truthy
|
expect(handler.can_handle?).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it "matches the legacy format" do
|
it "matches the legacy format" do
|
||||||
handler = described_class.new(mail, "#{mail_key}#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY}")
|
handler = described_class.new(mail, "#{mail_key}#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY}")
|
||||||
|
|
||||||
expect(handler.can_handle?).to be_truthy
|
expect(handler.can_handle?).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't match either format" do
|
it "doesn't match either format" do
|
||||||
handler = described_class.new(mail, "+#{mail_key}#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX}")
|
handler = described_class.new(mail, "+#{mail_key}#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX}")
|
||||||
|
|
||||||
expect(handler.can_handle?).to be_falsey
|
expect(handler.can_handle?).to be_falsey
|
||||||
end
|
end
|
||||||
|
@ -64,7 +64,7 @@ RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when using old style unsubscribe link' do
|
context 'when using old style unsubscribe link' do
|
||||||
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY}") }
|
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY}") }
|
||||||
|
|
||||||
it 'unsubscribes user from notable' do
|
it 'unsubscribes user from notable' do
|
||||||
expect { receiver.execute }.to change { noteable.subscribed?(user) }.from(true).to(false)
|
expect { receiver.execute }.to change { noteable.subscribed?(user) }.from(true).to(false)
|
||||||
|
|
|
@ -60,7 +60,7 @@ RSpec.describe Gitlab::Email::Handler do
|
||||||
|
|
||||||
describe 'regexps are set properly' do
|
describe 'regexps are set properly' do
|
||||||
let(:addresses) do
|
let(:addresses) do
|
||||||
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY}) +
|
%W(sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX} sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY}) +
|
||||||
%w(sent_notification_key path-to-project-123-user_email_token-merge-request) +
|
%w(sent_notification_key path-to-project-123-user_email_token-merge-request) +
|
||||||
%w(path-to-project-123-user_email_token-issue path-to-project-123-user_email_token-issue-123) +
|
%w(path-to-project-123-user_email_token-issue path-to-project-123-user_email_token-issue-123) +
|
||||||
%w(path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project)
|
%w(path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project)
|
||||||
|
|
|
@ -1,87 +1,17 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'fast_spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Gitlab::IncomingEmail do
|
RSpec.describe Gitlab::IncomingEmail do
|
||||||
describe "self.enabled?" do
|
let(:setting_name) { :incoming_email }
|
||||||
context "when reply by email is enabled" do
|
|
||||||
before do
|
|
||||||
stub_incoming_email_setting(enabled: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true' do
|
it_behaves_like 'common email methods'
|
||||||
expect(described_class.enabled?).to be(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when reply by email is disabled" do
|
describe 'self.key_from_address' do
|
||||||
before do
|
|
||||||
stub_incoming_email_setting(enabled: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns false" do
|
|
||||||
expect(described_class.enabled?).to be(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'self.supports_wildcard?' do
|
|
||||||
context 'address contains the wildcard placeholder' do
|
|
||||||
before do
|
before do
|
||||||
stub_incoming_email_setting(address: 'replies+%{key}@example.com')
|
stub_incoming_email_setting(address: 'replies+%{key}@example.com')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'confirms that wildcard is supported' do
|
|
||||||
expect(described_class.supports_wildcard?).to be(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "address doesn't contain the wildcard placeholder" do
|
|
||||||
before do
|
|
||||||
stub_incoming_email_setting(address: 'replies@example.com')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns that wildcard is not supported' do
|
|
||||||
expect(described_class.supports_wildcard?).to be(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'address is not set' do
|
|
||||||
before do
|
|
||||||
stub_incoming_email_setting(address: nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns that wildcard is not supported' do
|
|
||||||
expect(described_class.supports_wildcard?).to be(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'self.unsubscribe_address' do
|
|
||||||
before do
|
|
||||||
stub_incoming_email_setting(address: 'replies+%{key}@example.com')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the address with interpolated reply key and unsubscribe suffix' do
|
|
||||||
expect(described_class.unsubscribe_address('key')).to eq("replies+key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX}@example.com")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "self.reply_address" do
|
|
||||||
before do
|
|
||||||
stub_incoming_email_setting(address: "replies+%{key}@example.com")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns the address with an interpolated reply key" do
|
|
||||||
expect(described_class.reply_address("key")).to eq("replies+key@example.com")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "self.key_from_address" do
|
|
||||||
before do
|
|
||||||
stub_incoming_email_setting(address: "replies+%{key}@example.com")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns reply key" do
|
it "returns reply key" do
|
||||||
expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
|
expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
|
||||||
end
|
end
|
||||||
|
@ -101,25 +31,4 @@ RSpec.describe Gitlab::IncomingEmail do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'self.key_from_fallback_message_id' do
|
|
||||||
it 'returns reply key' do
|
|
||||||
expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'self.scan_fallback_references' do
|
|
||||||
let(:references) do
|
|
||||||
'<issue_1@localhost>' \
|
|
||||||
' <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>' \
|
|
||||||
',<exchange@microsoft.com>'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns reply key' do
|
|
||||||
expect(described_class.scan_fallback_references(references))
|
|
||||||
.to eq(%w[issue_1@localhost
|
|
||||||
reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost
|
|
||||||
exchange@microsoft.com])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,39 +1,11 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'fast_spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Gitlab::ServiceDeskEmail do
|
RSpec.describe Gitlab::ServiceDeskEmail do
|
||||||
describe '.enabled?' do
|
let(:setting_name) { :service_desk_email }
|
||||||
context 'when service_desk_email is enabled and address is set' do
|
|
||||||
before do
|
|
||||||
stub_service_desk_email_setting(enabled: true, address: 'foo')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true' do
|
it_behaves_like 'common email methods'
|
||||||
expect(described_class.enabled?).to be_truthy
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when service_desk_email is disabled' do
|
|
||||||
before do
|
|
||||||
stub_service_desk_email_setting(enabled: false, address: 'foo')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false' do
|
|
||||||
expect(described_class.enabled?).to be_falsey
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when service desk address is not set' do
|
|
||||||
before do
|
|
||||||
stub_service_desk_email_setting(enabled: true, address: nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false' do
|
|
||||||
expect(described_class.enabled?).to be_falsey
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '.key_from_address' do
|
describe '.key_from_address' do
|
||||||
context 'when service desk address is set' do
|
context 'when service desk address is set' do
|
||||||
|
@ -78,10 +50,4 @@ RSpec.describe Gitlab::ServiceDeskEmail do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'self.key_from_fallback_message_id' do
|
|
||||||
it 'returns reply key' do
|
|
||||||
expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require_migration!
|
||||||
|
|
||||||
|
RSpec.describe MigrateSidekiqQueuedJobs, :clean_gitlab_redis_queues do
|
||||||
|
around do |example|
|
||||||
|
Sidekiq::Testing.disable!(&example)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#up', :aggregate_failures, :silence_stdout do
|
||||||
|
before do
|
||||||
|
EmailReceiverWorker.sidekiq_options queue: 'email_receiver'
|
||||||
|
EmailReceiverWorker.perform_async('foo')
|
||||||
|
EmailReceiverWorker.perform_async('bar')
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
EmailReceiverWorker.set_queue
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with worker_queue_mappings mocked' do
|
||||||
|
it 'migrates the jobs to the correct destination queue' do
|
||||||
|
allow(Gitlab::SidekiqConfig).to receive(:worker_queue_mappings)
|
||||||
|
.and_return({ "EmailReceiverWorker" => "default" })
|
||||||
|
expect(queue_length('email_receiver')).to eq(2)
|
||||||
|
expect(queue_length('default')).to eq(0)
|
||||||
|
migrate!
|
||||||
|
expect(queue_length('email_receiver')).to eq(0)
|
||||||
|
expect(queue_length('default')).to eq(2)
|
||||||
|
|
||||||
|
jobs = list_jobs('default')
|
||||||
|
expect(jobs[0]).to include("class" => "EmailReceiverWorker", "args" => ["bar"])
|
||||||
|
expect(jobs[1]).to include("class" => "EmailReceiverWorker", "args" => ["foo"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without worker_queue_mappings mocked' do
|
||||||
|
it 'migration still works' do
|
||||||
|
# Assuming Settings.sidekiq.routing_rules is [['*', 'default']]
|
||||||
|
# If routing_rules or Gitlab::SidekiqConfig.worker_queue_mappings changed,
|
||||||
|
# this spec might be failing. We'll have to adjust the migration or this spec.
|
||||||
|
expect(queue_length('email_receiver')).to eq(2)
|
||||||
|
expect(queue_length('default')).to eq(0)
|
||||||
|
migrate!
|
||||||
|
expect(queue_length('email_receiver')).to eq(0)
|
||||||
|
expect(queue_length('default')).to eq(2)
|
||||||
|
|
||||||
|
jobs = list_jobs('default')
|
||||||
|
expect(jobs[0]).to include("class" => "EmailReceiverWorker", "args" => ["bar"])
|
||||||
|
expect(jobs[1]).to include("class" => "EmailReceiverWorker", "args" => ["foo"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with illegal JSON payload' do
|
||||||
|
let(:job) { '{foo: 1}' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Sidekiq.redis do |conn|
|
||||||
|
conn.lpush("queue:email_receiver", job)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'logs an error' do
|
||||||
|
allow(::Gitlab::BackgroundMigration::Logger).to receive(:build).and_return(Logger.new($stdout))
|
||||||
|
migrate!
|
||||||
|
expect($stdout.string).to include("Unmarshal JSON payload from SidekiqMigrateJobs failed. Job: #{job}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when run in GitLab.com' do
|
||||||
|
it 'skips the migration' do
|
||||||
|
allow(Gitlab).to receive(:com?).and_return(true)
|
||||||
|
expect(described_class::SidekiqMigrateJobs).not_to receive(:new)
|
||||||
|
migrate!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def queue_length(queue_name)
|
||||||
|
Sidekiq.redis do |conn|
|
||||||
|
conn.llen("queue:#{queue_name}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_jobs(queue_name)
|
||||||
|
Sidekiq.redis { |conn| conn.lrange("queue:#{queue_name}", 0, -1) }
|
||||||
|
.map { |item| Sidekiq.load_json item }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,66 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe JiraConnect::CorsPreflightChecksController do
|
||||||
|
shared_examples 'allows cross-origin requests on self managed' do
|
||||||
|
it 'renders not found' do
|
||||||
|
options path
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
expect(response.headers['Access-Control-Allow-Origin']).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with jira_connect_proxy_url setting' do
|
||||||
|
before do
|
||||||
|
stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
|
||||||
|
|
||||||
|
options path, headers: { 'Origin' => 'http://notgitlab.com' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 200' do
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'responds with access-control-allow headers', :aggregate_failures do
|
||||||
|
expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
|
||||||
|
expect(response.headers['Access-Control-Allow-Methods']).to eq allowed_methods
|
||||||
|
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when on GitLab.com' do
|
||||||
|
before do
|
||||||
|
allow(Gitlab).to receive(:com?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders not found' do
|
||||||
|
options path
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
expect(response.headers['Access-Control-Allow-Origin']).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'OPTIONS /-/jira_connect/oauth_application_id' do
|
||||||
|
let(:allowed_methods) { 'GET, OPTIONS' }
|
||||||
|
let(:path) { '/-/jira_connect/oauth_application_id' }
|
||||||
|
|
||||||
|
it_behaves_like 'allows cross-origin requests on self managed'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'OPTIONS /-/jira_connect/subscriptions' do
|
||||||
|
let(:allowed_methods) { 'GET, POST, OPTIONS' }
|
||||||
|
let(:path) { '/-/jira_connect/subscriptions' }
|
||||||
|
|
||||||
|
it_behaves_like 'allows cross-origin requests on self managed'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'OPTIONS /-/jira_connect/subscriptions/:id' do
|
||||||
|
let(:allowed_methods) { 'DELETE, OPTIONS' }
|
||||||
|
let(:path) { '/-/jira_connect/subscriptions/123' }
|
||||||
|
|
||||||
|
it_behaves_like 'allows cross-origin requests on self managed'
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,42 +3,12 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe JiraConnect::OauthApplicationIdsController do
|
RSpec.describe JiraConnect::OauthApplicationIdsController do
|
||||||
describe 'OPTIONS /-/jira_connect/oauth_application_id' do
|
|
||||||
before do
|
|
||||||
stub_application_setting(jira_connect_application_key: '123456')
|
|
||||||
|
|
||||||
options '/-/jira_connect/oauth_application_id', headers: { 'Origin' => 'http://notgitlab.com' }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns 200' do
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows cross-origin requests', :aggregate_failures do
|
|
||||||
expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
|
|
||||||
expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, OPTIONS'
|
|
||||||
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'on GitLab.com' do
|
|
||||||
before do
|
|
||||||
allow(Gitlab).to receive(:com?).and_return(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders not found' do
|
|
||||||
options '/-/jira_connect/oauth_application_id'
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
|
||||||
expect(response.headers['Access-Control-Allow-Origin']).not_to eq '*'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'GET /-/jira_connect/oauth_application_id' do
|
describe 'GET /-/jira_connect/oauth_application_id' do
|
||||||
let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
|
let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_application_setting(jira_connect_application_key: '123456')
|
stub_application_setting(jira_connect_application_key: '123456')
|
||||||
|
stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders the jira connect application id' do
|
it 'renders the jira connect application id' do
|
||||||
|
@ -51,7 +21,7 @@ RSpec.describe JiraConnect::OauthApplicationIdsController do
|
||||||
it 'allows cross-origin requests', :aggregate_failures do
|
it 'allows cross-origin requests', :aggregate_failures do
|
||||||
get '/-/jira_connect/oauth_application_id', headers: cors_request_headers
|
get '/-/jira_connect/oauth_application_id', headers: cors_request_headers
|
||||||
|
|
||||||
expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
|
expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
|
||||||
expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, OPTIONS'
|
expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, OPTIONS'
|
||||||
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
|
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,9 +10,16 @@ RSpec.describe JiraConnect::SubscriptionsController do
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
|
let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
|
||||||
|
let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
|
||||||
|
let(:path) { '/-/jira_connect/subscriptions' }
|
||||||
|
let(:params) { { jwt: jwt } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
|
||||||
|
end
|
||||||
|
|
||||||
subject(:content_security_policy) do
|
subject(:content_security_policy) do
|
||||||
get '/-/jira_connect/subscriptions', params: { jwt: jwt }
|
get path, params: params
|
||||||
|
|
||||||
response.headers['Content-Security-Policy']
|
response.headers['Content-Security-Policy']
|
||||||
end
|
end
|
||||||
|
@ -21,6 +28,14 @@ RSpec.describe JiraConnect::SubscriptionsController do
|
||||||
it { is_expected.to include('http://self-managed-gitlab.com/api/') }
|
it { is_expected.to include('http://self-managed-gitlab.com/api/') }
|
||||||
it { is_expected.to include('http://self-managed-gitlab.com/oauth/') }
|
it { is_expected.to include('http://self-managed-gitlab.com/oauth/') }
|
||||||
|
|
||||||
|
it 'allows cross-origin requests', :aggregate_failures do
|
||||||
|
get path, params: params, headers: cors_request_headers
|
||||||
|
|
||||||
|
expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
|
||||||
|
expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, POST, OPTIONS'
|
||||||
|
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
context 'with no self-managed instance configured' do
|
context 'with no self-managed instance configured' do
|
||||||
let_it_be(:installation) { create(:jira_connect_installation, instance_url: '') }
|
let_it_be(:installation) { create(:jira_connect_installation, instance_url: '') }
|
||||||
|
|
||||||
|
@ -39,4 +54,57 @@ RSpec.describe JiraConnect::SubscriptionsController do
|
||||||
it { is_expected.not_to include('http://self-managed-gitlab.com/oauth/') }
|
it { is_expected.not_to include('http://self-managed-gitlab.com/oauth/') }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'POST /-/jira_connect/subscriptions' do
|
||||||
|
let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'http://self-managed-gitlab.com') }
|
||||||
|
let_it_be(:group) { create(:group) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
let(:qsh) do
|
||||||
|
Atlassian::Jwt.create_query_string_hash('https://gitlab.test/subscriptions', 'GET', 'https://gitlab.test')
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
|
||||||
|
let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
|
||||||
|
let(:params) { { jwt: jwt, namespace_path: group.path, format: :json } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
group.add_maintainer(user)
|
||||||
|
sign_in(user)
|
||||||
|
stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows cross-origin requests', :aggregate_failures do
|
||||||
|
post '/-/jira_connect/subscriptions', params: params, headers: cors_request_headers
|
||||||
|
|
||||||
|
expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
|
||||||
|
expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, POST, OPTIONS'
|
||||||
|
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /-/jira_connect/subscriptions/:id' do
|
||||||
|
let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'http://self-managed-gitlab.com') }
|
||||||
|
let_it_be(:subscription) { create(:jira_connect_subscription, installation: installation) }
|
||||||
|
|
||||||
|
let(:qsh) do
|
||||||
|
Atlassian::Jwt.create_query_string_hash('https://gitlab.test/subscriptions', 'GET', 'https://gitlab.test')
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
|
||||||
|
let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
|
||||||
|
let(:params) { { jwt: jwt, format: :json } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows cross-origin requests', :aggregate_failures do
|
||||||
|
delete "/-/jira_connect/subscriptions/#{subscription.id}", params: params, headers: cors_request_headers
|
||||||
|
|
||||||
|
expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
|
||||||
|
expect(response.headers['Access-Control-Allow-Methods']).to eq 'DELETE, OPTIONS'
|
||||||
|
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@ RSpec.describe Oauth::TokensController do
|
||||||
let(:other_headers) { {} }
|
let(:other_headers) { {} }
|
||||||
let(:headers) { cors_request_headers.merge(other_headers) }
|
let(:headers) { cors_request_headers.merge(other_headers) }
|
||||||
let(:allowed_methods) { 'POST, OPTIONS' }
|
let(:allowed_methods) { 'POST, OPTIONS' }
|
||||||
|
let(:authorization_methods) { %w[Authorization X-CSRF-Token X-Requested-With] }
|
||||||
|
|
||||||
shared_examples 'cross-origin POST request' do
|
shared_examples 'cross-origin POST request' do
|
||||||
it 'allows cross-origin requests' do
|
it 'allows cross-origin requests' do
|
||||||
|
@ -25,7 +26,7 @@ RSpec.describe Oauth::TokensController do
|
||||||
it 'allows cross-origin requests' do
|
it 'allows cross-origin requests' do
|
||||||
expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
|
expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
|
||||||
expect(response.headers['Access-Control-Allow-Methods']).to eq allowed_methods
|
expect(response.headers['Access-Control-Allow-Methods']).to eq allowed_methods
|
||||||
expect(response.headers['Access-Control-Allow-Headers']).to eq 'Authorization'
|
expect(response.headers['Access-Control-Allow-Headers']).to eq authorization_methods
|
||||||
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
|
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -39,7 +40,7 @@ RSpec.describe Oauth::TokensController do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'OPTIONS /oauth/token' do
|
describe 'OPTIONS /oauth/token' do
|
||||||
let(:other_headers) { { 'Access-Control-Request-Headers' => 'Authorization', 'Access-Control-Request-Method' => 'POST' } }
|
let(:other_headers) { { 'Access-Control-Request-Headers' => authorization_methods, 'Access-Control-Request-Method' => 'POST' } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
options '/oauth/token', headers: headers
|
options '/oauth/token', headers: headers
|
||||||
|
@ -63,7 +64,7 @@ RSpec.describe Oauth::TokensController do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'OPTIONS /oauth/revoke' do
|
describe 'OPTIONS /oauth/revoke' do
|
||||||
let(:other_headers) { { 'Access-Control-Request-Headers' => 'Authorization', 'Access-Control-Request-Method' => 'POST' } }
|
let(:other_headers) { { 'Access-Control-Request-Headers' => authorization_methods, 'Access-Control-Request-Method' => 'POST' } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
options '/oauth/revoke', headers: headers
|
options '/oauth/revoke', headers: headers
|
||||||
|
|
|
@ -78,6 +78,7 @@ require_relative '../tooling/quality/test_level'
|
||||||
quality_level = Quality::TestLevel.new
|
quality_level = Quality::TestLevel.new
|
||||||
|
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
|
config.threadsafe = false
|
||||||
config.use_transactional_fixtures = true
|
config.use_transactional_fixtures = true
|
||||||
config.use_instantiated_fixtures = false
|
config.use_instantiated_fixtures = false
|
||||||
config.fixture_path = Rails.root
|
config.fixture_path = Rails.root
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Set the particular setting as a key-value pair
|
||||||
|
# Setting method is different depending on klass and must be defined in the calling spec
|
||||||
|
def stub_email_setting(key_value_pairs)
|
||||||
|
case setting_name
|
||||||
|
when :incoming_email
|
||||||
|
stub_incoming_email_setting(key_value_pairs)
|
||||||
|
when :service_desk_email
|
||||||
|
stub_service_desk_email_setting(key_value_pairs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples_for 'enabled? method for email' do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
subject { described_class.enabled? }
|
||||||
|
|
||||||
|
where(:value, :address, :result) do
|
||||||
|
false | nil | false
|
||||||
|
false | 'replies+%{key}@example.com' | false
|
||||||
|
true | nil | false
|
||||||
|
true | 'replies+%{key}@example.com' | true
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
before do
|
||||||
|
stub_email_setting(enabled: value)
|
||||||
|
stub_email_setting(address: address)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to eq result }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples_for 'supports_wildcard? method for email' do
|
||||||
|
subject { described_class.supports_wildcard? }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_incoming_email_setting(address: value)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when address contains the wildcard placeholder' do
|
||||||
|
let(:value) { 'replies+%{key}@example.com' }
|
||||||
|
|
||||||
|
it 'confirms that wildcard is supported' do
|
||||||
|
expect(subject).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when address doesn't contain the wildcard placeholder" do
|
||||||
|
let(:value) { 'replies@example.com' }
|
||||||
|
|
||||||
|
it 'returns that wildcard is not supported' do
|
||||||
|
expect(subject).to be_falsey
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when address is nil' do
|
||||||
|
let(:value) { nil }
|
||||||
|
|
||||||
|
it 'returns that wildcard is not supported' do
|
||||||
|
expect(subject).to be_falsey
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples_for 'unsubscribe_address method for email' do
|
||||||
|
before do
|
||||||
|
stub_incoming_email_setting(address: 'replies+%{key}@example.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the address with interpolated reply key and unsubscribe suffix' do
|
||||||
|
expect(described_class.unsubscribe_address('key'))
|
||||||
|
.to eq("replies+key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX}@example.com")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples_for 'key_from_fallback_message_id method for email' do
|
||||||
|
it 'returns reply key' do
|
||||||
|
expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples_for 'supports_issue_creation? method for email' do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
subject { described_class.supports_issue_creation? }
|
||||||
|
|
||||||
|
where(:enabled_value, :supports_wildcard_value, :result) do
|
||||||
|
false | false | false
|
||||||
|
false | true | false
|
||||||
|
true | false | false
|
||||||
|
true | true | true
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
before do
|
||||||
|
allow(described_class).to receive(:enabled?).and_return(enabled_value)
|
||||||
|
allow(described_class).to receive(:supports_wildcard?).and_return(supports_wildcard_value)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to eq result }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples_for 'reply_address method for email' do
|
||||||
|
before do
|
||||||
|
stub_incoming_email_setting(address: "replies+%{key}@example.com")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns the address with an interpolated reply key" do
|
||||||
|
expect(described_class.reply_address("key")).to eq("replies+key@example.com")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples_for 'scan_fallback_references method for email' do
|
||||||
|
let(:references) do
|
||||||
|
'<issue_1@localhost>' \
|
||||||
|
' <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>' \
|
||||||
|
',<exchange@microsoft.com>'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns reply key' do
|
||||||
|
expect(described_class.scan_fallback_references(references))
|
||||||
|
.to eq(%w[issue_1@localhost
|
||||||
|
reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost
|
||||||
|
exchange@microsoft.com])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples_for 'common email methods' do
|
||||||
|
it_behaves_like 'enabled? method for email'
|
||||||
|
it_behaves_like 'supports_wildcard? method for email'
|
||||||
|
it_behaves_like 'key_from_fallback_message_id method for email'
|
||||||
|
it_behaves_like 'supports_issue_creation? method for email'
|
||||||
|
it_behaves_like 'reply_address method for email'
|
||||||
|
it_behaves_like 'unsubscribe_address method for email'
|
||||||
|
it_behaves_like 'scan_fallback_references method for email'
|
||||||
|
end
|
Loading…
Reference in New Issue