Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
55e6eebd6f
commit
b70394d26f
|
@ -298,9 +298,13 @@ coverage-frontend:
|
|||
- *yarn-install
|
||||
- run_timed_command "retry yarn run webpack-prod"
|
||||
|
||||
qa-frontend-node:10:
|
||||
qa-frontend-node:12:
|
||||
extends: .qa-frontend-node
|
||||
image: ${GITLAB_DEPENDENCY_PROXY}node:dubnium
|
||||
image: ${GITLAB_DEPENDENCY_PROXY}node:12
|
||||
|
||||
qa-frontend-node:14:
|
||||
extends: .qa-frontend-node
|
||||
image: ${GITLAB_DEPENDENCY_PROXY}node:14
|
||||
|
||||
qa-frontend-node:latest:
|
||||
extends:
|
||||
|
|
|
@ -2548,8 +2548,6 @@ Gitlab/FeatureAvailableUsage:
|
|||
- 'ee/app/views/projects/settings/operations/_status_page.html.haml'
|
||||
- 'ee/app/views/projects/settings/repository/_protected_branches.html.haml'
|
||||
- 'ee/app/views/projects/sidebar/_repository_locked_files.html.haml'
|
||||
- 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml'
|
||||
- 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml'
|
||||
- 'ee/app/views/shared/issuable/_group_bulk_update_sidebar.html.haml'
|
||||
- 'ee/app/views/shared/issuable/form/_default_templates.html.haml'
|
||||
- 'ee/app/views/shared/labels/_create_label_help_text.html.haml'
|
||||
|
|
|
@ -1 +1 @@
|
|||
c0bef09e1ad59c7d119ecc5d58a8a4e6e98a8a65
|
||||
48d7984d9912c935a2c2abba3b55593cf0be2d8e
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
/* eslint-disable func-names, no-new */
|
||||
|
||||
import $ from 'jquery';
|
||||
import store from '~/boards/stores';
|
||||
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
|
||||
import createFlash from '~/flash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
import CreateLabelDropdown from '../../create_label';
|
||||
import { fullLabelId } from '../boards_util';
|
||||
import boardsStore from '../stores/boards_store';
|
||||
|
||||
function shouldCreateListGraphQL(label) {
|
||||
return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @gitlab/no-global-event-off
|
||||
$(document)
|
||||
.off('created.label')
|
||||
.on('created.label', (e, label, addNewList) => {
|
||||
if (!addNewList) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldCreateListGraphQL(label)) {
|
||||
store.dispatch('createList', { labelId: fullLabelId(label) });
|
||||
} else {
|
||||
boardsStore.new({
|
||||
title: label.title,
|
||||
position: boardsStore.state.lists.length - 2,
|
||||
list_type: 'label',
|
||||
label: {
|
||||
id: label.id,
|
||||
title: label.title,
|
||||
color: label.color,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default function initNewListDropdown() {
|
||||
$('.js-new-board-list').each(function () {
|
||||
const $dropdownToggle = $(this);
|
||||
const $dropdown = $dropdownToggle.closest('.dropdown');
|
||||
new CreateLabelDropdown(
|
||||
$dropdown.find('.dropdown-new-label'),
|
||||
$dropdownToggle.data('namespacePath'),
|
||||
$dropdownToggle.data('projectPath'),
|
||||
);
|
||||
|
||||
initDeprecatedJQueryDropdown($dropdownToggle, {
|
||||
data(term, callback) {
|
||||
const reqFailed = () => {
|
||||
$dropdownToggle.data('bs.dropdown').hide();
|
||||
createFlash({
|
||||
message: __('Error fetching labels.'),
|
||||
});
|
||||
};
|
||||
|
||||
if (store.getters.shouldUseGraphQL) {
|
||||
store
|
||||
.dispatch('fetchLabels')
|
||||
.then((data) => callback(data))
|
||||
.catch(reqFailed);
|
||||
} else {
|
||||
axios
|
||||
.get($dropdownToggle.attr('data-list-labels-path'))
|
||||
.then(({ data }) => callback(data))
|
||||
.catch(reqFailed);
|
||||
}
|
||||
},
|
||||
renderRow(label) {
|
||||
const active = store.getters.shouldUseGraphQL
|
||||
? store.getters.getListByLabelId(label.id)
|
||||
: boardsStore.findListByLabelId(label.id);
|
||||
const $li = $('<li />');
|
||||
const $a = $('<a />', {
|
||||
class: active ? `is-active js-board-list-${getIdFromGraphQLId(active.id)}` : '',
|
||||
text: label.title,
|
||||
href: '#',
|
||||
});
|
||||
const $labelColor = $('<span />', {
|
||||
class: 'dropdown-label-box',
|
||||
style: `background-color: ${label.color}`,
|
||||
});
|
||||
|
||||
return $li.append($a.prepend($labelColor));
|
||||
},
|
||||
search: {
|
||||
fields: ['title'],
|
||||
},
|
||||
filterable: true,
|
||||
selectable: true,
|
||||
multiSelect: true,
|
||||
containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
|
||||
clicked(options) {
|
||||
const { e } = options;
|
||||
const label = options.selectedObj;
|
||||
e.preventDefault();
|
||||
|
||||
if (shouldCreateListGraphQL(label)) {
|
||||
store.dispatch('createList', { labelId: label.id });
|
||||
} else if (!boardsStore.findListByLabelId(label.id)) {
|
||||
boardsStore.new({
|
||||
title: label.title,
|
||||
position: boardsStore.state.lists.length - 2,
|
||||
list_type: 'label',
|
||||
label: {
|
||||
id: label.id,
|
||||
title: label.title,
|
||||
color: label.color,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
|
@ -7,12 +7,7 @@ import { mapActions, mapGetters } from 'vuex';
|
|||
import 'ee_else_ce/boards/models/issue';
|
||||
import 'ee_else_ce/boards/models/list';
|
||||
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
|
||||
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
|
||||
import {
|
||||
setWeightFetchingState,
|
||||
setEpicFetchingState,
|
||||
getMilestoneTitle,
|
||||
} from 'ee_else_ce/boards/ee_functions';
|
||||
import { setWeightFetchingState, setEpicFetchingState } from 'ee_else_ce/boards/ee_functions';
|
||||
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
|
||||
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
|
||||
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
|
||||
|
@ -313,20 +308,6 @@ export default () => {
|
|||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-new, @gitlab/no-runtime-template-compiler
|
||||
new Vue({
|
||||
el: document.getElementById('js-add-list'),
|
||||
data() {
|
||||
return {
|
||||
filters: boardsStore.state.filters,
|
||||
...getMilestoneTitle($boardApp),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
initNewListDropdown();
|
||||
},
|
||||
});
|
||||
|
||||
const createColumnTriggerEl = document.querySelector('.js-create-column-trigger');
|
||||
if (createColumnTriggerEl) {
|
||||
// eslint-disable-next-line no-new
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
|
||||
import updateCurrentBranchMutation from '../../graphql/mutations/update_current_branch.mutation.graphql';
|
||||
import updateLastCommitBranchMutation from '../../graphql/mutations/update_last_commit_branch.mutation.graphql';
|
||||
import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
|
||||
import getCurrentBranch from '../../graphql/queries/client/current_branch.graphql';
|
||||
import getIsNewCiConfigFile from '../../graphql/queries/client/is_new_ci_config_file.graphql';
|
||||
import getPipelineEtag from '../../graphql/queries/client/pipeline_etag.graphql';
|
||||
|
@ -37,6 +36,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
commitSha: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -49,9 +53,6 @@ export default {
|
|||
isNewCiConfigFile: {
|
||||
query: getIsNewCiConfigFile,
|
||||
},
|
||||
commitSha: {
|
||||
query: getCommitSha,
|
||||
},
|
||||
currentBranch: {
|
||||
query: getCurrentBranch,
|
||||
},
|
||||
|
@ -96,13 +97,7 @@ export default {
|
|||
lastCommitId: this.commitSha,
|
||||
},
|
||||
update(store, { data }) {
|
||||
const commitSha = data?.commitCreate?.commit?.sha;
|
||||
const pipelineEtag = data?.commitCreate?.commit?.commitPipelinePath;
|
||||
|
||||
if (commitSha) {
|
||||
store.writeQuery({ query: getCommitSha, data: { commitSha } });
|
||||
}
|
||||
|
||||
if (pipelineEtag) {
|
||||
store.writeQuery({ query: getPipelineEtag, data: { pipelineEtag } });
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { EDITOR_READY_EVENT } from '~/editor/constants';
|
|||
import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
|
||||
import SourceEditor from '~/vue_shared/components/source_editor.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -12,14 +11,11 @@ export default {
|
|||
mixins: [glFeatureFlagMixin()],
|
||||
inject: ['ciConfigPath', 'projectPath', 'projectNamespace', 'defaultBranch'],
|
||||
inheritAttrs: false,
|
||||
data() {
|
||||
return {
|
||||
commitSha: '',
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
props: {
|
||||
commitSha: {
|
||||
query: getCommitSha,
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -158,11 +158,9 @@ export default {
|
|||
const updatedPath = setUrlParams({ branch_name: newBranch });
|
||||
historyPushState(updatedPath);
|
||||
|
||||
this.$emit('updateCommitSha', { newBranch });
|
||||
|
||||
// refetching the content will cause a lot of components to re-render,
|
||||
// including the text editor which uses the commit sha to register the CI schema
|
||||
// so we need to make sure the commit sha is updated first
|
||||
// so we need to make sure the currentBranch (and consequently, the commitSha) are updated first
|
||||
await this.$nextTick();
|
||||
this.$emit('refetchContent');
|
||||
},
|
||||
|
|
|
@ -33,6 +33,11 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
commitSha: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
isNewCiConfigFile: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
@ -54,7 +59,11 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div class="gl-mb-5">
|
||||
<pipeline-status v-if="showPipelineStatus" :class="$options.pipelineStatusClasses" />
|
||||
<pipeline-status
|
||||
v-if="showPipelineStatus"
|
||||
:commit-sha="commitSha"
|
||||
:class="$options.pipelineStatusClasses"
|
||||
/>
|
||||
<validation-segment :class="validationStyling" :ci-config="ciConfigData" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -3,7 +3,6 @@ import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
|
|||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { truncateSha } from '~/lib/utils/text_utility';
|
||||
import { s__ } from '~/locale';
|
||||
import getCommitSha from '~/pipeline_editor/graphql/queries/client/commit_sha.graphql';
|
||||
import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
|
||||
import getPipelineEtag from '~/pipeline_editor/graphql/queries/client/pipeline_etag.graphql';
|
||||
import {
|
||||
|
@ -33,10 +32,14 @@ export default {
|
|||
GlSprintf,
|
||||
},
|
||||
inject: ['projectFullPath'],
|
||||
apollo: {
|
||||
props: {
|
||||
commitSha: {
|
||||
query: getCommitSha,
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
pipelineEtag: {
|
||||
query: getPipelineEtag,
|
||||
},
|
||||
|
@ -51,7 +54,7 @@ export default {
|
|||
sha: this.commitSha,
|
||||
};
|
||||
},
|
||||
update: (data) => {
|
||||
update(data) {
|
||||
const { id, commitPath = '', detailedStatus = {} } = data.project?.pipeline || {};
|
||||
|
||||
return {
|
||||
|
@ -60,6 +63,11 @@ export default {
|
|||
detailedStatus,
|
||||
};
|
||||
},
|
||||
result(res) {
|
||||
if (res.data?.project?.pipeline) {
|
||||
this.hasError = false;
|
||||
}
|
||||
},
|
||||
error() {
|
||||
this.hasError = true;
|
||||
},
|
||||
|
@ -68,7 +76,6 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
commitSha: '',
|
||||
hasError: false,
|
||||
};
|
||||
},
|
||||
|
@ -84,7 +91,11 @@ export default {
|
|||
// (e.g. pipeline is null during fetch when the pipeline hasn't been
|
||||
// triggered yet), we can just show the loading state until the pipeline
|
||||
// details are ready to be fetched
|
||||
return this.$apollo.queries.pipeline.loading || (!this.hasPipelineData && !this.hasError);
|
||||
return (
|
||||
this.$apollo.queries.pipeline.loading ||
|
||||
this.commitSha.length === 0 ||
|
||||
(!this.hasPipelineData && !this.hasError)
|
||||
);
|
||||
},
|
||||
shortSha() {
|
||||
return truncateSha(this.commitSha);
|
||||
|
|
|
@ -69,6 +69,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
commitSha: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
appStatus: {
|
||||
|
@ -110,7 +115,7 @@ export default {
|
|||
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
|
||||
>
|
||||
<ci-editor-header />
|
||||
<text-editor :value="ciFileContent" v-on="$listeners" />
|
||||
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
|
||||
</editor-tab>
|
||||
<editor-tab
|
||||
class="gl-mb-3"
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
mutation updateCommitSha($commitSha: String) {
|
||||
updateCommitSha(commitSha: $commitSha) @client
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
query getCommitSha {
|
||||
commitSha @client
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import produce from 'immer';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import getCommitShaQuery from './queries/client/commit_sha.graphql';
|
||||
import getCurrentBranchQuery from './queries/client/current_branch.graphql';
|
||||
import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql';
|
||||
|
||||
|
@ -32,14 +31,6 @@ export const resolvers = {
|
|||
__typename: 'CiLintContent',
|
||||
}));
|
||||
},
|
||||
updateCommitSha: (_, { commitSha }, { cache }) => {
|
||||
cache.writeQuery({
|
||||
query: getCommitShaQuery,
|
||||
data: produce(cache.readQuery({ query: getCommitShaQuery }), (draftData) => {
|
||||
draftData.commitSha = commitSha;
|
||||
}),
|
||||
});
|
||||
},
|
||||
updateCurrentBranch: (_, { currentBranch }, { cache }) => {
|
||||
cache.writeQuery({
|
||||
query: getCurrentBranchQuery,
|
||||
|
|
|
@ -4,7 +4,6 @@ import VueApollo from 'vue-apollo';
|
|||
import createDefaultClient from '~/lib/graphql';
|
||||
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
|
||||
import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
|
||||
import getCommitSha from './graphql/queries/client/commit_sha.graphql';
|
||||
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
|
||||
import getLastCommitBranchQuery from './graphql/queries/client/last_commit_branch.query.graphql';
|
||||
import getPipelineEtag from './graphql/queries/client/pipeline_etag.graphql';
|
||||
|
@ -26,7 +25,6 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
|
|||
|
||||
const {
|
||||
// Add to apollo cache as it can be updated by future queries
|
||||
commitSha,
|
||||
initialBranchName,
|
||||
pipelineEtag,
|
||||
// Add to provide/inject API for static values
|
||||
|
@ -69,13 +67,6 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
|
|||
},
|
||||
});
|
||||
|
||||
cache.writeQuery({
|
||||
query: getCommitSha,
|
||||
data: {
|
||||
commitSha,
|
||||
},
|
||||
});
|
||||
|
||||
cache.writeQuery({
|
||||
query: getPipelineEtag,
|
||||
data: {
|
||||
|
|
|
@ -16,11 +16,9 @@ import {
|
|||
LOAD_FAILURE_UNKNOWN,
|
||||
STARTER_TEMPLATE_NAME,
|
||||
} from './constants';
|
||||
import updateCommitShaMutation from './graphql/mutations/update_commit_sha.mutation.graphql';
|
||||
import getBlobContent from './graphql/queries/blob_content.graphql';
|
||||
import getCiConfigData from './graphql/queries/ci_config.graphql';
|
||||
import getAppStatus from './graphql/queries/client/app_status.graphql';
|
||||
import getCommitSha from './graphql/queries/client/commit_sha.graphql';
|
||||
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
|
||||
import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
|
||||
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
|
||||
|
@ -156,7 +154,32 @@ export default {
|
|||
query: getAppStatus,
|
||||
},
|
||||
commitSha: {
|
||||
query: getCommitSha,
|
||||
query: getLatestCommitShaQuery,
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.projectFullPath,
|
||||
ref: this.currentBranch,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
const pipelineNodes = data.project?.pipelines?.nodes ?? [];
|
||||
|
||||
// it's possible to query for the commit sha too early after an update
|
||||
// (e.g. after committing a new branch, we might query for the commit sha
|
||||
// but the pipeline nodes are still empty).
|
||||
// in this case, we start polling until we get a commit sha.
|
||||
if (pipelineNodes.length === 0) {
|
||||
if (![EDITOR_APP_STATUS_LOADING, EDITOR_APP_STATUS_EMPTY].includes(this.appStatus)) {
|
||||
this.$apollo.queries.commitSha.startPolling(1000);
|
||||
return this.commitSha;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
this.$apollo.queries.commitSha.stopPolling();
|
||||
return pipelineNodes[0].sha;
|
||||
},
|
||||
},
|
||||
currentBranch: {
|
||||
query: getCurrentBranch,
|
||||
|
@ -257,38 +280,6 @@ export default {
|
|||
updateCiConfig(ciFileContent) {
|
||||
this.currentCiFileContent = ciFileContent;
|
||||
},
|
||||
async updateCommitSha({ newBranch }) {
|
||||
let fetchResults;
|
||||
|
||||
try {
|
||||
fetchResults = await this.$apollo.query({
|
||||
query: getLatestCommitShaQuery,
|
||||
variables: {
|
||||
projectPath: this.projectFullPath,
|
||||
ref: newBranch,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
this.showFetchError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fetchResults.errors?.length > 0) {
|
||||
this.showFetchError();
|
||||
return;
|
||||
}
|
||||
|
||||
const pipelineNodes = fetchResults?.data?.project?.pipelines?.nodes ?? [];
|
||||
if (pipelineNodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commitSha = pipelineNodes[0].sha;
|
||||
this.$apollo.mutate({
|
||||
mutation: updateCommitShaMutation,
|
||||
variables: { commitSha },
|
||||
});
|
||||
},
|
||||
updateOnCommit({ type }) {
|
||||
this.reportSuccess(type);
|
||||
|
||||
|
@ -336,12 +327,12 @@ export default {
|
|||
:ci-config-data="ciConfigData"
|
||||
:ci-file-content="currentCiFileContent"
|
||||
:is-new-ci-config-file="isNewCiConfigFile"
|
||||
:commit-sha="commitSha"
|
||||
@commit="updateOnCommit"
|
||||
@resetContent="resetContent"
|
||||
@showError="showErrorAlert"
|
||||
@refetchContent="refetchContent"
|
||||
@updateCiConfig="updateCiConfig"
|
||||
@updateCommitSha="updateCommitSha"
|
||||
/>
|
||||
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
|
||||
</div>
|
||||
|
|
|
@ -25,6 +25,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
commitSha: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
isNewCiConfigFile: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
@ -56,15 +61,22 @@ export default {
|
|||
<pipeline-editor-file-nav v-on="$listeners" />
|
||||
<pipeline-editor-header
|
||||
:ci-config-data="ciConfigData"
|
||||
:commit-sha="commitSha"
|
||||
:is-new-ci-config-file="isNewCiConfigFile"
|
||||
/>
|
||||
<pipeline-editor-tabs
|
||||
:ci-config-data="ciConfigData"
|
||||
:ci-file-content="ciFileContent"
|
||||
:commit-sha="commitSha"
|
||||
v-on="$listeners"
|
||||
@set-current-tab="setCurrentTab"
|
||||
/>
|
||||
<commit-section v-if="showCommitForm" :ci-file-content="ciFileContent" v-on="$listeners" />
|
||||
<commit-section
|
||||
v-if="showCommitForm"
|
||||
:ci-file-content="ciFileContent"
|
||||
:commit-sha="commitSha"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
<pipeline-editor-drawer v-if="showPipelineDrawer" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
td,
|
||||
th,
|
||||
li {
|
||||
:only-child {
|
||||
:first-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -386,15 +386,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.boards-add-list > .btn {
|
||||
text-align: left;
|
||||
|
||||
> svg {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.droplab-dropdown .dropdown-menu .filter-dropdown-item {
|
||||
|
|
|
@ -15,14 +15,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-issues-board-new {
|
||||
width: 320px;
|
||||
|
||||
.dropdown-content {
|
||||
max-height: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
.issue-board-dropdown-content {
|
||||
margin: 0;
|
||||
padding: $gl-padding-4 $gl-padding $gl-padding;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
self.gitlab_schema = :gitlab_main
|
||||
self.abstract_class = true
|
||||
|
||||
alias_method :reset, :reload
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
module Ci
|
||||
class ApplicationRecord < ::ApplicationRecord
|
||||
self.gitlab_schema = :gitlab_ci
|
||||
self.abstract_class = true
|
||||
|
||||
def self.table_name_prefix
|
||||
|
|
|
@ -12,51 +12,55 @@ module Ci
|
|||
scope :queued_before, ->(time) { where(arel_table[:created_at].lt(time)) }
|
||||
scope :with_instance_runners, -> { where(instance_runners_enabled: true) }
|
||||
|
||||
def self.upsert_from_build!(build)
|
||||
entry = self.new(args_from_build(build))
|
||||
class << self
|
||||
def upsert_from_build!(build)
|
||||
entry = self.new(args_from_build(build))
|
||||
|
||||
entry.validate!
|
||||
entry.validate!
|
||||
|
||||
self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id)
|
||||
end
|
||||
self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id)
|
||||
end
|
||||
|
||||
def self.args_from_build(build)
|
||||
args = {
|
||||
build: build,
|
||||
project: build.project,
|
||||
protected: build.protected?,
|
||||
namespace: build.project.namespace
|
||||
}
|
||||
private
|
||||
|
||||
def args_from_build(build)
|
||||
args = {
|
||||
build: build,
|
||||
project: build.project,
|
||||
protected: build.protected?,
|
||||
namespace: build.project.namespace
|
||||
}
|
||||
|
||||
if Feature.enabled?(:ci_pending_builds_maintain_tags_data, type: :development, default_enabled: :yaml)
|
||||
args.store(:tag_ids, build.tags_ids)
|
||||
end
|
||||
|
||||
if Feature.enabled?(:ci_pending_builds_maintain_shared_runners_data, type: :development, default_enabled: :yaml)
|
||||
args.store(:instance_runners_enabled, shareable?(build))
|
||||
end
|
||||
|
||||
if Feature.enabled?(:ci_pending_builds_maintain_shared_runners_data, type: :development, default_enabled: :yaml)
|
||||
args.merge(instance_runners_enabled: shareable?(build))
|
||||
else
|
||||
args
|
||||
end
|
||||
end
|
||||
private_class_method :args_from_build
|
||||
|
||||
def self.shareable?(build)
|
||||
shared_runner_enabled?(build) &&
|
||||
builds_access_level?(build) &&
|
||||
project_not_removed?(build)
|
||||
end
|
||||
private_class_method :shareable?
|
||||
def shareable?(build)
|
||||
shared_runner_enabled?(build) &&
|
||||
builds_access_level?(build) &&
|
||||
project_not_removed?(build)
|
||||
end
|
||||
|
||||
def self.shared_runner_enabled?(build)
|
||||
build.project.shared_runners.exists?
|
||||
end
|
||||
private_class_method :shared_runner_enabled?
|
||||
def shared_runner_enabled?(build)
|
||||
build.project.shared_runners.exists?
|
||||
end
|
||||
|
||||
def self.project_not_removed?(build)
|
||||
!build.project.pending_delete?
|
||||
end
|
||||
private_class_method :project_not_removed?
|
||||
def project_not_removed?(build)
|
||||
!build.project.pending_delete?
|
||||
end
|
||||
|
||||
def self.builds_access_level?(build)
|
||||
build.project.project_feature.builds_access_level.nil? || build.project.project_feature.builds_access_level > 0
|
||||
def builds_access_level?(build)
|
||||
build.project.project_feature.builds_access_level.nil? ||
|
||||
build.project.project_feature.builds_access_level > 0
|
||||
end
|
||||
end
|
||||
private_class_method :builds_access_level?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
module TaggableQueries
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
MAX_TAGS_IDS = 50
|
||||
|
||||
TooManyTagsError = Class.new(StandardError)
|
||||
|
||||
class_methods do
|
||||
# context is a name `acts_as_taggable context`
|
||||
def arel_tag_names_array(context = :tags)
|
||||
|
@ -34,4 +38,10 @@ module TaggableQueries
|
|||
where("EXISTS (?)", matcher)
|
||||
end
|
||||
end
|
||||
|
||||
def tags_ids
|
||||
tags.limit(MAX_TAGS_IDS).order('id ASC').pluck(:id).tap do |ids|
|
||||
raise TooManyTagsError if ids.size >= MAX_TAGS_IDS
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -254,8 +254,7 @@ class User < ApplicationRecord
|
|||
message: _("%{placeholder} is not a valid color scheme") % { placeholder: '%{value}' } }
|
||||
|
||||
before_validation :sanitize_attrs
|
||||
before_validation :set_public_email, if: :public_email_changed?
|
||||
before_validation :set_commit_email, if: :commit_email_changed?
|
||||
before_validation :reset_secondary_emails, if: :email_changed?
|
||||
before_save :default_private_profile_to_false
|
||||
before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
|
||||
before_save :set_commit_email, if: :commit_email_changed? # in case validation is skipped
|
||||
|
@ -928,24 +927,6 @@ class User < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def notification_email_verified
|
||||
return if read_attribute(:notification_email).blank? || temp_oauth_email?
|
||||
|
||||
errors.add(:notification_email, _("must be an email you have verified")) unless verified_emails.include?(notification_email)
|
||||
end
|
||||
|
||||
def public_email_verified
|
||||
return if public_email.blank?
|
||||
|
||||
errors.add(:public_email, _("must be an email you have verified")) unless verified_emails.include?(public_email)
|
||||
end
|
||||
|
||||
def commit_email_verified
|
||||
return if read_attribute(:commit_email).blank?
|
||||
|
||||
errors.add(:commit_email, _("must be an email you have verified")) unless verified_emails.include?(commit_email)
|
||||
end
|
||||
|
||||
# Define commit_email-related attribute methods explicitly instead of relying
|
||||
# on ActiveRecord to provide them. Some of the specs use the current state of
|
||||
# the model code but an older database schema, so we need to guard against the
|
||||
|
@ -1298,24 +1279,6 @@ class User < ApplicationRecord
|
|||
self.name = self.name.gsub(%r{</?[^>]*>}, '')
|
||||
end
|
||||
|
||||
def set_notification_email
|
||||
if notification_email.blank? || all_emails.exclude?(notification_email)
|
||||
self.notification_email = email
|
||||
end
|
||||
end
|
||||
|
||||
def set_public_email
|
||||
if public_email.blank? || all_emails.exclude?(public_email)
|
||||
self.public_email = ''
|
||||
end
|
||||
end
|
||||
|
||||
def set_commit_email
|
||||
if commit_email.blank? || verified_emails.exclude?(commit_email)
|
||||
self.commit_email = nil
|
||||
end
|
||||
end
|
||||
|
||||
def unset_secondary_emails_matching_deleted_email!(deleted_email)
|
||||
secondary_email_attribute_changed = false
|
||||
SECONDARY_EMAIL_ATTRIBUTES.each do |attribute|
|
||||
|
@ -2025,6 +1988,48 @@ class User < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def notification_email_verified
|
||||
return if read_attribute(:notification_email).blank? || temp_oauth_email?
|
||||
|
||||
errors.add(:notification_email, _("must be an email you have verified")) unless verified_emails.include?(notification_email)
|
||||
end
|
||||
|
||||
def public_email_verified
|
||||
return if public_email.blank?
|
||||
|
||||
errors.add(:public_email, _("must be an email you have verified")) unless verified_emails.include?(public_email)
|
||||
end
|
||||
|
||||
def commit_email_verified
|
||||
return if read_attribute(:commit_email).blank?
|
||||
|
||||
errors.add(:commit_email, _("must be an email you have verified")) unless verified_emails.include?(commit_email)
|
||||
end
|
||||
|
||||
def set_notification_email
|
||||
if notification_email.blank? || verified_emails.exclude?(notification_email)
|
||||
self.notification_email = nil
|
||||
end
|
||||
end
|
||||
|
||||
def set_public_email
|
||||
if public_email.blank? || verified_emails.exclude?(public_email)
|
||||
self.public_email = ''
|
||||
end
|
||||
end
|
||||
|
||||
def set_commit_email
|
||||
if commit_email.blank? || verified_emails.exclude?(commit_email)
|
||||
self.commit_email = nil
|
||||
end
|
||||
end
|
||||
|
||||
def reset_secondary_emails
|
||||
set_public_email
|
||||
set_commit_email
|
||||
set_notification_email
|
||||
end
|
||||
|
||||
def callouts_by_feature_name
|
||||
@callouts_by_feature_name ||= callouts.index_by(&:feature_name)
|
||||
end
|
||||
|
|
|
@ -17,11 +17,19 @@ module Ci
|
|||
end
|
||||
|
||||
def builds_matching_tag_ids(relation, ids)
|
||||
relation.merge(CommitStatus.matches_tag_ids(ids, table: 'ci_pending_builds', column: 'build_id'))
|
||||
if ::Feature.enabled?(:ci_queueing_denormalize_tags_information, runner, default_enabled: :yaml)
|
||||
relation.where('tag_ids <@ ARRAY[?]::int[]', runner.tags_ids)
|
||||
else
|
||||
relation.merge(CommitStatus.matches_tag_ids(ids, table: 'ci_pending_builds', column: 'build_id'))
|
||||
end
|
||||
end
|
||||
|
||||
def builds_with_any_tags(relation)
|
||||
relation.merge(CommitStatus.with_any_tags(table: 'ci_pending_builds', column: 'build_id'))
|
||||
if ::Feature.enabled?(:ci_queueing_denormalize_tags_information, runner, default_enabled: :yaml)
|
||||
relation.where('cardinality(tag_ids) > 0')
|
||||
else
|
||||
relation.merge(CommitStatus.with_any_tags(table: 'ci_pending_builds', column: 'build_id'))
|
||||
end
|
||||
end
|
||||
|
||||
def order(relation)
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
.dropdown.gl-display-flex.gl-align-items-center.gl-ml-3#js-add-list
|
||||
%button.gl-button.btn.btn-confirm.js-new-board-list{ type: "button", data: board_list_data }
|
||||
Add list
|
||||
.dropdown-menu.dropdown-extended-height.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
|
||||
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
|
||||
- if can?(current_user, :admin_label, board.resource_parent)
|
||||
= render partial: "shared/issuable/label_page_create", locals: { show_add_list: true, add_list: true, add_list_class: 'd-none' }
|
||||
= dropdown_loading
|
|
@ -207,10 +207,7 @@
|
|||
#js-board-epics-swimlanes-toggle
|
||||
.js-board-config{ data: { can_admin_list: user_can_admin_list.to_s, has_scope: board.scoped?.to_s } }
|
||||
- if user_can_admin_list
|
||||
- if Feature.enabled?(:board_new_list, board.resource_parent, default_enabled: :yaml) || board.to_type == "EpicBoard"
|
||||
.js-create-column-trigger{ data: board_list_data }
|
||||
- else
|
||||
= render 'shared/issuable/board_create_list_dropdown', board: board
|
||||
.js-create-column-trigger{ data: board_list_data }
|
||||
#js-toggle-focus-btn
|
||||
- elsif type != :productivity_analytics && show_sorting_dropdown
|
||||
= render 'shared/issuable/sort_dropdown'
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: between_uses_list_commits
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67591
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337960
|
||||
name: ci_build_tags_limit
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68380
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338929
|
||||
milestone: '14.2'
|
||||
type: development
|
||||
group: group::source code
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273755
|
|||
milestone: '13.6'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: board_new_list
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52061
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299366
|
||||
milestone: '13.8'
|
||||
name: ci_pending_builds_maintain_tags_data
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65648
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338363
|
||||
milestone: '14.2'
|
||||
type: development
|
||||
group: group::project management
|
||||
default_enabled: true
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: track_unique_visits
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33146
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255994
|
||||
milestone: '13.2'
|
||||
name: ci_queueing_denormalize_tags_information
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65648
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338366
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::optimize
|
||||
default_enabled: true
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This parameter describes a virtual context to indicate
|
||||
# table affinity to other tables.
|
||||
#
|
||||
# Table affinity limits cross-joins, cross-modifications,
|
||||
# foreign keys and validates relationship between tables
|
||||
#
|
||||
# By default it is undefined
|
||||
ActiveRecord::Base.class_attribute :gitlab_schema, default: nil
|
|
@ -13,3 +13,7 @@ raise "Counter cache is not disabled" if
|
|||
ActsAsTaggableOn::Tagging.include IgnorableColumns
|
||||
ActsAsTaggableOn::Tagging.ignore_column :id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
|
||||
ActsAsTaggableOn::Tagging.ignore_column :taggable_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
|
||||
|
||||
# The tags and taggings are supposed to be part of `gitlab_ci`
|
||||
ActsAsTaggableOn::Tag.gitlab_schema = :gitlab_ci
|
||||
ActsAsTaggableOn::Tagging.gitlab_schema = :gitlab_ci
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
---
|
||||
data_category: optional
|
||||
key_path: usage_activity_by_stage_monthly.manage.events
|
||||
description:
|
||||
description: Number of distinct users who have generated a manage event by month
|
||||
product_section: dev
|
||||
product_stage:
|
||||
product_stage: manage
|
||||
product_group: group::manage
|
||||
product_category:
|
||||
value_type: number
|
||||
status: data_available
|
||||
time_frame: 28d
|
||||
data_source:
|
||||
data_source: database
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
skip_validation: true
|
||||
- premium
|
||||
- ultimate
|
||||
performance_indicator_type:
|
||||
- umau
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTagsArrayToCiPendingBuilds < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_column :ci_pending_builds, :tag_ids, :integer, array: true, default: []
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_column :ci_pending_builds, :tag_ids
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTagIdsIndexToCiPendingBuild < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_ci_pending_builds_on_tag_ids'
|
||||
|
||||
def up
|
||||
add_concurrent_index(:ci_pending_builds, :tag_ids, name: INDEX_NAME, where: 'cardinality(tag_ids) > 0')
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(:ci_pending_builds, name: INDEX_NAME)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PrepareIndexesForEventsBigintConversion < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
TABLE_NAME = 'events'
|
||||
|
||||
def up
|
||||
prepare_async_index TABLE_NAME, :id_convert_to_bigint, unique: true,
|
||||
name: :index_events_on_id_convert_to_bigint
|
||||
|
||||
prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint],
|
||||
name: :index_events_on_project_id_and_id_convert_to_bigint
|
||||
|
||||
prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint], order: { id_convert_to_bigint: :desc },
|
||||
where: 'action = 7', name: :index_events_on_project_id_and_id_bigint_desc_on_merged_action
|
||||
end
|
||||
|
||||
def down
|
||||
unprepare_async_index_by_name TABLE_NAME, :index_events_on_id_convert_to_bigint
|
||||
unprepare_async_index_by_name TABLE_NAME, :index_events_on_project_id_and_id_convert_to_bigint
|
||||
unprepare_async_index_by_name TABLE_NAME, :index_events_on_project_id_and_id_bigint_desc_on_merged_action
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
837b9a56114c63064379cf276a3c7e2bbe845af9022a542c4fcec94a25062017
|
|
@ -0,0 +1 @@
|
|||
360bb1c16c93d7a6564ed70fa2dea4212e1fd00d101cfdc9017b54f67eae797d
|
|
@ -0,0 +1 @@
|
|||
88ca485c8513df96b1f1aec1585c385223dc53889e547db42b509b0cd1bea9b7
|
|
@ -10903,7 +10903,8 @@ CREATE TABLE ci_pending_builds (
|
|||
protected boolean DEFAULT false NOT NULL,
|
||||
instance_runners_enabled boolean DEFAULT false NOT NULL,
|
||||
namespace_id bigint,
|
||||
minutes_exceeded boolean DEFAULT false NOT NULL
|
||||
minutes_exceeded boolean DEFAULT false NOT NULL,
|
||||
tag_ids integer[] DEFAULT '{}'::integer[]
|
||||
);
|
||||
|
||||
CREATE SEQUENCE ci_pending_builds_id_seq
|
||||
|
@ -23472,6 +23473,8 @@ CREATE INDEX index_ci_pending_builds_on_namespace_id ON ci_pending_builds USING
|
|||
|
||||
CREATE INDEX index_ci_pending_builds_on_project_id ON ci_pending_builds USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_ci_pending_builds_on_tag_ids ON ci_pending_builds USING btree (tag_ids) WHERE (cardinality(tag_ids) > 0);
|
||||
|
||||
CREATE INDEX index_ci_pipeline_artifacts_failed_verification ON ci_pipeline_artifacts USING btree (verification_retry_at NULLS FIRST) WHERE (verification_state = 3);
|
||||
|
||||
CREATE INDEX index_ci_pipeline_artifacts_needs_verification ON ci_pipeline_artifacts USING btree (verification_state) WHERE ((verification_state = 0) OR (verification_state = 3));
|
||||
|
|
|
@ -1885,6 +1885,9 @@ variables:
|
|||
- echo "Hello runner selector feature"
|
||||
```
|
||||
|
||||
NOTE:
|
||||
In [GitLab 14.3](https://gitlab.com/gitlab-org/gitlab/-/issues/338479) and later, the number of tags must be less than `50`.
|
||||
|
||||
### `allow_failure`
|
||||
|
||||
Use `allow_failure` when you want to let a job fail without impacting the rest of the CI
|
||||
|
|
|
@ -22,7 +22,7 @@ Be wary of [the limitations that come with using Hamlit](https://github.com/k0ku
|
|||
We also use [SCSS](https://sass-lang.com) and plain JavaScript with
|
||||
modern ECMAScript standards supported through [Babel](https://babeljs.io/) and ES module support through [webpack](https://webpack.js.org/).
|
||||
|
||||
Working with our frontend assets requires Node (v10.13.0 or greater) and Yarn
|
||||
Working with our frontend assets requires Node (v12.22.1 or greater) and Yarn
|
||||
(v1.10.0 or greater). You can find information on how to install these on our
|
||||
[installation guide](../../install/installation.md#4-node).
|
||||
|
||||
|
|
|
@ -339,14 +339,14 @@ As in other list types, click the trash icon to remove a list.
|
|||
### Iteration lists **(PREMIUM)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250479) in GitLab 13.11.
|
||||
> - [Deployed behind the `board_new_list` and `iteration_board_lists` feature flags](../feature_flags.md), enabled by default.
|
||||
> - Enabled on GitLab.com.
|
||||
> - Recommended for production use.
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to disable the feature flags: [`board_new_list`](#enable-or-disable-new-add-list-form) and [`iteration_board_lists`](#enable-or-disable-iteration-lists-in-boards). **(PREMIUM SELF)**
|
||||
> - Enabled on GitLab.com and is ready for production use.
|
||||
> - Enabled with `iteration_board_lists` flag for self-managed GitLab and is ready for production use.
|
||||
> GitLab administrators can opt to [disable the feature flag](#enable-or-disable-iteration-lists-in-boards).
|
||||
|
||||
There can be
|
||||
[risks when disabling released features](../../administration/feature_flags.md#risks-when-disabling-released-features).
|
||||
Refer to this feature's version history for more details.
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature, ask an
|
||||
administrator to [disable the `iteration_board_lists` flag](../../administration/feature_flags.md).
|
||||
On GitLab.com, this feature is available.
|
||||
|
||||
You're also able to create lists of an iteration.
|
||||
These are lists that filter issues by the assigned
|
||||
|
@ -675,10 +675,6 @@ A few things to remember:
|
|||
|
||||
### Enable or disable GraphQL-based issue boards **(FREE SELF)**
|
||||
|
||||
NOTE:
|
||||
When enabling GraphQL-based issue boards, you must also enable the
|
||||
[new add list form](#enable-or-disable-new-add-list-form).
|
||||
|
||||
It is deployed behind a feature flag that is **enabled by default** as of GitLab 14.1.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
|
||||
can disable it.
|
||||
|
@ -695,30 +691,8 @@ To disable it:
|
|||
Feature.disable(:graphql_board_lists)
|
||||
```
|
||||
|
||||
### Enable or disable new add list form **(FREE SELF)**
|
||||
|
||||
The new form for adding lists is under development but ready for production use. It is
|
||||
deployed behind a feature flag that is **enabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
|
||||
can disable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:board_new_list)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:board_new_list)
|
||||
```
|
||||
|
||||
### Enable or disable iteration lists in boards **(PREMIUM SELF)**
|
||||
|
||||
NOTE:
|
||||
When disabling iteration lists in boards, you also need to disable the [new add list form](#enable-or-disable-new-add-list-form).
|
||||
|
||||
The iteration list is under development but ready for production use. It is
|
||||
deployed behind a feature flag that is **enabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
|
||||
|
|
|
@ -45,7 +45,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def read_zip_file!(file_path)
|
||||
if ::Feature.enabled?(:ci_new_artifact_file_reader, job.project, default_enabled: false)
|
||||
if ::Feature.enabled?(:ci_new_artifact_file_reader, job.project, default_enabled: :yaml)
|
||||
read_with_new_artifact_file_reader(file_path)
|
||||
else
|
||||
read_with_legacy_artifact_file_reader(file_path)
|
||||
|
|
|
@ -53,7 +53,7 @@ module Gitlab
|
|||
description: 'Set retry default value.',
|
||||
inherit: false
|
||||
|
||||
entry :tags, ::Gitlab::Config::Entry::ArrayOfStrings,
|
||||
entry :tags, Entry::Tags,
|
||||
description: 'Set the default tags.',
|
||||
inherit: false
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ module Gitlab
|
|||
description: 'Retry configuration for this job.',
|
||||
inherit: true
|
||||
|
||||
entry :tags, ::Gitlab::Config::Entry::ArrayOfStrings,
|
||||
entry :tags, Entry::Tags,
|
||||
description: 'Set the tags.',
|
||||
inherit: true
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
class Config
|
||||
module Entry
|
||||
##
|
||||
# Entry that represents an array of tags.
|
||||
#
|
||||
class Tags < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
|
||||
TAGS_LIMIT = 50
|
||||
|
||||
validations do
|
||||
validates :config, array_of_strings: true
|
||||
|
||||
validate do
|
||||
next unless ::Feature.enabled?(:ci_build_tags_limit, default_enabled: :yaml)
|
||||
|
||||
if config.is_a?(Array) && config.size >= TAGS_LIMIT
|
||||
errors.add(:config, _("must be less than the limit of %{tag_limit} tags") % { tag_limit: TAGS_LIMIT })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -116,13 +116,9 @@ module Gitlab
|
|||
return [] if Gitlab::Git.blank_ref?(base) || Gitlab::Git.blank_ref?(head)
|
||||
|
||||
wrapped_gitaly_errors do
|
||||
if Feature.enabled?(:between_uses_list_commits, default_enabled: :yaml)
|
||||
revisions = [head, "^#{base}"] # base..head
|
||||
revisions = [head, "^#{base}"] # base..head
|
||||
|
||||
repo.gitaly_commit_client.list_commits(revisions, reverse: true)
|
||||
else
|
||||
repo.gitaly_commit_client.between(base, head)
|
||||
end
|
||||
repo.gitaly_commit_client.list_commits(revisions, reverse: true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,84 +2,67 @@
|
|||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: i_analytics_dev_ops_adoption
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: i_analytics_dev_ops_score
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: p_analytics_merge_request
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: i_analytics_instance_statistics
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: g_analytics_contribution
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: g_analytics_insights
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: g_analytics_issues
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: g_analytics_productivity
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: g_analytics_valuestream
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: p_analytics_pipelines
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: p_analytics_code_reviews
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: p_analytics_valuestream
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: p_analytics_insights
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: p_analytics_issues
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: p_analytics_repo
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: i_analytics_cohorts
|
||||
category: analytics
|
||||
redis_slot: analytics
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
|
|
|
@ -4,27 +4,22 @@
|
|||
redis_slot: compliance
|
||||
category: compliance
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: g_compliance_audit_events
|
||||
category: compliance
|
||||
redis_slot: compliance
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: i_compliance_audit_events
|
||||
category: compliance
|
||||
redis_slot: compliance
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: i_compliance_credential_inventory
|
||||
category: compliance
|
||||
redis_slot: compliance
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: a_compliance_audit_events_api
|
||||
category: compliance
|
||||
redis_slot: compliance
|
||||
aggregation: weekly
|
||||
feature_flag: track_unique_visits
|
||||
- name: g_edit_by_web_ide
|
||||
category: ide_edit
|
||||
redis_slot: edit
|
||||
|
|
|
@ -4650,9 +4650,6 @@ msgstr ""
|
|||
msgid "Assignee lists not available with your current license"
|
||||
msgstr ""
|
||||
|
||||
msgid "Assignee lists show all issues assigned to the selected user."
|
||||
msgstr ""
|
||||
|
||||
msgid "Assignee(s)"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19448,9 +19445,6 @@ msgstr ""
|
|||
msgid "Label actions dropdown"
|
||||
msgstr ""
|
||||
|
||||
msgid "Label lists show all issues with the selected label."
|
||||
msgstr ""
|
||||
|
||||
msgid "Label priority"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21526,9 +21520,6 @@ msgstr ""
|
|||
msgid "Milestone lists not available with your current license"
|
||||
msgstr ""
|
||||
|
||||
msgid "Milestone lists show all issues from the selected milestone."
|
||||
msgstr ""
|
||||
|
||||
msgid "MilestoneCombobox|An error occurred while searching for milestones"
|
||||
msgstr ""
|
||||
|
||||
|
@ -39962,6 +39953,9 @@ msgstr ""
|
|||
msgid "must be inside the fork network"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be less than the limit of %{tag_limit} tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be unique by status and elapsed time within a policy"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -277,7 +277,7 @@
|
|||
"chokidar": "^3.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0",
|
||||
"node": ">=12.22.1",
|
||||
"yarn": "^1.10.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,6 @@ FactoryBot.define do
|
|||
confirmation_token { nil }
|
||||
can_create_group { true }
|
||||
|
||||
after(:stub) do |user|
|
||||
user.notification_email = user.email
|
||||
end
|
||||
|
||||
trait :admin do
|
||||
admin { true }
|
||||
end
|
||||
|
|
|
@ -4,8 +4,20 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe "Dashboard Issues Feed" do
|
||||
describe "GET /issues" do
|
||||
let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
|
||||
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
|
||||
let!(:user) do
|
||||
user = create(:user, email: 'private1@example.com')
|
||||
public_email = create(:email, :confirmed, user: user, email: 'public1@example.com')
|
||||
user.update!(public_email: public_email.email)
|
||||
user
|
||||
end
|
||||
|
||||
let!(:assignee) do
|
||||
user = create(:user, email: 'private2@example.com')
|
||||
public_email = create(:email, :confirmed, user: user, email: 'public2@example.com')
|
||||
user.update!(public_email: public_email.email)
|
||||
user
|
||||
end
|
||||
|
||||
let!(:project1) { create(:project) }
|
||||
let!(:project2) { create(:project) }
|
||||
|
||||
|
|
|
@ -4,11 +4,23 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'Issues Feed' do
|
||||
describe 'GET /issues' do
|
||||
let_it_be_with_reload(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
|
||||
let_it_be(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:issue) { create(:issue, author: user, assignees: [assignee], project: project, due_date: Date.today) }
|
||||
let_it_be_with_reload(:user) do
|
||||
user = create(:user, email: 'private1@example.com')
|
||||
public_email = create(:email, :confirmed, user: user, email: 'public1@example.com')
|
||||
user.update!(public_email: public_email.email)
|
||||
user
|
||||
end
|
||||
|
||||
let_it_be(:assignee) do
|
||||
user = create(:user, email: 'private2@example.com')
|
||||
public_email = create(:email, :confirmed, user: user, email: 'public2@example.com')
|
||||
user.update!(public_email: public_email.email)
|
||||
user
|
||||
end
|
||||
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:issue) { create(:issue, author: user, assignees: [assignee], project: project, due_date: Date.today) }
|
||||
let_it_be(:issuable) { issue } # "alias" for shared examples
|
||||
|
||||
before_all do
|
||||
|
|
|
@ -17,6 +17,8 @@ RSpec.describe 'User adds lists', :js do
|
|||
let_it_be(:project_label) { create(:label, project: project) }
|
||||
let_it_be(:group_backlog_list) { create(:backlog_list, board: group_board) }
|
||||
let_it_be(:project_backlog_list) { create(:backlog_list, board: project_board) }
|
||||
let_it_be(:backlog) { create(:group_label, group: group, name: 'Backlog') }
|
||||
let_it_be(:closed) { create(:group_label, group: group, name: 'Closed') }
|
||||
|
||||
let_it_be(:issue) { create(:labeled_issue, project: project, labels: [group_label, project_label]) }
|
||||
|
||||
|
@ -25,15 +27,11 @@ RSpec.describe 'User adds lists', :js do
|
|||
group.add_owner(user)
|
||||
end
|
||||
|
||||
where(:board_type, :graphql_board_lists_enabled, :board_new_list_enabled) do
|
||||
:project | true | true
|
||||
:project | false | true
|
||||
:project | true | false
|
||||
:project | false | false
|
||||
:group | true | true
|
||||
:group | false | true
|
||||
:group | true | false
|
||||
:group | false | false
|
||||
where(:board_type, :graphql_board_lists_enabled) do
|
||||
:project | true
|
||||
:project | false
|
||||
:group | true
|
||||
:group | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
@ -43,8 +41,7 @@ RSpec.describe 'User adds lists', :js do
|
|||
set_cookie('sidebar_collapsed', 'true')
|
||||
|
||||
stub_feature_flags(
|
||||
graphql_board_lists: graphql_board_lists_enabled,
|
||||
board_new_list: board_new_list_enabled
|
||||
graphql_board_lists: graphql_board_lists_enabled
|
||||
)
|
||||
|
||||
if board_type == :project
|
||||
|
@ -57,39 +54,44 @@ RSpec.describe 'User adds lists', :js do
|
|||
end
|
||||
|
||||
it 'creates new column for label containing labeled issue' do
|
||||
click_button button_text(board_new_list_enabled)
|
||||
click_button 'Create list'
|
||||
wait_for_all_requests
|
||||
|
||||
select_label(board_new_list_enabled, group_label)
|
||||
select_label(group_label)
|
||||
|
||||
wait_for_all_requests
|
||||
|
||||
expect(page).to have_selector('.board', text: group_label.title)
|
||||
expect(find('.board:nth-child(2) .board-card')).to have_content(issue.title)
|
||||
end
|
||||
end
|
||||
|
||||
def select_label(board_new_list_enabled, label)
|
||||
if board_new_list_enabled
|
||||
click_button 'Select a label'
|
||||
it 'creates new list for Backlog and closed labels' do
|
||||
click_button 'Create list'
|
||||
wait_for_requests
|
||||
|
||||
find('label', text: label.title).click
|
||||
select_label(backlog)
|
||||
|
||||
click_button 'Add to board'
|
||||
click_button 'Create list'
|
||||
wait_for_requests
|
||||
|
||||
wait_for_all_requests
|
||||
else
|
||||
page.within('.dropdown-menu-issues-board-new') do
|
||||
click_link label.title
|
||||
end
|
||||
select_label(closed)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.board', text: closed.title)
|
||||
expect(find('.board:nth-child(2) .board-header')).to have_content(backlog.title)
|
||||
expect(find('.board:nth-child(3) .board-header')).to have_content(closed.title)
|
||||
expect(find('.board:nth-child(4) .board-header')).to have_content('Closed')
|
||||
end
|
||||
end
|
||||
|
||||
def button_text(board_new_list_enabled)
|
||||
if board_new_list_enabled
|
||||
'Create list'
|
||||
else
|
||||
'Add list'
|
||||
end
|
||||
def select_label(label)
|
||||
click_button 'Select a label'
|
||||
|
||||
find('label', text: label.title).click
|
||||
|
||||
click_button 'Add to board'
|
||||
|
||||
wait_for_all_requests
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,8 +4,20 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'Dashboard Issues Calendar Feed' do
|
||||
describe 'GET /issues' do
|
||||
let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
|
||||
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
|
||||
let!(:user) do
|
||||
user = create(:user, email: 'private1@example.com')
|
||||
public_email = create(:email, :confirmed, user: user, email: 'public1@example.com')
|
||||
user.update!(public_email: public_email.email)
|
||||
user
|
||||
end
|
||||
|
||||
let!(:assignee) do
|
||||
user = create(:user, email: 'private2@example.com')
|
||||
public_email = create(:email, :confirmed, user: user, email: 'public2@example.com')
|
||||
user.update!(public_email: public_email.email)
|
||||
user
|
||||
end
|
||||
|
||||
let!(:project) { create(:project) }
|
||||
let(:milestone) { create(:milestone, project_id: project.id, title: 'v1.0') }
|
||||
|
||||
|
|
|
@ -4,8 +4,20 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'Group Issues Calendar Feed' do
|
||||
describe 'GET /issues' do
|
||||
let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
|
||||
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
|
||||
let!(:user) do
|
||||
user = create(:user, email: 'private1@example.com')
|
||||
public_email = create(:email, :confirmed, user: user, email: 'public1@example.com')
|
||||
user.update!(public_email: public_email.email)
|
||||
user
|
||||
end
|
||||
|
||||
let!(:assignee) do
|
||||
user = create(:user, email: 'private2@example.com')
|
||||
public_email = create(:email, :confirmed, user: user, email: 'public2@example.com')
|
||||
user.update!(public_email: public_email.email)
|
||||
user
|
||||
end
|
||||
|
||||
let!(:group) { create(:group) }
|
||||
let!(:project) { create(:project, group: group) }
|
||||
|
||||
|
|
|
@ -4,8 +4,20 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'Project Issues Calendar Feed' do
|
||||
describe 'GET /issues' do
|
||||
let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
|
||||
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
|
||||
let!(:user) do
|
||||
user = create(:user, email: 'private1@example.com')
|
||||
public_email = create(:email, :confirmed, user: user, email: 'public1@example.com')
|
||||
user.update!(public_email: public_email.email)
|
||||
user
|
||||
end
|
||||
|
||||
let!(:assignee) do
|
||||
user = create(:user, email: 'private2@example.com')
|
||||
public_email = create(:email, :confirmed, user: user, email: 'public2@example.com')
|
||||
user.update!(public_email: public_email.email)
|
||||
user
|
||||
end
|
||||
|
||||
let!(:project) { create(:project) }
|
||||
let!(:issue) { create(:issue, author: user, assignees: [assignee], project: project) }
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ RSpec.describe "User views issues" do
|
|||
.and have_content(open_issue2.title)
|
||||
.and have_no_content(closed_issue.title)
|
||||
.and have_content(moved_open_issue.title)
|
||||
.and have_no_selector(".js-new-board-list")
|
||||
.and have_no_content('Create list')
|
||||
end
|
||||
|
||||
it "opens issues by label" do
|
||||
|
@ -65,7 +65,7 @@ RSpec.describe "User views issues" do
|
|||
.and have_no_content(open_issue1.title)
|
||||
.and have_no_content(open_issue2.title)
|
||||
.and have_no_content(moved_open_issue.title)
|
||||
.and have_no_selector(".js-new-board-list")
|
||||
.and have_no_content('Create list')
|
||||
end
|
||||
|
||||
include_examples "opens issue from list" do
|
||||
|
@ -87,7 +87,7 @@ RSpec.describe "User views issues" do
|
|||
.and have_content(open_issue2.title)
|
||||
.and have_content(moved_open_issue.title)
|
||||
.and have_no_content('CLOSED (MOVED)')
|
||||
.and have_no_selector(".js-new-board-list")
|
||||
.and have_no_content('Create list')
|
||||
end
|
||||
|
||||
include_examples "opens issue from list" do
|
||||
|
|
|
@ -17,7 +17,6 @@ RSpec.describe 'Labels Hierarchy', :js do
|
|||
let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') }
|
||||
|
||||
before do
|
||||
stub_feature_flags(board_new_list: false)
|
||||
grandparent.add_owner(user)
|
||||
|
||||
sign_in(user)
|
||||
|
@ -307,88 +306,4 @@ RSpec.describe 'Labels Hierarchy', :js do
|
|||
it_behaves_like 'filtering by ancestor labels for groups', true
|
||||
end
|
||||
end
|
||||
|
||||
context 'creating boards lists' do
|
||||
before do
|
||||
stub_feature_flags(board_new_list: false)
|
||||
end
|
||||
|
||||
context 'on project boards' do
|
||||
let(:board) { create(:board, project: project_1) }
|
||||
|
||||
before do
|
||||
project_1.add_developer(user)
|
||||
visit project_board_path(project_1, board)
|
||||
find('.js-new-board-list').click
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'creates lists from all ancestor labels' do
|
||||
[grandparent_group_label, parent_group_label, project_label_1].each do |label|
|
||||
find('a', text: label.title).click
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title)
|
||||
expect(page).to have_selector('.board-title-text', text: parent_group_label.title)
|
||||
expect(page).to have_selector('.board-title-text', text: project_label_1.title)
|
||||
end
|
||||
end
|
||||
|
||||
context 'on group boards' do
|
||||
let(:board) { create(:board, group: parent) }
|
||||
|
||||
before do
|
||||
parent.add_developer(user)
|
||||
visit group_board_path(parent, board)
|
||||
find('.js-new-board-list').click
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
context 'when graphql_board_lists FF enabled' do
|
||||
it 'creates lists from all ancestor group labels' do
|
||||
[grandparent_group_label, parent_group_label].each do |label|
|
||||
find('a', text: label.title).click
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title)
|
||||
expect(page).to have_selector('.board-title-text', text: parent_group_label.title)
|
||||
end
|
||||
|
||||
it 'does not create lists from descendant groups' do
|
||||
expect(page).not_to have_selector('a', text: child_group_label.title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when graphql_board_lists FF disabled' do
|
||||
let(:board) { create(:board, group: parent) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(graphql_board_lists: false)
|
||||
parent.add_developer(user)
|
||||
visit group_board_path(parent, board)
|
||||
find('.js-new-board-list').click
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'creates lists from all ancestor group labels' do
|
||||
[grandparent_group_label, parent_group_label].each do |label|
|
||||
find('a', text: label.title).click
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title)
|
||||
expect(page).to have_selector('.board-title-text', text: parent_group_label.title)
|
||||
end
|
||||
|
||||
it 'does not create lists from descendant groups' do
|
||||
expect(page).not_to have_selector('a', text: child_group_label.title)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { GlFormTextarea, GlFormInput, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
|
||||
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
|
||||
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
|
||||
|
@ -48,7 +49,10 @@ describe('Pipeline Editor | Commit section', () => {
|
|||
let wrapper;
|
||||
let mockMutate;
|
||||
|
||||
const defaultProps = { ciFileContent: mockCiYml };
|
||||
const defaultProps = {
|
||||
ciFileContent: mockCiYml,
|
||||
commitSha: mockCommitSha,
|
||||
};
|
||||
|
||||
const createComponent = ({ props = {}, options = {}, provide = {} } = {}) => {
|
||||
mockMutate = jest.fn().mockResolvedValue({
|
||||
|
@ -67,7 +71,6 @@ describe('Pipeline Editor | Commit section', () => {
|
|||
provide: { ...mockProvide, ...provide },
|
||||
data() {
|
||||
return {
|
||||
commitSha: mockCommitSha,
|
||||
currentBranch: mockDefaultBranch,
|
||||
isNewCiConfigFile: Boolean(options?.isNewCiConfigfile),
|
||||
};
|
||||
|
@ -97,8 +100,7 @@ describe('Pipeline Editor | Commit section', () => {
|
|||
await findCommitForm().find('[data-testid="new-mr-checkbox"]').setChecked(openMergeRequest);
|
||||
}
|
||||
await findCommitForm().find('[type="submit"]').trigger('click');
|
||||
// Simulate the write to local cache that occurs after a commit
|
||||
await wrapper.setData({ commitSha: mockCommitNextSha });
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
const cancelCommitForm = async () => {
|
||||
|
@ -188,7 +190,6 @@ describe('Pipeline Editor | Commit section', () => {
|
|||
update: expect.any(Function),
|
||||
variables: {
|
||||
...mockVariables,
|
||||
lastCommitId: mockCommitNextSha,
|
||||
branch: mockDefaultBranch,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -42,15 +42,12 @@ describe('Pipeline Editor | Text editor component', () => {
|
|||
defaultBranch: mockDefaultBranch,
|
||||
glFeatures,
|
||||
},
|
||||
propsData: {
|
||||
commitSha: mockCommitSha,
|
||||
},
|
||||
attrs: {
|
||||
value: mockCiYml,
|
||||
},
|
||||
// Simulate graphQL client query result
|
||||
data() {
|
||||
return {
|
||||
commitSha: mockCommitSha,
|
||||
};
|
||||
},
|
||||
listeners: {
|
||||
[EDITOR_READY_EVENT]: editorReadyListener,
|
||||
},
|
||||
|
|
|
@ -247,15 +247,6 @@ describe('Pipeline editor branch switcher', () => {
|
|||
|
||||
expect(wrapper.emitted('refetchContent')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('emits the updateCommitSha event when selecting a different branch', async () => {
|
||||
expect(wrapper.emitted('updateCommitSha')).toBeUndefined();
|
||||
|
||||
const branch = findDropdownItems().at(1);
|
||||
branch.vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('updateCommitSha')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when searching', () => {
|
||||
|
|
|
@ -27,13 +27,11 @@ describe('Pipeline Status', () => {
|
|||
wrapper = shallowMount(PipelineStatus, {
|
||||
localVue,
|
||||
apolloProvider: mockApollo,
|
||||
propsData: {
|
||||
commitSha: mockCommitSha,
|
||||
},
|
||||
provide: mockProvide,
|
||||
stubs: { GlLink, GlSprintf },
|
||||
data() {
|
||||
return {
|
||||
commitSha: mockCommitSha,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -156,35 +156,33 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const mockNewCommitShaResults = {
|
||||
export const mockCommitShaResults = {
|
||||
data: {
|
||||
project: {
|
||||
pipelines: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Pipeline/1',
|
||||
sha: 'd0d56d363d8a3f67a8ab9fc00207d468f30032ca',
|
||||
sha: mockCommitSha,
|
||||
path: `/${mockProjectFullPath}/-/pipelines/488`,
|
||||
commitPath: `/${mockProjectFullPath}/-/commit/d0d56d363d8a3f67a8ab9fc00207d468f30032ca`,
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Pipeline/2',
|
||||
sha: 'fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa',
|
||||
path: `/${mockProjectFullPath}/-/pipelines/487`,
|
||||
commitPath: `/${mockProjectFullPath}/-/commit/fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa`,
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Pipeline/3',
|
||||
sha: '6c16b17c7f94a438ae19a96c285bb49e3c632cf4',
|
||||
path: `/${mockProjectFullPath}/-/pipelines/433`,
|
||||
commitPath: `/${mockProjectFullPath}/-/commit/6c16b17c7f94a438ae19a96c285bb49e3c632cf4`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockEmptyCommitShaResults = {
|
||||
data: {
|
||||
project: {
|
||||
pipelines: {
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockProjectBranches = {
|
||||
data: {
|
||||
project: {
|
||||
|
|
|
@ -26,9 +26,10 @@ import {
|
|||
mockBlobContentQueryResponseNoCiFile,
|
||||
mockCiYml,
|
||||
mockCommitSha,
|
||||
mockCommitShaResults,
|
||||
mockDefaultBranch,
|
||||
mockEmptyCommitShaResults,
|
||||
mockProjectFullPath,
|
||||
mockNewCommitShaResults,
|
||||
} from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
@ -54,7 +55,6 @@ describe('Pipeline editor app component', () => {
|
|||
let mockBlobContentData;
|
||||
let mockCiConfigData;
|
||||
let mockGetTemplate;
|
||||
let mockUpdateCommitSha;
|
||||
let mockLatestCommitShaQuery;
|
||||
let mockPipelineQuery;
|
||||
|
||||
|
@ -71,6 +71,11 @@ describe('Pipeline editor app component', () => {
|
|||
SourceEditor: MockSourceEditor,
|
||||
PipelineEditorEmptyState,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
commitSha: '',
|
||||
};
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
queries: {
|
||||
|
@ -96,18 +101,7 @@ describe('Pipeline editor app component', () => {
|
|||
[getPipelineQuery, mockPipelineQuery],
|
||||
];
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
commitSha() {
|
||||
return mockCommitSha;
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
updateCommitSha: mockUpdateCommitSha,
|
||||
},
|
||||
};
|
||||
|
||||
mockApollo = createMockApollo(handlers, resolvers);
|
||||
mockApollo = createMockApollo(handlers);
|
||||
|
||||
const options = {
|
||||
localVue,
|
||||
|
@ -137,7 +131,6 @@ describe('Pipeline editor app component', () => {
|
|||
mockBlobContentData = jest.fn();
|
||||
mockCiConfigData = jest.fn();
|
||||
mockGetTemplate = jest.fn();
|
||||
mockUpdateCommitSha = jest.fn();
|
||||
mockLatestCommitShaQuery = jest.fn();
|
||||
mockPipelineQuery = jest.fn();
|
||||
});
|
||||
|
@ -159,11 +152,16 @@ describe('Pipeline editor app component', () => {
|
|||
beforeEach(() => {
|
||||
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
|
||||
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
|
||||
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
|
||||
});
|
||||
|
||||
describe('when file exists', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponentWithApollo();
|
||||
|
||||
jest
|
||||
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
|
||||
.mockImplementation(jest.fn());
|
||||
});
|
||||
|
||||
it('shows pipeline editor home component', () => {
|
||||
|
@ -181,18 +179,32 @@ describe('Pipeline editor app component', () => {
|
|||
sha: mockCommitSha,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not poll for the commit sha', () => {
|
||||
expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no CI config file exists', () => {
|
||||
it('shows an empty state and does not show editor home component', async () => {
|
||||
beforeEach(async () => {
|
||||
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
|
||||
await createComponentWithApollo();
|
||||
|
||||
jest
|
||||
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
|
||||
.mockImplementation(jest.fn());
|
||||
});
|
||||
|
||||
it('shows an empty state and does not show editor home component', async () => {
|
||||
expect(findEmptyState().exists()).toBe(true);
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
expect(findEditorHome().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not poll for the commit sha', () => {
|
||||
expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
describe('because of a fetching error', () => {
|
||||
it('shows a unkown error message', async () => {
|
||||
const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.';
|
||||
|
@ -230,6 +242,7 @@ describe('Pipeline editor app component', () => {
|
|||
describe('when landing on the empty state with feature flag on', () => {
|
||||
it('user can click on CTA button and see an empty editor', async () => {
|
||||
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
|
||||
mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
|
||||
|
||||
await createComponentWithApollo({
|
||||
provide: {
|
||||
|
@ -254,9 +267,9 @@ describe('Pipeline editor app component', () => {
|
|||
const updateSuccessMessage = 'Your changes have been successfully committed.';
|
||||
|
||||
describe('and the commit mutation succeeds', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
window.scrollTo = jest.fn();
|
||||
createComponent();
|
||||
await createComponentWithApollo();
|
||||
|
||||
findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
|
||||
});
|
||||
|
@ -268,7 +281,32 @@ describe('Pipeline editor app component', () => {
|
|||
it('scrolls to the top of the page to bring attention to the confirmation message', () => {
|
||||
expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
it('polls for commit sha while pipeline data is not yet available', async () => {
|
||||
jest
|
||||
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
|
||||
.mockImplementation(jest.fn());
|
||||
|
||||
// simulate updating current branch (which triggers commitSha refetch)
|
||||
// while pipeline data is not yet available
|
||||
mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
|
||||
await wrapper.vm.$apollo.queries.commitSha.refetch();
|
||||
|
||||
expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('stops polling for commit sha when pipeline data is available', async () => {
|
||||
jest
|
||||
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling')
|
||||
.mockImplementation(jest.fn());
|
||||
|
||||
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
|
||||
await wrapper.vm.$apollo.queries.commitSha.refetch();
|
||||
|
||||
expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the commit mutation fails', () => {
|
||||
const commitFailedReasons = ['Commit failed'];
|
||||
|
||||
|
@ -320,6 +358,10 @@ describe('Pipeline editor app component', () => {
|
|||
});
|
||||
|
||||
describe('when refetching content', () => {
|
||||
beforeEach(() => {
|
||||
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
|
||||
});
|
||||
|
||||
it('refetches blob content', async () => {
|
||||
await createComponentWithApollo();
|
||||
jest
|
||||
|
@ -352,6 +394,7 @@ describe('Pipeline editor app component', () => {
|
|||
const originalLocation = window.location.href;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
|
||||
setWindowLocation('?template=Android');
|
||||
});
|
||||
|
||||
|
@ -371,45 +414,4 @@ describe('Pipeline editor app component', () => {
|
|||
expect(findTextEditor().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updating commit sha', () => {
|
||||
const newCommitSha = mockNewCommitShaResults.data.project.pipelines.nodes[0].sha;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockUpdateCommitSha.mockResolvedValue(newCommitSha);
|
||||
mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults);
|
||||
await createComponentWithApollo();
|
||||
});
|
||||
|
||||
it('fetches updated commit sha for the new branch', async () => {
|
||||
expect(mockLatestCommitShaQuery).not.toHaveBeenCalled();
|
||||
|
||||
wrapper
|
||||
.findComponent(PipelineEditorHome)
|
||||
.vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockLatestCommitShaQuery).toHaveBeenCalledWith({
|
||||
projectPath: mockProjectFullPath,
|
||||
ref: 'new-branch',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates commit sha with the newly fetched commit sha', async () => {
|
||||
expect(mockUpdateCommitSha).not.toHaveBeenCalled();
|
||||
|
||||
wrapper
|
||||
.findComponent(PipelineEditorHome)
|
||||
.vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockUpdateCommitSha).toHaveBeenCalled();
|
||||
expect(mockUpdateCommitSha).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
{ commitSha: mockNewCommitShaResults.data.project.pipelines.nodes[0].sha },
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -498,7 +498,7 @@ RSpec.describe ProjectsHelper do
|
|||
context 'user has a configured commit email' do
|
||||
before do
|
||||
confirmed_email = create(:email, :confirmed, user: user)
|
||||
user.update!(commit_email: confirmed_email)
|
||||
user.update!(commit_email: confirmed_email.email)
|
||||
end
|
||||
|
||||
it 'returns the commit email' do
|
||||
|
|
|
@ -618,6 +618,29 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job is using tags' do
|
||||
context 'when limit is reached' do
|
||||
let(:tags) { Array.new(100) { |i| "tag-#{i}" } }
|
||||
let(:config) { { tags: tags, script: 'test' } }
|
||||
|
||||
it 'returns error', :aggregate_failures do
|
||||
expect(entry).not_to be_valid
|
||||
expect(entry.errors)
|
||||
.to include "tags config must be less than the limit of #{Gitlab::Ci::Config::Entry::Tags::TAGS_LIMIT} tags"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when limit is not reached' do
|
||||
let(:config) { { tags: %w[tag1 tag2], script: 'test' } }
|
||||
|
||||
it 'returns a valid entry', :aggregate_failures do
|
||||
expect(entry).to be_valid
|
||||
expect(entry.errors).to be_empty
|
||||
expect(entry.tags).to eq(%w[tag1 tag2])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#manual_action?' do
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Entry::Tags do
|
||||
let(:entry) { described_class.new(config) }
|
||||
|
||||
describe 'validation' do
|
||||
context 'when tags config value is correct' do
|
||||
let(:config) { %w[tag1 tag2] }
|
||||
|
||||
describe '#value' do
|
||||
it 'returns tags configuration' do
|
||||
expect(entry.value).to eq config
|
||||
end
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it 'is valid' do
|
||||
expect(entry).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when entry value is not correct' do
|
||||
describe '#errors' do
|
||||
context 'when tags config is not an array of strings' do
|
||||
let(:config) { [1, 2] }
|
||||
|
||||
it 'reports error' do
|
||||
expect(entry.errors)
|
||||
.to include 'tags config should be an array of strings'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when tags limit is reached' do
|
||||
let(:config) { Array.new(50) {|i| "tag-#{i}" } }
|
||||
|
||||
context 'when ci_build_tags_limit is enabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_build_tags_limit: true)
|
||||
end
|
||||
|
||||
it 'reports error' do
|
||||
expect(entry.errors)
|
||||
.to include "tags config must be less than the limit of #{described_class::TAGS_LIMIT} tags"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ci_build_tags_limit is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_build_tags_limit: false)
|
||||
end
|
||||
|
||||
it 'does not report an error' do
|
||||
expect(entry.errors).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -370,14 +370,6 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
|
|||
end
|
||||
|
||||
it { is_expected.to contain_exactly(SeedRepo::Commit::ID) }
|
||||
|
||||
context 'between_uses_list_commits FF disabled' do
|
||||
before do
|
||||
stub_feature_flags(between_uses_list_commits: false)
|
||||
end
|
||||
|
||||
it { is_expected.to contain_exactly(SeedRepo::Commit::ID) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.shas_with_signatures' do
|
||||
|
|
|
@ -113,5 +113,31 @@ RSpec.describe Ci::PendingBuild do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build has tags' do
|
||||
let!(:build) { create(:ci_build, :tags) }
|
||||
|
||||
subject(:ci_pending_build) { described_class.last }
|
||||
|
||||
context 'when ci_pending_builds_maintain_tags_data is enabled' do
|
||||
it 'sets tag_ids' do
|
||||
described_class.upsert_from_build!(build)
|
||||
|
||||
expect(ci_pending_build.tag_ids).to eq(build.tags_ids)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ci_pending_builds_maintain_tags_data is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_pending_builds_maintain_tags_data: false)
|
||||
end
|
||||
|
||||
it 'does not set tag_ids' do
|
||||
described_class.upsert_from_build!(build)
|
||||
|
||||
expect(ci_pending_build.tag_ids).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -465,24 +465,19 @@ RSpec.describe User do
|
|||
user.commit_email = confirmed.email
|
||||
|
||||
expect(user).to be_valid
|
||||
expect(user.commit_email).to eq(confirmed.email)
|
||||
end
|
||||
|
||||
it 'can not be set to an unconfirmed email' do
|
||||
unconfirmed = create(:email, user: user)
|
||||
user.commit_email = unconfirmed.email
|
||||
|
||||
# This should set the commit_email attribute to the primary email
|
||||
expect(user).to be_valid
|
||||
expect(user.commit_email).to eq(user.email)
|
||||
expect(user).not_to be_valid
|
||||
end
|
||||
|
||||
it 'can not be set to a non-existent email' do
|
||||
user.commit_email = 'non-existent-email@nonexistent.nonexistent'
|
||||
|
||||
# This should set the commit_email attribute to the primary email
|
||||
expect(user).to be_valid
|
||||
expect(user.commit_email).to eq(user.email)
|
||||
expect(user).not_to be_valid
|
||||
end
|
||||
|
||||
it 'can not be set to an invalid email, even if confirmed' do
|
||||
|
@ -691,75 +686,6 @@ RSpec.describe User do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'owns_notification_email' do
|
||||
it 'accepts temp_oauth_email emails' do
|
||||
user = build(:user, email: "temp-email-for-oauth@example.com")
|
||||
expect(user).to be_valid
|
||||
end
|
||||
|
||||
it 'does not accept not verified emails' do
|
||||
email = create(:email)
|
||||
user = email.user
|
||||
user.notification_email = email.email
|
||||
|
||||
expect(user).to be_invalid
|
||||
expect(user.errors[:notification_email]).to include(_('must be an email you have verified'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'owns_public_email' do
|
||||
it 'accepts verified emails' do
|
||||
email = create(:email, :confirmed, email: 'test@test.com')
|
||||
user = email.user
|
||||
user.notification_email = email.email
|
||||
|
||||
expect(user).to be_valid
|
||||
end
|
||||
|
||||
it 'does not accept not verified emails' do
|
||||
email = create(:email)
|
||||
user = email.user
|
||||
user.public_email = email.email
|
||||
|
||||
expect(user).to be_invalid
|
||||
expect(user.errors[:public_email]).to include(_('must be an email you have verified'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'set_commit_email' do
|
||||
it 'keeps commit email when private commit email is being used' do
|
||||
user = create(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN)
|
||||
|
||||
expect(user.read_attribute(:commit_email)).to eq(Gitlab::PrivateCommitEmail::TOKEN)
|
||||
end
|
||||
|
||||
it 'keeps the commit email when nil' do
|
||||
user = create(:user, commit_email: nil)
|
||||
|
||||
expect(user.read_attribute(:commit_email)).to be_nil
|
||||
end
|
||||
|
||||
it 'reverts to nil when email is not verified' do
|
||||
user = create(:user, commit_email: "foo@bar.com")
|
||||
|
||||
expect(user.read_attribute(:commit_email)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'owns_commit_email' do
|
||||
it 'accepts private commit email' do
|
||||
user = build(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN)
|
||||
|
||||
expect(user).to be_valid
|
||||
end
|
||||
|
||||
it 'accepts nil commit email' do
|
||||
user = build(:user, commit_email: nil)
|
||||
|
||||
expect(user).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::CreatePipelineService do
|
||||
describe 'tags:' do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { project.owner }
|
||||
|
||||
let(:ref) { 'refs/heads/master' }
|
||||
let(:source) { :push }
|
||||
let(:service) { described_class.new(project, user, { ref: ref }) }
|
||||
let(:pipeline) { service.execute(source).payload }
|
||||
|
||||
before do
|
||||
stub_ci_pipeline_yaml_file(config)
|
||||
end
|
||||
|
||||
context 'with valid config' do
|
||||
let(:config) { YAML.dump({ test: { script: 'ls', tags: %w[tag1 tag2] } }) }
|
||||
|
||||
it 'creates a pipeline', :aggregate_failures do
|
||||
expect(pipeline).to be_created_successfully
|
||||
expect(pipeline.builds.first.tag_list).to eq(%w[tag1 tag2])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with too many tags' do
|
||||
let(:tags) { Array.new(50) {|i| "tag-#{i}" } }
|
||||
let(:config) { YAML.dump({ test: { script: 'ls', tags: tags } }) }
|
||||
|
||||
it 'creates a pipeline without builds', :aggregate_failures do
|
||||
expect(pipeline).not_to be_created_successfully
|
||||
expect(pipeline.builds).to be_empty
|
||||
expect(pipeline.yaml_errors).to eq("jobs:test:tags config must be less than the limit of #{Gitlab::Ci::Config::Entry::Tags::TAGS_LIMIT} tags")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -739,6 +739,22 @@ module Ci
|
|||
|
||||
include_examples 'handles runner assignment'
|
||||
end
|
||||
|
||||
context 'with ci_queueing_denormalize_tags_information enabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_queueing_denormalize_tags_information: true)
|
||||
end
|
||||
|
||||
include_examples 'handles runner assignment'
|
||||
end
|
||||
|
||||
context 'with ci_queueing_denormalize_tags_information disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_queueing_denormalize_tags_information: false)
|
||||
end
|
||||
|
||||
include_examples 'handles runner assignment'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not using pending builds table' do
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This module stores the CI-related database tables which are
|
||||
# going to be moved to a separate database.
|
||||
module Database
|
||||
module CiTables
|
||||
def self.include?(name)
|
||||
ci_tables.include?(name)
|
||||
end
|
||||
|
||||
def self.ci_tables
|
||||
@@ci_tables ||= Set.new.tap do |tables| # rubocop:disable Style/ClassVars
|
||||
tables.merge(Ci::ApplicationRecord.descendants.map(&:table_name).compact)
|
||||
|
||||
# It was decided that taggings/tags are best placed with CI
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/333413
|
||||
tables.add('taggings')
|
||||
tables.add('tags')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This module gathes information about table to schema mapping
|
||||
# to understand table affinity
|
||||
module Database
|
||||
module GitlabSchema
|
||||
def self.table_schemas(tables)
|
||||
tables.map { |table| table_schema(table) }.to_set
|
||||
end
|
||||
|
||||
def self.table_schema(name)
|
||||
tables_to_schema[name] || :undefined
|
||||
end
|
||||
|
||||
def self.tables_to_schema
|
||||
@tables_to_schema ||= all_classes_with_schema.to_h do |klass|
|
||||
[klass.table_name, klass.gitlab_schema]
|
||||
end
|
||||
end
|
||||
|
||||
def self.all_classes_with_schema
|
||||
ActiveRecord::Base.descendants.reject(&:abstract_class?).select(&:gitlab_schema?) # rubocop:disable Database/MultipleDatabases
|
||||
end
|
||||
end
|
||||
end
|
|
@ -75,17 +75,17 @@ module Database
|
|||
return if cross_database_context[:transaction_depth_by_db].values.all?(&:zero?)
|
||||
|
||||
tables = PgQuery.parse(sql).dml_tables
|
||||
|
||||
return if tables.empty?
|
||||
|
||||
cross_database_context[:modified_tables_by_db][database].merge(tables)
|
||||
|
||||
all_tables = cross_database_context[:modified_tables_by_db].values.map(&:to_a).flatten
|
||||
schemas = Database::GitlabSchema.table_schemas(all_tables)
|
||||
|
||||
unless PreventCrossJoins.only_ci_or_only_main?(all_tables)
|
||||
if schemas.many?
|
||||
raise Database::PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError,
|
||||
"Cross-database data modification queries (CI and Main) were detected within " \
|
||||
"a transaction '#{all_tables.join(", ")}' discovered"
|
||||
"Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \
|
||||
"a transaction modifying the '#{all_tables.to_a.join(", ")}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,20 +27,15 @@ module Database
|
|||
# PgQuery might fail in some cases due to limited nesting:
|
||||
# https://github.com/pganalyze/pg_query/issues/209
|
||||
tables = PgQuery.parse(sql).tables
|
||||
schemas = Database::GitlabSchema.table_schemas(tables)
|
||||
|
||||
unless only_ci_or_only_main?(tables)
|
||||
if schemas.many?
|
||||
raise CrossJoinAcrossUnsupportedTablesError,
|
||||
"Unsupported cross-join across '#{tables.join(", ")}' discovered " \
|
||||
"Unsupported cross-join across '#{tables.join(", ")}' modifying '#{schemas.to_a.join(", ")}' discovered " \
|
||||
"when executing query '#{sql}'"
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if a set includes only CI tables, or includes only non-CI tables
|
||||
def self.only_ci_or_only_main?(tables)
|
||||
tables.all? { |table| CiTables.include?(table) } ||
|
||||
tables.none? { |table| CiTables.include?(table) }
|
||||
end
|
||||
|
||||
module SpecHelpers
|
||||
def with_cross_joins_prevented
|
||||
subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |event|
|
||||
|
|
|
@ -3,14 +3,10 @@
|
|||
RSpec.shared_examples 'multiple issue boards' do
|
||||
context 'authorized user' do
|
||||
before do
|
||||
stub_feature_flags(board_new_list: false)
|
||||
|
||||
parent.add_maintainer(user)
|
||||
|
||||
login_as(user)
|
||||
|
||||
stub_feature_flags(board_new_list: false)
|
||||
|
||||
visit boards_path
|
||||
wait_for_requests
|
||||
end
|
||||
|
@ -79,13 +75,13 @@ RSpec.shared_examples 'multiple issue boards' do
|
|||
expect(page).to have_content(board2.name)
|
||||
end
|
||||
|
||||
click_button 'Add list'
|
||||
click_button 'Create list'
|
||||
|
||||
wait_for_requests
|
||||
click_button 'Select a label'
|
||||
|
||||
page.within '.dropdown-menu-issues-board-new' do
|
||||
click_link planning.title
|
||||
end
|
||||
page.choose(planning.title)
|
||||
|
||||
click_button 'Add to board'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
|
|||
pipeline.touch
|
||||
end
|
||||
end
|
||||
end.to raise_error /Cross-database data modification queries/
|
||||
end.to raise_error /Cross-database data modification/
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -84,7 +84,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
|
|||
context 'when data modification happens in a transaction' do
|
||||
it 'raises error' do
|
||||
Project.transaction do
|
||||
expect { run_queries }.to raise_error /Cross-database data modification queries/
|
||||
expect { run_queries }.to raise_error /Cross-database data modification/
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -93,7 +93,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
|
|||
Project.transaction(requires_new: true) do
|
||||
project.touch
|
||||
Project.transaction(requires_new: true) do
|
||||
expect { pipeline.touch }.to raise_error /Cross-database data modification queries/
|
||||
expect { pipeline.touch }.to raise_error /Cross-database data modification/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -127,7 +127,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
|
|||
ApplicationRecord.transaction do
|
||||
create(:ci_pipeline)
|
||||
end
|
||||
end.to raise_error /Cross-database data modification queries/
|
||||
end.to raise_error /Cross-database data modification/
|
||||
end
|
||||
|
||||
it 'skips raising error on factory creation' do
|
||||
|
|
Loading…
Reference in New Issue