Add latest changes from gitlab-org/gitlab@master
|
@ -20,6 +20,8 @@ default:
|
|||
- gitlab-org
|
||||
# All jobs are interruptible by default
|
||||
interruptible: true
|
||||
# Default job timeout set to 90m https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/10520
|
||||
timeout: 90m
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
|
|
|
@ -172,6 +172,7 @@ dependency_scanning:
|
|||
# # - 'export DAST_AUTH_URL="${DAST_WEBSITE}/users/sign_in"'
|
||||
# # - 'export DAST_PASSWORD="${REVIEW_APPS_ROOT_PASSWORD}"'
|
||||
# - /analyze -t $DAST_WEBSITE
|
||||
# timeout: 4h
|
||||
# artifacts:
|
||||
# paths:
|
||||
# - gl-dast-report.json # GitLab-specific
|
||||
|
|
|
@ -9,6 +9,7 @@ cache gems:
|
|||
stage: test
|
||||
needs: ["setup-test-env"]
|
||||
variables:
|
||||
BUNDLE_INSTALL_FLAGS: --with=production --with=development --with=test --jobs=2 --path=vendor --retry=3 --quiet
|
||||
SETUP_DB: "false"
|
||||
script:
|
||||
- bundle package --all --all-platforms
|
||||
|
|
|
@ -1 +1 @@
|
|||
8.36.0
|
||||
8.37.0
|
||||
|
|
|
@ -70,8 +70,19 @@ export const fetchImport = ({ state, commit }, { newName, targetNamespace, repo
|
|||
repoId: repo.id,
|
||||
}),
|
||||
)
|
||||
.catch(() => {
|
||||
createFlash(s__('ImportProjects|Importing the project failed'));
|
||||
.catch(e => {
|
||||
const serverErrorMessage = e?.response?.data?.errors;
|
||||
const flashMessage = serverErrorMessage
|
||||
? sprintf(
|
||||
s__('ImportProjects|Importing the project failed: %{reason}'),
|
||||
{
|
||||
reason: serverErrorMessage,
|
||||
},
|
||||
false,
|
||||
)
|
||||
: s__('ImportProjects|Importing the project failed');
|
||||
|
||||
createFlash(flashMessage);
|
||||
|
||||
commit(types.RECEIVE_IMPORT_ERROR, repo.id);
|
||||
});
|
||||
|
|
|
@ -9,15 +9,11 @@ import {
|
|||
GlNewDropdown,
|
||||
GlNewDropdownItem,
|
||||
} from '@gitlab/ui';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { refreshCurrentPage } from '~/lib/utils/url_utility';
|
||||
import createFlash from '~/flash';
|
||||
import {
|
||||
I18N_ALERT_SETTINGS_FORM,
|
||||
NO_ISSUE_TEMPLATE_SELECTED,
|
||||
TAKING_INCIDENT_ACTION_DOCS_LINK,
|
||||
ISSUE_TEMPLATES_DOCS_LINK,
|
||||
ERROR_MSG,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
|
@ -31,7 +27,7 @@ export default {
|
|||
GlNewDropdown,
|
||||
GlNewDropdownItem,
|
||||
},
|
||||
inject: ['alertSettings', 'operationsSettingsEndpoint'],
|
||||
inject: ['service', 'alertSettings'],
|
||||
data() {
|
||||
return {
|
||||
templates: [NO_ISSUE_TEMPLATE_SELECTED, ...this.alertSettings.templates],
|
||||
|
@ -65,23 +61,10 @@ export default {
|
|||
},
|
||||
updateAlertsIntegrationSettings() {
|
||||
this.loading = true;
|
||||
return axios
|
||||
.patch(this.operationsSettingsEndpoint, {
|
||||
project: {
|
||||
incident_management_setting_attributes: this.formData,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
refreshCurrentPage();
|
||||
})
|
||||
.catch(({ response }) => {
|
||||
const message = response?.data?.message || '';
|
||||
|
||||
createFlash(`${ERROR_MSG} ${message}`, 'alert');
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
this.service.updateSettings(this.formData).catch(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<script>
|
||||
import { GlButton, GlTabs, GlTab } from '@gitlab/ui';
|
||||
import AlertsSettingsForm from './alerts_form.vue';
|
||||
import PagerDutySettingsForm from './pagerduty_form.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { INTEGRATION_TABS_CONFIG, I18N_INTEGRATION_TABS } from '../constants';
|
||||
|
||||
export default {
|
||||
|
@ -9,9 +11,19 @@ export default {
|
|||
GlTabs,
|
||||
GlTab,
|
||||
AlertsSettingsForm,
|
||||
PagerDutySettingsForm,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
tabs: INTEGRATION_TABS_CONFIG,
|
||||
i18n: I18N_INTEGRATION_TABS,
|
||||
methods: {
|
||||
isFeatureFlagEnabled(tab) {
|
||||
if (tab.featureFlag) {
|
||||
return this.glFeatures[tab.featureFlag];
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -37,7 +49,7 @@ export default {
|
|||
<gl-tabs>
|
||||
<gl-tab
|
||||
v-for="(tab, index) in $options.tabs"
|
||||
v-if="tab.active"
|
||||
v-if="tab.active && isFeatureFlagEnabled(tab)"
|
||||
:key="`${tab.title}_${index}`"
|
||||
:title="tab.title"
|
||||
>
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
<script>
|
||||
import {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlIcon,
|
||||
GlFormGroup,
|
||||
GlFormInputGroup,
|
||||
GlToggle,
|
||||
GlModal,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import { I18N_PAGERDUTY_SETTINGS_FORM, CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK } from '../constants';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlIcon,
|
||||
GlFormGroup,
|
||||
GlFormInputGroup,
|
||||
GlToggle,
|
||||
GlModal,
|
||||
ClipboardButton,
|
||||
},
|
||||
directives: {
|
||||
'gl-modal': GlModalDirective,
|
||||
},
|
||||
inject: ['service', 'pagerDutySettings'],
|
||||
data() {
|
||||
return {
|
||||
active: this.pagerDutySettings.active,
|
||||
webhookUrl: this.pagerDutySettings.webhookUrl,
|
||||
loading: false,
|
||||
resettingWebhook: false,
|
||||
webhookUpdateFailed: false,
|
||||
showAlert: false,
|
||||
};
|
||||
},
|
||||
i18n: I18N_PAGERDUTY_SETTINGS_FORM,
|
||||
CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK,
|
||||
computed: {
|
||||
formData() {
|
||||
return {
|
||||
pagerduty_active: this.active,
|
||||
};
|
||||
},
|
||||
isFormUpdated() {
|
||||
return isEqual(this.pagerDutySettings, {
|
||||
active: this.active,
|
||||
webhookUrl: this.webhookUrl,
|
||||
});
|
||||
},
|
||||
isSaveDisabled() {
|
||||
return this.isFormUpdated || this.loading || this.resettingWebhook;
|
||||
},
|
||||
webhookUpdateAlertMsg() {
|
||||
return this.webhookUpdateFailed
|
||||
? this.$options.i18n.webhookUrl.updateErrMsg
|
||||
: this.$options.i18n.webhookUrl.updateSuccessMsg;
|
||||
},
|
||||
webhookUpdateAlertVariant() {
|
||||
return this.webhookUpdateFailed ? 'danger' : 'success';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updatePagerDutyIntegrationSettings() {
|
||||
this.loading = true;
|
||||
|
||||
this.service.updateSettings(this.formData).catch(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
resetWebhookUrl() {
|
||||
this.resettingWebhook = true;
|
||||
|
||||
this.service
|
||||
.resetWebhookUrl()
|
||||
.then(({ data: { pagerduty_webhook_url: url } }) => {
|
||||
this.webhookUrl = url;
|
||||
this.showAlert = true;
|
||||
this.webhookUpdateFailed = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.showAlert = true;
|
||||
this.webhookUpdateFailed = true;
|
||||
})
|
||||
.finally(() => {
|
||||
this.resettingWebhook = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-alert
|
||||
v-if="showAlert"
|
||||
class="gl-mb-3"
|
||||
:variant="webhookUpdateAlertVariant"
|
||||
@dismiss="showAlert = false"
|
||||
>
|
||||
{{ webhookUpdateAlertMsg }}
|
||||
</gl-alert>
|
||||
|
||||
<p>{{ $options.i18n.introText }}</p>
|
||||
<form ref="settingsForm" @submit.prevent="updatePagerDutyIntegrationSettings">
|
||||
<gl-form-group class="col-8 col-md-9 gl-p-0">
|
||||
<gl-toggle
|
||||
id="active"
|
||||
v-model="active"
|
||||
:is-loading="loading"
|
||||
:label="$options.i18n.activeToggle.label"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
class="col-8 col-md-9 gl-p-0"
|
||||
:label="$options.i18n.webhookUrl.label"
|
||||
label-for="url"
|
||||
label-class="label-bold"
|
||||
>
|
||||
<gl-form-input-group id="url" data-testid="webhook-url" readonly :value="webhookUrl">
|
||||
<template #append>
|
||||
<clipboard-button
|
||||
:text="webhookUrl"
|
||||
:title="$options.i18n.webhookUrl.copyToClipboard"
|
||||
/>
|
||||
</template>
|
||||
</gl-form-input-group>
|
||||
|
||||
<div class="gl-text-gray-400 gl-pt-2">
|
||||
<gl-sprintf :message="$options.i18n.webhookUrl.helpText">
|
||||
<template #docsLink>
|
||||
<gl-link
|
||||
:href="$options.CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK"
|
||||
target="_blank"
|
||||
class="gl-display-inline-flex"
|
||||
>
|
||||
<span>{{ $options.i18n.webhookUrl.helpDocsLink }}</span>
|
||||
<gl-icon name="external-link" />
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
<gl-button
|
||||
v-gl-modal.resetWebhookModal
|
||||
class="gl-mt-3"
|
||||
:disabled="loading"
|
||||
:loading="resettingWebhook"
|
||||
data-testid="webhook-reset-btn"
|
||||
>
|
||||
{{ $options.i18n.webhookUrl.resetWebhookUrl }}
|
||||
</gl-button>
|
||||
<gl-modal
|
||||
modal-id="resetWebhookModal"
|
||||
:title="$options.i18n.webhookUrl.resetWebhookUrl"
|
||||
:ok-title="$options.i18n.webhookUrl.resetWebhookUrl"
|
||||
ok-variant="danger"
|
||||
@ok="resetWebhookUrl"
|
||||
>
|
||||
{{ $options.i18n.webhookUrl.restKeyInfo }}
|
||||
</gl-modal>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-button
|
||||
ref="submitBtn"
|
||||
:disabled="isSaveDisabled"
|
||||
variant="success"
|
||||
type="submit"
|
||||
class="js-no-auto-disable"
|
||||
>
|
||||
{{ $options.i18n.saveBtnLabel }}
|
||||
</gl-button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
|
@ -1,5 +1,6 @@
|
|||
import { __, s__ } from '~/locale';
|
||||
|
||||
/* Integration tabs constants */
|
||||
export const INTEGRATION_TABS_CONFIG = [
|
||||
{
|
||||
title: s__('IncidentSettings|Alert integration'),
|
||||
|
@ -8,8 +9,9 @@ export const INTEGRATION_TABS_CONFIG = [
|
|||
},
|
||||
{
|
||||
title: s__('IncidentSettings|PagerDuty integration'),
|
||||
component: '',
|
||||
active: false,
|
||||
component: 'PagerDutySettingsForm',
|
||||
active: true,
|
||||
featureFlag: 'pagerdutyWebhook',
|
||||
},
|
||||
{
|
||||
title: s__('IncidentSettings|Grafana integration'),
|
||||
|
@ -21,12 +23,13 @@ export const INTEGRATION_TABS_CONFIG = [
|
|||
export const I18N_INTEGRATION_TABS = {
|
||||
headerText: s__('IncidentSettings|Incidents'),
|
||||
expandBtnLabel: __('Expand'),
|
||||
saveBtnLabel: __('Save changes'),
|
||||
subHeaderText: s__(
|
||||
'IncidentSettings|Set up integrations with external tools to help better manage incidents.',
|
||||
),
|
||||
};
|
||||
|
||||
/* Alerts integration settings constants */
|
||||
|
||||
export const I18N_ALERT_SETTINGS_FORM = {
|
||||
saveBtnLabel: __('Save changes'),
|
||||
introText: __('Action to take when receiving an alert. %{docsLink}'),
|
||||
|
@ -48,4 +51,33 @@ export const TAKING_INCIDENT_ACTION_DOCS_LINK =
|
|||
export const ISSUE_TEMPLATES_DOCS_LINK =
|
||||
'/help/user/project/description_templates#creating-issue-templates';
|
||||
|
||||
/* PagerDuty integration settings constants */
|
||||
|
||||
export const I18N_PAGERDUTY_SETTINGS_FORM = {
|
||||
introText: s__(
|
||||
'PagerDutySettings|Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident.',
|
||||
),
|
||||
activeToggle: {
|
||||
label: s__('PagerDutySettings|Active'),
|
||||
},
|
||||
webhookUrl: {
|
||||
label: s__('PagerDutySettings|Webhook URL'),
|
||||
helpText: s__(
|
||||
'PagerDutySettings|Create a GitLab issue for each PagerDuty incident by %{docsLink}',
|
||||
),
|
||||
helpDocsLink: s__('PagerDutySettings|configuring a webhook in PagerDuty'),
|
||||
resetWebhookUrl: s__('PagerDutySettings|Reset webhook URL'),
|
||||
copyToClipboard: __('Copy'),
|
||||
updateErrMsg: s__('PagerDutySettings|Failed to update Webhook URL'),
|
||||
updateSuccessMsg: s__('PagerDutySettings|Webhook URL update was successful'),
|
||||
restKeyInfo: s__(
|
||||
"PagerDutySettings|Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty.",
|
||||
),
|
||||
},
|
||||
saveBtnLabel: __('Save changes'),
|
||||
};
|
||||
|
||||
export const CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK = 'https://support.pagerduty.com/docs/webhooks';
|
||||
|
||||
/* common constants */
|
||||
export const ERROR_MSG = __('There was an error saving your changes.');
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import { refreshCurrentPage } from '~/lib/utils/url_utility';
|
||||
import createFlash from '~/flash';
|
||||
import { ERROR_MSG } from './constants';
|
||||
|
||||
export default class IncidentsSettingsService {
|
||||
constructor(settingsEndpoint, webhookUpdateEndpoint) {
|
||||
this.settingsEndpoint = settingsEndpoint;
|
||||
this.webhookUpdateEndpoint = webhookUpdateEndpoint;
|
||||
}
|
||||
|
||||
updateSettings(data) {
|
||||
return axios
|
||||
.patch(this.settingsEndpoint, {
|
||||
project: {
|
||||
incident_management_setting_attributes: data,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
refreshCurrentPage();
|
||||
})
|
||||
.catch(({ response }) => {
|
||||
const message = response?.data?.message || '';
|
||||
|
||||
createFlash(`${ERROR_MSG} ${message}`, 'alert');
|
||||
});
|
||||
}
|
||||
|
||||
resetWebhookUrl() {
|
||||
return axios.post(this.webhookUpdateEndpoint);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import SettingsTabs from './components/incidents_settings_tabs.vue';
|
||||
import IncidentsSettingsService from './incidents_settings_service';
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('.js-incidents-settings');
|
||||
|
@ -10,19 +11,33 @@ export default () => {
|
|||
}
|
||||
|
||||
const {
|
||||
dataset: { operationsSettingsEndpoint, templates, createIssue, issueTemplateKey, sendEmail },
|
||||
dataset: {
|
||||
operationsSettingsEndpoint,
|
||||
templates,
|
||||
createIssue,
|
||||
issueTemplateKey,
|
||||
sendEmail,
|
||||
pagerdutyActive,
|
||||
pagerdutyWebhookUrl,
|
||||
pagerdutyResetKeyPath,
|
||||
},
|
||||
} = el;
|
||||
|
||||
const service = new IncidentsSettingsService(operationsSettingsEndpoint, pagerdutyResetKeyPath);
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
operationsSettingsEndpoint,
|
||||
service,
|
||||
alertSettings: {
|
||||
templates: JSON.parse(templates),
|
||||
createIssue: parseBoolean(createIssue),
|
||||
issueTemplateKey,
|
||||
sendEmail: parseBoolean(sendEmail),
|
||||
},
|
||||
pagerDutySettings: {
|
||||
active: parseBoolean(pagerdutyActive),
|
||||
webhookUrl: pagerdutyWebhookUrl,
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(SettingsTabs);
|
||||
|
|
|
@ -29,9 +29,9 @@ export default {
|
|||
return [
|
||||
'gl-display-flex',
|
||||
'gl-flex-direction-column',
|
||||
'gl-absolute',
|
||||
'gl-fixed',
|
||||
'gl-right-1',
|
||||
'gl-top-0',
|
||||
'gl-top-66vh',
|
||||
'gl-w-max-content',
|
||||
'gl-px-5',
|
||||
'gl-py-4',
|
||||
|
|
|
@ -43,6 +43,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
downstreamMarginTop: null,
|
||||
jobName: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -91,13 +92,9 @@ export default {
|
|||
/**
|
||||
* Calculates the margin top of the clicked downstream pipeline by
|
||||
* subtracting the clicked downstream pipelines offsetTop by it's parent's
|
||||
* offsetTop and then subtracting either 15 (if child) or 30 (if not a child)
|
||||
* due to the height of node and stage name margin bottom.
|
||||
* offsetTop and then subtracting 15
|
||||
*/
|
||||
this.downstreamMarginTop = this.calculateMarginTop(
|
||||
downstreamNode,
|
||||
downstreamNode.classList.contains('child-pipeline') ? 15 : 30,
|
||||
);
|
||||
this.downstreamMarginTop = this.calculateMarginTop(downstreamNode, 15);
|
||||
|
||||
/**
|
||||
* If the expanded trigger is defined and the id is different than the
|
||||
|
@ -120,6 +117,9 @@ export default {
|
|||
hasUpstream(index) {
|
||||
return index === 0 && this.hasTriggeredBy;
|
||||
},
|
||||
setJob(jobName) {
|
||||
this.jobName = jobName;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -180,6 +180,7 @@ export default {
|
|||
:is-first-column="isFirstColumn(index)"
|
||||
:has-triggered-by="hasTriggeredBy"
|
||||
:action="stage.status.action"
|
||||
:job-hovered="jobName"
|
||||
@refreshPipelineGraph="refreshPipelineGraph"
|
||||
/>
|
||||
</ul>
|
||||
|
@ -191,6 +192,7 @@ export default {
|
|||
:project-id="pipelineProjectId"
|
||||
graph-position="right"
|
||||
@linkedPipelineClick="handleClickedDownstream"
|
||||
@downstreamHovered="setJob"
|
||||
/>
|
||||
|
||||
<pipeline-graph
|
||||
|
|
|
@ -31,6 +31,7 @@ import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
|
|||
*/
|
||||
|
||||
export default {
|
||||
hoverClass: 'gl-inset-border-1-blue-500',
|
||||
components: {
|
||||
ActionComponent,
|
||||
JobNameComponent,
|
||||
|
@ -55,6 +56,11 @@ export default {
|
|||
required: false,
|
||||
default: Infinity,
|
||||
},
|
||||
jobHovered: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
boundary() {
|
||||
|
@ -95,6 +101,11 @@ export default {
|
|||
hasAction() {
|
||||
return this.job.status && this.job.status.action && this.job.status.action.path;
|
||||
},
|
||||
jobClasses() {
|
||||
return this.job.name === this.jobHovered
|
||||
? `${this.$options.hoverClass} ${this.cssClassJobName}`
|
||||
: this.cssClassJobName;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
pipelineActionRequestComplete() {
|
||||
|
@ -120,8 +131,9 @@ export default {
|
|||
v-else
|
||||
v-gl-tooltip="{ boundary, placement: 'bottom' }"
|
||||
:title="tooltipText"
|
||||
:class="cssClassJobName"
|
||||
:class="jobClasses"
|
||||
class="js-job-component-tooltip non-details-job-component"
|
||||
data-testid="job-without-link"
|
||||
>
|
||||
<job-name-component :name="job.name" :status="job.status" />
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
|
||||
import CiStatus from '~/vue_shared/components/ci_icon.vue';
|
||||
import { __ } from '~/locale';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
|
@ -28,7 +28,8 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
tooltipText() {
|
||||
return `${this.projectName} - ${this.pipelineStatus.label}`;
|
||||
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label}
|
||||
${this.sourceJobInfo}`;
|
||||
},
|
||||
buttonId() {
|
||||
return `js-linked-pipeline-${this.pipeline.id}`;
|
||||
|
@ -39,25 +40,32 @@ export default {
|
|||
projectName() {
|
||||
return this.pipeline.project.name;
|
||||
},
|
||||
downstreamTitle() {
|
||||
return this.childPipeline ? __('child-pipeline') : this.pipeline.project.name;
|
||||
},
|
||||
parentPipeline() {
|
||||
// Refactor string match when BE returns Upstream/Downstream indicators
|
||||
return this.projectId === this.pipeline.project.id && this.columnTitle === __('Upstream');
|
||||
},
|
||||
childPipeline() {
|
||||
// Refactor string match when BE returns Upstream/Downstream indicators
|
||||
return this.projectId === this.pipeline.project.id && this.columnTitle === __('Downstream');
|
||||
return this.projectId === this.pipeline.project.id && this.isDownstream;
|
||||
},
|
||||
label() {
|
||||
return this.parentPipeline ? __('Parent') : __('Child');
|
||||
if (this.parentPipeline) {
|
||||
return __('Parent');
|
||||
} else if (this.childPipeline) {
|
||||
return __('Child');
|
||||
}
|
||||
return __('Multi-project');
|
||||
},
|
||||
childTooltipText() {
|
||||
return __('This pipeline was triggered by a parent pipeline');
|
||||
isDownstream() {
|
||||
return this.columnTitle === __('Downstream');
|
||||
},
|
||||
parentTooltipText() {
|
||||
return __('This pipeline triggered a child pipeline');
|
||||
},
|
||||
labelToolTipText() {
|
||||
return this.label === __('Parent') ? this.parentTooltipText : this.childTooltipText;
|
||||
sourceJobInfo() {
|
||||
return this.isDownstream
|
||||
? sprintf(__('Created by %{job}'), { job: this.pipeline.source_job.name })
|
||||
: '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -68,6 +76,12 @@ export default {
|
|||
hideTooltips() {
|
||||
this.$root.$emit('bv::hide::tooltip');
|
||||
},
|
||||
onDownstreamHovered() {
|
||||
this.$emit('downstreamHovered', this.pipeline.source_job.name);
|
||||
},
|
||||
onDownstreamHoverLeave() {
|
||||
this.$emit('downstreamHovered', '');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -76,8 +90,10 @@ export default {
|
|||
<li
|
||||
ref="linkedPipeline"
|
||||
class="linked-pipeline build"
|
||||
:class="{ 'child-pipeline': childPipeline }"
|
||||
:class="{ 'downstream-pipeline': isDownstream }"
|
||||
data-qa-selector="child_pipeline"
|
||||
@mouseover="onDownstreamHovered"
|
||||
@mouseleave="onDownstreamHoverLeave"
|
||||
>
|
||||
<gl-deprecated-button
|
||||
:id="buttonId"
|
||||
|
@ -95,15 +111,9 @@ export default {
|
|||
css-classes="position-top-0"
|
||||
class="js-linked-pipeline-status"
|
||||
/>
|
||||
<span class="str-truncated align-bottom"> {{ projectName }} • #{{ pipeline.id }} </span>
|
||||
<div v-if="parentPipeline || childPipeline" class="parent-child-label-container">
|
||||
<span
|
||||
v-gl-tooltip.bottom
|
||||
:title="labelToolTipText"
|
||||
class="badge badge-primary"
|
||||
@mouseover="hideTooltips"
|
||||
>{{ label }}</span
|
||||
>
|
||||
<span class="str-truncated"> {{ downstreamTitle }} • #{{ pipeline.id }} </span>
|
||||
<div class="gl-pt-2">
|
||||
<span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span>
|
||||
</div>
|
||||
</gl-deprecated-button>
|
||||
</li>
|
||||
|
|
|
@ -41,6 +41,9 @@ export default {
|
|||
onPipelineClick(downstreamNode, pipeline, index) {
|
||||
this.$emit('linkedPipelineClick', pipeline, index, downstreamNode);
|
||||
},
|
||||
onDownstreamHovered(jobName) {
|
||||
this.$emit('downstreamHovered', jobName);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -61,6 +64,7 @@ export default {
|
|||
:column-title="columnTitle"
|
||||
:project-id="projectId"
|
||||
@pipelineClicked="onPipelineClick($event, pipeline, index)"
|
||||
@downstreamHovered="onDownstreamHovered"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -36,6 +36,11 @@ export default {
|
|||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
jobHovered: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasAction() {
|
||||
|
@ -80,6 +85,7 @@ export default {
|
|||
<job-item
|
||||
v-if="group.size === 1"
|
||||
:job="group.jobs[0]"
|
||||
:job-hovered="jobHovered"
|
||||
css-class-job-name="build-content"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
|
|
|
@ -43,7 +43,7 @@ export default {
|
|||
<div v-if="hasSuites" class="test-reports-table gl-mb-3 js-test-cases-table">
|
||||
<div role="row" class="gl-responsive-table-row table-row-header font-weight-bold fgray">
|
||||
<div role="rowheader" class="table-section section-20">
|
||||
{{ __('Class') }}
|
||||
{{ __('Suite') }}
|
||||
</div>
|
||||
<div role="rowheader" class="table-section section-20">
|
||||
{{ __('Name') }}
|
||||
|
@ -70,7 +70,7 @@ export default {
|
|||
class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row"
|
||||
>
|
||||
<div class="table-section section-20 section-wrap">
|
||||
<div role="rowheader" class="table-mobile-header">{{ __('Class') }}</div>
|
||||
<div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
|
||||
<div
|
||||
v-gl-tooltip
|
||||
:title="testCase.classname"
|
||||
|
|
|
@ -85,7 +85,7 @@ export default {
|
|||
<div class="row mt-2">
|
||||
<div class="col-4 col-md">
|
||||
<span class="js-total-tests">{{
|
||||
sprintf(s__('TestReports|%{count} jobs'), { count: report.total_count })
|
||||
sprintf(s__('TestReports|%{count} tests'), { count: report.total_count })
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ export default {
|
|||
heading: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: s__('TestReports|Test suites'),
|
||||
default: s__('TestReports|Jobs'),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
@ -47,7 +47,7 @@ export default {
|
|||
<div v-if="hasSuites" class="test-reports-table gl-mb-3 js-test-suites-table">
|
||||
<div role="row" class="gl-responsive-table-row table-row-header font-weight-bold">
|
||||
<div role="rowheader" class="table-section section-25 pl-3">
|
||||
{{ __('Suite') }}
|
||||
{{ __('Job') }}
|
||||
</div>
|
||||
<div role="rowheader" class="table-section section-25">
|
||||
{{ __('Duration') }}
|
||||
|
|
|
@ -21,7 +21,8 @@ export default {
|
|||
props: {
|
||||
summary: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
statusIcon: {
|
||||
type: String,
|
||||
|
@ -58,8 +59,8 @@ export default {
|
|||
class="report-block-list-issue-description-text"
|
||||
data-testid="test-summary-row-description"
|
||||
>
|
||||
{{ summary
|
||||
}}<span v-if="popoverOptions" class="text-nowrap"
|
||||
<slot name="summary">{{ summary }}</slot
|
||||
><span v-if="popoverOptions" class="text-nowrap"
|
||||
> <popover v-if="popoverOptions" :options="popoverOptions" class="align-top" />
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -109,6 +109,8 @@
|
|||
padding: $gl-padding;
|
||||
list-style: none;
|
||||
transition: background $gl-transition-duration-medium $general-hover-transition-curve;
|
||||
border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box
|
||||
border-top-right-radius: $border-radius-default;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
|
|
|
@ -1101,7 +1101,3 @@ button.mini-pipeline-graph-dropdown-toggle {
|
|||
.progress-bar.bg-primary {
|
||||
background-color: $blue-500 !important;
|
||||
}
|
||||
|
||||
.parent-child-label-container {
|
||||
padding-top: $gl-padding-4;
|
||||
}
|
||||
|
|
|
@ -109,3 +109,7 @@
|
|||
.gl-transition-property-stroke {
|
||||
transition-property: stroke;
|
||||
}
|
||||
|
||||
.gl-top-66vh {
|
||||
top: 66vh;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ module ApprovableBase
|
|||
has_many :approved_by_users, through: :approvals, source: :user
|
||||
end
|
||||
|
||||
def has_approved?(user)
|
||||
def approved_by?(user)
|
||||
return false unless user
|
||||
|
||||
approved_by_users.include?(user)
|
||||
|
|
|
@ -38,6 +38,11 @@ class Member < ApplicationRecord
|
|||
scope: [:source_type, :source_id],
|
||||
allow_nil: true
|
||||
}
|
||||
validates :user_id,
|
||||
uniqueness: {
|
||||
message: _('project bots cannot be added to other groups / projects')
|
||||
},
|
||||
if: :project_bot?
|
||||
|
||||
# This scope encapsulates (most of) the conditions a row in the member table
|
||||
# must satisfy if it is a valid permission. Of particular note:
|
||||
|
@ -473,6 +478,10 @@ class Member < ApplicationRecord
|
|||
def update_highest_role_attribute
|
||||
user_id
|
||||
end
|
||||
|
||||
def project_bot?
|
||||
user&.project_bot?
|
||||
end
|
||||
end
|
||||
|
||||
Member.prepend_if_ee('EE::Member')
|
||||
|
|
|
@ -17,14 +17,7 @@ class GroupMember < Member
|
|||
|
||||
scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
|
||||
scope :of_ldap_type, -> { where(ldap: true) }
|
||||
|
||||
scope :count_users_by_group_id, -> do
|
||||
if Feature.enabled?(:optimized_count_users_by_group_id)
|
||||
group(:source_id).count
|
||||
else
|
||||
joins(:user).group(:source_id).count
|
||||
end
|
||||
end
|
||||
scope :count_users_by_group_id, -> { group(:source_id).count }
|
||||
|
||||
after_create :update_two_factor_requirement, unless: :invite?
|
||||
after_destroy :update_two_factor_requirement, unless: :invite?
|
||||
|
|
|
@ -22,6 +22,7 @@ class Namespace < ApplicationRecord
|
|||
|
||||
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :project_statistics
|
||||
has_one :namespace_settings, inverse_of: :namespace, class_name: 'NamespaceSetting', autosave: true
|
||||
|
||||
has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
|
||||
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NamespaceSetting < ApplicationRecord
|
||||
belongs_to :namespace, inverse_of: :namespace_settings
|
||||
|
||||
self.primary_key = :namespace_id
|
||||
end
|
||||
|
||||
NamespaceSetting.prepend_if_ee('EE::NamespaceSetting')
|
|
@ -1270,7 +1270,8 @@ class User < ApplicationRecord
|
|||
namespace.path = username if username_changed?
|
||||
namespace.name = name if name_changed?
|
||||
else
|
||||
build_namespace(path: username, name: name)
|
||||
namespace = build_namespace(path: username, name: name)
|
||||
namespace.build_namespace_settings
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -28,7 +28,11 @@ module Groups
|
|||
@group.build_chat_team(name: response['name'], team_id: response['id'])
|
||||
end
|
||||
|
||||
@group.add_owner(current_user) if @group.save
|
||||
if @group.save
|
||||
@group.add_owner(current_user)
|
||||
add_settings_record
|
||||
end
|
||||
|
||||
@group
|
||||
end
|
||||
|
||||
|
@ -79,6 +83,10 @@ module Groups
|
|||
|
||||
params[:visibility_level] = Gitlab::CurrentSettings.current_application_settings.default_group_visibility
|
||||
end
|
||||
|
||||
def add_settings_record
|
||||
@group.create_namespace_settings
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ module Members
|
|||
errors = []
|
||||
|
||||
members.each do |member|
|
||||
if member.errors.any?
|
||||
if member.invalid?
|
||||
current_error =
|
||||
# Invited users may not have an associated user
|
||||
if member.user.present?
|
||||
|
|
|
@ -4,7 +4,7 @@ module MergeRequests
|
|||
class RemoveApprovalService < MergeRequests::BaseService
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def execute(merge_request)
|
||||
return unless merge_request.has_approved?(current_user)
|
||||
return unless merge_request.approved_by?(current_user)
|
||||
|
||||
# paranoid protection against running wrong deletes
|
||||
return unless merge_request.id && current_user.id
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add include_parent_milestones param to milestones API
|
||||
merge_request: 36944
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update GITLAB_WORKHORSE_VERSION to 8.37.0
|
||||
merge_request: 36988
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Expands Jira integration to allow viewing and searching a list of of Jira issues
|
||||
directly within GitLab
|
||||
merge_request: 36435
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add namespace settings table
|
||||
merge_request: 36321
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make DAG annotations stick
|
||||
merge_request: 37068
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add correlation between trigger job and child pipeline
|
||||
merge_request: 36750
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix background overflow when design note is selected
|
||||
merge_request: 36931
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove optimized_count_users_by_group_id feature flag
|
||||
merge_request: 36953
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix displaying import errors from server
|
||||
merge_request: 37073
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateNamespaceSettings < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
create_table :namespace_settings, id: false do |t|
|
||||
t.timestamps_with_timezone null: false
|
||||
t.references :namespace, primary_key: true, default: nil, type: :integer, index: false, foreign_key: { on_delete: :cascade }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :namespace_settings
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BackfillNamespaceSettings < ActiveRecord::Migration[5.2]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
MIGRATION = 'BackfillNamespaceSettings'
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 10_000
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
class Namespace < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'namespaces'
|
||||
end
|
||||
|
||||
def up
|
||||
say "Scheduling `#{MIGRATION}` jobs"
|
||||
|
||||
queue_background_migration_jobs_by_range_at_intervals(Namespace, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
|
||||
end
|
||||
|
||||
def down
|
||||
# NOOP
|
||||
end
|
||||
end
|
|
@ -13118,6 +13118,12 @@ CREATE TABLE public.namespace_root_storage_statistics (
|
|||
snippets_size bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE public.namespace_settings (
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
namespace_id integer NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE public.namespace_statistics (
|
||||
id integer NOT NULL,
|
||||
namespace_id integer NOT NULL,
|
||||
|
@ -17894,6 +17900,9 @@ ALTER TABLE ONLY public.namespace_limits
|
|||
ALTER TABLE ONLY public.namespace_root_storage_statistics
|
||||
ADD CONSTRAINT namespace_root_storage_statistics_pkey PRIMARY KEY (namespace_id);
|
||||
|
||||
ALTER TABLE ONLY public.namespace_settings
|
||||
ADD CONSTRAINT namespace_settings_pkey PRIMARY KEY (namespace_id);
|
||||
|
||||
ALTER TABLE ONLY public.namespace_statistics
|
||||
ADD CONSTRAINT namespace_statistics_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -21776,6 +21785,9 @@ ALTER TABLE ONLY public.analytics_cycle_analytics_project_stages
|
|||
ALTER TABLE ONLY public.issue_user_mentions
|
||||
ADD CONSTRAINT fk_rails_3861d9fefa FOREIGN KEY (note_id) REFERENCES public.notes(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.namespace_settings
|
||||
ADD CONSTRAINT fk_rails_3896d4fae5 FOREIGN KEY (namespace_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.self_managed_prometheus_alert_events
|
||||
ADD CONSTRAINT fk_rails_3936dadc62 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -23825,6 +23837,8 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200702201039
|
||||
20200703064117
|
||||
20200703121557
|
||||
20200703124823
|
||||
20200703125016
|
||||
20200703154822
|
||||
20200704143633
|
||||
20200704161600
|
||||
|
|
|
@ -25,13 +25,14 @@ GET /projects/:id/milestones?search=version
|
|||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ------ | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `iids[]` | integer array | optional | Return only the milestones having the given `iid` |
|
||||
| `state` | string | optional | Return only `active` or `closed` milestones |
|
||||
| `title` | string | optional | Return only the milestones having the given `title` |
|
||||
| `search` | string | optional | Return only milestones with a title or description matching the provided string |
|
||||
| Attribute | Type | Required | Description |
|
||||
| ---------------------------- | ------ | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `iids[]` | integer array | optional | Return only the milestones having the given `iid`. Will be ignored if `include_parent_milestones` is set to `true` |
|
||||
| `state` | string | optional | Return only `active` or `closed` milestones |
|
||||
| `title` | string | optional | Return only the milestones having the given `title` |
|
||||
| `search` | string | optional | Return only milestones with a title or description matching the provided string |
|
||||
| `include_parent_milestones` | boolean | optional | Include milestones from parent group and ancestors. Introduced in [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36944) |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/milestones"
|
||||
|
|
|
@ -9,7 +9,7 @@ pipeline](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6665).
|
|||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["build-qa-image, gitlab:assets:compile pull-cache<br/>(canonical default refs only)"];
|
||||
A["build-qa-image, compile-production-assets<br/>(canonical default refs only)"];
|
||||
B[review-build-cng];
|
||||
C[review-deploy];
|
||||
D[CNG-mirror];
|
||||
|
@ -44,23 +44,25 @@ subgraph "CNG-mirror pipeline"
|
|||
|
||||
### Detailed explanation
|
||||
|
||||
1. On every [pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) during the `test` stage, the
|
||||
[`gitlab:assets:compile`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724487) job is automatically started.
|
||||
- Once it's done, it starts the [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808)
|
||||
manual job since the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) pipeline triggered in the
|
||||
1. On every [pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) during the `prepare` stage, the
|
||||
[`compile-production-assets`](https://gitlab.com/gitlab-org/gitlab/-/jobs/641770154) job is automatically started.
|
||||
- Once it's done, the [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808)
|
||||
job starts since the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) pipeline triggered in the
|
||||
following step depends on it.
|
||||
1. The [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808) job [triggers a pipeline](https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657)
|
||||
1. Once `compile-production-assets` is done, the [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808)
|
||||
job [triggers a pipeline](https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657)
|
||||
in the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) project.
|
||||
- The `review-build-cng` job automatically starts only if your MR includes
|
||||
[CI or frontend changes](../pipelines.md#changes-patterns). In other cases, the job is manual.
|
||||
- The [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657) pipeline creates the Docker images of
|
||||
each component (e.g. `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.)
|
||||
based on the commit from the [GitLab pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) and stores
|
||||
them in its [registry](https://gitlab.com/gitlab-org/build/CNG-mirror/container_registry).
|
||||
- We use the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) project so that the `CNG`, (Cloud
|
||||
Native GitLab), project's registry is not overloaded with a
|
||||
lot of transient Docker images.
|
||||
Native GitLab), project's registry is not overloaded with a lot of transient Docker images.
|
||||
- Note that the official CNG images are built by the `cloud-native-image`
|
||||
job, which runs only for tags, and triggers itself a [`CNG`](https://gitlab.com/gitlab-org/build/CNG) pipeline.
|
||||
1. Once the `test` stage is done, the [`review-deploy`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724810) job
|
||||
1. Once `review-build-cng` is done, the [`review-deploy`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724810) job
|
||||
deploys the Review App using [the official GitLab Helm chart](https://gitlab.com/gitlab-org/charts/gitlab/) to
|
||||
the [`review-apps`](https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps?project=gitlab-review-apps)
|
||||
Kubernetes cluster on GCP.
|
||||
|
@ -94,10 +96,9 @@ subgraph "CNG-mirror pipeline"
|
|||
- The manual `review-stop` can be used to
|
||||
stop a Review App manually, and is also started by GitLab once a merge
|
||||
request's branch is deleted after being merged.
|
||||
- The Kubernetes cluster is connected to the `gitlab-{ce,ee}` projects using
|
||||
- The Kubernetes cluster is connected to the `gitlab` projects using
|
||||
[GitLab's Kubernetes integration](../../user/project/clusters/index.md). This basically
|
||||
allows to have a link to the Review App directly from the merge request
|
||||
widget.
|
||||
allows to have a link to the Review App directly from the merge request widget.
|
||||
|
||||
### Auto-stopping of Review Apps
|
||||
|
||||
|
|
|
@ -26,6 +26,11 @@ all security features will be configured by default.
|
|||
|
||||
## Limitations
|
||||
|
||||
It is not possible to enable or disable a feature using the configuration page.
|
||||
However, instructions on how to enable or disable a feature can be found through
|
||||
the links next to each feature on that page.
|
||||
It is not yet possible to enable or disable most features using the
|
||||
configuration page. However, instructions on how to enable or disable a feature
|
||||
can be found through the links next to each feature on that page.
|
||||
|
||||
If a project does not have an existing CI configuration, then the SAST feature
|
||||
can be enabled by clicking on the "Enable with Merge Request" button under the
|
||||
"Manage" column. Future work will expand this to editing _existing_ CI
|
||||
configurations, and to other security features.
|
||||
|
|
Before Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 8.5 KiB |
|
@ -32,7 +32,7 @@ You can enable container scanning by doing one of the following:
|
|||
GitLab compares the found vulnerabilities between the source and target branches, and shows the
|
||||
information directly in the merge request.
|
||||
|
||||
![Container Scanning Widget](img/container_scanning_v13_1.png)
|
||||
![Container Scanning Widget](img/container_scanning_v13_2.png)
|
||||
|
||||
<!-- NOTE: The container scanning tool references the following heading in the code, so if you
|
||||
make a change to this heading, make sure to update the documentation URLs used in the
|
||||
|
|
Before Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 6.6 KiB |
|
@ -36,7 +36,7 @@ NOTE: **Note:**
|
|||
This comparison logic uses only the latest pipeline executed for the target branch's base commit.
|
||||
Running the pipeline on any other commit has no effect on the merge request.
|
||||
|
||||
![DAST Widget](img/dast_all_v13_1.png)
|
||||
![DAST Widget](img/dast_v13_2.png)
|
||||
|
||||
By clicking on one of the detected linked vulnerabilities, you can
|
||||
see the details and the URL(s) affected.
|
||||
|
|
Before Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 10 KiB |
|
@ -27,7 +27,7 @@ GitLab checks the Dependency Scanning report, compares the found vulnerabilities
|
|||
between the source and target branches, and shows the information on the
|
||||
merge request.
|
||||
|
||||
![Dependency Scanning Widget](img/dependency_scanning_v13_1.png)
|
||||
![Dependency Scanning Widget](img/dependency_scanning_v13_2.png)
|
||||
|
||||
The results are sorted by the severity of the vulnerability:
|
||||
|
||||
|
|
Before Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 7.5 KiB |
|
@ -28,7 +28,7 @@ You can take advantage of SAST by doing one of the following:
|
|||
GitLab checks the SAST report, compares the found vulnerabilities between the
|
||||
source and target branches, and shows the information right on the merge request.
|
||||
|
||||
![SAST Widget](img/sast_v13_1.png)
|
||||
![SAST Widget](img/sast_v13_2.png)
|
||||
|
||||
The results are sorted by the priority of the vulnerability:
|
||||
|
||||
|
|
Before Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 5.7 KiB |
|
@ -25,7 +25,7 @@ GitLab displays identified secrets as part of the SAST reports visibly in a few
|
|||
- Pipelines' **Security** tab
|
||||
- Report in the merge request widget
|
||||
|
||||
![Secret Detection in merge request widget](img/secret-detection-merge-request-ui.png)
|
||||
![Secret Detection in merge request widget](img/secret_detection_v13_2.png)
|
||||
|
||||
## Use cases
|
||||
|
||||
|
|
Before Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 83 KiB |
|
@ -17,7 +17,7 @@ for merging into production.
|
|||
|
||||
To access the Compliance Dashboard for a group, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu.
|
||||
|
||||
![Compliance Dashboard](img/compliance_dashboard_v12_10.png)
|
||||
![Compliance Dashboard](img/compliance_dashboard_v13_2.png)
|
||||
|
||||
## Use cases
|
||||
|
||||
|
@ -27,6 +27,7 @@ You can use the dashboard to:
|
|||
|
||||
- Get an overview of the latest Merge Request for each project.
|
||||
- See if Merge Requests were approved and by whom.
|
||||
- See Merge Request authors.
|
||||
- See the latest [CI Pipeline](../../../ci/pipelines/index.md) result for each Merge Request.
|
||||
|
||||
## Permissions
|
||||
|
|
|
@ -8,11 +8,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
# Iterations **(STARTER)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214713) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.1.
|
||||
> - It's deployed behind a feature flag, disabled by default.
|
||||
> - It's disabled on GitLab.com.
|
||||
> - It's able to be enabled or disabled per-group
|
||||
> - It's not recommended for production use.
|
||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-iterations-core-only). **(CORE ONLY)**
|
||||
> - It was deployed behind a feature flag, disabled by default.
|
||||
> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) on GitLab 13.2.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It's able to be enabled or disabled per-group.
|
||||
> - It's recommended for production use.
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#disable-iterations-core-only). **(CORE ONLY)**
|
||||
|
||||
Iterations are a way to track issues over a period of time. This allows teams
|
||||
to track velocity and volatility metrics. Iterations can be used with [milestones](../../project/milestones/index.md)
|
||||
|
@ -56,12 +57,11 @@ You need Developer [permissions](../../permissions.md) or higher to edit an iter
|
|||
|
||||
To edit an iteration, click the three-dot menu (**{ellipsis_v}**) > **Edit iteration**.
|
||||
|
||||
## Enable Iterations **(CORE ONLY)**
|
||||
## Disable Iterations **(CORE ONLY)**
|
||||
|
||||
GitLab Iterations feature is under development and not ready for production use.
|
||||
It is deployed behind a feature flag that is **disabled by default**.
|
||||
GitLab Iterations feature is deployed with a feature flag that is **enabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||
can enable it for your instance. `:group_iterations` can be enabled or disabled per-group.
|
||||
can disable it for your instance. `:group_iterations` can be enabled or disabled per-group.
|
||||
|
||||
To enable it:
|
||||
|
||||
|
|
After Width: | Height: | Size: 128 KiB |
|
@ -55,29 +55,43 @@ In order to enable the Jira service in GitLab, you need to first configure the p
|
|||
|
||||
> **Notes:**
|
||||
>
|
||||
> - The currently supported Jira versions are `v6.x, v7.x, v8.x` . GitLab 7.8 or
|
||||
> higher is required.
|
||||
> - GitLab 8.14 introduced a new way to integrate with Jira which greatly simplified
|
||||
> the configuration options you have to enter. If you are using an older version,
|
||||
> [follow this documentation](https://gitlab.com/gitlab-org/gitlab/blob/8-13-stable-ee/doc/project_services/jira.md).
|
||||
> - The supported Jira versions are `v6.x`, `v7.x`, and `v8.x`.
|
||||
> - In order to support Oracle's Access Manager, GitLab will send additional cookies
|
||||
> to enable Basic Auth. The cookie being added to each request is `OBBasicAuth` with
|
||||
> a value of `fromDialog`.
|
||||
|
||||
To enable the Jira integration in a project, navigate to the
|
||||
[Integrations page](overview.md#accessing-integrations), click
|
||||
the **Jira** service, and fill in the required details on the page as described
|
||||
in the table below.
|
||||
[Integrations page](overview.md#accessing-integrations) and click
|
||||
the **Jira** service.
|
||||
|
||||
Select **Enable integration**.
|
||||
|
||||
Select a **Trigger** action. This determines whether a mention of a Jira issue in GitLab commits, merge requests, or both, should link the Jira issue back to that source commit/MR and transition the Jira issue, if indicated.
|
||||
|
||||
To include a comment on the Jira issue when the above referene is made in GitLab, check **Enable comments**.
|
||||
|
||||
Enter the further details on the page as described in the following table.
|
||||
|
||||
| Field | Description |
|
||||
| ----- | ----------- |
|
||||
| `Web URL` | The base URL to the Jira instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
|
||||
| `Jira API URL` | The base URL to the Jira instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. Leave this field blank (or use the same value of `Web URL`) if using **Jira Cloud**. |
|
||||
| `Username/Email` | Created when [configuring Jira step](#configuring-jira). Use `username` for **Jira Server** or `email` for **Jira Cloud**. |
|
||||
| `Password/API token` |Created in [configuring Jira step](#configuring-jira). Use `password` for **Jira Server** or `API token` for **Jira Cloud**. |
|
||||
| `Transition ID` | This is the ID of a transition that moves issues to the desired state. It is possible to insert transition ids separated by `,` or `;` which means the issue will be moved to each state after another using the given order. **Closing Jira issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
|
||||
| `Username or Email` | Created in [configuring Jira](#configuring-jira) step. Use `username` for **Jira Server** or `email` for **Jira Cloud**. |
|
||||
| `Password/API token` |Created in [configuring Jira](#configuring-jira) step. Use `password` for **Jira Server** or `API token` for **Jira Cloud**. |
|
||||
| `Transition ID` | Required for closing Jira issues via commits or merge requests. This is the ID of a transition in Jira that moves issues to a desired state. (See [Obtaining a transition ID](#obtaining-a-transition-id).) If you insert multiple transition IDs separated by `,` or `;`, the issue is moved to each state, one after another, using the given order. |
|
||||
|
||||
### Obtaining a transition ID
|
||||
To enable users to view Jira issues inside GitLab, select **Enable Jira issues** and enter a project key. **(PREMIUM)**
|
||||
|
||||
CAUTION: **Caution:**
|
||||
If you enable Jira issues with the setting above, all users that have access to this GitLab project will be able to view all issues from the specified Jira project.
|
||||
|
||||
When you have configured all settings, click **Test settings and save changes**.
|
||||
|
||||
Your GitLab project can now interact with all Jira projects in your instance and the project now displays a Jira link that opens the Jira project.
|
||||
|
||||
![Jira service page](img/jira_service_page_v12_2.png)
|
||||
|
||||
#### Obtaining a transition ID
|
||||
|
||||
In the most recent Jira user interface, you can no longer see transition IDs in the workflow
|
||||
administration UI. You can get the ID you need in either of the following ways:
|
||||
|
@ -90,19 +104,11 @@ administration UI. You can get the ID you need in either of the following ways:
|
|||
Note that the transition ID may vary between workflows (e.g., bug vs. story),
|
||||
even if the status you are changing to is the same.
|
||||
|
||||
After saving the configuration, your GitLab project will be able to interact
|
||||
with all Jira projects in your Jira instance and you'll see the Jira link on the GitLab project pages that takes you to the appropriate Jira project.
|
||||
#### Disabling comments on Jira issues
|
||||
|
||||
![Jira service page](img/jira_service_page_v12_2.png)
|
||||
You can continue to have GitLab cross-link a source commit/MR with a Jira issue while disabling the comment added to the issue.
|
||||
|
||||
### Disabling comments on Jira issues
|
||||
|
||||
When you reference a Jira issue, it will always link back to the source commit/MR in GitLab, however, you can control whether GitLab will also cross-post a comment to the Jira issue. That functionality is enabled by default.
|
||||
|
||||
To disable the automated commenting on Jira issues:
|
||||
|
||||
1. Open the [Integrations page](overview.md#accessing-integrations) and select **Jira**.
|
||||
1. In the **Event Action** section, uncheck **Comment**.
|
||||
See the [Configuring GitLab](#configuring-gitlab) section and uncheck the **Enable comments** setting.
|
||||
|
||||
## Jira issues
|
||||
|
||||
|
@ -111,7 +117,7 @@ By now you should have [configured Jira](#configuring-jira) and enabled the
|
|||
you should be able to reference and close Jira issues by just mentioning their
|
||||
ID in GitLab commits and merge requests.
|
||||
|
||||
### Referencing Jira Issues
|
||||
### Reference Jira issues
|
||||
|
||||
When GitLab project has Jira issue tracker configured and enabled, mentioning
|
||||
Jira issue in GitLab will automatically add a comment in Jira issue with the
|
||||
|
@ -138,7 +144,7 @@ For example, the following commit will reference the Jira issue with `PROJECT-1`
|
|||
git commit -m "PROJECT-1 Fix spelling and grammar"
|
||||
```
|
||||
|
||||
### Closing Jira Issues
|
||||
### Close Jira issues
|
||||
|
||||
Jira issues can be closed directly from GitLab by using trigger words in
|
||||
commits and merge requests. When a commit which contains the trigger word
|
||||
|
@ -162,8 +168,6 @@ where `PROJECT-1` is the ID of the Jira issue.
|
|||
> [project settings](img/jira_project_settings.png).
|
||||
> - The Jira issue will not be transitioned if it has a resolution.
|
||||
|
||||
### Jira issue closing example
|
||||
|
||||
Let's consider the following example:
|
||||
|
||||
1. For the project named `PROJECT` in Jira, we implemented a new feature
|
||||
|
@ -185,6 +189,45 @@ with a link to the commit that resolved the issue.
|
|||
|
||||
![The GitLab integration closes Jira issue](img/jira_service_close_issue.png)
|
||||
|
||||
### View Jira issues **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3622) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
|
||||
|
||||
You can browse and search issues from a selected Jira project directly in GitLab. This requires [configuration](#configuring-gitlab) in GitLab by an administrator.
|
||||
|
||||
![Jira issues integration enabled](img/jira/open_jira_issues_list_v13.2.png)
|
||||
|
||||
From the **Jira Issues** menu, click **Issues List**. The issue list defaults to sort by **Created date**, with the newest issues listed at the top. You can change this to **Last updated**.
|
||||
|
||||
Issues are grouped into tabs based on their [Jira status](https://confluence.atlassian.com/adminjiraserver070/defining-status-field-values-749382903.html).
|
||||
|
||||
- The **Open** tab displays all issues with a Jira status in any category other than Done.
|
||||
- The **Closed** tab displays all issues with a Jira status categorized as Done.
|
||||
- The **All** tab displays all issues of any status.
|
||||
|
||||
Click an issue title to open its original Jira issue page for full details.
|
||||
|
||||
#### Search and filter the issues list
|
||||
|
||||
To refine the list of issues, use the search bar to search for any text
|
||||
contained in an issue summary (title) or description.
|
||||
|
||||
You can also filter by labels, status, reporter, and assignee using URL parameters.
|
||||
Enhancements to be able to use these through the user interface are [planned](https://gitlab.com/groups/gitlab-org/-/epics/3622).
|
||||
|
||||
- To filter issues by `labels`, specify one or more labels as part of the `labels[]`
|
||||
parameter in the URL. When using multiple labels, only issues that contain all specified
|
||||
labels are listed. `/-/integrations/jira/issues?labels[]=backend&labels[]=feature&labels[]=QA`
|
||||
|
||||
- To filter issues by `status`, specify the `status` parameter in the URL.
|
||||
`/-/integrations/jira/issues?status=In Progress`
|
||||
|
||||
- To filter issues by `reporter`, specify a reporter's Jira display name for the
|
||||
`author_username` parameter in the URL. `/-/integrations/jira/issues?author_username=John Smith`
|
||||
|
||||
- To filter issues by `assignee`, specify their Jira display name for the
|
||||
`assignee_username` parameter in the URL. `/-/integrations/jira/issues?assignee_username=John Smith`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If these features do not work as expected, it is likely due to a problem with the way the integration settings were configured.
|
||||
|
|
|
@ -4,11 +4,11 @@ module API
|
|||
module Entities
|
||||
class MergeRequestApprovals < Grape::Entity
|
||||
expose :user_has_approved do |merge_request, options|
|
||||
merge_request.has_approved?(options[:current_user])
|
||||
merge_request.approved_by?(options[:current_user])
|
||||
end
|
||||
|
||||
expose :user_can_approve do |merge_request, options|
|
||||
!merge_request.has_approved?(options[:current_user]) &&
|
||||
!merge_request.approved_by?(options[:current_user]) &&
|
||||
options[:current_user].can?(:approve_merge_request, merge_request)
|
||||
end
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@ module API
|
|||
|
||||
if !member
|
||||
not_allowed! # This currently can only be reached in EE
|
||||
elsif member.persisted? && member.valid?
|
||||
elsif member.valid? && member.persisted?
|
||||
present_members(member)
|
||||
else
|
||||
render_validation_error!(member)
|
||||
|
|
|
@ -31,12 +31,14 @@ module API
|
|||
end
|
||||
|
||||
def list_milestones_for(parent)
|
||||
milestones = parent.milestones.order_id_desc
|
||||
milestones = Milestone.filter_by_state(milestones, params[:state])
|
||||
milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present?
|
||||
milestones = filter_by_title(milestones, params[:title]) if params[:title]
|
||||
finder_params = params.merge(milestones_finder_params(parent))
|
||||
milestones = MilestonesFinder.new(finder_params).execute
|
||||
milestones = filter_by_search(milestones, params[:search]) if params[:search]
|
||||
|
||||
if params[:iids].present? && !params[:include_parent_milestones]
|
||||
milestones = filter_by_iid(milestones, params[:iids])
|
||||
end
|
||||
|
||||
present paginate(milestones), with: Entities::Milestone
|
||||
end
|
||||
|
||||
|
@ -96,6 +98,25 @@ module API
|
|||
[MergeRequestsFinder, Entities::MergeRequestBasic]
|
||||
end
|
||||
end
|
||||
|
||||
def milestones_finder_params(parent)
|
||||
if parent.is_a?(Group)
|
||||
{ group_ids: parent.id }
|
||||
else
|
||||
{
|
||||
project_ids: parent.id,
|
||||
group_ids: parent_group_ids(parent)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def parent_group_ids(parent)
|
||||
return unless params[:include_parent_milestones].present?
|
||||
|
||||
parent.group.self_and_ancestors
|
||||
.public_or_visible_to_user(current_user)
|
||||
.select(:id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,6 +16,8 @@ module API
|
|||
end
|
||||
params do
|
||||
use :list_params
|
||||
optional :include_parent_milestones, type: Boolean, default: false,
|
||||
desc: 'Include milestones from parent group and ancestors'
|
||||
end
|
||||
get ":id/milestones" do
|
||||
authorize! :read_milestone, user_project
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# Backfillnamespace_settings for a range of namespaces
|
||||
class BackfillNamespaceSettings
|
||||
def perform(start_id, end_id)
|
||||
ActiveRecord::Base.connection.execute <<~SQL
|
||||
INSERT INTO namespace_settings (namespace_id, created_at, updated_at)
|
||||
SELECT namespaces.id, now(), now()
|
||||
FROM namespaces
|
||||
WHERE namespaces.id BETWEEN #{start_id} AND #{end_id}
|
||||
ON CONFLICT (namespace_id) DO NOTHING;
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,6 +6,7 @@ module Gitlab
|
|||
module Danger
|
||||
module Helper
|
||||
RELEASE_TOOLS_BOT = 'gitlab-release-tools-bot'
|
||||
DRAFT_REGEX = /\A*#{Regexp.union(/(?i)(\[WIP\]\s*|WIP:\s*|WIP$)/, /(?i)(\[draft\]|\(draft\)|draft:|draft\s\-\s|draft$)/)}+\s*/i.freeze
|
||||
|
||||
# Returns a list of all files that have been added, modified or renamed.
|
||||
# `git.modified_files` might contain paths that already have been renamed,
|
||||
|
@ -210,7 +211,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def sanitize_mr_title(title)
|
||||
title.gsub(/^WIP: */, '').gsub(/`/, '\\\`')
|
||||
title.gsub(DRAFT_REGEX, '').gsub(/`/, '\\\`')
|
||||
end
|
||||
|
||||
def security_mr?
|
||||
|
|
|
@ -568,25 +568,25 @@ msgstr[1] ""
|
|||
msgid "%{remaining_approvals} left"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities out of %{total}."
|
||||
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}."
|
||||
msgstr ""
|
||||
|
||||
msgid "%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities."
|
||||
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities."
|
||||
msgstr ""
|
||||
|
||||
msgid "%{reportType} %{status} detected %{critical} critical severity vulnerabilities out of %{total}."
|
||||
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities out of %{total}."
|
||||
msgstr ""
|
||||
|
||||
msgid "%{reportType} %{status} detected %{critical} critical severity vulnerability."
|
||||
msgid_plural "%{reportType} %{status} detected %{critical} critical severity vulnerabilities."
|
||||
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerability."
|
||||
msgid_plural "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%{reportType} %{status} detected %{high} high severity vulnerabilities out of %{total}."
|
||||
msgid "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}."
|
||||
msgstr ""
|
||||
|
||||
msgid "%{reportType} %{status} detected %{high} high severity vulnerability."
|
||||
msgid_plural "%{reportType} %{status} detected %{high} high severity vulnerabilities."
|
||||
msgid "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerability."
|
||||
msgid_plural "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
|
@ -4782,9 +4782,6 @@ msgstr ""
|
|||
msgid "CiVariable|Validation failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Class"
|
||||
msgstr ""
|
||||
|
||||
msgid "Classification Label (optional)"
|
||||
msgstr ""
|
||||
|
||||
|
@ -6113,6 +6110,9 @@ msgstr ""
|
|||
msgid "Complete"
|
||||
msgstr ""
|
||||
|
||||
msgid "Completed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Compliance"
|
||||
msgstr ""
|
||||
|
||||
|
@ -7001,6 +7001,9 @@ msgstr ""
|
|||
msgid "Created branch '%{branch_name}' and a merge request to resolve this issue."
|
||||
msgstr ""
|
||||
|
||||
msgid "Created by %{job}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Created by me"
|
||||
msgstr ""
|
||||
|
||||
|
@ -12521,6 +12524,9 @@ msgstr ""
|
|||
msgid "ImportProjects|Importing the project failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "ImportProjects|Importing the project failed: %{reason}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ImportProjects|Requesting your %{provider} repositories failed"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14393,6 +14399,9 @@ msgstr ""
|
|||
msgid "Maximum job timeout has a value which could not be accepted"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum length 100 characters"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum lifetime allowable for Personal Access Tokens is active, your expire date must be set before %{maximum_allowable_date}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -15321,6 +15330,9 @@ msgstr ""
|
|||
msgid "MrDeploymentActions|Stop environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Multi-project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Multiple IP address ranges are supported."
|
||||
msgstr ""
|
||||
|
||||
|
@ -15360,6 +15372,9 @@ msgstr ""
|
|||
msgid "Name has already been taken"
|
||||
msgstr ""
|
||||
|
||||
msgid "Name is required"
|
||||
msgstr ""
|
||||
|
||||
msgid "Name new label"
|
||||
msgstr ""
|
||||
|
||||
|
@ -15752,10 +15767,10 @@ msgstr ""
|
|||
msgid "No grouping"
|
||||
msgstr ""
|
||||
|
||||
msgid "No iteration"
|
||||
msgid "No issues found"
|
||||
msgstr ""
|
||||
|
||||
msgid "No iterations found"
|
||||
msgid "No iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "No iterations to show"
|
||||
|
@ -16693,6 +16708,33 @@ msgstr ""
|
|||
msgid "Page was successfully deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "PagerDutySettings|Active"
|
||||
msgstr ""
|
||||
|
||||
msgid "PagerDutySettings|Create a GitLab issue for each PagerDuty incident by %{docsLink}"
|
||||
msgstr ""
|
||||
|
||||
msgid "PagerDutySettings|Failed to update Webhook URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "PagerDutySettings|Reset webhook URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "PagerDutySettings|Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty."
|
||||
msgstr ""
|
||||
|
||||
msgid "PagerDutySettings|Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident."
|
||||
msgstr ""
|
||||
|
||||
msgid "PagerDutySettings|Webhook URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "PagerDutySettings|Webhook URL update was successful"
|
||||
msgstr ""
|
||||
|
||||
msgid "PagerDutySettings|configuring a webhook in PagerDuty"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pages"
|
||||
msgstr ""
|
||||
|
||||
|
@ -23179,13 +23221,13 @@ msgstr ""
|
|||
msgid "TestReports|%{count} failures"
|
||||
msgstr ""
|
||||
|
||||
msgid "TestReports|%{count} jobs"
|
||||
msgid "TestReports|%{count} tests"
|
||||
msgstr ""
|
||||
|
||||
msgid "TestReports|%{rate}%{sign} success rate"
|
||||
msgstr ""
|
||||
|
||||
msgid "TestReports|Test suites"
|
||||
msgid "TestReports|Jobs"
|
||||
msgstr ""
|
||||
|
||||
msgid "TestReports|Tests"
|
||||
|
@ -24221,12 +24263,6 @@ msgstr ""
|
|||
msgid "This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>"
|
||||
msgstr ""
|
||||
|
||||
msgid "This pipeline triggered a child pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "This pipeline was triggered by a parent pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "This pipeline was triggered by a schedule."
|
||||
msgstr ""
|
||||
|
||||
|
@ -27581,6 +27617,9 @@ msgstr ""
|
|||
msgid "cannot merge"
|
||||
msgstr ""
|
||||
|
||||
msgid "child-pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|%{degradedNum} degraded"
|
||||
msgstr ""
|
||||
|
||||
|
@ -28609,6 +28648,9 @@ msgstr ""
|
|||
msgid "project avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "project bots cannot be added to other groups / projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "project is read-only"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
export SETUP_DB=${SETUP_DB:-true}
|
||||
export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
|
||||
export BUNDLE_INSTALL_FLAGS="--without=production --without=development --jobs=$(nproc) --path=vendor --retry=3 --quiet"
|
||||
export BUNDLE_INSTALL_FLAGS=${BUNDLE_INSTALL_FLAGS:-"--without=production --without=development --jobs=$(nproc) --path=vendor --retry=3 --quiet"}
|
||||
|
||||
if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
|
||||
bundle --version
|
||||
|
|
|
@ -106,6 +106,29 @@ RSpec.describe Projects::ProjectMembersController do
|
|||
expect(response).to redirect_to(project_project_members_path(project))
|
||||
end
|
||||
end
|
||||
|
||||
context 'adding project bot' do
|
||||
let_it_be(:project_bot) { create(:user, :project_bot) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
|
||||
unrelated_project = create(:project)
|
||||
unrelated_project.add_maintainer(project_bot)
|
||||
end
|
||||
|
||||
it 'returns error' do
|
||||
post :create, params: {
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
user_ids: project_bot.id,
|
||||
access_level: Gitlab::Access::GUEST
|
||||
}
|
||||
|
||||
expect(flash[:alert]).to include('project bots cannot be added to other groups / projects')
|
||||
expect(response).to redirect_to(project_project_members_path(project))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT update' do
|
||||
|
|
|
@ -384,7 +384,7 @@ RSpec.describe 'Pipeline', :js do
|
|||
find('.js-tests-tab-link').click
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('Test suites')
|
||||
expect(page).to have_content('Jobs')
|
||||
expect(page).to have_selector('.js-tests-detail', visible: :all)
|
||||
end
|
||||
end
|
||||
|
@ -412,7 +412,7 @@ RSpec.describe 'Pipeline', :js do
|
|||
it 'calls summary.json endpoint', :js do
|
||||
find('.js-tests-tab-link').click
|
||||
|
||||
expect(page).to have_content('Test suites')
|
||||
expect(page).to have_content('Jobs')
|
||||
expect(page).to have_selector('.js-tests-detail', visible: :all)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import createFlash from '~/flash';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
@ -22,6 +23,8 @@ import {
|
|||
} from '~/import_projects/store/actions';
|
||||
import state from '~/import_projects/store/state';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
describe('import_projects store actions', () => {
|
||||
let localState;
|
||||
const repos = [{ id: 1 }, { id: 2 }];
|
||||
|
@ -130,10 +133,10 @@ describe('import_projects store actions', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR on an unsuccessful request', () => {
|
||||
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows generic error message on an unsuccessful request', async () => {
|
||||
mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500);
|
||||
|
||||
return testAction(
|
||||
await testAction(
|
||||
fetchImport,
|
||||
importPayload,
|
||||
localState,
|
||||
|
@ -143,6 +146,26 @@ describe('import_projects store actions', () => {
|
|||
],
|
||||
[],
|
||||
);
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith('Importing the project failed');
|
||||
});
|
||||
|
||||
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows detailed error message on an unsuccessful request with errors fields in response', async () => {
|
||||
const ERROR_MESSAGE = 'dummy';
|
||||
mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500, { errors: ERROR_MESSAGE });
|
||||
|
||||
await testAction(
|
||||
fetchImport,
|
||||
importPayload,
|
||||
localState,
|
||||
[
|
||||
{ type: REQUEST_IMPORT, payload: importPayload.repo.id },
|
||||
{ type: RECEIVE_IMPORT_ERROR, payload: importPayload.repo.id },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith(`Importing the project failed: ${ERROR_MESSAGE}`);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -48,7 +48,14 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
|
|||
data-testid="AlertsSettingsForm-tab"
|
||||
/>
|
||||
</gl-tab-stub>
|
||||
<!---->
|
||||
<gl-tab-stub
|
||||
title="PagerDuty integration"
|
||||
>
|
||||
<pagerdutysettingsform-stub
|
||||
class="gl-pt-3"
|
||||
data-testid="PagerDutySettingsForm-tab"
|
||||
/>
|
||||
</gl-tab-stub>
|
||||
<!---->
|
||||
</gl-tabs-stub>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Alert integration settings form should match the default snapshot 1`] = `
|
||||
<div>
|
||||
<!---->
|
||||
|
||||
<p>
|
||||
Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident.
|
||||
</p>
|
||||
|
||||
<form>
|
||||
<gl-form-group-stub
|
||||
class="col-8 col-md-9 gl-p-0"
|
||||
>
|
||||
<gl-toggle-stub
|
||||
id="active"
|
||||
label="Active"
|
||||
labelposition="top"
|
||||
value="true"
|
||||
/>
|
||||
</gl-form-group-stub>
|
||||
|
||||
<gl-form-group-stub
|
||||
class="col-8 col-md-9 gl-p-0"
|
||||
label="Webhook URL"
|
||||
label-class="label-bold"
|
||||
label-for="url"
|
||||
>
|
||||
<gl-form-input-group-stub
|
||||
data-testid="webhook-url"
|
||||
id="url"
|
||||
predefinedoptions="[object Object]"
|
||||
readonly=""
|
||||
value="pagerduty.webhook.com"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="gl-text-gray-400 gl-pt-2"
|
||||
>
|
||||
<gl-sprintf-stub
|
||||
message="Create a GitLab issue for each PagerDuty incident by %{docsLink}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<gl-button-stub
|
||||
category="tertiary"
|
||||
class="gl-mt-3"
|
||||
data-testid="webhook-reset-btn"
|
||||
icon=""
|
||||
role="button"
|
||||
size="medium"
|
||||
tabindex="0"
|
||||
variant="default"
|
||||
>
|
||||
|
||||
Reset webhook URL
|
||||
|
||||
</gl-button-stub>
|
||||
|
||||
<gl-modal-stub
|
||||
modalclass=""
|
||||
modalid="resetWebhookModal"
|
||||
ok-title="Reset webhook URL"
|
||||
ok-variant="danger"
|
||||
size="md"
|
||||
title="Reset webhook URL"
|
||||
titletag="h4"
|
||||
>
|
||||
|
||||
Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty.
|
||||
|
||||
</gl-modal-stub>
|
||||
</gl-form-group-stub>
|
||||
|
||||
<gl-button-stub
|
||||
category="tertiary"
|
||||
class="js-no-auto-disable"
|
||||
icon=""
|
||||
size="medium"
|
||||
type="submit"
|
||||
variant="success"
|
||||
>
|
||||
|
||||
Save changes
|
||||
|
||||
</gl-button-stub>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
|
@ -1,24 +1,16 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import AlertsSettingsForm from '~/incidents_settings/components/alerts_form.vue';
|
||||
import { ERROR_MSG } from '~/incidents_settings/constants';
|
||||
import createFlash from '~/flash';
|
||||
import { refreshCurrentPage } from '~/lib/utils/url_utility';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/lib/utils/url_utility');
|
||||
|
||||
describe('Alert integration settings form', () => {
|
||||
let wrapper;
|
||||
const service = { updateSettings: jest.fn().mockResolvedValue() };
|
||||
|
||||
const findForm = () => wrapper.find({ ref: 'settingsForm' });
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(AlertsSettingsForm, {
|
||||
provide: {
|
||||
operationsSettingsEndpoint: 'operations/endpoint',
|
||||
service,
|
||||
alertSettings: {
|
||||
issueTemplateKey: 'selecte_tmpl',
|
||||
createIssue: true,
|
||||
|
@ -32,6 +24,7 @@ describe('Alert integration settings form', () => {
|
|||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -42,30 +35,15 @@ describe('Alert integration settings form', () => {
|
|||
});
|
||||
|
||||
describe('form', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('should refresh the page on successful submit', () => {
|
||||
mock.onPatch().reply(200);
|
||||
it('should call service `updateSettings` on submit', () => {
|
||||
findForm().trigger('submit');
|
||||
return waitForPromises().then(() => {
|
||||
expect(refreshCurrentPage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a flah message on unsuccessful submit', () => {
|
||||
mock.onPatch().reply(400);
|
||||
findForm().trigger('submit');
|
||||
return waitForPromises().then(() => {
|
||||
expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(ERROR_MSG), 'alert');
|
||||
});
|
||||
expect(service.updateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
create_issue: wrapper.vm.createIssueEnabled,
|
||||
issue_template_key: wrapper.vm.issueTemplate,
|
||||
send_email: wrapper.vm.sendEmailEnabled,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
|
||||
import { ERROR_MSG } from '~/incidents_settings/constants';
|
||||
import createFlash from '~/flash';
|
||||
import { refreshCurrentPage } from '~/lib/utils/url_utility';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/lib/utils/url_utility');
|
||||
|
||||
describe('IncidentsSettingsService', () => {
|
||||
const settingsEndpoint = 'operations/settings';
|
||||
const webhookUpdateEndpoint = 'webhook/update';
|
||||
let mock;
|
||||
let service;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new AxiosMockAdapter(axios);
|
||||
service = new IncidentsSettingsService(settingsEndpoint, webhookUpdateEndpoint);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('updateSettings', () => {
|
||||
it('should refresh the page on successful update', () => {
|
||||
mock.onPatch().reply(httpStatusCodes.OK);
|
||||
|
||||
return service.updateSettings({}).then(() => {
|
||||
expect(refreshCurrentPage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a flash message on update error', () => {
|
||||
mock.onPatch().reply(httpStatusCodes.BAD_REQUEST);
|
||||
|
||||
return service.updateSettings({}).then(() => {
|
||||
expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(ERROR_MSG), 'alert');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetWebhookUrl', () => {
|
||||
it('should make a call for webhook update', () => {
|
||||
jest.spyOn(axios, 'post');
|
||||
mock.onPost().reply(httpStatusCodes.OK);
|
||||
|
||||
return service.resetWebhookUrl().then(() => {
|
||||
expect(axios.post).toHaveBeenCalledWith(webhookUpdateEndpoint);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,7 +6,9 @@ describe('IncidentsSettingTabs', () => {
|
|||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(IncidentsSettingTabs);
|
||||
wrapper = shallowMount(IncidentsSettingTabs, {
|
||||
provide: { glFeatures: { pagerdutyWebhook: true } },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import PagerDutySettingsForm from '~/incidents_settings/components/pagerduty_form.vue';
|
||||
import { GlAlert, GlModal } from '@gitlab/ui';
|
||||
|
||||
describe('Alert integration settings form', () => {
|
||||
let wrapper;
|
||||
const resetWebhookUrl = jest.fn();
|
||||
const service = { updateSettings: jest.fn().mockResolvedValue(), resetWebhookUrl };
|
||||
|
||||
const findForm = () => wrapper.find({ ref: 'settingsForm' });
|
||||
const findWebhookInput = () => wrapper.find('[data-testid="webhook-url"]');
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
const findAlert = () => wrapper.find(GlAlert);
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(PagerDutySettingsForm, {
|
||||
provide: {
|
||||
service,
|
||||
pagerDutySettings: {
|
||||
active: true,
|
||||
webhookUrl: 'pagerduty.webhook.com',
|
||||
webhookUpdateEndpoint: 'webhook/update',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
}
|
||||
});
|
||||
|
||||
it('should match the default snapshot', () => {
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should call service `updateSettings` on form submit', () => {
|
||||
findForm().trigger('submit');
|
||||
expect(service.updateSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pagerduty_active: wrapper.vm.active }),
|
||||
);
|
||||
});
|
||||
|
||||
describe('Webhook reset', () => {
|
||||
it('should make a call for webhook reset and reset form values', async () => {
|
||||
const newWebhookUrl = 'new.webhook.url?token=token';
|
||||
resetWebhookUrl.mockResolvedValueOnce({
|
||||
data: { pagerduty_webhook_url: newWebhookUrl },
|
||||
});
|
||||
findModal().vm.$emit('ok');
|
||||
await waitForPromises();
|
||||
expect(resetWebhookUrl).toHaveBeenCalled();
|
||||
expect(findWebhookInput().attributes('value')).toBe(newWebhookUrl);
|
||||
expect(findAlert().attributes('variant')).toBe('success');
|
||||
});
|
||||
|
||||
it('should show error message and NOT reset webhook url', async () => {
|
||||
resetWebhookUrl.mockRejectedValueOnce();
|
||||
findModal().vm.$emit('ok');
|
||||
await waitForPromises();
|
||||
expect(findAlert().attributes('variant')).toBe('danger');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,6 +5,8 @@ import JobItem from '~/pipelines/components/graph/job_item.vue';
|
|||
describe('pipeline graph job item', () => {
|
||||
let wrapper;
|
||||
|
||||
const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]');
|
||||
|
||||
const createWrapper = propsData => {
|
||||
wrapper = mount(JobItem, {
|
||||
propsData,
|
||||
|
@ -57,7 +59,7 @@ describe('pipeline graph job item', () => {
|
|||
});
|
||||
|
||||
describe('name without link', () => {
|
||||
it('it should render status and name', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
job: {
|
||||
id: 4257,
|
||||
|
@ -71,13 +73,22 @@ describe('pipeline graph job item', () => {
|
|||
has_details: false,
|
||||
},
|
||||
},
|
||||
cssClassJobName: 'css-class-job-name',
|
||||
jobHovered: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('it should render status and name', () => {
|
||||
expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
|
||||
expect(wrapper.find('a').exists()).toBe(false);
|
||||
|
||||
expect(trimText(wrapper.find('.ci-status-text').text())).toEqual(mockJob.name);
|
||||
});
|
||||
|
||||
it('should apply hover class and provided class name', () => {
|
||||
expect(findJobWithoutLink().classes()).toContain('gl-inset-border-1-blue-500');
|
||||
expect(findJobWithoutLink().classes()).toContain('css-class-job-name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('action icon', () => {
|
||||
|
|
|
@ -11,7 +11,10 @@ const invalidTriggeredPipelineId = mockPipeline.project.id + 5;
|
|||
|
||||
describe('Linked pipeline', () => {
|
||||
let wrapper;
|
||||
|
||||
const findButton = () => wrapper.find('button');
|
||||
const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
|
||||
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
|
||||
|
||||
const createWrapper = propsData => {
|
||||
wrapper = mount(LinkedPipelineComponent, {
|
||||
|
@ -69,6 +72,8 @@ describe('Linked pipeline', () => {
|
|||
it('should correctly compute the tooltip text', () => {
|
||||
expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name);
|
||||
expect(wrapper.vm.tooltipText).toContain(mockPipeline.details.status.label);
|
||||
expect(wrapper.vm.tooltipText).toContain(mockPipeline.source_job.name);
|
||||
expect(wrapper.vm.tooltipText).toContain(mockPipeline.id);
|
||||
});
|
||||
|
||||
it('should render the tooltip text as the title attribute', () => {
|
||||
|
@ -83,9 +88,8 @@ describe('Linked pipeline', () => {
|
|||
expect(wrapper.find('.js-linked-pipeline-loading').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not display child label when pipeline project id is not the same as triggered pipeline project id', () => {
|
||||
const labelContainer = wrapper.find('.parent-child-label-container');
|
||||
expect(labelContainer.exists()).toBe(false);
|
||||
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
|
||||
expect(findPipelineLabel().text()).toBe('Multi-project');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -103,17 +107,17 @@ describe('Linked pipeline', () => {
|
|||
|
||||
it('parent/child label container should exist', () => {
|
||||
createWrapper(downstreamProps);
|
||||
expect(wrapper.find('.parent-child-label-container').exists()).toBe(true);
|
||||
expect(findPipelineLabel().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should display child label when pipeline project id is the same as triggered pipeline project id', () => {
|
||||
createWrapper(downstreamProps);
|
||||
expect(wrapper.find('.parent-child-label-container').text()).toContain('Child');
|
||||
expect(findPipelineLabel().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
|
||||
createWrapper(upstreamProps);
|
||||
expect(wrapper.find('.parent-child-label-container').text()).toContain('Parent');
|
||||
expect(findPipelineLabel().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -133,7 +137,7 @@ describe('Linked pipeline', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('on click', () => {
|
||||
describe('on click/hover', () => {
|
||||
const props = {
|
||||
pipeline: mockPipeline,
|
||||
projectId: validTriggeredPipelineId,
|
||||
|
@ -160,5 +164,15 @@ describe('Linked pipeline', () => {
|
|||
'js-linked-pipeline-34993051',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should emit downstreamHovered with job name on mouseover', () => {
|
||||
findLinkedPipeline().trigger('mouseover');
|
||||
expect(wrapper.emitted().downstreamHovered).toStrictEqual([['trigger_job']]);
|
||||
});
|
||||
|
||||
it('should emit downstreamHovered with empty string on mouseleave', () => {
|
||||
findLinkedPipeline().trigger('mouseleave');
|
||||
expect(wrapper.emitted().downstreamHovered).toStrictEqual([['']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,6 +14,9 @@ export default {
|
|||
active: false,
|
||||
coverage: null,
|
||||
source: 'push',
|
||||
source_job: {
|
||||
name: 'trigger_job',
|
||||
},
|
||||
created_at: '2018-06-05T11:31:30.452Z',
|
||||
updated_at: '2018-10-31T16:35:31.305Z',
|
||||
path: '/gitlab-org/gitlab-runner/pipelines/23211253',
|
||||
|
@ -381,6 +384,9 @@ export default {
|
|||
active: false,
|
||||
coverage: null,
|
||||
source: 'pipeline',
|
||||
source_job: {
|
||||
name: 'trigger_job',
|
||||
},
|
||||
path: '/gitlab-com/gitlab-docs/pipelines/34993051',
|
||||
details: {
|
||||
status: {
|
||||
|
@ -889,6 +895,9 @@ export default {
|
|||
active: false,
|
||||
coverage: null,
|
||||
source: 'pipeline',
|
||||
source_job: {
|
||||
name: 'trigger_job',
|
||||
},
|
||||
path: '/gitlab-com/gitlab-docs/pipelines/34993051',
|
||||
details: {
|
||||
status: {
|
||||
|
@ -1402,6 +1411,9 @@ export default {
|
|||
active: false,
|
||||
coverage: null,
|
||||
source: 'pipeline',
|
||||
source_job: {
|
||||
name: 'trigger_job',
|
||||
},
|
||||
path: '/gitlab-com/gitlab-docs/pipelines/34993051',
|
||||
details: {
|
||||
status: {
|
||||
|
@ -1912,6 +1924,9 @@ export default {
|
|||
active: false,
|
||||
coverage: null,
|
||||
source: 'pipeline',
|
||||
source_job: {
|
||||
name: 'trigger_job',
|
||||
},
|
||||
path: '/gitlab-com/gitlab-docs/pipelines/34993051',
|
||||
details: {
|
||||
status: {
|
||||
|
@ -2412,6 +2427,9 @@ export default {
|
|||
active: false,
|
||||
coverage: null,
|
||||
source: 'push',
|
||||
source_job: {
|
||||
name: 'trigger_job',
|
||||
},
|
||||
created_at: '2019-01-06T17:48:37.599Z',
|
||||
updated_at: '2019-01-06T17:48:38.371Z',
|
||||
path: '/h5bp/html5-boilerplate/pipelines/26',
|
||||
|
@ -3743,6 +3761,9 @@ export default {
|
|||
active: false,
|
||||
coverage: null,
|
||||
source: 'push',
|
||||
source_job: {
|
||||
name: 'trigger_job',
|
||||
},
|
||||
path: '/gitlab-org/gitlab-test/pipelines/4',
|
||||
details: {
|
||||
status: {
|
||||
|
|
|
@ -60,7 +60,7 @@ describe('Test reports summary', () => {
|
|||
});
|
||||
|
||||
it('displays the correct total', () => {
|
||||
expect(totalTests().text()).toBe('4 jobs');
|
||||
expect(totalTests().text()).toBe('4 tests');
|
||||
});
|
||||
|
||||
it('displays the correct failure count', () => {
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import Vue from 'vue';
|
||||
import mountComponent from 'helpers/vue_mount_component_helper';
|
||||
import component from '~/reports/components/summary_row.vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import SummaryRow from '~/reports/components/summary_row.vue';
|
||||
|
||||
describe('Summary row', () => {
|
||||
const Component = Vue.extend(component);
|
||||
let vm;
|
||||
let wrapper;
|
||||
|
||||
const props = {
|
||||
summary: 'SAST detected 1 new vulnerability and 1 fixed vulnerability',
|
||||
|
@ -15,23 +13,42 @@ describe('Summary row', () => {
|
|||
statusIcon: 'warning',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Component, props);
|
||||
});
|
||||
const createComponent = ({ propsData = {}, slots = {} } = {}) => {
|
||||
wrapper = mount(SummaryRow, {
|
||||
propsData: {
|
||||
...props,
|
||||
...propsData,
|
||||
},
|
||||
slots,
|
||||
});
|
||||
};
|
||||
|
||||
const findSummary = () => wrapper.find('.report-block-list-issue-description-text');
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('renders provided summary', () => {
|
||||
expect(
|
||||
vm.$el.querySelector('.report-block-list-issue-description-text').textContent.trim(),
|
||||
).toEqual(props.summary);
|
||||
createComponent();
|
||||
expect(findSummary().text()).toEqual(props.summary);
|
||||
});
|
||||
|
||||
it('renders provided icon', () => {
|
||||
expect(vm.$el.querySelector('.report-block-list-icon span').classList).toContain(
|
||||
createComponent();
|
||||
expect(wrapper.find('.report-block-list-icon span').classes()).toContain(
|
||||
'js-ci-status-icon-warning',
|
||||
);
|
||||
});
|
||||
|
||||
describe('summary slot', () => {
|
||||
it('replaces the summary prop', () => {
|
||||
const summarySlotContent = 'Summary slot content';
|
||||
createComponent({ slots: { summary: summarySlotContent } });
|
||||
|
||||
expect(wrapper.text()).not.toContain(props.summary);
|
||||
expect(findSummary().text()).toEqual(summarySlotContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceSettings, schema: 20200703125016 do
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:namespace_settings) { table(:namespace_settings) }
|
||||
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
describe '#perform' do
|
||||
it 'creates settings for all projects in range' do
|
||||
namespaces.create!(id: 5, name: 'test1', path: 'test1')
|
||||
namespaces.create!(id: 7, name: 'test2', path: 'test2')
|
||||
namespaces.create!(id: 8, name: 'test3', path: 'test3')
|
||||
|
||||
subject.perform(5, 7)
|
||||
|
||||
expect(namespace_settings.all.pluck(:namespace_id)).to contain_exactly(5, 7)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -24,7 +24,15 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
|
|||
confirmed_at: 1.day.ago)
|
||||
end
|
||||
|
||||
let(:migration_bot) { User.migration_bot }
|
||||
let!(:migration_bot) do
|
||||
users.create(id: 100,
|
||||
email: "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}",
|
||||
user_type: HasUserType::USER_TYPES[:migration_bot],
|
||||
name: 'GitLab Migration Bot',
|
||||
projects_limit: 10,
|
||||
username: 'bot')
|
||||
end
|
||||
|
||||
let!(:snippet_with_repo) { snippets.create(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
|
||||
let!(:snippet_with_empty_repo) { snippets.create(id: 2, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
|
||||
let!(:snippet_without_repo) { snippets.create(id: 3, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
|
||||
|
|
|
@ -363,6 +363,11 @@ RSpec.describe Gitlab::Danger::Helper do
|
|||
where(:mr_title, :expected_mr_title) do
|
||||
'My MR title' | 'My MR title'
|
||||
'WIP: My MR title' | 'My MR title'
|
||||
'Draft: My MR title' | 'My MR title'
|
||||
'(Draft) My MR title' | 'My MR title'
|
||||
'[Draft] My MR title' | 'My MR title'
|
||||
'[DRAFT] My MR title' | 'My MR title'
|
||||
'DRAFT: My MR title' | 'My MR title'
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'post_migrate', '20200703125016_backfill_namespace_settings.rb')
|
||||
|
||||
RSpec.describe BackfillNamespaceSettings, :sidekiq, schema: 20200703124823 do
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
|
||||
describe '#up' do
|
||||
before do
|
||||
stub_const("#{described_class}::BATCH_SIZE", 2)
|
||||
|
||||
namespaces.create!(id: 1, name: 'test1', path: 'test1')
|
||||
namespaces.create!(id: 2, name: 'test2', path: 'test2')
|
||||
namespaces.create!(id: 3, name: 'test3', path: 'test3')
|
||||
end
|
||||
|
||||
it 'schedules BackfillNamespaceSettings background jobs' do
|
||||
Sidekiq::Testing.fake! do
|
||||
Timecop.freeze do
|
||||
migrate!
|
||||
|
||||
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 1, 2)
|
||||
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 3, 3)
|
||||
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,11 +3,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ApprovableBase do
|
||||
describe '#has_approved?' do
|
||||
describe '#approved_by?' do
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { merge_request.has_approved?(user) }
|
||||
subject { merge_request.approved_by?(user) }
|
||||
|
||||
context 'when a user has not approved' do
|
||||
it 'returns false' do
|
||||
|
|
|
@ -88,6 +88,28 @@ RSpec.describe Member do
|
|||
expect(child_member).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'project bots' do
|
||||
let_it_be(:project_bot) { create(:user, :project_bot) }
|
||||
let(:new_member) { build(:project_member, user_id: project_bot.id) }
|
||||
|
||||
context 'not a member of any group or project' do
|
||||
it 'is valid' do
|
||||
expect(new_member).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'already member of a project' do
|
||||
before do
|
||||
unrelated_project = create(:project)
|
||||
unrelated_project.add_maintainer(project_bot)
|
||||
end
|
||||
|
||||
it 'is not valid' do
|
||||
expect(new_member).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Scopes & finders' do
|
||||
|
|
|
@ -4,50 +4,18 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe GroupMember do
|
||||
context 'scopes' do
|
||||
shared_examples '.count_users_by_group_id' do
|
||||
it 'counts users by group ID' do
|
||||
user_1 = create(:user)
|
||||
user_2 = create(:user)
|
||||
group_1 = create(:group)
|
||||
group_2 = create(:group)
|
||||
it 'counts users by group ID' do
|
||||
user_1 = create(:user)
|
||||
user_2 = create(:user)
|
||||
group_1 = create(:group)
|
||||
group_2 = create(:group)
|
||||
|
||||
group_1.add_owner(user_1)
|
||||
group_1.add_owner(user_2)
|
||||
group_2.add_owner(user_1)
|
||||
group_1.add_owner(user_1)
|
||||
group_1.add_owner(user_2)
|
||||
group_2.add_owner(user_1)
|
||||
|
||||
expect(described_class.count_users_by_group_id).to eq(group_1.id => 2,
|
||||
group_2.id => 1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.count_users_by_group_id with optimized_count_users_by_group_id feature flag on' do
|
||||
before do
|
||||
stub_feature_flags(optimized_count_users_by_group_id: true)
|
||||
end
|
||||
|
||||
it_behaves_like '.count_users_by_group_id'
|
||||
|
||||
it 'does not JOIN users' do
|
||||
scope = described_class.all
|
||||
expect(scope).not_to receive(:joins).with(:user)
|
||||
|
||||
scope.count_users_by_group_id
|
||||
end
|
||||
end
|
||||
|
||||
describe '.count_users_by_group_id with optimized_count_users_by_group_id feature flag off' do
|
||||
before do
|
||||
stub_feature_flags(optimized_count_users_by_group_id: false)
|
||||
end
|
||||
|
||||
it_behaves_like '.count_users_by_group_id'
|
||||
|
||||
it 'does JOIN users' do
|
||||
scope = described_class.all
|
||||
expect(scope).to receive(:joins).with(:user).and_call_original
|
||||
|
||||
scope.count_users_by_group_id
|
||||
end
|
||||
expect(described_class.count_users_by_group_id).to eq(group_1.id => 2,
|
||||
group_2.id => 1)
|
||||
end
|
||||
|
||||
describe '.of_ldap_type' do
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe NamespaceSetting, type: :model do
|
||||
it { is_expected.to belong_to(:namespace) }
|
||||
end
|
|
@ -17,6 +17,7 @@ RSpec.describe Namespace do
|
|||
it { is_expected.to have_many :children }
|
||||
it { is_expected.to have_one :root_storage_statistics }
|
||||
it { is_expected.to have_one :aggregation_schedule }
|
||||
it { is_expected.to have_one :namespace_settings }
|
||||
it { is_expected.to have_many :custom_emoji }
|
||||
end
|
||||
|
||||
|
|