Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d298c12de5
commit
28515f6389
|
@ -0,0 +1,490 @@
|
|||
<script>
|
||||
import {
|
||||
GlAlert,
|
||||
GlIcon,
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormTextarea,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
GlLoadingIcon,
|
||||
GlSafeHtmlDirective as SafeHtml,
|
||||
} from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { uniqueId } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { backOff } from '~/lib/utils/common_utils';
|
||||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import { s__, __, n__ } from '~/locale';
|
||||
import {
|
||||
VARIABLE_TYPE,
|
||||
FILE_TYPE,
|
||||
CONFIG_VARIABLES_TIMEOUT,
|
||||
CC_VALIDATION_REQUIRED_ERROR,
|
||||
} from '../constants';
|
||||
import filterVariables from '../utils/filter_variables';
|
||||
import RefsDropdown from './refs_dropdown.vue';
|
||||
|
||||
const i18n = {
|
||||
variablesDescription: s__(
|
||||
'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.',
|
||||
),
|
||||
defaultError: __('Something went wrong on our end. Please try again.'),
|
||||
refsLoadingErrorTitle: s__('Pipeline|Branches or tags could not be loaded.'),
|
||||
submitErrorTitle: s__('Pipeline|Pipeline cannot be run.'),
|
||||
warningTitle: __('The form contains the following warning:'),
|
||||
maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
|
||||
removeVariableLabel: s__('CiVariables|Remove variable'),
|
||||
};
|
||||
|
||||
export default {
|
||||
typeOptions: {
|
||||
[VARIABLE_TYPE]: __('Variable'),
|
||||
[FILE_TYPE]: __('File'),
|
||||
},
|
||||
i18n,
|
||||
formElementClasses: 'gl-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0',
|
||||
// this height value is used inline on the textarea to match the input field height
|
||||
// it's used to prevent the overwrite if 'gl-h-7' or 'gl-h-7!' were used
|
||||
textAreaStyle: { height: '32px' },
|
||||
components: {
|
||||
GlAlert,
|
||||
GlIcon,
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormTextarea,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
GlLoadingIcon,
|
||||
RefsDropdown,
|
||||
CcValidationRequiredAlert: () =>
|
||||
import('ee_component/billings/components/cc_validation_required_alert.vue'),
|
||||
},
|
||||
directives: { SafeHtml },
|
||||
props: {
|
||||
pipelinesPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
configVariablesPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultBranch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
settingsLink: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileParams: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
refParam: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
variableParams: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
maxWarnings: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
refValue: {
|
||||
shortName: this.refParam,
|
||||
},
|
||||
form: {},
|
||||
errorTitle: null,
|
||||
error: null,
|
||||
warnings: [],
|
||||
totalWarnings: 0,
|
||||
isWarningDismissed: false,
|
||||
isLoading: false,
|
||||
submitted: false,
|
||||
ccAlertDismissed: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
overMaxWarningsLimit() {
|
||||
return this.totalWarnings > this.maxWarnings;
|
||||
},
|
||||
warningsSummary() {
|
||||
return n__('%d warning found:', '%d warnings found:', this.warnings.length);
|
||||
},
|
||||
summaryMessage() {
|
||||
return this.overMaxWarningsLimit ? i18n.maxWarningsSummary : this.warningsSummary;
|
||||
},
|
||||
shouldShowWarning() {
|
||||
return this.warnings.length > 0 && !this.isWarningDismissed;
|
||||
},
|
||||
refShortName() {
|
||||
return this.refValue.shortName;
|
||||
},
|
||||
refFullName() {
|
||||
return this.refValue.fullName;
|
||||
},
|
||||
variables() {
|
||||
return this.form[this.refFullName]?.variables ?? [];
|
||||
},
|
||||
descriptions() {
|
||||
return this.form[this.refFullName]?.descriptions ?? {};
|
||||
},
|
||||
ccRequiredError() {
|
||||
return this.error === CC_VALIDATION_REQUIRED_ERROR && !this.ccAlertDismissed;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
refValue() {
|
||||
this.loadConfigVariablesForm();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// this is needed until we add support for ref type in url query strings
|
||||
// ensure default branch is called with full ref on load
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
|
||||
if (this.refValue.shortName === this.defaultBranch) {
|
||||
this.refValue.fullName = `refs/heads/${this.refValue.shortName}`;
|
||||
}
|
||||
|
||||
this.loadConfigVariablesForm();
|
||||
},
|
||||
methods: {
|
||||
addEmptyVariable(refValue) {
|
||||
const { variables } = this.form[refValue];
|
||||
|
||||
const lastVar = variables[variables.length - 1];
|
||||
if (lastVar?.key === '' && lastVar?.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
variables.push({
|
||||
uniqueId: uniqueId(`var-${refValue}`),
|
||||
variable_type: VARIABLE_TYPE,
|
||||
key: '',
|
||||
value: '',
|
||||
});
|
||||
},
|
||||
setVariable(refValue, type, key, value) {
|
||||
const { variables } = this.form[refValue];
|
||||
|
||||
const variable = variables.find((v) => v.key === key);
|
||||
if (variable) {
|
||||
variable.type = type;
|
||||
variable.value = value;
|
||||
} else {
|
||||
variables.push({
|
||||
uniqueId: uniqueId(`var-${refValue}`),
|
||||
key,
|
||||
value,
|
||||
variable_type: type,
|
||||
});
|
||||
}
|
||||
},
|
||||
setVariableType(key, type) {
|
||||
const { variables } = this.form[this.refFullName];
|
||||
const variable = variables.find((v) => v.key === key);
|
||||
variable.variable_type = type;
|
||||
},
|
||||
setVariableParams(refValue, type, paramsObj) {
|
||||
Object.entries(paramsObj).forEach(([key, value]) => {
|
||||
this.setVariable(refValue, type, key, value);
|
||||
});
|
||||
},
|
||||
removeVariable(index) {
|
||||
this.variables.splice(index, 1);
|
||||
},
|
||||
canRemove(index) {
|
||||
return index < this.variables.length - 1;
|
||||
},
|
||||
loadConfigVariablesForm() {
|
||||
// Skip when variables already cached in `form`
|
||||
if (this.form[this.refFullName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchConfigVariables(this.refFullName || this.refShortName)
|
||||
.then(({ descriptions, params }) => {
|
||||
Vue.set(this.form, this.refFullName, {
|
||||
variables: [],
|
||||
descriptions,
|
||||
});
|
||||
|
||||
// Add default variables from yml
|
||||
this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
|
||||
})
|
||||
.catch(() => {
|
||||
Vue.set(this.form, this.refFullName, {
|
||||
variables: [],
|
||||
descriptions: {},
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
// Add/update variables, e.g. from query string
|
||||
if (this.variableParams) {
|
||||
this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
|
||||
}
|
||||
if (this.fileParams) {
|
||||
this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
|
||||
}
|
||||
|
||||
// Adds empty var at the end of the form
|
||||
this.addEmptyVariable(this.refFullName);
|
||||
});
|
||||
},
|
||||
fetchConfigVariables(refValue) {
|
||||
this.isLoading = true;
|
||||
|
||||
return backOff((next, stop) => {
|
||||
axios
|
||||
.get(this.configVariablesPath, {
|
||||
params: {
|
||||
sha: refValue,
|
||||
},
|
||||
})
|
||||
.then(({ data, status }) => {
|
||||
if (status === httpStatusCodes.NO_CONTENT) {
|
||||
next();
|
||||
} else {
|
||||
this.isLoading = false;
|
||||
stop(data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
stop(error);
|
||||
});
|
||||
}, CONFIG_VARIABLES_TIMEOUT)
|
||||
.then((data) => {
|
||||
const params = {};
|
||||
const descriptions = {};
|
||||
|
||||
Object.entries(data).forEach(([key, { value, description }]) => {
|
||||
if (description) {
|
||||
params[key] = value;
|
||||
descriptions[key] = description;
|
||||
}
|
||||
});
|
||||
|
||||
return { params, descriptions };
|
||||
})
|
||||
.catch((error) => {
|
||||
this.isLoading = false;
|
||||
|
||||
Sentry.captureException(error);
|
||||
|
||||
return { params: {}, descriptions: {} };
|
||||
});
|
||||
},
|
||||
createPipeline() {
|
||||
this.submitted = true;
|
||||
this.ccAlertDismissed = false;
|
||||
|
||||
return axios
|
||||
.post(this.pipelinesPath, {
|
||||
// send shortName as fall back for query params
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
|
||||
ref: this.refValue.fullName || this.refShortName,
|
||||
variables_attributes: filterVariables(this.variables),
|
||||
})
|
||||
.then(({ data }) => {
|
||||
redirectTo(`${this.pipelinesPath}/${data.id}`);
|
||||
})
|
||||
.catch((err) => {
|
||||
// always re-enable submit button
|
||||
this.submitted = false;
|
||||
|
||||
const {
|
||||
errors = [],
|
||||
warnings = [],
|
||||
total_warnings: totalWarnings = 0,
|
||||
} = err.response.data;
|
||||
const [error] = errors;
|
||||
|
||||
this.reportError({
|
||||
title: i18n.submitErrorTitle,
|
||||
error,
|
||||
warnings,
|
||||
totalWarnings,
|
||||
});
|
||||
});
|
||||
},
|
||||
onRefsLoadingError(error) {
|
||||
this.reportError({ title: i18n.refsLoadingErrorTitle });
|
||||
|
||||
Sentry.captureException(error);
|
||||
},
|
||||
reportError({ title = null, error = i18n.defaultError, warnings = [], totalWarnings = 0 }) {
|
||||
this.errorTitle = title;
|
||||
this.error = error;
|
||||
this.warnings = warnings;
|
||||
this.totalWarnings = totalWarnings;
|
||||
},
|
||||
dismissError() {
|
||||
this.ccAlertDismissed = true;
|
||||
this.error = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form @submit.prevent="createPipeline">
|
||||
<cc-validation-required-alert v-if="ccRequiredError" class="gl-pb-5" @dismiss="dismissError" />
|
||||
<gl-alert
|
||||
v-else-if="error"
|
||||
:title="errorTitle"
|
||||
:dismissible="false"
|
||||
variant="danger"
|
||||
class="gl-mb-4"
|
||||
data-testid="run-pipeline-error-alert"
|
||||
>
|
||||
<span v-safe-html="error"></span>
|
||||
</gl-alert>
|
||||
<gl-alert
|
||||
v-if="shouldShowWarning"
|
||||
:title="$options.i18n.warningTitle"
|
||||
variant="warning"
|
||||
class="gl-mb-4"
|
||||
data-testid="run-pipeline-warning-alert"
|
||||
@dismiss="isWarningDismissed = true"
|
||||
>
|
||||
<details>
|
||||
<summary>
|
||||
<gl-sprintf :message="summaryMessage">
|
||||
<template #total>
|
||||
{{ totalWarnings }}
|
||||
</template>
|
||||
<template #warningsDisplayed>
|
||||
{{ maxWarnings }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</summary>
|
||||
<p
|
||||
v-for="(warning, index) in warnings"
|
||||
:key="`warning-${index}`"
|
||||
data-testid="run-pipeline-warning"
|
||||
>
|
||||
{{ warning }}
|
||||
</p>
|
||||
</details>
|
||||
</gl-alert>
|
||||
<gl-form-group :label="s__('Pipeline|Run for branch name or tag')">
|
||||
<refs-dropdown v-model="refValue" @loadingError="onRefsLoadingError" />
|
||||
</gl-form-group>
|
||||
|
||||
<gl-loading-icon v-if="isLoading" class="gl-mb-5" size="lg" />
|
||||
|
||||
<gl-form-group v-else :label="s__('Pipeline|Variables')">
|
||||
<div
|
||||
v-for="(variable, index) in variables"
|
||||
:key="variable.uniqueId"
|
||||
class="gl-mb-3 gl-ml-n3 gl-pb-2"
|
||||
data-testid="ci-variable-row"
|
||||
data-qa-selector="ci_variable_row_container"
|
||||
>
|
||||
<div
|
||||
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
|
||||
>
|
||||
<gl-dropdown
|
||||
:text="$options.typeOptions[variable.variable_type]"
|
||||
:class="$options.formElementClasses"
|
||||
data-testid="pipeline-form-ci-variable-type"
|
||||
>
|
||||
<gl-dropdown-item
|
||||
v-for="type in Object.keys($options.typeOptions)"
|
||||
:key="type"
|
||||
@click="setVariableType(variable.key, type)"
|
||||
>
|
||||
{{ $options.typeOptions[type] }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
<gl-form-input
|
||||
v-model="variable.key"
|
||||
:placeholder="s__('CiVariables|Input variable key')"
|
||||
:class="$options.formElementClasses"
|
||||
data-testid="pipeline-form-ci-variable-key"
|
||||
data-qa-selector="ci_variable_key_field"
|
||||
@change="addEmptyVariable(refFullName)"
|
||||
/>
|
||||
<gl-form-textarea
|
||||
v-model="variable.value"
|
||||
:placeholder="s__('CiVariables|Input variable value')"
|
||||
class="gl-mb-3"
|
||||
:style="$options.textAreaStyle"
|
||||
:no-resize="false"
|
||||
data-testid="pipeline-form-ci-variable-value"
|
||||
data-qa-selector="ci_variable_value_field"
|
||||
/>
|
||||
|
||||
<template v-if="variables.length > 1">
|
||||
<gl-button
|
||||
v-if="canRemove(index)"
|
||||
class="gl-md-ml-3 gl-mb-3"
|
||||
data-testid="remove-ci-variable-row"
|
||||
variant="danger"
|
||||
category="secondary"
|
||||
:aria-label="$options.i18n.removeVariableLabel"
|
||||
@click="removeVariable(index)"
|
||||
>
|
||||
<gl-icon class="gl-mr-0! gl-display-none gl-md-display-block" name="clear" />
|
||||
<span class="gl-md-display-none">{{ $options.i18n.removeVariableLabel }}</span>
|
||||
</gl-button>
|
||||
<gl-button
|
||||
v-else
|
||||
class="gl-md-ml-3 gl-mb-3 gl-display-none gl-md-display-block gl-visibility-hidden"
|
||||
icon="clear"
|
||||
:aria-label="$options.i18n.removeVariableLabel"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="descriptions[variable.key]" class="gl-text-gray-500 gl-mb-3">
|
||||
{{ descriptions[variable.key] }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #description
|
||||
><gl-sprintf :message="$options.i18n.variablesDescription">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="settingsLink">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf></template
|
||||
>
|
||||
</gl-form-group>
|
||||
<div class="gl-pt-5 gl-display-flex">
|
||||
<gl-button
|
||||
type="submit"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
class="js-no-auto-disable gl-mr-3"
|
||||
data-qa-selector="run_pipeline_button"
|
||||
data-testid="run_pipeline_button"
|
||||
:disabled="submitted"
|
||||
>{{ s__('Pipeline|Run pipeline') }}</gl-button
|
||||
>
|
||||
<gl-button :href="pipelinesPath">{{ __('Cancel') }}</gl-button>
|
||||
</div>
|
||||
</gl-form>
|
||||
</template>
|
|
@ -1,22 +1,22 @@
|
|||
import Vue from 'vue';
|
||||
import LegacyPipelineNewForm from './components/legacy_pipeline_new_form.vue';
|
||||
import PipelineNewForm from './components/pipeline_new_form.vue';
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-new-pipeline');
|
||||
const mountLegacyPipelineNewForm = (el) => {
|
||||
const {
|
||||
// provide/inject
|
||||
projectRefsEndpoint,
|
||||
|
||||
// props
|
||||
projectId,
|
||||
pipelinesPath,
|
||||
configVariablesPath,
|
||||
defaultBranch,
|
||||
refParam,
|
||||
varParam,
|
||||
fileParam,
|
||||
settingsLink,
|
||||
maxWarnings,
|
||||
pipelinesPath,
|
||||
projectId,
|
||||
refParam,
|
||||
settingsLink,
|
||||
varParam,
|
||||
} = el.dataset;
|
||||
|
||||
const variableParams = JSON.parse(varParam);
|
||||
|
@ -28,19 +28,74 @@ export default () => {
|
|||
projectRefsEndpoint,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(PipelineNewForm, {
|
||||
return createElement(LegacyPipelineNewForm, {
|
||||
props: {
|
||||
projectId,
|
||||
pipelinesPath,
|
||||
configVariablesPath,
|
||||
defaultBranch,
|
||||
refParam,
|
||||
variableParams,
|
||||
fileParams,
|
||||
settingsLink,
|
||||
maxWarnings: Number(maxWarnings),
|
||||
pipelinesPath,
|
||||
projectId,
|
||||
refParam,
|
||||
settingsLink,
|
||||
variableParams,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const mountPipelineNewForm = (el) => {
|
||||
const {
|
||||
// provide/inject
|
||||
projectRefsEndpoint,
|
||||
|
||||
// props
|
||||
configVariablesPath,
|
||||
defaultBranch,
|
||||
fileParam,
|
||||
maxWarnings,
|
||||
pipelinesPath,
|
||||
projectId,
|
||||
refParam,
|
||||
settingsLink,
|
||||
varParam,
|
||||
} = el.dataset;
|
||||
|
||||
const variableParams = JSON.parse(varParam);
|
||||
const fileParams = JSON.parse(fileParam);
|
||||
|
||||
// TODO: add apolloProvider
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
projectRefsEndpoint,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(PipelineNewForm, {
|
||||
props: {
|
||||
configVariablesPath,
|
||||
defaultBranch,
|
||||
fileParams,
|
||||
maxWarnings: Number(maxWarnings),
|
||||
pipelinesPath,
|
||||
projectId,
|
||||
refParam,
|
||||
settingsLink,
|
||||
variableParams,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-new-pipeline');
|
||||
|
||||
if (gon.features?.runPipelineGraphql) {
|
||||
mountPipelineNewForm(el);
|
||||
} else {
|
||||
mountLegacyPipelineNewForm(el);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:pipeline_tabs_vue, @project)
|
||||
push_frontend_feature_flag(:run_pipeline_graphql, @project)
|
||||
end
|
||||
|
||||
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Ci
|
||||
module Job
|
||||
class ArtifactsDestroy < Base
|
||||
graphql_name 'JobArtifactsDestroy'
|
||||
|
||||
authorize :destroy_artifacts
|
||||
|
||||
field :job,
|
||||
Types::Ci::JobType,
|
||||
null: true,
|
||||
description: 'Job with artifacts to be deleted.'
|
||||
|
||||
field :destroyed_artifacts_count,
|
||||
GraphQL::Types::Int,
|
||||
null: false,
|
||||
description: 'Number of artifacts deleted.'
|
||||
|
||||
def find_object(id: )
|
||||
GlobalID::Locator.locate(id)
|
||||
end
|
||||
|
||||
def resolve(id:)
|
||||
job = authorized_find!(id: id)
|
||||
|
||||
result = ::Ci::JobArtifacts::DestroyBatchService.new(job.job_artifacts, pick_up_at: Time.current).execute
|
||||
{
|
||||
job: job,
|
||||
destroyed_artifacts_count: result[:destroyed_artifacts_count],
|
||||
errors: Array(result[:errors])
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -120,6 +120,7 @@ module Types
|
|||
milestone: '15.0'
|
||||
}
|
||||
mount_mutation Mutations::Ci::ProjectCiCdSettingsUpdate
|
||||
mount_mutation Mutations::Ci::Job::ArtifactsDestroy
|
||||
mount_mutation Mutations::Ci::Job::Play
|
||||
mount_mutation Mutations::Ci::Job::Retry
|
||||
mount_mutation Mutations::Ci::Job::Cancel
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module Ci
|
||||
class BuildPolicy < CommitStatusPolicy
|
||||
delegate { @subject.project }
|
||||
|
||||
condition(:protected_ref) do
|
||||
access = ::Gitlab::UserAccess.new(@user, container: @subject.project)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: api_caching_branches
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61157
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330371
|
||||
milestone: '13.12'
|
||||
name: run_pipeline_graphql
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96633
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372310
|
||||
milestone: '15.4'
|
||||
type: development
|
||||
group: group::source code
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
|
@ -172,6 +172,7 @@ options:
|
|||
- p_ci_templates_liquibase
|
||||
- p_ci_templates_matlab
|
||||
- p_ci_templates_themekit
|
||||
- p_ci_templates_katalon
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
key_path: redis_hll_counters.ci_templates.p_ci_templates_katalon_monthly
|
||||
description: 'Monthly counts of times users have executed katalon_tests jobs'
|
||||
product_section: 'ops'
|
||||
product_stage: 'analytics'
|
||||
product_group: 'pipeline_authoring'
|
||||
product_category: 'pipeline_authoring'
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: "15.4"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86484
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- p_ci_templates_katalon
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -172,6 +172,7 @@ options:
|
|||
- p_ci_templates_liquibase
|
||||
- p_ci_templates_matlab
|
||||
- p_ci_templates_themekit
|
||||
- p_ci_templates_katalon
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
key_path: redis_hll_counters.ci_templates.p_ci_templates_katalon_weekly
|
||||
description: 'Weekly counts of times users have executed katalon_tests jobs'
|
||||
product_section: 'ops'
|
||||
product_stage: 'analytics'
|
||||
product_group: 'pipeline_authoring'
|
||||
product_category: 'pipeline_authoring'
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: "15.4"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86484
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- p_ci_templates_katalon
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -3463,6 +3463,26 @@ Input type: `JiraImportUsersInput`
|
|||
| <a id="mutationjiraimportuserserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationjiraimportusersjirausers"></a>`jiraUsers` | [`[JiraUser!]`](#jirauser) | Users returned from Jira, matched by email and name if possible. |
|
||||
|
||||
### `Mutation.jobArtifactsDestroy`
|
||||
|
||||
Input type: `JobArtifactsDestroyInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationjobartifactsdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationjobartifactsdestroyid"></a>`id` | [`CiBuildID!`](#cibuildid) | ID of the job to mutate. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationjobartifactsdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationjobartifactsdestroydestroyedartifactscount"></a>`destroyedArtifactsCount` | [`Int!`](#int) | Number of artifacts deleted. |
|
||||
| <a id="mutationjobartifactsdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationjobartifactsdestroyjob"></a>`job` | [`CiJob`](#cijob) | Job with artifacts to be deleted. |
|
||||
|
||||
### `Mutation.jobCancel`
|
||||
|
||||
Input type: `JobCancelInput`
|
||||
|
|
|
@ -52,25 +52,15 @@ module API
|
|||
|
||||
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
|
||||
|
||||
if Feature.enabled?(:api_caching_branches, user_project, type: :development)
|
||||
present_cached(
|
||||
branches,
|
||||
with: Entities::Branch,
|
||||
current_user: current_user,
|
||||
project: user_project,
|
||||
merged_branch_names: merged_branch_names,
|
||||
expires_in: 10.minutes,
|
||||
cache_context: -> (branch) { [current_user&.cache_key, merged_branch_names.include?(branch.name)] }
|
||||
)
|
||||
else
|
||||
present(
|
||||
branches,
|
||||
with: Entities::Branch,
|
||||
current_user: current_user,
|
||||
project: user_project,
|
||||
merged_branch_names: merged_branch_names
|
||||
)
|
||||
end
|
||||
present_cached(
|
||||
branches,
|
||||
with: Entities::Branch,
|
||||
current_user: current_user,
|
||||
project: user_project,
|
||||
merged_branch_names: merged_branch_names,
|
||||
expires_in: 10.minutes,
|
||||
cache_context: -> (branch) { [current_user&.cache_key, merged_branch_names.include?(branch.name)] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
# This template is provided and maintained by Katalon, an official Technology Partner with GitLab.
|
||||
#
|
||||
# Use this template to run a Katalon Studio test from this repository.
|
||||
# You can:
|
||||
# - Copy and paste this template into a new `.gitlab-ci.yml` file.
|
||||
# - Add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword.
|
||||
#
|
||||
# In either case, you must also select which job you want to run, `.katalon_tests`
|
||||
# or `.katalon_tests_with_artifacts` (see configuration below), and add that configuration
|
||||
# to a new job with `extends:`. For example:
|
||||
#
|
||||
# Katalon-tests:
|
||||
# extends:
|
||||
# - .katalon_tests_with_artifacts
|
||||
#
|
||||
# Requirements:
|
||||
# - A Katalon Studio project with the content saved in the root GitLab repository folder.
|
||||
# - An active KRE license.
|
||||
# - A valid Katalon API key.
|
||||
#
|
||||
# CI/CD variables, set in the project CI/CD settings:
|
||||
# - KATALON_TEST_SUITE_PATH: The default path is `Test Suites/<Your Test Suite Name>`.
|
||||
# Defines which test suite to run.
|
||||
# - KATALON_API_KEY: The Katalon API key.
|
||||
# - KATALON_PROJECT_DIR: Optional. Add if the project is in another location.
|
||||
# - KATALON_ORG_ID: Optional. Add if you are part of multiple Katalon orgs.
|
||||
# Set to the Org ID that has KRE licenses assigned. For more info on the Org ID,
|
||||
# see https://support.katalon.com/hc/en-us/articles/4724459179545-How-to-get-Organization-ID-
|
||||
|
||||
.katalon_tests:
|
||||
# Use the latest version of the Katalon Runtime Engine. You can also use other versions of the
|
||||
# Katalon Runtime Engine by specifying another tag, for example `katalonstudio/katalon:8.1.2`
|
||||
# or `katalonstudio/katalon:8.3.0`.
|
||||
image: 'katalonstudio/katalon'
|
||||
services:
|
||||
- docker:dind
|
||||
variables:
|
||||
# Specify the Katalon Studio project directory. By default, it is stored under the root project folder.
|
||||
KATALON_PROJECT_DIR: $CI_PROJECT_DIR
|
||||
|
||||
# The following bash script has two different versions, one if you set the KATALON_ORG_ID
|
||||
# CI/CD variable, and the other if you did not set it. If you have more than one org in
|
||||
# admin.katalon.com you must set the KATALON_ORG_ID variable with an ORG ID or
|
||||
# the Katalon Test Suite fails to run.
|
||||
#
|
||||
# You can update or add additional `katalonc` commands below. To see all of the arguments
|
||||
# `katalonc` supports, go to https://docs.katalon.com/katalon-studio/docs/console-mode-execution.html
|
||||
script:
|
||||
- |-
|
||||
if [[ $KATALON_ORG_ID == "" ]]; then
|
||||
katalonc.sh -projectPath=$KATALON_PROJECT_DIR -apiKey=$KATALON_API_KEY -browserType="Chrome" -retry=0 -statusDelay=20 -testSuitePath="$KATALON_TEST_SUITE_PATH" -reportFolder=Reports/
|
||||
else
|
||||
katalonc.sh -projectPath=$KATALON_PROJECT_DIR -apiKey=$KATALON_API_KEY -browserType="Chrome" -retry=0 -statusDelay=20 -orgID=$KATALON_ORG_ID -testSuitePath="$KATALON_TEST_SUITE_PATH" -reportFolder=Reports/
|
||||
fi
|
||||
|
||||
# Upload the artifacts and make the junit report accessible under the Pipeline Tests
|
||||
.katalon_tests_with_artifacts:
|
||||
extends: .katalon_tests
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- Reports/
|
||||
reports:
|
||||
junit:
|
||||
Reports/*/*/*/*.xml
|
|
@ -231,6 +231,10 @@
|
|||
category: ci_templates
|
||||
redis_slot: ci_templates
|
||||
aggregation: weekly
|
||||
- name: p_ci_templates_katalon
|
||||
category: ci_templates
|
||||
redis_slot: ci_templates
|
||||
aggregation: weekly
|
||||
- name: p_ci_templates_mono
|
||||
category: ci_templates
|
||||
redis_slot: ci_templates
|
||||
|
|
|
@ -5,7 +5,7 @@ module QA
|
|||
module Project
|
||||
module Pipeline
|
||||
class New < QA::Page::Base
|
||||
view 'app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue' do
|
||||
view 'app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue' do
|
||||
element :run_pipeline_button, required: true
|
||||
element :ci_variable_row_container
|
||||
element :ci_variable_key_field
|
||||
|
|
|
@ -7,24 +7,42 @@ RSpec.describe "Populate new pipeline CI variables with url params", :js do
|
|||
let(:project) { create(:project) }
|
||||
let(:page_path) { new_project_pipeline_path(project) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
project.add_maintainer(user)
|
||||
shared_examples 'form pre-filled with URL params' do
|
||||
before do
|
||||
sign_in(user)
|
||||
project.add_maintainer(user)
|
||||
|
||||
visit "#{page_path}?var[key1]=value1&file_var[key2]=value2"
|
||||
end
|
||||
visit "#{page_path}?var[key1]=value1&file_var[key2]=value2"
|
||||
end
|
||||
|
||||
it "var[key1]=value1 populates env_var variable correctly" do
|
||||
page.within(all("[data-testid='ci-variable-row']")[0]) do
|
||||
expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key1')
|
||||
expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value1')
|
||||
it "var[key1]=value1 populates env_var variable correctly" do
|
||||
page.within(all("[data-testid='ci-variable-row']")[0]) do
|
||||
expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key1')
|
||||
expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value1')
|
||||
end
|
||||
end
|
||||
|
||||
it "file_var[key2]=value2 populates file variable correctly" do
|
||||
page.within(all("[data-testid='ci-variable-row']")[1]) do
|
||||
expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key2')
|
||||
expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value2')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "file_var[key2]=value2 populates file variable correctly" do
|
||||
page.within(all("[data-testid='ci-variable-row']")[1]) do
|
||||
expect(find("[data-testid='pipeline-form-ci-variable-key']").value).to eq('key2')
|
||||
expect(find("[data-testid='pipeline-form-ci-variable-value']").value).to eq('value2')
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(run_pipeline_graphql: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'form pre-filled with URL params'
|
||||
end
|
||||
|
||||
context 'when feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(run_pipeline_graphql: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'form pre-filled with URL params'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -674,6 +674,7 @@ RSpec.describe 'Pipelines', :js do
|
|||
let(:project) { create(:project, :repository) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(run_pipeline_graphql: false)
|
||||
visit new_project_pipeline_path(project)
|
||||
end
|
||||
|
||||
|
|
|
@ -656,19 +656,7 @@ RSpec.describe 'Pipelines', :js do
|
|||
describe 'POST /:project/-/pipelines' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
before do
|
||||
visit new_project_pipeline_path(project)
|
||||
end
|
||||
|
||||
context 'for valid commit', :js do
|
||||
before do
|
||||
click_button project.default_branch
|
||||
wait_for_requests
|
||||
|
||||
find('p', text: 'master').click
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
shared_examples 'run pipeline form with gitlab-ci.yml' do
|
||||
context 'with gitlab-ci.yml', :js do
|
||||
before do
|
||||
stub_ci_pipeline_to_return_yaml_file
|
||||
|
@ -702,7 +690,9 @@ RSpec.describe 'Pipelines', :js do
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'run pipeline form without gitlab-ci.yml' do
|
||||
context 'without gitlab-ci.yml' do
|
||||
before do
|
||||
click_on 'Run pipeline'
|
||||
|
@ -722,6 +712,51 @@ RSpec.describe 'Pipelines', :js do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Run Pipeline form with REST endpoints
|
||||
# TODO: Clean up tests when run_pipeline_graphql is enabled
|
||||
context 'with feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(run_pipeline_graphql: false)
|
||||
visit new_project_pipeline_path(project)
|
||||
end
|
||||
|
||||
context 'for valid commit', :js do
|
||||
before do
|
||||
click_button project.default_branch
|
||||
wait_for_requests
|
||||
|
||||
find('p', text: 'master').click
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it_behaves_like 'run pipeline form with gitlab-ci.yml'
|
||||
|
||||
it_behaves_like 'run pipeline form without gitlab-ci.yml'
|
||||
end
|
||||
end
|
||||
|
||||
# Run Pipeline form with GraphQL
|
||||
context 'with feature flag enabled' do
|
||||
before do
|
||||
stub_feature_flags(run_pipeline_graphql: true)
|
||||
visit new_project_pipeline_path(project)
|
||||
end
|
||||
|
||||
context 'for valid commit', :js do
|
||||
before do
|
||||
click_button project.default_branch
|
||||
wait_for_requests
|
||||
|
||||
find('p', text: 'master').click
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it_behaves_like 'run pipeline form with gitlab-ci.yml'
|
||||
|
||||
it_behaves_like 'run pipeline form without gitlab-ci.yml'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Reset runner caches' do
|
||||
|
|
|
@ -0,0 +1,456 @@
|
|||
import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { nextTick } from 'vue';
|
||||
import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import LegacyPipelineNewForm from '~/pipeline_new/components/legacy_pipeline_new_form.vue';
|
||||
import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
|
||||
import {
|
||||
mockQueryParams,
|
||||
mockPostParams,
|
||||
mockProjectId,
|
||||
mockError,
|
||||
mockRefs,
|
||||
mockCreditCardValidationRequiredError,
|
||||
} from '../mock_data';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
redirectTo: jest.fn(),
|
||||
}));
|
||||
|
||||
const projectRefsEndpoint = '/root/project/refs';
|
||||
const pipelinesPath = '/root/project/-/pipelines';
|
||||
const configVariablesPath = '/root/project/-/pipelines/config_variables';
|
||||
const newPipelinePostResponse = { id: 1 };
|
||||
const defaultBranch = 'main';
|
||||
|
||||
describe('Pipeline New Form', () => {
|
||||
let wrapper;
|
||||
let mock;
|
||||
let dummySubmitEvent;
|
||||
|
||||
const findForm = () => wrapper.find(GlForm);
|
||||
const findRefsDropdown = () => wrapper.findComponent(RefsDropdown);
|
||||
const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]');
|
||||
const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
|
||||
const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
|
||||
const findDropdowns = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-type"]');
|
||||
const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
|
||||
const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]');
|
||||
const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
|
||||
const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
|
||||
const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf);
|
||||
const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
|
||||
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
|
||||
const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
|
||||
|
||||
const selectBranch = (branch) => {
|
||||
// Select a branch in the dropdown
|
||||
findRefsDropdown().vm.$emit('input', {
|
||||
shortName: branch,
|
||||
fullName: `refs/heads/${branch}`,
|
||||
});
|
||||
};
|
||||
|
||||
const createComponent = (props = {}, method = shallowMount) => {
|
||||
wrapper = method(LegacyPipelineNewForm, {
|
||||
provide: {
|
||||
projectRefsEndpoint,
|
||||
},
|
||||
propsData: {
|
||||
projectId: mockProjectId,
|
||||
pipelinesPath,
|
||||
configVariablesPath,
|
||||
defaultBranch,
|
||||
refParam: defaultBranch,
|
||||
settingsLink: '',
|
||||
maxWarnings: 25,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {});
|
||||
mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs);
|
||||
|
||||
dummySubmitEvent = {
|
||||
preventDefault: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('Form', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent(mockQueryParams, mount);
|
||||
|
||||
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('displays the correct values for the provided query params', async () => {
|
||||
expect(findDropdowns().at(0).props('text')).toBe('Variable');
|
||||
expect(findDropdowns().at(1).props('text')).toBe('File');
|
||||
expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
|
||||
expect(findVariableRows()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('displays a variable from provided query params', () => {
|
||||
expect(findKeyInputs().at(0).element.value).toBe('test_var');
|
||||
expect(findValueInputs().at(0).element.value).toBe('test_var_val');
|
||||
});
|
||||
|
||||
it('displays an empty variable for the user to fill out', async () => {
|
||||
expect(findKeyInputs().at(2).element.value).toBe('');
|
||||
expect(findValueInputs().at(2).element.value).toBe('');
|
||||
expect(findDropdowns().at(2).props('text')).toBe('Variable');
|
||||
});
|
||||
|
||||
it('does not display remove icon for last row', () => {
|
||||
expect(findRemoveIcons()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('removes ci variable row on remove icon button click', async () => {
|
||||
findRemoveIcons().at(1).trigger('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findVariableRows()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('creates blank variable on input change event', async () => {
|
||||
const input = findKeyInputs().at(2);
|
||||
input.element.value = 'test_var_2';
|
||||
input.trigger('change');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findVariableRows()).toHaveLength(4);
|
||||
expect(findKeyInputs().at(3).element.value).toBe('');
|
||||
expect(findValueInputs().at(3).element.value).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pipeline creation', () => {
|
||||
beforeEach(async () => {
|
||||
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('does not submit the native HTML form', async () => {
|
||||
createComponent();
|
||||
|
||||
findForm().vm.$emit('submit', dummySubmitEvent);
|
||||
|
||||
expect(dummySubmitEvent.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables the submit button immediately after submitting', async () => {
|
||||
createComponent();
|
||||
|
||||
expect(findSubmitButton().props('disabled')).toBe(false);
|
||||
|
||||
findForm().vm.$emit('submit', dummySubmitEvent);
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSubmitButton().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('creates pipeline with full ref and variables', async () => {
|
||||
createComponent();
|
||||
|
||||
findForm().vm.$emit('submit', dummySubmitEvent);
|
||||
await waitForPromises();
|
||||
|
||||
expect(getFormPostParams().ref).toEqual(`refs/heads/${defaultBranch}`);
|
||||
expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
|
||||
});
|
||||
|
||||
it('creates a pipeline with short ref and variables from the query params', async () => {
|
||||
createComponent(mockQueryParams);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
findForm().vm.$emit('submit', dummySubmitEvent);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(getFormPostParams()).toEqual(mockPostParams);
|
||||
expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the ref has been changed', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({}, mount);
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
it('variables persist between ref changes', async () => {
|
||||
selectBranch('main');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
const mainInput = findKeyInputs().at(0);
|
||||
mainInput.element.value = 'build_var';
|
||||
mainInput.trigger('change');
|
||||
|
||||
await nextTick();
|
||||
|
||||
selectBranch('branch-1');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
const branchOneInput = findKeyInputs().at(0);
|
||||
branchOneInput.element.value = 'deploy_var';
|
||||
branchOneInput.trigger('change');
|
||||
|
||||
await nextTick();
|
||||
|
||||
selectBranch('main');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findKeyInputs().at(0).element.value).toBe('build_var');
|
||||
expect(findVariableRows().length).toBe(2);
|
||||
|
||||
selectBranch('branch-1');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
|
||||
expect(findVariableRows().length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when yml defines a variable', () => {
|
||||
const mockYmlKey = 'yml_var';
|
||||
const mockYmlValue = 'yml_var_val';
|
||||
const mockYmlMultiLineValue = `A value
|
||||
with multiple
|
||||
lines`;
|
||||
const mockYmlDesc = 'A var from yml.';
|
||||
|
||||
it('loading icon is shown when content is requested and hidden when received', async () => {
|
||||
createComponent(mockQueryParams, mount);
|
||||
|
||||
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
|
||||
[mockYmlKey]: {
|
||||
value: mockYmlValue,
|
||||
description: mockYmlDesc,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('multi-line strings are added to the value field without removing line breaks', async () => {
|
||||
createComponent(mockQueryParams, mount);
|
||||
|
||||
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
|
||||
[mockYmlKey]: {
|
||||
value: mockYmlMultiLineValue,
|
||||
description: mockYmlDesc,
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findValueInputs().at(0).element.value).toBe(mockYmlMultiLineValue);
|
||||
});
|
||||
|
||||
describe('with description', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent(mockQueryParams, mount);
|
||||
|
||||
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
|
||||
[mockYmlKey]: {
|
||||
value: mockYmlValue,
|
||||
description: mockYmlDesc,
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('displays all the variables', async () => {
|
||||
expect(findVariableRows()).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('displays a variable from yml', () => {
|
||||
expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey);
|
||||
expect(findValueInputs().at(0).element.value).toBe(mockYmlValue);
|
||||
});
|
||||
|
||||
it('displays a variable from provided query params', () => {
|
||||
expect(findKeyInputs().at(1).element.value).toBe('test_var');
|
||||
expect(findValueInputs().at(1).element.value).toBe('test_var_val');
|
||||
});
|
||||
|
||||
it('adds a description to the first variable from yml', () => {
|
||||
expect(findVariableRows().at(0).text()).toContain(mockYmlDesc);
|
||||
});
|
||||
|
||||
it('removes the description when a variable key changes', async () => {
|
||||
findKeyInputs().at(0).element.value = 'yml_var_modified';
|
||||
findKeyInputs().at(0).trigger('change');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findVariableRows().at(0).text()).not.toContain(mockYmlDesc);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without description', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent(mockQueryParams, mount);
|
||||
|
||||
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
|
||||
[mockYmlKey]: {
|
||||
value: mockYmlValue,
|
||||
description: null,
|
||||
},
|
||||
yml_var2: {
|
||||
value: 'yml_var2_val',
|
||||
},
|
||||
yml_var3: {
|
||||
description: '',
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('displays all the variables', async () => {
|
||||
expect(findVariableRows()).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form errors and warnings', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('when the refs cannot be loaded', () => {
|
||||
beforeEach(() => {
|
||||
mock
|
||||
.onGet(projectRefsEndpoint, { params: { search: '' } })
|
||||
.reply(httpStatusCodes.INTERNAL_SERVER_ERROR);
|
||||
|
||||
findRefsDropdown().vm.$emit('loadingError');
|
||||
});
|
||||
|
||||
it('shows both an error alert', () => {
|
||||
expect(findErrorAlert().exists()).toBe(true);
|
||||
expect(findWarningAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the error response can be handled', () => {
|
||||
beforeEach(async () => {
|
||||
mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError);
|
||||
|
||||
findForm().vm.$emit('submit', dummySubmitEvent);
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('shows both error and warning', () => {
|
||||
expect(findErrorAlert().exists()).toBe(true);
|
||||
expect(findWarningAlert().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows the correct error', () => {
|
||||
expect(findErrorAlert().text()).toBe(mockError.errors[0]);
|
||||
});
|
||||
|
||||
it('shows the correct warning title', () => {
|
||||
const { length } = mockError.warnings;
|
||||
|
||||
expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
|
||||
});
|
||||
|
||||
it('shows the correct amount of warnings', () => {
|
||||
expect(findWarnings()).toHaveLength(mockError.warnings.length);
|
||||
});
|
||||
|
||||
it('re-enables the submit button', () => {
|
||||
expect(findSubmitButton().props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not show the credit card validation required alert', () => {
|
||||
expect(findCCAlert().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when the error response is credit card validation required', () => {
|
||||
beforeEach(async () => {
|
||||
mock
|
||||
.onPost(pipelinesPath)
|
||||
.reply(httpStatusCodes.BAD_REQUEST, mockCreditCardValidationRequiredError);
|
||||
|
||||
window.gon = {
|
||||
subscriptions_url: TEST_HOST,
|
||||
payment_form_url: TEST_HOST,
|
||||
};
|
||||
|
||||
findForm().vm.$emit('submit', dummySubmitEvent);
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('shows credit card validation required alert', () => {
|
||||
expect(findErrorAlert().exists()).toBe(false);
|
||||
expect(findCCAlert().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('clears error and hides the alert on dismiss', async () => {
|
||||
expect(findCCAlert().exists()).toBe(true);
|
||||
expect(wrapper.vm.$data.error).toBe(mockCreditCardValidationRequiredError.errors[0]);
|
||||
|
||||
findCCAlert().vm.$emit('dismiss');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findCCAlert().exists()).toBe(false);
|
||||
expect(wrapper.vm.$data.error).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the error response cannot be handled', () => {
|
||||
beforeEach(async () => {
|
||||
mock
|
||||
.onPost(pipelinesPath)
|
||||
.reply(httpStatusCodes.INTERNAL_SERVER_ERROR, 'something went wrong');
|
||||
|
||||
findForm().vm.$emit('submit', dummySubmitEvent);
|
||||
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('re-enables the submit button', () => {
|
||||
expect(findSubmitButton().props('disabled')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Katalon.gitlab-ci.yml' do
|
||||
subject(:template) do
|
||||
<<~YAML
|
||||
include:
|
||||
- template: 'Katalon.gitlab-ci.yml'
|
||||
|
||||
katalon_tests_placeholder:
|
||||
extends: .katalon_tests
|
||||
stage: test
|
||||
script:
|
||||
- echo "katalon tests"
|
||||
|
||||
katalon_tests_with_artifacts_placeholder:
|
||||
extends: .katalon_tests_with_artifacts
|
||||
stage: test
|
||||
script:
|
||||
- echo "katalon tests with artifacts"
|
||||
YAML
|
||||
end
|
||||
|
||||
describe 'the created pipeline' do
|
||||
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
|
||||
let(:user) { project.first_owner }
|
||||
|
||||
let(:service) { Ci::CreatePipelineService.new(project, user, ref: 'master' ) }
|
||||
let(:pipeline) { service.execute!(:push).payload }
|
||||
let(:build_names) { pipeline.builds.pluck(:name) }
|
||||
|
||||
before do
|
||||
stub_ci_pipeline_yaml_file(template)
|
||||
end
|
||||
|
||||
it 'create katalon tests jobs' do
|
||||
expect(build_names).to match_array(%w[katalon_tests_placeholder katalon_tests_with_artifacts_placeholder])
|
||||
|
||||
expect(pipeline.builds.find_by(name: 'katalon_tests_placeholder').options).to include(
|
||||
image: { name: 'katalonstudio/katalon' },
|
||||
services: [{ name: 'docker:dind' }]
|
||||
)
|
||||
|
||||
expect(pipeline.builds.find_by(name: 'katalon_tests_with_artifacts_placeholder').options).to include(
|
||||
image: { name: 'katalonstudio/katalon' },
|
||||
services: [{ name: 'docker:dind' }],
|
||||
artifacts: { when: 'always', paths: ['Reports/'], reports: { junit: ['Reports/*/*/*/*.xml'] } }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'JobArtifactsDestroy' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:job) { create(:ci_build) }
|
||||
|
||||
let(:mutation) do
|
||||
variables = {
|
||||
id: job.to_global_id.to_s
|
||||
}
|
||||
graphql_mutation(:job_artifacts_destroy, variables, <<~FIELDS)
|
||||
job {
|
||||
name
|
||||
}
|
||||
destroyedArtifactsCount
|
||||
errors
|
||||
FIELDS
|
||||
end
|
||||
|
||||
before do
|
||||
create(:ci_job_artifact, :archive, job: job)
|
||||
create(:ci_job_artifact, :junit, job: job)
|
||||
end
|
||||
|
||||
it 'returns an error if the user is not allowed to destroy the job artifacts' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(graphql_errors).not_to be_empty
|
||||
expect(job.reload.job_artifacts.count).to be(2)
|
||||
end
|
||||
|
||||
it 'destroys the job artifacts and returns the expected data' do
|
||||
job.project.add_maintainer(user)
|
||||
expected_data = {
|
||||
'jobArtifactsDestroy' => {
|
||||
'errors' => [],
|
||||
'destroyedArtifactsCount' => 2,
|
||||
'job' => {
|
||||
'name' => job.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(graphql_data).to eq(expected_data)
|
||||
expect(job.reload.job_artifacts.count).to be(0)
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue