Commit f60e175d authored by Payton Burdette's avatar Payton Burdette Committed by Miguel Rincon

Send full ref

Send full ref for branch
or tag when running a manual
pipeline.
parent 74fed087
...@@ -12,10 +12,12 @@ import { ...@@ -12,10 +12,12 @@ import {
GlLink, GlLink,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType, GlSearchBoxByType,
GlSprintf, GlSprintf,
GlLoadingIcon, GlLoadingIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper';
import { s__, __, n__ } from '~/locale'; import { s__, __, n__ } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
...@@ -46,6 +48,7 @@ export default { ...@@ -46,6 +48,7 @@ export default {
GlLink, GlLink,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType, GlSearchBoxByType,
GlSprintf, GlSprintf,
GlLoadingIcon, GlLoadingIcon,
...@@ -59,11 +62,19 @@ export default { ...@@ -59,11 +62,19 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
defaultBranch: {
type: String,
required: true,
},
projectId: { projectId: {
type: String, type: String,
required: true, required: true,
}, },
refs: { branches: {
type: Array,
required: true,
},
tags: {
type: Array, type: Array,
required: true, required: true,
}, },
...@@ -94,7 +105,9 @@ export default { ...@@ -94,7 +105,9 @@ export default {
data() { data() {
return { return {
searchTerm: '', searchTerm: '',
refValue: this.refParam, refValue: {
shortName: this.refParam,
},
form: {}, form: {},
error: null, error: null,
warnings: [], warnings: [],
...@@ -104,9 +117,21 @@ export default { ...@@ -104,9 +117,21 @@ export default {
}; };
}, },
computed: { computed: {
filteredRefs() { lowerCasedSearchTerm() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); return this.searchTerm.toLowerCase();
return this.refs.filter(ref => ref.toLowerCase().includes(lowerCasedSearchTerm)); },
filteredBranches() {
return this.branches.filter(branch =>
branch.shortName.toLowerCase().includes(this.lowerCasedSearchTerm),
);
},
filteredTags() {
return this.tags.filter(tag =>
tag.shortName.toLowerCase().includes(this.lowerCasedSearchTerm),
);
},
hasTags() {
return this.tags.length > 0;
}, },
overMaxWarningsLimit() { overMaxWarningsLimit() {
return this.totalWarnings > this.maxWarnings; return this.totalWarnings > this.maxWarnings;
...@@ -120,14 +145,27 @@ export default { ...@@ -120,14 +145,27 @@ export default {
shouldShowWarning() { shouldShowWarning() {
return this.warnings.length > 0 && !this.isWarningDismissed; return this.warnings.length > 0 && !this.isWarningDismissed;
}, },
refShortName() {
return this.refValue.shortName;
},
refFullName() {
return this.refValue.fullName;
},
variables() { variables() {
return this.form[this.refValue]?.variables ?? []; return this.form[this.refFullName]?.variables ?? [];
}, },
descriptions() { descriptions() {
return this.form[this.refValue]?.descriptions ?? {}; return this.form[this.refFullName]?.descriptions ?? {};
}, },
}, },
created() { 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.setRefSelected(this.refValue); this.setRefSelected(this.refValue);
}, },
methods: { methods: {
...@@ -170,19 +208,19 @@ export default { ...@@ -170,19 +208,19 @@ export default {
setRefSelected(refValue) { setRefSelected(refValue) {
this.refValue = refValue; this.refValue = refValue;
if (!this.form[refValue]) { if (!this.form[this.refFullName]) {
this.fetchConfigVariables(refValue) this.fetchConfigVariables(this.refFullName || this.refShortName)
.then(({ descriptions, params }) => { .then(({ descriptions, params }) => {
Vue.set(this.form, refValue, { Vue.set(this.form, this.refFullName, {
variables: [], variables: [],
descriptions, descriptions,
}); });
// Add default variables from yml // Add default variables from yml
this.setVariableParams(refValue, VARIABLE_TYPE, params); this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
}) })
.catch(() => { .catch(() => {
Vue.set(this.form, refValue, { Vue.set(this.form, this.refFullName, {
variables: [], variables: [],
descriptions: {}, descriptions: {},
}); });
...@@ -190,20 +228,19 @@ export default { ...@@ -190,20 +228,19 @@ export default {
.finally(() => { .finally(() => {
// Add/update variables, e.g. from query string // Add/update variables, e.g. from query string
if (this.variableParams) { if (this.variableParams) {
this.setVariableParams(refValue, VARIABLE_TYPE, this.variableParams); this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
} }
if (this.fileParams) { if (this.fileParams) {
this.setVariableParams(refValue, FILE_TYPE, this.fileParams); this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
} }
// Adds empty var at the end of the form // Adds empty var at the end of the form
this.addEmptyVariable(refValue); this.addEmptyVariable(this.refFullName);
}); });
} }
}, },
isSelected(ref) { isSelected(ref) {
return ref === this.refValue; return ref.fullName === this.refValue.fullName;
}, },
removeVariable(index) { removeVariable(index) {
this.variables.splice(index, 1); this.variables.splice(index, 1);
...@@ -211,7 +248,6 @@ export default { ...@@ -211,7 +248,6 @@ export default {
canRemove(index) { canRemove(index) {
return index < this.variables.length - 1; return index < this.variables.length - 1;
}, },
fetchConfigVariables(refValue) { fetchConfigVariables(refValue) {
if (!gon?.features?.newPipelineFormPrefilledVars) { if (!gon?.features?.newPipelineFormPrefilledVars) {
return Promise.resolve({ params: {}, descriptions: {} }); return Promise.resolve({ params: {}, descriptions: {} });
...@@ -251,9 +287,11 @@ export default { ...@@ -251,9 +287,11 @@ export default {
return { params, descriptions }; return { params, descriptions };
}) })
.catch(() => { .catch(error => {
this.isLoading = false; this.isLoading = false;
Sentry.captureException(error);
return { params: {}, descriptions: {} }; return { params: {}, descriptions: {} };
}); });
}, },
...@@ -268,7 +306,9 @@ export default { ...@@ -268,7 +306,9 @@ export default {
return axios return axios
.post(this.pipelinesPath, { .post(this.pipelinesPath, {
ref: this.refValue, // send shortName as fall back for query params
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
ref: this.refValue.fullName || this.refShortName,
variables_attributes: filteredVariables, variables_attributes: filteredVariables,
}) })
.then(({ data }) => { .then(({ data }) => {
...@@ -326,20 +366,29 @@ export default { ...@@ -326,20 +366,29 @@ export default {
</details> </details>
</gl-alert> </gl-alert>
<gl-form-group :label="s__('Pipeline|Run for')"> <gl-form-group :label="s__('Pipeline|Run for')">
<gl-dropdown :text="refValue" block> <gl-dropdown :text="refShortName" block>
<gl-search-box-by-type <gl-search-box-by-type v-model.trim="searchTerm" :placeholder="__('Search refs')" />
v-model.trim="searchTerm" <gl-dropdown-section-header>{{ __('Branches') }}</gl-dropdown-section-header>
:placeholder="__('Search branches and tags')" <gl-dropdown-item
/> v-for="branch in filteredBranches"
:key="branch.fullName"
class="gl-font-monospace"
is-check-item
:is-checked="isSelected(branch)"
@click="setRefSelected(branch)"
>
{{ branch.shortName }}
</gl-dropdown-item>
<gl-dropdown-section-header v-if="hasTags">{{ __('Tags') }}</gl-dropdown-section-header>
<gl-dropdown-item <gl-dropdown-item
v-for="(ref, index) in filteredRefs" v-for="tag in filteredTags"
:key="index" :key="tag.fullName"
class="gl-font-monospace" class="gl-font-monospace"
is-check-item is-check-item
:is-checked="isSelected(ref)" :is-checked="isSelected(tag)"
@click="setRefSelected(ref)" @click="setRefSelected(tag)"
> >
{{ ref }} {{ tag.shortName }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
...@@ -372,7 +421,7 @@ export default { ...@@ -372,7 +421,7 @@ export default {
:placeholder="s__('CiVariables|Input variable key')" :placeholder="s__('CiVariables|Input variable key')"
:class="$options.formElementClasses" :class="$options.formElementClasses"
data-testid="pipeline-form-ci-variable-key" data-testid="pipeline-form-ci-variable-key"
@change="addEmptyVariable(refValue)" @change="addEmptyVariable(refFullName)"
/> />
<gl-form-input <gl-form-input
v-model="variable.value" v-model="variable.value"
......
export const VARIABLE_TYPE = 'env_var'; export const VARIABLE_TYPE = 'env_var';
export const FILE_TYPE = 'file'; export const FILE_TYPE = 'file';
export const CONFIG_VARIABLES_TIMEOUT = 5000; export const CONFIG_VARIABLES_TIMEOUT = 5000;
export const BRANCH_REF_TYPE = 'branch';
export const TAG_REF_TYPE = 'tag';
import Vue from 'vue'; import Vue from 'vue';
import PipelineNewForm from './components/pipeline_new_form.vue'; import PipelineNewForm from './components/pipeline_new_form.vue';
import formatRefs from './utils/format_refs';
export default () => { export default () => {
const el = document.getElementById('js-new-pipeline'); const el = document.getElementById('js-new-pipeline');
...@@ -7,17 +8,20 @@ export default () => { ...@@ -7,17 +8,20 @@ export default () => {
projectId, projectId,
pipelinesPath, pipelinesPath,
configVariablesPath, configVariablesPath,
defaultBranch,
refParam, refParam,
varParam, varParam,
fileParam, fileParam,
refNames, branchRefs,
tagRefs,
settingsLink, settingsLink,
maxWarnings, maxWarnings,
} = el?.dataset; } = el?.dataset;
const variableParams = JSON.parse(varParam); const variableParams = JSON.parse(varParam);
const fileParams = JSON.parse(fileParam); const fileParams = JSON.parse(fileParam);
const refs = JSON.parse(refNames); const branches = formatRefs(JSON.parse(branchRefs), 'branch');
const tags = formatRefs(JSON.parse(tagRefs), 'tag');
return new Vue({ return new Vue({
el, el,
...@@ -27,10 +31,12 @@ export default () => { ...@@ -27,10 +31,12 @@ export default () => {
projectId, projectId,
pipelinesPath, pipelinesPath,
configVariablesPath, configVariablesPath,
defaultBranch,
refParam, refParam,
variableParams, variableParams,
fileParams, fileParams,
refs, branches,
tags,
settingsLink, settingsLink,
maxWarnings: Number(maxWarnings), maxWarnings: Number(maxWarnings),
}, },
......
import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '../constants';
export default (refs, type) => {
let fullName;
return refs.map(ref => {
if (type === BRANCH_REF_TYPE) {
fullName = `refs/heads/${ref}`;
} else if (type === TAG_REF_TYPE) {
fullName = `refs/tags/${ref}`;
}
return {
shortName: ref,
fullName,
};
});
};
...@@ -10,10 +10,12 @@ ...@@ -10,10 +10,12 @@
#js-new-pipeline{ data: { project_id: @project.id, #js-new-pipeline{ data: { project_id: @project.id,
pipelines_path: project_pipelines_path(@project), pipelines_path: project_pipelines_path(@project),
config_variables_path: config_variables_namespace_project_pipelines_path(@project.namespace, @project), config_variables_path: config_variables_namespace_project_pipelines_path(@project.namespace, @project),
default_branch: @project.default_branch,
ref_param: params[:ref] || @project.default_branch, ref_param: params[:ref] || @project.default_branch,
var_param: params[:var].to_json, var_param: params[:var].to_json,
file_param: params[:file_var].to_json, file_param: params[:file_var].to_json,
ref_names: @project.repository.ref_names.to_json.html_safe, branch_refs: @project.repository.branch_names.to_json.html_safe,
tag_refs: @project.repository.tag_names.to_json.html_safe,
settings_link: project_settings_ci_cd_path(@project), settings_link: project_settings_ci_cd_path(@project),
max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } } max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
......
---
title: Manually trigger pipelines correctly when branches and tags have the same name. Separate tags and branches in trigger pipeline form.
merge_request: 48142
author:
type: fixed
...@@ -23919,6 +23919,9 @@ msgstr "" ...@@ -23919,6 +23919,9 @@ msgstr ""
msgid "Search projects..." msgid "Search projects..."
msgstr "" msgstr ""
msgid "Search refs"
msgstr ""
msgid "Search requirements" msgid "Search requirements"
msgstr "" msgstr ""
......
...@@ -5,7 +5,14 @@ import waitForPromises from 'helpers/wait_for_promises'; ...@@ -5,7 +5,14 @@ import waitForPromises from 'helpers/wait_for_promises';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue'; import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
import { mockRefs, mockParams, mockPostParams, mockProjectId, mockError } from '../mock_data'; import {
mockBranches,
mockTags,
mockParams,
mockPostParams,
mockProjectId,
mockError,
} from '../mock_data';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
...@@ -37,6 +44,10 @@ describe('Pipeline New Form', () => { ...@@ -37,6 +44,10 @@ describe('Pipeline New Form', () => {
const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]'); const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const getExpectedPostParams = () => JSON.parse(mock.history.post[0].data); const getExpectedPostParams = () => JSON.parse(mock.history.post[0].data);
const changeRef = i =>
findDropdownItems()
.at(i)
.vm.$emit('click');
const createComponent = (term = '', props = {}, method = shallowMount) => { const createComponent = (term = '', props = {}, method = shallowMount) => {
wrapper = method(PipelineNewForm, { wrapper = method(PipelineNewForm, {
...@@ -44,7 +55,8 @@ describe('Pipeline New Form', () => { ...@@ -44,7 +55,8 @@ describe('Pipeline New Form', () => {
projectId: mockProjectId, projectId: mockProjectId,
pipelinesPath, pipelinesPath,
configVariablesPath, configVariablesPath,
refs: mockRefs, branches: mockBranches,
tags: mockTags,
defaultBranch: 'master', defaultBranch: 'master',
settingsLink: '', settingsLink: '',
maxWarnings: 25, maxWarnings: 25,
...@@ -76,8 +88,11 @@ describe('Pipeline New Form', () => { ...@@ -76,8 +88,11 @@ describe('Pipeline New Form', () => {
}); });
it('displays dropdown with all branches and tags', () => { it('displays dropdown with all branches and tags', () => {
const refLength = mockBranches.length + mockTags.length;
createComponent(); createComponent();
expect(findDropdownItems()).toHaveLength(mockRefs.length);
expect(findDropdownItems()).toHaveLength(refLength);
}); });
it('when user enters search term the list is filtered', () => { it('when user enters search term the list is filtered', () => {
...@@ -130,15 +145,6 @@ describe('Pipeline New Form', () => { ...@@ -130,15 +145,6 @@ describe('Pipeline New Form', () => {
expect(findVariableRows()).toHaveLength(2); expect(findVariableRows()).toHaveLength(2);
}); });
it('creates a pipeline on submit', async () => {
findForm().vm.$emit('submit', dummySubmitEvent);
await waitForPromises();
expect(getExpectedPostParams()).toEqual(mockPostParams);
expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`);
});
it('creates blank variable on input change event', async () => { it('creates blank variable on input change event', async () => {
const input = findKeyInputs().at(2); const input = findKeyInputs().at(2);
input.element.value = 'test_var_2'; input.element.value = 'test_var_2';
...@@ -150,45 +156,81 @@ describe('Pipeline New Form', () => { ...@@ -150,45 +156,81 @@ describe('Pipeline New Form', () => {
expect(findKeyInputs().at(3).element.value).toBe(''); expect(findKeyInputs().at(3).element.value).toBe('');
expect(findValueInputs().at(3).element.value).toBe(''); expect(findValueInputs().at(3).element.value).toBe('');
}); });
});
describe('when the form has been modified', () => { describe('Pipeline creation', () => {
const selectRef = i =>
findDropdownItems()
.at(i)
.vm.$emit('click');
beforeEach(async () => { beforeEach(async () => {
const input = findKeyInputs().at(0); mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse);
input.element.value = 'test_var_2';
input.trigger('change');
findRemoveIcons()
.at(1)
.trigger('click');
await wrapper.vm.$nextTick(); await waitForPromises();
}); });
it('creates pipeline with full ref and variables', async () => {
createComponent();
it('form values are restored when the ref changes', async () => { changeRef(0);
expect(findVariableRows()).toHaveLength(2);
findForm().vm.$emit('submit', dummySubmitEvent);
selectRef(1);
await waitForPromises(); await waitForPromises();
expect(findVariableRows()).toHaveLength(3); expect(getExpectedPostParams().ref).toEqual(wrapper.vm.$data.refValue.fullName);
expect(findKeyInputs().at(0).element.value).toBe('test_var'); expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`);
}); });
it('creates a pipeline with short ref and variables', async () => {
// query params are used
createComponent('', mockParams);
it('form values are restored again when the ref is reverted', async () => {
selectRef(1);
await waitForPromises(); await waitForPromises();
selectRef(2); findForm().vm.$emit('submit', dummySubmitEvent);
await waitForPromises(); await waitForPromises();
expect(findVariableRows()).toHaveLength(2); expect(getExpectedPostParams()).toEqual(mockPostParams);
expect(findKeyInputs().at(0).element.value).toBe('test_var_2'); expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`);
});
});
describe('When the ref has been changed', () => {
beforeEach(async () => {
createComponent('', {}, mount);
await waitForPromises();
}); });
it('variables persist between ref changes', async () => {
changeRef(0); // change to master
await waitForPromises();
const masterInput = findKeyInputs().at(0);
masterInput.element.value = 'build_var';
masterInput.trigger('change');
await wrapper.vm.$nextTick();
changeRef(1); // change to branch-1
await waitForPromises();
const branchOneInput = findKeyInputs().at(0);
branchOneInput.element.value = 'deploy_var';
branchOneInput.trigger('change');
await wrapper.vm.$nextTick();
changeRef(0); // change back to master
await waitForPromises();
expect(findKeyInputs().at(0).element.value).toBe('build_var');
expect(findVariableRows().length).toBe(2);
changeRef(1); // change back to branch-1
await waitForPromises();
expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
expect(findVariableRows().length).toBe(2);
}); });
}); });
...@@ -321,6 +363,7 @@ describe('Pipeline New Form', () => { ...@@ -321,6 +363,7 @@ describe('Pipeline New Form', () => {
it('shows the correct warning title', () => { it('shows the correct warning title', () => {
const { length } = mockError.warnings; const { length } = mockError.warnings;
expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`); expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
}); });
......
export const mockRefs = ['master', 'branch-1', 'tag-1']; export const mockBranches = [
{ shortName: 'master', fullName: 'refs/heads/master' },
{ shortName: 'branch-1', fullName: 'refs/heads/branch-1' },
{ shortName: 'branch-2', fullName: 'refs/heads/branch-2' },
];
export const mockTags = [
{ shortName: '1.0.0', fullName: 'refs/tags/1.0.0' },
{ shortName: '1.1.0', fullName: 'refs/tags/1.1.0' },
{ shortName: '1.2.0', fullName: 'refs/tags/1.2.0' },
];
export const mockParams = { export const mockParams = {
refParam: 'tag-1', refParam: 'tag-1',
...@@ -31,3 +41,7 @@ export const mockError = { ...@@ -31,3 +41,7 @@ export const mockError = {
], ],
total_warnings: 7, total_warnings: 7,
}; };
export const mockBranchRefs = ['master', 'dev', 'release'];
export const mockTagRefs = ['1.0.0', '1.1.0', '1.2.0'];
import formatRefs from '~/pipeline_new/utils/format_refs';
import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '~/pipeline_new/constants';
import { mockBranchRefs, mockTagRefs } from '../mock_data';
describe('Format refs util', () => {
it('formats branch ref correctly', () => {
expect(formatRefs(mockBranchRefs, BRANCH_REF_TYPE)).toEqual([
{ fullName: 'refs/heads/master', shortName: 'master' },
{ fullName: 'refs/heads/dev', shortName: 'dev' },
{ fullName: 'refs/heads/release', shortName: 'release' },
]);
});
it('formats tag ref correctly', () => {
expect(formatRefs(mockTagRefs, TAG_REF_TYPE)).toEqual([
{ fullName: 'refs/tags/1.0.0', shortName: '1.0.0' },
{ fullName: 'refs/tags/1.1.0', shortName: '1.1.0' },
{ fullName: 'refs/tags/1.2.0', shortName: '1.2.0' },
]);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment