Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-04 18:10:08 +00:00
parent d4194db620
commit f9ddf689da
98 changed files with 1206 additions and 375 deletions

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@ -71,7 +71,9 @@ export const createContextCommits = ({ state }, { commits, forceReload = false }
})
.catch(() => {
if (forceReload) {
createFlash(s__('ContextCommits|Failed to create context commits. Please try again.'));
createFlash({
message: s__('ContextCommits|Failed to create context commits. Please try again.'),
});
}
return false;
@ -111,7 +113,9 @@ export const removeContextCommits = ({ state }, forceReload = false) =>
})
.catch(() => {
if (forceReload) {
createFlash(s__('ContextCommits|Failed to delete context commits. Please try again.'));
createFlash({
message: s__('ContextCommits|Failed to delete context commits. Please try again.'),
});
}
return false;

View File

@ -1,5 +1,5 @@
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@ -21,5 +21,7 @@ export const receiveStatisticsSuccess = ({ commit }, statistics) =>
export const receiveStatisticsError = ({ commit }, error) => {
commit(types.RECEIVE_STATISTICS_ERROR, error);
createFlash(s__('AdminDashboard|Error loading the statistics. Please try again'));
createFlash({
message: s__('AdminDashboard|Error loading the statistics. Please try again'),
});
};

View File

@ -3,7 +3,7 @@
import $ from 'jquery';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import initCodeQualityWalkthrough from '~/code_quality_walkthrough';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
import BlobFileDropzone from '../blob/blob_file_dropzone';
@ -84,7 +84,11 @@ export default () => {
initPopovers();
initCodeQualityWalkthroughStep();
})
.catch((e) => createFlash(e));
.catch((e) =>
createFlash({
message: e,
}),
);
cancelLink.on('click', () => {
window.onbeforeunload = null;

View File

@ -1,7 +1,7 @@
import $ from 'jquery';
import EditorLite from '~/editor/editor_lite';
import { FileTemplateExtension } from '~/editor/extensions/editor_file_template_ext';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
import { insertFinalNewline } from '~/lib/utils/text_utility';
@ -21,7 +21,11 @@ export default class EditBlob {
this.editor.use(new MarkdownExtension());
addEditorMarkdownListeners(this.editor);
})
.catch((e) => createFlash(`${BLOB_EDITOR_ERROR}: ${e}`));
.catch((e) =>
createFlash({
message: `${BLOB_EDITOR_ERROR}: ${e}`,
}),
);
}
this.initModePanesAndLinks();
@ -94,7 +98,11 @@ export default class EditBlob {
currentPane.empty().append(data);
currentPane.renderGFM();
})
.catch(() => createFlash(BLOB_PREVIEW_ERROR));
.catch(() =>
createFlash({
message: BLOB_PREVIEW_ERROR,
}),
);
}
this.$toggleButton.show();

View File

@ -1,5 +1,5 @@
import Vue from 'vue';
import { deprecatedCreateFlash as createFlash } from '../flash';
import createFlash from '../flash';
import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
import DivergenceGraph from './components/divergence_graph.vue';
@ -51,6 +51,8 @@ export default (endpoint, defaultBranch) => {
});
})
.catch(() =>
createFlash(__('Error fetching diverging counts for branches. Please try again.')),
createFlash({
message: __('Error fetching diverging counts for branches. Please try again.'),
}),
);
};

View File

@ -1,5 +1,5 @@
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
@ -48,7 +48,9 @@ export const addVariable = ({ state, dispatch }) => {
dispatch('fetchVariables');
})
.catch((error) => {
createFlash(error.response.data[0]);
createFlash({
message: error.response.data[0],
});
dispatch('receiveAddVariableError', error);
});
};
@ -78,7 +80,9 @@ export const updateVariable = ({ state, dispatch }) => {
dispatch('fetchVariables');
})
.catch((error) => {
createFlash(error.response.data[0]);
createFlash({
message: error.response.data[0],
});
dispatch('receiveUpdateVariableError', error);
});
};
@ -105,7 +109,9 @@ export const fetchVariables = ({ dispatch, state }) => {
dispatch('receiveVariablesSuccess', prepareDataForDisplay(data.variables));
})
.catch(() => {
createFlash(__('There was an error fetching the variables.'));
createFlash({
message: __('There was an error fetching the variables.'),
});
});
};
@ -133,7 +139,9 @@ export const deleteVariable = ({ dispatch, state }) => {
dispatch('fetchVariables');
})
.catch((error) => {
createFlash(error.response.data[0]);
createFlash({
message: error.response.data[0],
});
dispatch('receiveDeleteVariableError', error);
});
};
@ -154,7 +162,9 @@ export const fetchEnvironments = ({ dispatch, state }) => {
dispatch('receiveEnvironmentsSuccess', prepareEnvironments(res.data));
})
.catch(() => {
createFlash(__('There was an error fetching the environments information.'));
createFlash({
message: __('There was an error fetching the environments information.'),
});
});
};

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { DEFAULT_REGION } from '../constants';
@ -102,7 +102,9 @@ export const createClusterSuccess = (_, location) => {
export const createClusterError = ({ commit }, error) => {
commit(types.CREATE_CLUSTER_ERROR, error);
createFlash(getErrorMessage(error));
createFlash({
message: getErrorMessage(error),
});
};
export const setRegion = ({ commit }, payload) => {

View File

@ -1,5 +1,5 @@
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import * as types from './mutation_types';
@ -26,7 +26,9 @@ const receiveFreezePeriod = (store, request) => {
dispatch('fetchFreezePeriods');
})
.catch((error) => {
createFlash(__('Error: Unable to create deploy freeze'));
createFlash({
message: __('Error: Unable to create deploy freeze'),
});
dispatch('receiveFreezePeriodError', error);
});
};
@ -58,7 +60,9 @@ export const fetchFreezePeriods = ({ commit, state }) => {
commit(types.RECEIVE_FREEZE_PERIODS_SUCCESS, data);
})
.catch(() => {
createFlash(__('There was an error fetching the deploy freezes.'));
createFlash({
message: __('There was an error fetching the deploy freezes.'),
});
});
};

View File

@ -1,7 +1,7 @@
import Cookies from 'js-cookie';
import Vue from 'vue';
import api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { diffViewerModes } from '~/ide/constants';
import axios from '~/lib/utils/axios_utils';
import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils';
@ -240,7 +240,10 @@ export const fetchCoverageFiles = ({ commit, state }) => {
coveragePoll.stop();
}
},
errorCallback: () => createFlash(__('Something went wrong on our end. Please try again!')),
errorCallback: () =>
createFlash({
message: __('Something went wrong on our end. Please try again!'),
}),
});
coveragePoll.makeRequest();
@ -504,7 +507,11 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
.then((discussion) => dispatch('assignDiscussionsToDiff', [discussion]))
.then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true }))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
.catch(() =>
createFlash({
message: s__('MergeRequests|Saving the comment failed'),
}),
);
};
export const toggleTreeOpen = ({ commit }, path) => {
@ -595,7 +602,9 @@ export const cacheTreeListWidth = (_, size) => {
export const receiveFullDiffError = ({ commit }, filePath) => {
commit(types.RECEIVE_FULL_DIFF_ERROR, filePath);
createFlash(s__('MergeRequest|Error loading full diff. Please try again.'));
createFlash({
message: s__('MergeRequest|Error loading full diff. Please try again.'),
});
};
export const setExpandedDiffLines = ({ commit }, { file, data }) => {
@ -727,7 +736,9 @@ export const setSuggestPopoverDismissed = ({ commit, state }) =>
commit(types.SET_SHOW_SUGGEST_POPOVER);
})
.catch(() => {
createFlash(s__('MergeRequest|Error dismissing suggestion popover. Please try again.'));
createFlash({
message: s__('MergeRequest|Error dismissing suggestion popover. Please try again.'),
});
});
export function changeCurrentCommit({ dispatch, commit, state }, { commitId }) {

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import service from '../services';
@ -17,7 +17,11 @@ export const updateStatus = ({ commit }, { endpoint, redirectUrl, status }) =>
return resp.data.result;
})
.catch(() => createFlash(__('Failed to update issue status')));
.catch(() =>
createFlash({
message: __('Failed to update issue status'),
}),
);
export const updateResolveStatus = ({ commit, dispatch }, params) => {
commit(types.SET_UPDATING_RESOLVE_STATUS, true);

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import service from '../../services';
@ -26,7 +26,9 @@ export function startPollingStacktrace({ commit }, endpoint) {
},
errorCallback: () => {
commit(types.SET_LOADING_STACKTRACE, false);
createFlash(__('Failed to load stacktrace.'));
createFlash({
message: __('Failed to load stacktrace.'),
});
},
});

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import Service from '../../services';
@ -33,7 +33,9 @@ export function startPolling({ state, commit, dispatch }) {
},
errorCallback: () => {
commit(types.SET_LOADING, false);
createFlash(__('Failed to load errors from Sentry.'));
createFlash({
message: __('Failed to load errors from Sentry.'),
});
},
});

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@ -46,7 +46,10 @@ export const requestSettings = ({ commit }) => {
export const receiveSettingsError = ({ commit }, { response = {} }) => {
const message = response.data && response.data.message ? response.data.message : '';
createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
createFlash({
message: `${__('There was an error saving your changes.')} ${message}`,
type: 'alert',
});
commit(types.UPDATE_SETTINGS_LOADING, false);
};

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@ -55,7 +55,9 @@ export const receiveFeatureFlagSuccess = ({ commit }, response) =>
commit(types.RECEIVE_FEATURE_FLAG_SUCCESS, response);
export const receiveFeatureFlagError = ({ commit }) => {
commit(types.RECEIVE_FEATURE_FLAG_ERROR);
createFlash(__('Something went wrong on our end. Please try again!'));
createFlash({
message: __('Something went wrong on our end. Please try again!'),
});
};
export const toggleActive = ({ commit }, active) => commit(types.TOGGLE_ACTIVE, active);

View File

@ -1,6 +1,6 @@
import { __ } from '~/locale';
import AjaxFilter from '../droplab/plugins/ajax_filter';
import { deprecatedCreateFlash as createFlash } from '../flash';
import createFlash from '../flash';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
@ -27,7 +27,9 @@ export default class DropdownAjaxFilter extends FilteredSearchDropdown {
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
onError() {
createFlash(__('An error occurred fetching the dropdown data.'));
createFlash({
message: __('An error occurred fetching the dropdown data.'),
});
},
};
}

View File

@ -1,5 +1,5 @@
import $ from 'jquery';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { parseQueryStringIntoObject } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@ -16,7 +16,10 @@ export default class GpgBadges {
badges.html('<span class="gl-spinner gl-spinner-orange gl-spinner-sm"></span>');
badges.children().attr('aria-label', __('Loading'));
const displayError = () => createFlash(__('An error occurred while loading commit signatures'));
const displayError = () =>
createFlash({
message: __('An error occurred while loading commit signatures'),
});
const endpoint = tag.data('signaturesPath');
if (!endpoint) {

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@ -38,5 +38,8 @@ export const receiveGrafanaIntegrationUpdateError = (_, error) => {
const { response } = error;
const message = response.data && response.data.message ? response.data.message : '';
createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
createFlash({
message: `${__('There was an error saving your changes.')} ${message}`,
type: 'alert',
});
};

View File

@ -1,7 +1,7 @@
<script>
import { GlModal, GlButton } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import { deprecatedCreateFlash as flash } from '~/flash';
import createFlash from '~/flash';
import { __, sprintf, s__ } from '~/locale';
import { modalTypes } from '../../constants';
import { trimPathComponents, getPathParent } from '../../utils';
@ -57,16 +57,16 @@ export default {
if (this.modalType === modalTypes.rename) {
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
flash(
sprintf(s__('The name "%{name}" is already taken in this directory.'), {
createFlash({
message: sprintf(s__('The name "%{name}" is already taken in this directory.'), {
name: this.entryName,
}),
'alert',
document,
null,
false,
true,
);
type: 'alert',
parent: document,
actionConfig: null,
fadeTransition: false,
addBodyClass: true,
});
} else {
let parentPath = this.entryName.split('/');
const name = parentPath.pop();

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as flash } from '~/flash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '../../constants';
import service from '../../services';
@ -34,14 +34,14 @@ export const getMergeRequestsForBranch = (
}
})
.catch((e) => {
flash(
__(`Error fetching merge requests for ${branchId}`),
'alert',
document,
null,
false,
true,
);
createFlash({
message: __(`Error fetching merge requests for ${branchId}`),
type: 'alert',
parent: document,
actionConfig: null,
fadeTransition: false,
addBodyClass: true,
});
throw e;
});
};
@ -236,7 +236,7 @@ export const openMergeRequest = async (
await dispatch('openMergeRequestChanges', changes);
} catch (e) {
flash(__('Error while loading the merge request. Please try again.'));
createFlash({ message: __('Error while loading the merge request. Please try again.') });
throw e;
}
};

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as flash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import * as terminalService from '../../../../services/terminals';
@ -26,7 +26,7 @@ export const receiveStartSessionSuccess = ({ commit, dispatch }, data) => {
};
export const receiveStartSessionError = ({ dispatch }) => {
flash(messages.UNEXPECTED_ERROR_STARTING);
createFlash({ message: messages.UNEXPECTED_ERROR_STARTING });
dispatch('killSession');
};
@ -59,7 +59,7 @@ export const receiveStopSessionSuccess = ({ dispatch }) => {
};
export const receiveStopSessionError = ({ dispatch }) => {
flash(messages.UNEXPECTED_ERROR_STOPPING);
createFlash({ message: messages.UNEXPECTED_ERROR_STOPPING });
dispatch('killSession');
};

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as flash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as messages from '../messages';
import * as types from '../mutation_types';
@ -42,7 +42,7 @@ export const receiveSessionStatusSuccess = ({ commit, dispatch }, data) => {
};
export const receiveSessionStatusError = ({ dispatch }) => {
flash(messages.UNEXPECTED_ERROR_STATUS);
createFlash({ message: messages.UNEXPECTED_ERROR_STATUS });
dispatch('killSession');
};

View File

@ -15,7 +15,7 @@ import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
import addValidationErrorMutation from '../graphql/mutations/add_validation_error.mutation.graphql';
import removeValidationErrorMutation from '../graphql/mutations/remove_validation_error.mutation.graphql';
import groupQuery from '../graphql/queries/group.query.graphql';
import groupAndProjectQuery from '../graphql/queries/groupAndProject.query.graphql';
const DEBOUNCE_INTERVAL = 300;
@ -47,21 +47,21 @@ export default {
},
apollo: {
existingGroup: {
query: groupQuery,
existingGroupAndProject: {
query: groupAndProjectQuery,
debounce: DEBOUNCE_INTERVAL,
variables() {
return {
fullPath: this.fullPath,
};
},
update({ existingGroup }) {
update({ existingGroup, existingProject }) {
const variables = {
field: 'new_name',
sourceGroupId: this.group.id,
};
if (!existingGroup) {
if (!existingGroup && !existingProject) {
this.$apollo.mutate({
mutation: removeValidationErrorMutation,
variables,
@ -71,7 +71,7 @@ export default {
mutation: addValidationErrorMutation,
variables: {
...variables,
message: s__('BulkImport|Name already exists.'),
message: this.$options.i18n.NAME_ALREADY_EXISTS,
},
});
}
@ -115,6 +115,10 @@ export default {
return joinPaths(gon.relative_url_root || '/', this.fullPath);
},
},
i18n: {
NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
},
};
</script>

View File

@ -1,5 +0,0 @@
query group($fullPath: ID!) {
existingGroup: group(fullPath: $fullPath) {
id
}
}

View File

@ -0,0 +1,9 @@
query groupAndProject($fullPath: ID!) {
existingGroup: group(fullPath: $fullPath) {
id
}
existingProject: project(fullPath: $fullPath) {
id
}
}

View File

@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
@ -75,19 +75,19 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else if (tooManyRequests(e)) {
createFlash(
sprintf(s__('ImportProjects|%{provider} rate limit exceeded. Try again later'), {
createFlash({
message: sprintf(s__('ImportProjects|%{provider} rate limit exceeded. Try again later'), {
provider: capitalizeFirstCharacter(provider),
}),
);
});
commit(types.RECEIVE_REPOS_ERROR);
} else {
createFlash(
sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), {
createFlash({
message: sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), {
provider,
}),
);
});
commit(types.RECEIVE_REPOS_ERROR);
}
@ -126,7 +126,9 @@ const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, gett
)
: s__('ImportProjects|Importing the project failed');
createFlash(flashMessage);
createFlash({
message: flashMessage,
});
commit(types.RECEIVE_IMPORT_ERROR, repoId);
});
@ -149,7 +151,9 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else {
createFlash(s__('ImportProjects|Update of imported projects with realtime changes failed'));
createFlash({
message: s__('ImportProjects|Update of imported projects with realtime changes failed'),
});
}
},
});
@ -175,7 +179,9 @@ const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) =
commit(types.RECEIVE_NAMESPACES_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
)
.catch(() => {
createFlash(s__('ImportProjects|Requesting namespaces failed'));
createFlash({
message: s__('ImportProjects|Requesting namespaces failed'),
});
commit(types.RECEIVE_NAMESPACES_ERROR);
});

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { ERROR_MSG } from './constants';
@ -22,7 +22,10 @@ export default class IncidentsSettingsService {
.catch(({ response }) => {
const message = response?.data?.message || '';
createFlash(`${ERROR_MSG} ${message}`, 'alert');
createFlash({
message: `${ERROR_MSG} ${message}`,
type: 'alert',
});
});
}

View File

@ -3,7 +3,7 @@ import {
getBoardSortableDefaultOptions,
sortableStart,
} from '~/boards/mixins/sortable_default_options';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
@ -15,7 +15,9 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
group_full_path: issueList.dataset.groupFullPath,
})
.catch(() => {
createFlash(s__("ManualOrdering|Couldn't save the order of the issues"));
createFlash({
message: s__("ManualOrdering|Couldn't save the order of the issues"),
});
});
const initManualOrdering = (draggableSelector = 'li.issue') => {

View File

@ -1,7 +1,7 @@
/* eslint-disable func-names, no-underscore-dangle, consistent-return */
import $ from 'jquery';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/vue_merge_request_widget/event_hub';
import axios from './lib/utils/axios_utils';
@ -36,11 +36,11 @@ function MergeRequest(opts) {
document.querySelector('#task_status_short').innerText = result.task_status_short;
},
onError: () => {
createFlash(
__(
createFlash({
message: __(
'Someone edited this merge request at the same time you did. Please refresh the page to see changes.',
),
);
});
},
});
}
@ -93,7 +93,9 @@ MergeRequest.prototype.initMRBtnListeners = function () {
})
.catch(() => {
draftToggle.removeAttribute('disabled');
createFlash(__('Something went wrong. Please try again.'));
createFlash({
message: __('Something went wrong. Please try again.'),
});
});
});
});
@ -169,7 +171,10 @@ MergeRequest.hideCloseButton = function () {
MergeRequest.toggleDraftStatus = function (title, isReady) {
if (isReady) {
createFlash(__('The merge request can now be merged.'), 'notice');
createFlash({
message: __('The merge request can now be merged.'),
type: 'notice',
});
}
const titleEl = document.querySelector('.merge-request .detail-page-description .title');

View File

@ -1,5 +1,5 @@
import * as Sentry from '@sentry/browser';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
@ -134,15 +134,17 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
if (state.showErrorBanner) {
if (error.response.data && error.response.data.message) {
const { message } = error.response.data;
createFlash(
sprintf(
createFlash({
message: sprintf(
s__('Metrics|There was an error while retrieving metrics. %{message}'),
{ message },
false,
),
);
});
} else {
createFlash(s__('Metrics|There was an error while retrieving metrics'));
createFlash({
message: s__('Metrics|There was an error while retrieving metrics'),
});
}
}
});
@ -174,7 +176,10 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
dispatch('fetchDeploymentsData');
if (!state.timeRange) {
createFlash(s__(`Metrics|Invalid time range, please verify.`), 'warning');
createFlash({
message: s__(`Metrics|Invalid time range, please verify.`),
type: 'warning',
});
return Promise.reject();
}
@ -202,7 +207,10 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
});
})
.catch(() => {
createFlash(s__(`Metrics|There was an error while retrieving metrics`), 'warning');
createFlash({
message: s__(`Metrics|There was an error while retrieving metrics`),
type: 'warning',
});
});
};
@ -254,7 +262,9 @@ export const fetchDeploymentsData = ({ state, dispatch }) => {
.then((resp) => resp.data)
.then((response) => {
if (!response || !response.deployments) {
createFlash(s__('Metrics|Unexpected deployment data response from prometheus endpoint'));
createFlash({
message: s__('Metrics|Unexpected deployment data response from prometheus endpoint'),
});
}
dispatch('receiveDeploymentsDataSuccess', response.deployments);
@ -262,7 +272,9 @@ export const fetchDeploymentsData = ({ state, dispatch }) => {
.catch((error) => {
Sentry.captureException(error);
dispatch('receiveDeploymentsDataFailure');
createFlash(s__('Metrics|There was an error getting deployment information.'));
createFlash({
message: s__('Metrics|There was an error getting deployment information.'),
});
});
};
export const receiveDeploymentsDataSuccess = ({ commit }, data) => {
@ -290,9 +302,11 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
)
.then((environments) => {
if (!environments) {
createFlash(
s__('Metrics|There was an error fetching the environments data, please try again'),
);
createFlash({
message: s__(
'Metrics|There was an error fetching the environments data, please try again',
),
});
}
dispatch('receiveEnvironmentsDataSuccess', environments);
@ -300,7 +314,9 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveEnvironmentsDataFailure');
createFlash(s__('Metrics|There was an error getting environments information.'));
createFlash({
message: s__('Metrics|There was an error getting environments information.'),
});
});
};
export const requestEnvironmentsData = ({ commit }) => {
@ -332,7 +348,9 @@ export const fetchAnnotations = ({ state, dispatch, getters }) => {
.then(parseAnnotationsResponse)
.then((annotations) => {
if (!annotations) {
createFlash(s__('Metrics|There was an error fetching annotations. Please try again.'));
createFlash({
message: s__('Metrics|There was an error fetching annotations. Please try again.'),
});
}
dispatch('receiveAnnotationsSuccess', annotations);
@ -340,7 +358,9 @@ export const fetchAnnotations = ({ state, dispatch, getters }) => {
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveAnnotationsFailure');
createFlash(s__('Metrics|There was an error getting annotations information.'));
createFlash({
message: s__('Metrics|There was an error getting annotations information.'),
});
});
};
@ -377,9 +397,11 @@ export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) =
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveDashboardValidationWarningsFailure');
createFlash(
s__('Metrics|There was an error getting dashboard validation warnings information.'),
);
createFlash({
message: s__(
'Metrics|There was an error getting dashboard validation warnings information.',
),
});
});
};
@ -480,11 +502,14 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data });
})
.catch(() => {
createFlash(
sprintf(s__('Metrics|There was an error getting options for variable "%{name}".'), {
name: variable.name,
}),
);
createFlash({
message: sprintf(
s__('Metrics|There was an error getting options for variable "%{name}".'),
{
name: variable.name,
},
),
});
});
optionsRequests.push(optionsRequest);
}

View File

@ -1,7 +1,7 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { getDraftReplyFormData, getDraftFormData } from '~/batch_comments/utils';
import { TEXT_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { clearDraft } from '~/lib/utils/autosave';
import { s__ } from '~/locale';
import { formatLineRange } from '~/notes/components/multiline_comment_utils';
@ -42,7 +42,9 @@ export default {
this.handleClearForm(this.discussion.line_code);
})
.catch(() => {
createFlash(s__('MergeRequests|An error occurred while saving the draft comment.'));
createFlash({
message: s__('MergeRequests|An error occurred while saving the draft comment.'),
});
});
},
addToReview(note) {
@ -80,7 +82,9 @@ export default {
}
})
.catch(() => {
createFlash(s__('MergeRequests|An error occurred while saving the draft comment.'));
createFlash({
message: s__('MergeRequests|An error occurred while saving the draft comment.'),
});
});
},
handleClearForm(lineCode) {

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@ -35,5 +35,8 @@ export const receiveSaveChangesError = (_, error) => {
const { response = {} } = error;
const message = response.data && response.data.message ? response.data.message : '';
createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
createFlash({
message: `${__('There was an error saving your changes.')} ${message}`,
type: 'alert',
});
};

View File

@ -1,5 +1,5 @@
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
import {
@ -43,7 +43,9 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
dispatch('receivePackagesListSuccess', { data, headers });
})
.catch(() => {
createFlash(FETCH_PACKAGES_LIST_ERROR_MESSAGE);
createFlash({
message: FETCH_PACKAGES_LIST_ERROR_MESSAGE,
});
})
.finally(() => {
dispatch('setLoading', false);
@ -52,7 +54,9 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
export const requestDeletePackage = ({ dispatch, state }, { _links }) => {
if (!_links || !_links.delete_api_path) {
createFlash(DELETE_PACKAGE_ERROR_MESSAGE);
createFlash({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
const error = new Error(MISSING_DELETE_PATH_ERROR);
return Promise.reject(error);
}
@ -65,10 +69,15 @@ export const requestDeletePackage = ({ dispatch, state }, { _links }) => {
const page = getNewPaginationPage(currentPage, perPage, total - 1);
dispatch('requestPackagesList', { page });
createFlash(DELETE_PACKAGE_SUCCESS_MESSAGE, 'success');
createFlash({
message: DELETE_PACKAGE_SUCCESS_MESSAGE,
type: 'success',
});
})
.catch(() => {
dispatch('setLoading', false);
createFlash(DELETE_PACKAGE_ERROR_MESSAGE);
createFlash({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
});
};

View File

@ -2,7 +2,7 @@ import emojiRegex from 'emoji-regex';
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import EmojiMenu from './emoji_menu';
@ -81,4 +81,8 @@ Emoji.initEmojiMap()
}
});
})
.catch(() => createFlash(__('Failed to load emoji list.')));
.catch(() =>
createFlash({
message: __('Failed to load emoji list.'),
}),
);

View File

@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
@ -169,7 +169,11 @@ export default {
this.service
.postAction(endpoint)
.then(() => this.updateTable())
.catch(() => createFlash(__('An error occurred while making the request.')));
.catch(() =>
createFlash({
message: __('An error occurred while making the request.'),
}),
);
},
/**
@ -189,9 +193,11 @@ export default {
.runMRPipeline(options)
.then(() => this.updateTable())
.catch(() => {
createFlash(
__('An error occurred while trying to run a new pipeline for this merge request.'),
);
createFlash({
message: __(
'An error occurred while trying to run a new pipeline for this merge request.',
),
});
})
.finally(() => this.store.toggleIsRunningPipeline(false));
},

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@ -12,7 +12,9 @@ export const fetchSummary = ({ state, commit, dispatch }) => {
commit(types.SET_SUMMARY, data);
})
.catch(() => {
createFlash(s__('TestReports|There was an error fetching the summary.'));
createFlash({
message: s__('TestReports|There was an error fetching the summary.'),
});
})
.finally(() => {
dispatch('toggleLoading');
@ -36,7 +38,9 @@ export const fetchTestSuite = ({ state, commit, dispatch }, index) => {
.get(state.suiteEndpoint, { params: { build_ids } })
.then(({ data }) => commit(types.SET_SUITE, { suite: data, index }))
.catch(() => {
createFlash(s__('TestReports|There was an error fetching the test suite.'));
createFlash({
message: s__('TestReports|There was an error fetching the test suite.'),
});
})
.finally(() => {
dispatch('toggleLoading');

View File

@ -1,7 +1,7 @@
<script>
import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { escape } from 'lodash';
import { deprecatedCreateFlash as Flash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__, sprintf } from '~/locale';
@ -85,15 +85,16 @@ Please update your Git repository remotes as soon as possible.`),
return axios
.put(this.actionUrl, putData)
.then((result) => {
Flash(result.data.message, 'notice');
createFlash({ message: result.data.message, type: 'notice' });
this.username = username;
this.isRequestPending = false;
})
.catch((error) => {
Flash(
error?.response?.data?.message ||
createFlash({
message:
error?.response?.data?.message ||
s__('Profiles|An error occurred while updating your username, please try again.'),
);
});
this.isRequestPending = false;
throw error;
});

View File

@ -1,5 +1,5 @@
import * as Sentry from '@sentry/browser';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@ -13,7 +13,9 @@ export default {
commit(types.COMMITS_AUTHORS, authors);
},
receiveAuthorsError() {
createFlash(__('An error occurred fetching the project authors.'));
createFlash({
message: __('An error occurred fetching the project authors.'),
});
},
fetchAuthors({ dispatch, state }, author = null) {
const { projectId } = state;

View File

@ -23,7 +23,7 @@ Your caret can stop touching a `rawReference` can happen in a variety of ways:
and hide the `AddIssuableForm` area.
*/
import { deprecatedCreateFlash as Flash } from '~/flash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import {
relatedIssuesRemoveErrorMap,
@ -122,11 +122,11 @@ export default {
})
.catch((res) => {
if (res && res.status !== 404) {
Flash(relatedIssuesRemoveErrorMap[this.issuableType]);
createFlash({ message: relatedIssuesRemoveErrorMap[this.issuableType] });
}
});
} else {
Flash(pathIndeterminateErrorMap[this.issuableType]);
createFlash({ message: pathIndeterminateErrorMap[this.issuableType] });
}
},
onToggleAddRelatedIssuesForm() {
@ -155,7 +155,7 @@ export default {
if (response && response.data && response.data.message) {
errorMessage = response.data.message;
}
Flash(errorMessage);
createFlash({ message: errorMessage });
})
.finally(() => {
this.isSubmitting = false;
@ -176,7 +176,7 @@ export default {
})
.catch(() => {
this.store.setRelatedIssues([]);
Flash(__('An error occurred while fetching issues.'));
createFlash({ message: __('An error occurred while fetching issues.') });
})
.finally(() => {
this.isFetching = false;
@ -197,7 +197,7 @@ export default {
}
})
.catch(() => {
Flash(__('An error occurred while reordering issues.'));
createFlash({ message: __('An error occurred while reordering issues.') });
});
}
},

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
@ -29,6 +29,8 @@ export const fetchMergeRequests = ({ state, dispatch }) => {
})
.catch(() => {
dispatch('receiveDataError');
createFlash(s__('Something went wrong while fetching related merge requests.'));
createFlash({
message: s__('Something went wrong while fetching related merge requests.'),
});
});
};

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql';
@ -39,7 +39,9 @@ export const fetchRelease = async ({ commit, state }) => {
commit(types.RECEIVE_RELEASE_SUCCESS, release);
} catch (error) {
commit(types.RECEIVE_RELEASE_ERROR, error);
createFlash(s__('Release|Something went wrong while getting the release details.'));
createFlash({
message: s__('Release|Something went wrong while getting the release details.'),
});
}
};
@ -124,7 +126,9 @@ export const createRelease = async ({ commit, dispatch, state, getters }) => {
dispatch('receiveSaveReleaseSuccess', response.data.releaseCreate.release.links.selfUrl);
} catch (error) {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
createFlash(s__('Release|Something went wrong while creating a new release.'));
createFlash({
message: s__('Release|Something went wrong while creating a new release.'),
});
}
};
@ -214,6 +218,8 @@ export const updateRelease = async ({ commit, dispatch, state, getters }) => {
dispatch('receiveSaveReleaseSuccess', state.release._links.self);
} catch (error) {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
createFlash(s__('Release|Something went wrong while saving the release details.'));
createFlash({
message: s__('Release|Something went wrong while saving the release details.'),
});
}
};

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { PAGE_SIZE } from '~/releases/constants';
import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
@ -57,7 +57,9 @@ export const fetchReleases = ({ dispatch, commit, state }, { before, after }) =>
export const receiveReleasesError = ({ commit }) => {
commit(types.RECEIVE_RELEASES_ERROR);
createFlash(__('An error occurred while fetching the releases. Please try again.'));
createFlash({
message: __('An error occurred while fetching the releases. Please try again.'),
});
};
export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data);

View File

@ -1,4 +1,4 @@
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
import statusCodes from '~/lib/utils/http_status';
@ -59,7 +59,9 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
.then((data) => {
if (data === TIMEOUT) {
dispatch('receiveFunctionsTimeout');
createFlash(__('Loading functions timed out. Please reload the page to try again.'));
createFlash({
message: __('Loading functions timed out. Please reload the page to try again.'),
});
} else if (data.functions !== null && data.functions.length) {
dispatch('receiveFunctionsSuccess', data);
} else {
@ -68,7 +70,9 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
})
.catch((error) => {
dispatch('receiveFunctionsError', error);
createFlash(error);
createFlash({
message: error,
});
});
};
@ -120,6 +124,8 @@ export const fetchMetrics = ({ dispatch }, { metricsPath, hasPrometheus }) => {
})
.catch((error) => {
dispatch('receiveMetricsError', error);
createFlash(error);
createFlash({
message: error,
});
});
};

View File

@ -3,7 +3,7 @@
import $ from 'jquery';
import { spriteIcon } from '~/lib/utils/common_utils';
import FilesCommentButton from './files_comment_button';
import { deprecatedCreateFlash as createFlash } from './flash';
import createFlash from './flash';
import initImageDiffHelper from './image_diff/helpers/init_image_diff';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
@ -95,7 +95,9 @@ export default class SingleFileDiff {
if (cb) cb();
})
.catch(() => {
createFlash(__('An error occurred while retrieving diff'));
createFlash({
message: __('An error occurred while retrieving diff'),
});
});
}
}

View File

@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
import { sprintf } from '~/locale';
@ -63,7 +63,7 @@ export default {
.catch((e) => this.flashAPIFailure(e));
},
flashAPIFailure(err) {
Flash(sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }));
createFlash({ message: sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }) });
},
},
};

View File

@ -1,5 +1,5 @@
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
@ -24,7 +24,9 @@ export function fetchBranches({ commit, state }, search = '') {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_BRANCHES_ERROR, status);
createFlash(__('Failed to load branches. Please try again.'));
createFlash({
message: __('Failed to load branches. Please try again.'),
});
});
}
@ -41,7 +43,9 @@ export const fetchMilestones = ({ commit, state }, search_title = '') => {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_MILESTONES_ERROR, status);
createFlash(__('Failed to load milestones. Please try again.'));
createFlash({
message: __('Failed to load milestones. Please try again.'),
});
});
};
@ -57,7 +61,9 @@ export const fetchLabels = ({ commit, state }, search = '') => {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_LABELS_ERROR, status);
createFlash(__('Failed to load labels. Please try again.'));
createFlash({
message: __('Failed to load labels. Please try again.'),
});
});
};
@ -80,7 +86,9 @@ function fetchUser(options = {}) {
.catch(({ response }) => {
const { status } = response;
commit(`RECEIVE_${action}_ERROR`, status);
createFlash(errorMessage);
createFlash({
message: errorMessage,
});
});
}

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331695
milestone: '14.0'
type: development
group: group::continuous integration
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: redirect_to_latest_template_jobs_browser_performance_testing
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144
rollout_issue_url:
milestone: '14.0'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: redirect_to_latest_template_jobs_deploy
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332660
milestone: '14.0'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: redirect_to_latest_template_security_api_fuzzing
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144
rollout_issue_url:
milestone: '14.0'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: redirect_to_latest_template_security_dast
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144
rollout_issue_url:
milestone: '14.0'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: redirect_to_latest_template_terraform
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144
rollout_issue_url:
milestone: '14.0'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: redirect_to_latest_template_verify_browser_performance
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144
rollout_issue_url:
milestone: '14.0'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -42,7 +42,7 @@ There should be no more than one Sentinel on the same machine though.
You also need to take into consideration the underlying network topology,
making sure you have redundant connectivity between Redis / Sentinel and
GitLab instances, otherwise the networks will become a single point of
GitLab instances, otherwise the networks become a single point of
failure.
Running Redis in a scaled environment requires a few things:
@ -73,7 +73,7 @@ whole cluster down, invalidating the failover effort.
## Recommended setup
For a minimal setup, you will install the Omnibus GitLab package in `3`
For a minimal setup, you need to install the Omnibus GitLab package in `3`
**independent** machines, both with **Redis** and **Sentinel**:
- Redis Primary + Sentinel
@ -84,7 +84,7 @@ If you are not sure or don't understand why and where the amount of nodes come
from, read [Redis setup overview](#redis-setup-overview) and
[Sentinel setup overview](#sentinel-setup-overview).
For a recommended setup that can resist more failures, you will install
For a recommended setup that can resist more failures, you need to install
the Omnibus GitLab package in `5` **independent** machines, both with
**Redis** and **Sentinel**:
@ -99,9 +99,9 @@ the Omnibus GitLab package in `5` **independent** machines, both with
You must have at least `3` Redis servers: `1` primary, `2` Replicas, and they
need to each be on independent machines (see explanation above).
You can have additional Redis nodes, that will help survive a situation
You can have additional Redis nodes, that helps to survive a situation
where more nodes goes down. Whenever there is only `2` nodes online, a failover
will not be initiated.
is not initiated.
As an example, if you have `6` Redis nodes, a maximum of `3` can be
simultaneously down.
@ -117,7 +117,7 @@ in a failover situation, any **Replica** can be promoted as the new **Primary**
the Sentinel servers.
The replication requires authentication, so you need to define a password to
protect all Redis nodes and the Sentinels. They will all share the same
protect all Redis nodes and the Sentinels. All of them share the same
password, and all instances must be able to talk to
each other over the network.
@ -130,7 +130,7 @@ of Sentinels agreeing a node is down) to be able to start a failover.
Whenever the **quorum** is met, the **majority** of all known Sentinel nodes
need to be available and reachable, so that they can elect the Sentinel **leader**
who will take all the decisions to restore the service availability by:
who takes all the decisions to restore the service availability by:
- Promoting a new **Primary**
- Reconfiguring the other **Replicas** and make them point to the new **Primary**
@ -150,7 +150,7 @@ consensus algorithm to be effective in the case of a failure.
In a `3` nodes topology, you can only afford `1` Sentinel node going down.
Whenever the **majority** of the Sentinels goes down, the network partition
protection prevents destructive actions and a failover **will not be started**.
protection prevents destructive actions and a failover **is not started**.
Here are some examples:
@ -159,11 +159,11 @@ Here are some examples:
The **Leader** election can sometimes fail the voting round when **consensus**
is not achieved (see the odd number of nodes requirement above). In that case,
a new attempt will be made after the amount of time defined in
a new attempt is made after the amount of time defined in
`sentinel['failover_timeout']` (in milliseconds).
NOTE:
We will see where `sentinel['failover_timeout']` is defined later.
We can see where `sentinel['failover_timeout']` is defined later.
The `failover_timeout` variable has a lot of different use cases. According to
the official documentation:
@ -183,7 +183,7 @@ the official documentation:
- The maximum time a failover in progress waits for all the replicas to be
reconfigured as replicas of the new primary. However even after this time
the replicas will be reconfigured by the Sentinels anyway, but not with
the replicas are reconfigured by the Sentinels anyway, but not with
the exact parallel-syncs progression as specified.
## Configuring Redis
@ -195,7 +195,7 @@ If you already have Redis installed and running, read how to
[switch from a single-machine installation](#switching-from-an-existing-single-machine-installation).
NOTE:
Redis nodes (both primary and replica) will need the same password defined in
Redis nodes (both primary and replica) need the same password defined in
`redis['password']`. At any time during a failover the Sentinels can
reconfigure a node and change its status from primary to replica and vice versa.
@ -218,14 +218,14 @@ The requirements for a Redis setup are the following:
### Switching from an existing single-machine installation
If you already have a single-machine GitLab install running, you will need to
If you already have a single-machine GitLab install running, you need to
replicate from this machine first, before de-activating the Redis instance
inside it.
Your single-machine install will be the initial **Primary**, and the `3` others
Your single-machine install is the initial **Primary**, and the `3` others
should be configured as **Replica** pointing to this machine.
After replication catches up, you will need to stop services in the
After replication catches up, you need to stop services in the
single-machine install, to rotate the **Primary** to one of the new nodes.
Make the required changes in configuration and restart the new nodes again.
@ -259,7 +259,7 @@ If you fail to replicate first, you may loose data (unprocessed background jobs)
# sure you add extra firewall rules to prevent unauthorized access.
redis['bind'] = '10.0.0.1'
# Define a port so Redis can listen for TCP requests which will allow other
# Define a port so Redis can listen for TCP requests which allows other
# machines to connect to it.
redis['port'] = 6379
@ -303,7 +303,7 @@ Read more about [roles](https://docs.gitlab.com/omnibus/roles/).
# sure you add extra firewall rules to prevent unauthorized access.
redis['bind'] = '10.0.0.2'
# Define a port so Redis can listen for TCP requests which will allow other
# Define a port so Redis can listen for TCP requests which allows other
# machines to connect to it.
redis['port'] = 6379
@ -333,8 +333,8 @@ You can specify multiple roles like sentinel and Redis as:
Read more about [roles](https://docs.gitlab.com/omnibus/roles/).
These values don't have to be changed again in `/etc/gitlab/gitlab.rb` after
a failover, as the nodes will be managed by the Sentinels, and even after a
`gitlab-ctl reconfigure`, they will get their configuration restored by
a failover, as the nodes are managed by the Sentinels, and even after a
`gitlab-ctl reconfigure`, they get their configuration restored by
the same Sentinels.
### Step 3. Configuring the Redis Sentinel instances
@ -342,7 +342,7 @@ the same Sentinels.
NOTE:
If you are using an external Redis Sentinel instance, be sure
to exclude the `requirepass` parameter from the Sentinel
configuration. This parameter will cause clients to report `NOAUTH
configuration. This parameter causes clients to report `NOAUTH
Authentication required.`. [Redis Sentinel 3.2.x does not support
password authentication](https://github.com/antirez/redis/issues/3279).
@ -362,8 +362,8 @@ multiple machines with the Sentinel daemon.
---
1. SSH into the server that will host Redis Sentinel.
1. **You can omit this step if the Sentinels will be hosted in the same node as
1. SSH into the server that hosts Redis Sentinel.
1. **You can omit this step if the Sentinels is hosted in the same node as
the other Redis instances.**
[Download/install](https://about.gitlab.com/install/) the
@ -389,7 +389,7 @@ multiple machines with the Sentinel daemon.
# The IP of the primary Redis node.
redis['master_ip'] = '10.0.0.1'
# Define a port so Redis can listen for TCP requests which will allow other
# Define a port so Redis can listen for TCP requests which allows other
# machines to connect to it.
redis['port'] = 6379
@ -437,7 +437,7 @@ multiple machines with the Sentinel daemon.
##
## - The maximum time a failover in progress waits for all the replica to be
## reconfigured as replicas of the new primary. However even after this time
## the replicas will be reconfigured by the Sentinels anyway, but not with
## the replicas are reconfigured by the Sentinels anyway, but not with
## the exact parallel-syncs progression as specified.
# sentinel['failover_timeout'] = 60000
```
@ -511,7 +511,7 @@ If you enable Monitoring, it must be enabled on **all** Redis servers.
retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z),
}
# Set the network addresses that the exporters will listen on
# Set the network addresses that the exporters listen on
node_exporter['listen_address'] = '0.0.0.0:9100'
redis_exporter['listen_address'] = '0.0.0.0:9121'
```
@ -528,7 +528,7 @@ In a real world usage, you would also set up firewall rules to prevent
unauthorized access from other machines and block traffic from the
outside (Internet).
We will use the same `3` nodes with **Redis** + **Sentinel** topology
We use the same `3` nodes with **Redis** + **Sentinel** topology
discussed in [Redis setup overview](#redis-setup-overview) and
[Sentinel setup overview](#sentinel-setup-overview) documentation.
@ -540,11 +540,11 @@ Here is a list and description of each **machine** and the assigned **IP**:
- `10.0.0.4`: GitLab application
Please note that after the initial configuration, if a failover is initiated
by the Sentinel nodes, the Redis nodes will be reconfigured and the **Primary**
will change permanently (including in `redis.conf`) from one node to the other,
by the Sentinel nodes, the Redis nodes are reconfigured and the **Primary**
changes permanently (including in `redis.conf`) from one node to the other,
until a new failover is initiated again.
The same thing will happen with `sentinel.conf` that will be overridden after the
The same thing happens with `sentinel.conf` that is overridden after the
initial execution, after any new sentinel node starts watching the **Primary**,
or a failover promotes a different **Primary** node.
@ -691,7 +691,7 @@ To make this work with Sentinel:
```
NOTE:
For each persistence class, GitLab will default to using the
For each persistence class, GitLab defaults to using the
configuration specified in `gitlab_rails['redis_sentinels']` unless
overridden by the previously described settings.
@ -726,7 +726,7 @@ redis_replica_role['enable'] = true # enable only one of them
# When Redis primary or Replica role are enabled, the following services are
# enabled/disabled. Note that if Redis and Sentinel roles are combined, both
# services will be enabled.
# services are enabled.
# The following services are disabled
sentinel['enable'] = false

View File

@ -0,0 +1,154 @@
---
stage: Release
group: Release
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: concepts, howto
---
# Group-level protected environments API **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215888) in [GitLab Premium](https://about.gitlab.com/pricing/) 14.0.
> - [Deployed behind a feature flag](../user/feature_flags.md), disabled by default.
> - Disabled on GitLab.com.
> - Not recommended for production use.
> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](../ci/environments/protected_environments.md#enable-or-disable-group-level-protected-environments). **(FREE SELF)**
This in-development feature might not be available for your use. There can be
[risks when enabling features still in development](../user/feature_flags.md#risks-when-enabling-features-still-in-development).
Refer to this feature's version history for more details.
Read more about [group-level protected environments](../ci/environments/protected_environments.md#group-level-protected-environments),
## Valid access levels
The access levels are defined in the `ProtectedEnvironment::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method.
Currently, these levels are recognized:
```plaintext
30 => Developer access
40 => Maintainer access
60 => Admin access
```
## List group-level protected environments
Gets a list of protected environments from a group.
```shell
GET /groups/:id/protected_environments
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) maintained by the authenticated user. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/protected_environments/"
```
Example response:
```json
[
{
"name":"production",
"deploy_access_levels":[
{
"access_level":40,
"access_level_description":"Maintainers",
"user_id":null,
"group_id":null
}
]
}
]
```
## Get a single protected environment
Gets a single protected environment.
```shell
GET /groups/:id/protected_environments/:name
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) maintained by the authenticated user. |
| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).|
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/protected_environments/production"
```
Example response:
```json
{
"name":"production",
"deploy_access_levels":[
{
"access_level":40,
"access_level_description":"Maintainers",
"user_id":null,
"group_id":null
}
]
}
```
## Protect an environment
Protects a single environment.
```shell
POST /groups/:id/protected_environments
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) maintained by the authenticated user. |
| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).|
| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. |
The assignable `user_id` are the users who belong to the given group with the Maintainer role (or above).
The assignable `group_id` are the sub-groups under the given group.
```shell
curl --header 'Content-Type: application/json' --request POST --data '{"name": "production", "deploy_access_levels": [{"group_id": 9899826}]}' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_environments"
```
Example response:
```json
{
"name":"production",
"deploy_access_levels":[
{
"access_level":40,
"access_level_description":"protected-access-group",
"user_id":null,
"group_id":9899826
}
]
}
```
## Unprotect environment
Unprotects the given protected environment.
```shell
DELETE /groups/:id/protected_environments/:name
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) maintained by the authenticated user. |
| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).|
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/protected_environments/staging"
```
The response should return a 200 code.

View File

@ -154,6 +154,129 @@ be re-entered if the environment is re-protected.
For more information, see [Deployment safety](deployment_safety.md).
## Group-level protected environments
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215888) in [GitLab Premium](https://about.gitlab.com/pricing/) 14.0.
> - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - Disabled on GitLab.com.
> - Not recommended for production use.
> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-group-level-protected-environments). **(FREE SELF)**
This in-development feature might not be available for your use. There can be
[risks when enabling features still in development](../../user/feature_flags.md#risks-when-enabling-features-still-in-development).
Refer to this feature's version history for more details.
Typically, large enterprise organizations have an explicit permission boundary
between [developers and operators](https://about.gitlab.com/topics/devops/).
Developers build and test their code, and operators deploy and monitor the
application. With group-level protected environments, the permission of each
group is carefully configured in order to prevent unauthorized access and
maintain proper separation of duty. Group-level protected environments
extend the [project-level protected environments](#protecting-environments)
to the group-level.
The permissions of deployments can be illustrated in the following table:
| Environment | Developer | Operator | Category |
|-------------|------------|----------|----------|
| Development | Allowed | Allowed | Lower environment |
| Testing | Allowed | Allowed | Lower environment |
| Staging | Disallowed | Allowed | Higher environment |
| Production | Disallowed | Allowed | Higher environment |
_(Reference: [Deployment environments on Wikipedia](https://en.wikipedia.org/wiki/Deployment_environment))_
### Group-level protected environments names
Contrary to project-level protected environments, group-level protected
environments use the [deployment tier](index.md#deployment-tier-of-environments)
as their name.
A group may consist of many project environments that have unique names.
For example, Project-A has a `gprd` environment and Project-B has a `Production`
environment, so protecting a specific environment name doesn't scale well.
By using deployment tiers, both are recognized as `production` deployment tier
and are protected at the same time.
### Configure group-level memberships
In an enterprise organization, with thousands of projects under a single group,
ensuring that all of the [project-level protected environments](#protecting-environments)
are properly configured is not a scalable solution. For example, a developer
might gain privileged access to a higher environment when they are added as a
maintainer to a new project. Group-level protected environments can be a solution
in this situation.
To maximize the effectiveness of group-level protected environments,
[group-level memberships](../../user/group/index.md) must be correctly
configured:
- Operators should be assigned the [maintainer role](../../user/permissions.md)
(or above) to the top-level group. They can maintain CI/CD configurations for
the higher environments (such as production) in the group-level settings page,
wnich includes group-level protected environments,
[group-level runners](../runners/README.md#group-runners),
[group-level clusters](../../user/group/clusters/index.md), etc. Those
configurations are inherited to the child projects as read-only entries.
This ensures that only operators can configure the organization-wide
deployment ruleset.
- Developers should be assigned the [developer role](../../user/permissions.md)
(or below) at the top-level group, or explicitly assigned to a child project
as maintainers. They do *NOT* have access to the CI/CD configurations in the
top-level group, so operators can ensure that the critical configuration won't
be accidentally changed by the developers.
- For sub-groups and child projects:
- Regarding [sub-groups](../../user/group/subgroups/index.md), if a higher
group has configured the group-level protected environment, the lower groups
cannot override it.
- [Project-level protected environments](#protecting-environments) can be
combined with the group-level setting. If both group-level and project-level
environment configurations exist, the user must be allowed in **both**
rulesets in order to run a deployment job.
- Within a project or a sub-group of the top-level group, developers can be
safely assigned the Maintainer role to tune their lower environments (such
as `testing`).
Having this configuration in place:
- If a user is about to run a deployment job in a project and allowed to deploy
to the environment, the deployment job proceeds.
- If a user is about to run a deployment job in a project but disallowed to
deploy to the environment, the deployment job fails with an error message.
### Protect a group-level environment
To protect a group-level environment:
1. Make sure your environments have the correct
[`deployment_tier`](index.md#deployment-tier-of-environments) defined in
`gitlab-ci.yml`.
1. Configure the group-level protected environments via the
[REST API](../../api/group_protected_environments.md).
NOTE:
Configuration [via the UI](https://gitlab.com/gitlab-org/gitlab/-/issues/325249)
is scheduled for a later release.
### Enable or disable Group-level protected environments **(FREE SELF)**
Group-level protected environments is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:group_level_protected_environments)
```
To disable it:
```ruby
Feature.disable(:group_level_protected_environments)
```
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues

View File

@ -307,6 +307,26 @@ include:
- remote: https://gitlab.com/gitlab-org/gitlab/-/raw/v13.0.1-ee/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
```
### Use a feature flag to roll out a `latest` template
With a major version release like 13.0 or 14.0, [stable templates](#stable-version) must be
updated with their corresponding [latest template versions](#latest-version).
It may be hard to gauge the impact of this change, so use the `redirect_to_latest_template_<name>`
feature flag to test the impact on a subset of users. Using a feature flag can help
reduce the risk of reverts or rollbacks on production.
For example, to redirect the stable `Jobs/Deploy` template to its latest template in 25% of
projects on `gitlab.com`:
```shell
/chatops run feature set redirect_to_latest_template_jobs_deploy 25 --actors
```
After you're confident the latest template can be moved to stable:
1. Update the stable template with the content of the latest version.
1. Remove the corresponding feature flag.
### Further reading
There is an [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/17716) about

View File

@ -33,6 +33,13 @@ You can change the maximum push size for your repository.
Navigate to **Admin Area > Settings > General**, then expand **Account and Limit**.
From here, you can increase or decrease by changing the value in `Maximum push size (MB)`.
NOTE:
When you [add files to a repository](../../project/repository/web_editor.md#create-a-file)
through the web UI, the maximum **attachment** size is the limiting factor,
because the [web server](../../../development/architecture.md#components)
must receive the file before GitLab can generate the commit.
Use [Git LFS](../../../topics/git/lfs/index.md) to add large files to a repository.
## Max import size
You can change the maximum file size for imports in GitLab.

View File

@ -89,8 +89,9 @@ artifacts, as described in the [troubleshooting documentation](../../../administ
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50889) in GitLab Core 13.9.
When enabled (default), the artifacts for the most recent pipeline for a ref are
locked against deletion and kept regardless of the expiry time.
When enabled (default), the artifacts of the most recent pipeline for each Git ref
([branches and tags](https://git-scm.com/book/en/v2/Git-Internals-Git-References))
are locked against deletion and kept regardless of the expiry time.
When disabled, the latest artifacts for any **new** successful or fixed pipelines
are allowed to expire.

View File

@ -7,7 +7,7 @@ type: reference, concepts
# Squash and merge **(FREE)**
> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18956) to GitLab Free in 11.0.
> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18956) from GitLab Premium to GitLab Free in 11.0.
With squash and merge you can combine all your merge request's commits into one
and retain a clean history.

View File

@ -6,7 +6,7 @@ module Gitlab
module External
module File
class Template < Base
attr_reader :location, :project
attr_reader :location
SUFFIX = '.gitlab-ci.yml'
@ -41,7 +41,7 @@ module Gitlab
end
def fetch_template_content
Gitlab::Template::GitlabCiYmlTemplate.find(template_name, project)&.content
Gitlab::Template::GitlabCiYmlTemplate.find(template_name, context.project)&.content
end
end
end

View File

@ -68,11 +68,19 @@ module Gitlab
end
def expand_value(value, keep_undefined: false)
value.gsub(ExpandVariables::VARIABLES_REGEXP) do
value.gsub(Item::VARIABLES_REGEXP) do
match = Regexp.last_match
result = @variables_by_key[match[1] || match[2]]&.value
result ||= match[0] if keep_undefined
result
if match[:key]
# we matched variable
if variable = @variables_by_key[match[:key]]
variable.value
elsif keep_undefined
match[0]
end
else
# we escape sequence
match[0]
end
end
end

View File

@ -7,6 +7,9 @@ module Gitlab
class Item
include Gitlab::Utils::StrongMemoize
VARIABLES_REGEXP = /\$\$|%%|\$(?<key>[a-zA-Z_][a-zA-Z0-9_]*)|\${\g<key>?}|%\g<key>%/.freeze.freeze
VARIABLE_REF_CHARS = %w[$ %].freeze
def initialize(key:, value:, public: true, file: false, masked: false, raw: false)
raise ArgumentError, "`#{key}` must be of type String or nil value, while it was: #{value.class}" unless
value.is_a?(String) || value.nil?
@ -34,9 +37,9 @@ module Gitlab
strong_memoize(:depends_on) do
next if raw
next unless ExpandVariables.possible_var_reference?(value)
next unless self.class.possible_var_reference?(value)
value.scan(ExpandVariables::VARIABLES_REGEXP).map(&:first)
value.scan(VARIABLES_REGEXP).filter_map(&:last)
end
end
@ -64,6 +67,12 @@ module Gitlab
end
end
def self.possible_var_reference?(value)
return unless value
VARIABLE_REF_CHARS.any? { |symbol| value.include?(symbol) }
end
def to_s
return to_runner_variable.to_s unless depends_on

View File

@ -5,11 +5,20 @@ module Gitlab
class GitlabCiYmlTemplate < BaseTemplate
BASE_EXCLUDED_PATTERNS = [%r{\.latest\.}].freeze
TEMPLATES_WITH_LATEST_VERSION = {
'Jobs/Deploy' => true,
'Jobs/Browser-Performance-Testing' => true,
'Security/API-Fuzzing' => true,
'Security/DAST' => true,
'Terraform' => true
}.freeze
def description
"# This file is a template, and might need editing before it works on your project."
end
class << self
extend ::Gitlab::Utils::Override
include Gitlab::Utils::StrongMemoize
def extension
@ -54,6 +63,31 @@ module Gitlab
excluded_patterns: self.excluded_patterns
)
end
override :find
def find(key, project = nil)
if try_redirect_to_latest?(key, project)
key += '.latest'
end
super(key, project)
end
private
# To gauge the impact of the latest template,
# you can redirect the stable template to the latest template by enabling the feature flag.
# See https://docs.gitlab.com/ee/development/cicd/templates.html#versioning for more information.
def try_redirect_to_latest?(key, project)
return false unless templates_with_latest_version[key]
flag_name = "redirect_to_latest_template_#{key.underscore.tr('/', '_')}"
::Feature.enabled?(flag_name, project, default_enabled: :yaml)
end
def templates_with_latest_version
TEMPLATES_WITH_LATEST_VERSION
end
end
end
end

View File

@ -5,7 +5,7 @@ import * as actions from '~/ci_variable_list/store/actions';
import * as types from '~/ci_variable_list/store/mutation_types';
import getInitialState from '~/ci_variable_list/store/state';
import { prepareDataForDisplay, prepareEnvironments } from '~/ci_variable_list/store/utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import mockData from '../services/mock_data';
@ -240,7 +240,9 @@ describe('CI variable list store actions', () => {
mock.onGet(state.endpoint).reply(500);
testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }], () => {
expect(createFlash).toHaveBeenCalledWith('There was an error fetching the variables.');
expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching the variables.',
});
done();
});
});
@ -278,9 +280,9 @@ describe('CI variable list store actions', () => {
[],
[{ type: 'requestEnvironments' }],
() => {
expect(createFlash).toHaveBeenCalledWith(
'There was an error fetching the environments information.',
);
expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching the environments information.',
});
done();
},
);

View File

@ -24,7 +24,7 @@ import {
CREATE_CLUSTER_ERROR,
} from '~/create_cluster/eks_cluster/store/mutation_types';
import createState from '~/create_cluster/eks_cluster/store/state';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash');
@ -358,7 +358,9 @@ describe('EKS Cluster Store Actions', () => {
testAction(actions.createClusterError, payload, state, [
{ type: CREATE_CLUSTER_ERROR, payload },
]).then(() => {
expect(createFlash).toHaveBeenCalledWith(payload.name[0]);
expect(createFlash).toHaveBeenCalledWith({
message: payload.name[0],
});
}));
});
});

View File

@ -4,7 +4,7 @@ import Api from '~/api';
import * as actions from '~/deploy_freeze/store/actions';
import * as types from '~/deploy_freeze/store/mutation_types';
import getInitialState from '~/deploy_freeze/store/state';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
@ -189,9 +189,9 @@ describe('deploy freeze store actions', () => {
[{ type: types.REQUEST_FREEZE_PERIODS }],
[],
() =>
expect(createFlash).toHaveBeenCalledWith(
'There was an error fetching the deploy freezes.',
),
expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching the deploy freezes.',
}),
);
});
});

View File

@ -54,7 +54,7 @@ import {
} from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import * as utils from '~/diffs/store/utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@ -293,7 +293,9 @@ describe('DiffsStoreActions', () => {
testAction(fetchCoverageFiles, {}, { endpointCoverage }, [], [], () => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong'));
expect(createFlash).toHaveBeenCalledWith({
message: expect.stringMatching('Something went wrong'),
});
done();
});
});

View File

@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/actions';
import * as types from '~/error_tracking/store/mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';

View File

@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';

View File

@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/list/actions';
import * as types from '~/error_tracking/store/list/mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';

View File

@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue';
import { createStore } from '~/grafana_integration/store';
import axios from '~/lib/utils/axios_utils';
@ -112,10 +112,10 @@ describe('grafana integration component', () => {
.$nextTick()
.then(jest.runAllTicks)
.then(() =>
expect(createFlash).toHaveBeenCalledWith(
`There was an error saving your changes. ${message}`,
'alert',
),
expect(createFlash).toHaveBeenCalledWith({
message: `There was an error saving your changes. ${message}`,
type: 'alert',
}),
);
});
});

View File

@ -1,6 +1,6 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import modal from '~/ide/components/new_dropdown/modal.vue';
import { createStore } from '~/ide/stores';
@ -182,14 +182,14 @@ describe('new file modal component', () => {
vm.submitForm();
expect(createFlash).toHaveBeenCalledWith(
'The name "test-path/test" is already taken in this directory.',
'alert',
expect.anything(),
null,
false,
true,
);
expect(createFlash).toHaveBeenCalledWith({
message: 'The name "test-path/test" is already taken in this directory.',
type: 'alert',
parent: expect.anything(),
actionConfig: null,
fadeTransition: false,
addBodyClass: true,
});
});
it('does not throw error when target entry does not exist', () => {

View File

@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { range } from 'lodash';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '~/ide/constants';
import service from '~/ide/services';
import { createStore } from '~/ide/stores';
@ -145,7 +145,9 @@ describe('IDE store merge request actions', () => {
.dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
.catch(() => {
expect(createFlash).toHaveBeenCalled();
expect(createFlash.mock.calls[0][0]).toBe('Error fetching merge requests for bar');
expect(createFlash.mock.calls[0][0].message).toBe(
'Error fetching merge requests for bar',
);
})
.then(done)
.catch(done.fail);
@ -562,7 +564,9 @@ describe('IDE store merge request actions', () => {
openMergeRequest(store, mr)
.catch(() => {
expect(createFlash).toHaveBeenCalledWith(expect.any(String));
expect(createFlash).toHaveBeenCalledWith({
message: expect.any(String),
});
})
.then(done)
.catch(done.fail);

View File

@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import * as actions from '~/ide/stores/modules/terminal/actions/session_controls';
import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
@ -89,7 +89,9 @@ describe('IDE store terminal session controls actions', () => {
it('flashes message', () => {
actions.receiveStartSessionError({ dispatch });
expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STARTING);
expect(createFlash).toHaveBeenCalledWith({
message: messages.UNEXPECTED_ERROR_STARTING,
});
});
it('sets session status', () => {
@ -161,7 +163,9 @@ describe('IDE store terminal session controls actions', () => {
it('flashes message', () => {
actions.receiveStopSessionError({ dispatch });
expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STOPPING);
expect(createFlash).toHaveBeenCalledWith({
message: messages.UNEXPECTED_ERROR_STOPPING,
});
});
it('kills the session', () => {

View File

@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import * as actions from '~/ide/stores/modules/terminal/actions/session_status';
import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
@ -115,7 +115,9 @@ describe('IDE store terminal session controls actions', () => {
it('flashes message', () => {
actions.receiveSessionStatusError({ dispatch });
expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STATUS);
expect(createFlash).toHaveBeenCalledWith({
message: messages.UNEXPECTED_ERROR_STATUS,
});
});
it('kills the session', () => {

View File

@ -5,11 +5,15 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { STATUSES } from '~/import_entities/constants';
import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
import groupQuery from '~/import_entities/import_groups/graphql/queries/group.query.graphql';
import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql';
import removeValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql';
import groupAndProjectQuery from '~/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql';
import { availableNamespacesFixture } from '../graphql/fixtures';
Vue.use(VueApollo);
const { i18n: I18N } = ImportTableRow;
const getFakeGroup = (status) => ({
web_url: 'https://fake.host/',
full_path: 'fake_group_1',
@ -25,6 +29,7 @@ const getFakeGroup = (status) => ({
const EXISTING_GROUP_TARGET_NAMESPACE = 'existing-group';
const EXISTING_GROUP_PATH = 'existing-path';
const EXISTING_PROJECT_PATH = 'existing-project-path';
describe('import table row', () => {
let wrapper;
@ -41,13 +46,19 @@ describe('import table row', () => {
const createComponent = (props) => {
apolloProvider = createMockApollo([
[
groupQuery,
groupAndProjectQuery,
({ fullPath }) => {
const existingGroup =
fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_GROUP_PATH}`
? { id: 1 }
: null;
return Promise.resolve({ data: { existingGroup } });
const existingProject =
fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_PROJECT_PATH}`
? { id: 1 }
: null;
return Promise.resolve({ data: { existingGroup, existingProject } });
},
],
]);
@ -173,7 +184,7 @@ describe('import table row', () => {
});
describe('validations', () => {
it('Reports invalid group name when name is not matching regex', () => {
it('reports invalid group name when name is not matching regex', () => {
createComponent({
group: {
...getFakeGroup(STATUSES.NONE),
@ -188,7 +199,7 @@ describe('import table row', () => {
expect(wrapper.text()).toContain('Please choose a group URL with no special characters.');
});
it('Reports invalid group name if relevant validation error exists', async () => {
it('reports invalid group name if relevant validation error exists', async () => {
const FAKE_ERROR_MESSAGE = 'fake error';
createComponent({
@ -208,5 +219,101 @@ describe('import table row', () => {
expect(wrapper.text()).toContain(FAKE_ERROR_MESSAGE);
});
it('sets validation error when targetting existing group', async () => {
const testGroup = getFakeGroup(STATUSES.NONE);
createComponent({
group: {
...testGroup,
import_target: {
target_namespace: EXISTING_GROUP_TARGET_NAMESPACE,
new_name: EXISTING_GROUP_PATH,
},
},
});
jest.spyOn(wrapper.vm.$apollo, 'mutate');
jest.runOnlyPendingTimers();
await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: addValidationErrorMutation,
variables: {
field: 'new_name',
message: I18N.NAME_ALREADY_EXISTS,
sourceGroupId: testGroup.id,
},
});
});
it('sets validation error when targetting existing project', async () => {
const testGroup = getFakeGroup(STATUSES.NONE);
createComponent({
group: {
...testGroup,
import_target: {
target_namespace: EXISTING_GROUP_TARGET_NAMESPACE,
new_name: EXISTING_PROJECT_PATH,
},
},
});
jest.spyOn(wrapper.vm.$apollo, 'mutate');
jest.runOnlyPendingTimers();
await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: addValidationErrorMutation,
variables: {
field: 'new_name',
message: I18N.NAME_ALREADY_EXISTS,
sourceGroupId: testGroup.id,
},
});
});
it('clears validation error when target is updated', async () => {
const testGroup = getFakeGroup(STATUSES.NONE);
createComponent({
group: {
...testGroup,
import_target: {
target_namespace: EXISTING_GROUP_TARGET_NAMESPACE,
new_name: EXISTING_PROJECT_PATH,
},
},
});
jest.runOnlyPendingTimers();
await nextTick();
jest.spyOn(wrapper.vm.$apollo, 'mutate');
await wrapper.setProps({
group: {
...testGroup,
import_target: {
target_namespace: 'valid_namespace',
new_name: 'valid_path',
},
},
});
jest.runOnlyPendingTimers();
await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: removeValidationErrorMutation,
variables: {
field: 'new_name',
sourceGroupId: testGroup.id,
},
});
});
});
});

View File

@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { STATUSES } from '~/import_entities/constants';
import actionsFactory from '~/import_entities/import_projects/store/actions';
import { getImportTarget } from '~/import_entities/import_projects/store/getters';
@ -168,7 +168,9 @@ describe('import_projects store actions', () => {
[],
);
expect(createFlash).toHaveBeenCalledWith('Provider rate limit exceeded. Try again later');
expect(createFlash).toHaveBeenCalledWith({
message: 'Provider rate limit exceeded. Try again later',
});
});
});
@ -245,7 +247,9 @@ describe('import_projects store actions', () => {
[],
);
expect(createFlash).toHaveBeenCalledWith('Importing the project failed');
expect(createFlash).toHaveBeenCalledWith({
message: '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 () => {
@ -266,7 +270,9 @@ describe('import_projects store actions', () => {
[],
);
expect(createFlash).toHaveBeenCalledWith(`Importing the project failed: ${ERROR_MESSAGE}`);
expect(createFlash).toHaveBeenCalledWith({
message: `Importing the project failed: ${ERROR_MESSAGE}`,
});
});
});
@ -365,7 +371,9 @@ describe('import_projects store actions', () => {
[],
);
expect(createFlash).toHaveBeenCalledWith('Requesting namespaces failed');
expect(createFlash).toHaveBeenCalledWith({
message: 'Requesting namespaces failed',
});
});
});

View File

@ -1,5 +1,5 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { ERROR_MSG } from '~/incidents_settings/constants';
import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
import axios from '~/lib/utils/axios_utils';
@ -37,7 +37,10 @@ describe('IncidentsSettingsService', () => {
mock.onPatch().reply(httpStatusCodes.BAD_REQUEST);
return service.updateSettings({}).then(() => {
expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(ERROR_MSG), 'alert');
expect(createFlash).toHaveBeenCalledWith({
message: expect.stringContaining(ERROR_MSG),
type: 'alert',
});
});
});
});

View File

@ -6,7 +6,7 @@ import {
issuable1,
issuable2,
} from 'jest/vue_shared/components/issue/related_issuable_mock_data';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import { linkedIssueTypesMap } from '~/related_issues/constants';
@ -195,7 +195,9 @@ describe('RelatedIssuesRoot', () => {
wrapper.vm.onPendingFormSubmit(input);
return waitForPromises().then(() => {
expect(createFlash).toHaveBeenCalledWith(message);
expect(createFlash).toHaveBeenCalledWith({
message,
});
});
});
});

View File

@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import statusCodes from '~/lib/utils/http_status';
@ -257,9 +257,9 @@ describe('Monitoring store actions', () => {
'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'),
);
expect(createFlash).toHaveBeenCalledWith(
expect.stringContaining(mockDashboardsErrorResponse.message),
);
expect(createFlash).toHaveBeenCalledWith({
message: expect.stringContaining(mockDashboardsErrorResponse.message),
});
done();
})
.catch(done.fail);
@ -1148,9 +1148,9 @@ describe('Monitoring store actions', () => {
return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then(
() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(
expect.stringContaining('error getting options for variable "label1"'),
);
expect(createFlash).toHaveBeenCalledWith({
message: expect.stringContaining('error getting options for variable "label1"'),
});
},
);
});

View File

@ -1,7 +1,7 @@
import { GlButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { timezones } from '~/monitoring/format_date';
@ -203,10 +203,10 @@ describe('operation settings external dashboard component', () => {
.$nextTick()
.then(jest.runAllTicks)
.then(() =>
expect(createFlash).toHaveBeenCalledWith(
`There was an error saving your changes. ${message}`,
'alert',
),
expect(createFlash).toHaveBeenCalledWith({
message: `There was an error saving your changes. ${message}`,
type: 'alert',
}),
);
});
});

View File

@ -2,7 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { MISSING_DELETE_PATH_ERROR } from '~/packages/list/constants';
import * as actions from '~/packages/list/stores/actions';
import * as types from '~/packages/list/stores/mutation_types';
@ -241,7 +241,9 @@ describe('Actions Package list store', () => {
`('should reject and createFlash when $property is missing', ({ actionPayload }, done) => {
testAction(actions.requestDeletePackage, actionPayload, null, [], []).catch((e) => {
expect(e).toEqual(new Error(MISSING_DELETE_PATH_ERROR));
expect(createFlash).toHaveBeenCalledWith(DELETE_PACKAGE_ERROR_MESSAGE);
expect(createFlash).toHaveBeenCalledWith({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
done();
});
});

View File

@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/pipelines/stores/test_reports/actions';
import * as types from '~/pipelines/stores/test_reports/mutation_types';

View File

@ -2,7 +2,7 @@ import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import UpdateUsername from '~/profile/account/components/update_username.vue';
@ -146,7 +146,9 @@ describe('UpdateUsername component', () => {
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
expect(createFlash).toBeCalledWith('Invalid username');
expect(createFlash).toBeCalledWith({
message: 'Invalid username',
});
});
it("shows a fallback error message if the error response doesn't have a `message` property", async () => {
@ -156,9 +158,9 @@ describe('UpdateUsername component', () => {
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
expect(createFlash).toBeCalledWith(
'An error occurred while updating your username, please try again.',
);
expect(createFlash).toBeCalledWith({
message: 'An error occurred while updating your username, please try again.',
});
});
});
});

View File

@ -1,7 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import actions from '~/projects/commits/store/actions';
import * as types from '~/projects/commits/store/mutation_types';
import createState from '~/projects/commits/store/state';
@ -39,7 +39,9 @@ describe('Project commits actions', () => {
actions.receiveAuthorsError(mockDispatchContext);
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith('An error occurred fetching the project authors.');
expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred fetching the project authors.',
});
});
});

View File

@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/related_merge_requests/store/actions';
import * as types from '~/related_merge_requests/store/mutation_types';
@ -100,7 +100,9 @@ describe('RelatedMergeRequest store actions', () => {
[{ type: 'requestData' }, { type: 'receiveDataError' }],
() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong'));
expect(createFlash).toHaveBeenCalledWith({
message: expect.stringMatching('Something went wrong'),
});
done();
},

View File

@ -1,7 +1,7 @@
import { cloneDeep } from 'lodash';
import { getJSONFixture } from 'helpers/fixtures';
import testAction from 'helpers/vuex_action_helper';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { ASSET_LINK_TYPE } from '~/releases/constants';
import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_release_link.mutation.graphql';
@ -151,9 +151,9 @@ describe('Release edit/new actions', () => {
it(`shows a flash message`, () => {
return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(
'Something went wrong while getting the release details.',
);
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while getting the release details.',
});
});
});
});
@ -352,9 +352,9 @@ describe('Release edit/new actions', () => {
.createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
.then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(
'Something went wrong while creating a new release.',
);
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while creating a new release.',
});
});
});
});
@ -483,9 +483,9 @@ describe('Release edit/new actions', () => {
await actions.updateRelease({ commit, dispatch, state, getters });
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(
'Something went wrong while saving the release details.',
);
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while saving the release details.',
});
});
});
@ -503,9 +503,9 @@ describe('Release edit/new actions', () => {
await actions.updateRelease({ commit, dispatch, state, getters });
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(
'Something went wrong while saving the release details.',
);
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while saving the release details.',
});
});
};

View File

@ -4,7 +4,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
@ -125,9 +125,9 @@ describe('Snippet Blob Edit component', () => {
it('should call flash', async () => {
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith(
"Can't fetch content for the blob: Error: Request failed with status code 500",
);
expect(createFlash).toHaveBeenCalledWith({
message: "Can't fetch content for the blob: Error: Request failed with status code 500",
});
});
});

View File

@ -1,5 +1,5 @@
import Vue from 'vue';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
@ -63,10 +63,10 @@ describe('Wip', () => {
setImmediate(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
expect(createFlash).toHaveBeenCalledWith(
'The merge request can now be merged.',
'notice',
);
expect(createFlash).toHaveBeenCalledWith({
message: 'The merge request can now be merged.',
type: 'notice',
});
done();
});
});

View File

@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require 'fast_spec_helper'
require 'rspec-parameterized'
RSpec.describe ExpandVariables do
shared_examples 'common variable expansion' do |expander|
@ -231,41 +232,4 @@ RSpec.describe ExpandVariables do
end
end
end
describe '#possible_var_reference?' do
context 'table tests' do
using RSpec::Parameterized::TableSyntax
where do
{
"empty value": {
value: '',
result: false
},
"normal value": {
value: 'some value',
result: false
},
"simple expansions": {
value: 'key$variable',
result: true
},
"complex expansions": {
value: 'key${variable}${variable2}',
result: true
},
"complex expansions for Windows": {
value: 'key%variable%%variable2%',
result: true
}
}
end
with_them do
subject { ExpandVariables.possible_var_reference?(value) }
it { is_expected.to eq(result) }
end
end
end
end

View File

@ -20,6 +20,13 @@ RSpec.describe 'CI YML Templates' do
all_templates - excluded_templates
end
before do
stub_feature_flags(
redirect_to_latest_template_terraform: false,
redirect_to_latest_template_security_api_fuzzing: false,
redirect_to_latest_template_security_dast: false)
end
with_them do
let(:content) do
if template_name == 'Security/DAST-API.gitlab-ci.yml'

View File

@ -70,6 +70,43 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do
end
end
describe '.possible_var_reference?' do
context 'table tests' do
using RSpec::Parameterized::TableSyntax
where do
{
"empty value": {
value: '',
result: false
},
"normal value": {
value: 'some value',
result: false
},
"simple expansions": {
value: 'key$variable',
result: true
},
"complex expansions": {
value: 'key${variable}${variable2}',
result: true
},
"complex expansions for Windows": {
value: 'key%variable%%variable2%',
result: true
}
}
end
with_them do
subject { Gitlab::Ci::Variables::Collection::Item.possible_var_reference?(value) }
it { is_expected.to eq(result) }
end
end
end
describe '#depends_on' do
let(:item) { Gitlab::Ci::Variables::Collection::Item.new(**variable) }
@ -128,7 +165,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do
end
it 'supports using an active record resource' do
variable = create(:ci_variable, key: 'CI_VAR', value: '123')
variable = build(:ci_variable, key: 'CI_VAR', value: '123')
resource = described_class.fabricate(variable)
expect(resource).to be_a(described_class)

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require 'fast_spec_helper'
require 'rspec-parameterized'
RSpec.describe Gitlab::Ci::Variables::Collection::Sort do
describe '#initialize with non-Collection value' do
@ -57,9 +58,9 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sort do
},
"variable containing escaped variable reference": {
variables: [
{ key: 'variable_a', value: 'value' },
{ key: 'variable_b', value: '$$variable_a' },
{ key: 'variable_c', value: '$variable_b' }
{ key: 'variable_c', value: '$variable_a' },
{ key: 'variable_a', value: 'value' }
],
expected_errors: nil
}
@ -144,11 +145,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sort do
},
"variable containing escaped variable reference": {
variables: [
{ key: 'variable_c', value: '$variable_b' },
{ key: 'variable_b', value: '$$variable_a' },
{ key: 'variable_c', value: '$variable_a' },
{ key: 'variable_a', value: 'value' }
],
result: %w[variable_a variable_b variable_c]
result: %w[variable_b variable_a variable_c]
}
}
end

View File

@ -253,6 +253,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
value: 'key${MISSING_VAR}-${CI_JOB_NAME}',
result: 'key${MISSING_VAR}-test-1',
keep_undefined: true
},
"escaped characters are kept intact": {
value: 'key-$TEST1-%%HOME%%-$${HOME}',
result: 'key-test-3-%%HOME%%-$${HOME}',
keep_undefined: false
}
}
end
@ -315,6 +320,14 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
],
keep_undefined: false
},
"escaped characters in complex expansions are kept intact": {
variables: [
{ key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' },
{ key: 'variable', value: '$variable2' },
{ key: 'variable2', value: 'value2' }
],
keep_undefined: false
},
"array with cyclic dependency": {
variables: [
{ key: 'variable', value: '$variable2' },
@ -415,6 +428,30 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
{ key: 'variable3', value: 'keyvalueresult' }
]
},
"escaped characters in complex expansions keeping undefined are kept intact": {
variables: [
{ key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' },
{ key: 'variable', value: '$variable2' },
{ key: 'variable2', value: 'value' }
],
keep_undefined: true,
result: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'value' },
{ key: 'variable3', value: 'key_value_$${HOME}_%%HOME%%' }
]
},
"escaped characters in complex expansions discarding undefined are kept intact": {
variables: [
{ key: 'variable2', value: 'key_${variable4}_$${HOME}_%%HOME%%' },
{ key: 'variable', value: 'value_$${HOME}_%%HOME%%' }
],
keep_undefined: false,
result: [
{ key: 'variable', value: 'value_$${HOME}_%%HOME%%' },
{ key: 'variable2', value: 'key__$${HOME}_%%HOME%%' }
]
},
"out-of-order expansion": {
variables: [
{ key: 'variable3', value: 'key$variable2$variable' },
@ -441,7 +478,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
{ key: 'variable3', value: 'keyresultvalue' }
]
},
"missing variable": {
"missing variable discarding original": {
variables: [
{ key: 'variable2', value: 'key$variable' }
],
@ -485,6 +522,19 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
{ key: 'variable3', value: 'key_$variable2_value2' }
]
},
"variable value referencing password with special characters": {
variables: [
{ key: 'VAR', value: '$PASSWORD' },
{ key: 'PASSWORD', value: 'my_password$$_%%_$A' },
{ key: 'A', value: 'value' }
],
keep_undefined: false,
result: [
{ key: 'VAR', value: 'my_password$$_%%_value' },
{ key: 'PASSWORD', value: 'my_password$$_%%_value' },
{ key: 'A', value: 'value' }
]
},
"cyclic dependency causes original array to be returned": {
variables: [
{ key: 'variable', value: '$variable2' },

View File

@ -21,6 +21,55 @@ RSpec.describe Gitlab::Template::GitlabCiYmlTemplate do
end
end
describe '.find' do
let_it_be(:project) { create(:project) }
let_it_be(:other_project) { create(:project) }
described_class::TEMPLATES_WITH_LATEST_VERSION.keys.each do |key|
it "finds the latest template for #{key}" do
result = described_class.find(key, project)
expect(result.full_name).to eq("#{key}.latest.gitlab-ci.yml")
expect(result.content).to be_present
end
context 'when `redirect_to_latest_template` feature flag is disabled' do
before do
stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => false)
end
it "finds the stable template for #{key}" do
result = described_class.find(key, project)
expect(result.full_name).to eq("#{key}.gitlab-ci.yml")
expect(result.content).to be_present
end
end
context 'when `redirect_to_latest_template` feature flag is enabled on the project' do
before do
stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => project)
end
it "finds the latest template for #{key}" do
result = described_class.find(key, project)
expect(result.full_name).to eq("#{key}.latest.gitlab-ci.yml")
expect(result.content).to be_present
end
end
context 'when `redirect_to_latest_template` feature flag is enabled on the other project' do
before do
stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => other_project)
end
it "finds the stable template for #{key}" do
result = described_class.find(key, project)
expect(result.full_name).to eq("#{key}.gitlab-ci.yml")
expect(result.content).to be_present
end
end
end
end
describe '#content' do
it 'loads the full file' do
gitignore = subject.new(Rails.root.join('lib/gitlab/ci/templates/Ruby.gitlab-ci.yml'))

View File

@ -5,7 +5,6 @@ import (
"context"
"fmt"
"io"
"mime"
"net/http"
"os"
"os/exec"
@ -53,14 +52,6 @@ func (e *entry) Inject(w http.ResponseWriter, r *http.Request, sendData string)
}
}
func detectFileContentType(fileName string) string {
contentType := mime.TypeByExtension(filepath.Ext(fileName))
if contentType == "" {
contentType = "application/octet-stream"
}
return contentType
}
func unpackFileFromZip(ctx context.Context, archivePath, encodedFilename string, headers http.Header, output io.Writer) error {
fileName, err := zipartifacts.DecodeFileEntry(encodedFilename)
if err != nil {
@ -97,7 +88,15 @@ func unpackFileFromZip(ctx context.Context, archivePath, encodedFilename string,
// Write http headers about the file
headers.Set("Content-Length", contentLength)
headers.Set("Content-Type", detectFileContentType(fileName))
// Using application/octet-stream tells the client that we don't
// really know what Content-Type is. Since this file is being sent
// as attachment, browsers don't need to know to save the
// file. Chrome doesn't appear to pay attention to Content-Type when
// Content-Disposition is an attachment, and Firefox only uses it if there
// is no extension in the filename. Thus, there's no need for
// Workhorse to guess Content-Type based on the filename.
headers.Set("Content-Type", "application/octet-stream")
headers.Set("Content-Disposition", "attachment; filename=\""+escapeQuotes(basename)+"\"")
// Copy file body to client
if _, err := io.Copy(output, reader); err != nil {

View File

@ -54,7 +54,7 @@ func TestDownloadingFromValidArchive(t *testing.T) {
testhelper.RequireResponseHeader(t, response,
"Content-Type",
"text/plain; charset=utf-8")
"application/octet-stream")
testhelper.RequireResponseHeader(t, response,
"Content-Disposition",
"attachment; filename=\"test.txt\"")
@ -88,7 +88,7 @@ func TestDownloadingFromValidHTTPArchive(t *testing.T) {
testhelper.RequireResponseHeader(t, response,
"Content-Type",
"text/plain; charset=utf-8")
"application/octet-stream")
testhelper.RequireResponseHeader(t, response,
"Content-Disposition",
"attachment; filename=\"test.txt\"")