Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7b29a4f84e
commit
44d4b37b52
|
@ -1,8 +1,17 @@
|
||||||
<!-- Title suggestion: [Feature flag] Enable description of feature -->
|
<!-- Title suggestion: [Feature flag] Enable description of feature -->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Set the main issue link: The main issue is the one that describes the problem to solve,
|
||||||
|
the one this feature flag is being added for. For example:
|
||||||
|
|
||||||
|
[main-issue]: https://gitlab.com/gitlab-org/gitlab/-/issues/123456
|
||||||
|
-->
|
||||||
|
|
||||||
|
[main-issue]: MAIN-ISSUE-LINK
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
This issue is to rollout [the feature](ISSUE LINK) on production,
|
This issue is to rollout [the feature][main-issue] on production,
|
||||||
that is currently behind the `<feature-flag-name>` feature flag.
|
that is currently behind the `<feature-flag-name>` feature flag.
|
||||||
|
|
||||||
<!-- Short description of what the feature is about and link to relevant other issues. -->
|
<!-- Short description of what the feature is about and link to relevant other issues. -->
|
||||||
|
@ -89,7 +98,7 @@ _Consider adding links to check for Sentry errors, Production logs for 5xx, 302s
|
||||||
- [ ] Ensure that you or a representative in development can be available for at least 2 hours after feature flag updates in production.
|
- [ ] Ensure that you or a representative in development can be available for at least 2 hours after feature flag updates in production.
|
||||||
If a different developer will be covering, or an exception is needed, please inform the oncall SRE by using the `@sre-oncall` Slack alias.
|
If a different developer will be covering, or an exception is needed, please inform the oncall SRE by using the `@sre-oncall` Slack alias.
|
||||||
- [ ] Ensure that documentation has been updated ([More info](https://docs.gitlab.com/ee/development/documentation/feature_flags.html#features-that-became-enabled-by-default)).
|
- [ ] Ensure that documentation has been updated ([More info](https://docs.gitlab.com/ee/development/documentation/feature_flags.html#features-that-became-enabled-by-default)).
|
||||||
- [ ] Announce on [the feature issue](ISSUE LINK) an estimated time this will be enabled on GitLab.com.
|
- [ ] Leave a comment on [the feature issue][main-issue] announcing estimated time when this feature flag will be enabled on GitLab.com.
|
||||||
- [ ] Ensure that any breaking changes have been announced following the [release post process](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations-removals-and-breaking-changes) to ensure GitLab customers are aware.
|
- [ ] Ensure that any breaking changes have been announced following the [release post process](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations-removals-and-breaking-changes) to ensure GitLab customers are aware.
|
||||||
- [ ] Notify `#support_gitlab-com` and your team channel ([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#communicate-the-change)).
|
- [ ] Notify `#support_gitlab-com` and your team channel ([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#communicate-the-change)).
|
||||||
|
|
||||||
|
@ -104,7 +113,7 @@ For visibility, all `/chatops` commands that target production should be execute
|
||||||
- [ ] `/chatops run feature set <feature-flag-name> <rollout-percentage> --random`
|
- [ ] `/chatops run feature set <feature-flag-name> <rollout-percentage> --random`
|
||||||
- Enable the feature globally on production environment.
|
- Enable the feature globally on production environment.
|
||||||
- [ ] `/chatops run feature set <feature-flag-name> true`
|
- [ ] `/chatops run feature set <feature-flag-name> true`
|
||||||
- [ ] Announce on [the feature issue](ISSUE LINK) that the feature has been globally enabled.
|
- [ ] Leave a comment on [the feature issue][main-issue] announcing that the feature has been globally enabled.
|
||||||
- [ ] Wait for [at least one day for the verification term](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#including-a-feature-behind-feature-flag-in-the-final-release).
|
- [ ] Wait for [at least one day for the verification term](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#including-a-feature-behind-feature-flag-in-the-final-release).
|
||||||
|
|
||||||
### (Optional) Release the feature with the feature flag
|
### (Optional) Release the feature with the feature flag
|
||||||
|
@ -122,7 +131,7 @@ To do so, follow these steps:
|
||||||
- [ ] `/chatops run release check <merge-request-url> <milestone>`
|
- [ ] `/chatops run release check <merge-request-url> <milestone>`
|
||||||
- [ ] Consider cleaning up the feature flag from all environments by running these chatops command in `#production` channel. Otherwise these settings may override the default enabled.
|
- [ ] Consider cleaning up the feature flag from all environments by running these chatops command in `#production` channel. Otherwise these settings may override the default enabled.
|
||||||
- [ ] `/chatops run feature delete <feature-flag-name> --dev --staging --staging-ref --production`
|
- [ ] `/chatops run feature delete <feature-flag-name> --dev --staging --staging-ref --production`
|
||||||
- [ ] Close [the feature issue](ISSUE LINK) to indicate the feature will be released in the current milestone.
|
- [ ] Close [the feature issue][main-issue] to indicate the feature will be released in the current milestone.
|
||||||
- [ ] Set the next milestone to this rollout issue for scheduling [the flag removal](#release-the-feature).
|
- [ ] Set the next milestone to this rollout issue for scheduling [the flag removal](#release-the-feature).
|
||||||
- [ ] (Optional) You can [create a separate issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20Flag%20Cleanup) for scheduling the steps below to [Release the feature](#release-the-feature).
|
- [ ] (Optional) You can [create a separate issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20Flag%20Cleanup) for scheduling the steps below to [Release the feature](#release-the-feature).
|
||||||
- [ ] Set the title to "[Feature flag] Cleanup `<feature-flag-name>`".
|
- [ ] Set the title to "[Feature flag] Cleanup `<feature-flag-name>`".
|
||||||
|
@ -155,7 +164,7 @@ You can either [create a follow-up issue for Feature Flag Cleanup](https://gitla
|
||||||
If the merge request was deployed before [the monthly release was tagged](https://about.gitlab.com/handbook/engineering/releases/#self-managed-releases-1),
|
If the merge request was deployed before [the monthly release was tagged](https://about.gitlab.com/handbook/engineering/releases/#self-managed-releases-1),
|
||||||
the feature can be officially announced in a release blog post.
|
the feature can be officially announced in a release blog post.
|
||||||
- [ ] `/chatops run release check <merge-request-url> <milestone>`
|
- [ ] `/chatops run release check <merge-request-url> <milestone>`
|
||||||
- [ ] Close [the feature issue](ISSUE LINK) to indicate the feature will be released in the current milestone.
|
- [ ] Close [the feature issue][main-issue] to indicate the feature will be released in the current milestone.
|
||||||
- [ ] If not already done, clean up the feature flag from all environments by running these chatops command in `#production` channel:
|
- [ ] If not already done, clean up the feature flag from all environments by running these chatops command in `#production` channel:
|
||||||
- [ ] `/chatops run feature delete <feature-flag-name> --dev --staging --staging-ref --production`
|
- [ ] `/chatops run feature delete <feature-flag-name> --dev --staging --staging-ref --production`
|
||||||
- [ ] Close this rollout issue.
|
- [ ] Close this rollout issue.
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
/* eslint-disable func-names */
|
|
||||||
|
|
||||||
import Dropzone from 'dropzone';
|
|
||||||
import $ from 'jquery';
|
|
||||||
import { sprintf, __ } from '~/locale';
|
|
||||||
import { HIDDEN_CLASS } from '../lib/utils/constants';
|
|
||||||
import csrf from '../lib/utils/csrf';
|
|
||||||
import { visitUrl } from '../lib/utils/url_utility';
|
|
||||||
|
|
||||||
Dropzone.autoDiscover = false;
|
|
||||||
|
|
||||||
function toggleLoading($el, $icon, loading) {
|
|
||||||
if (loading) {
|
|
||||||
$el.disable();
|
|
||||||
$icon.removeClass(HIDDEN_CLASS);
|
|
||||||
} else {
|
|
||||||
$el.enable();
|
|
||||||
$icon.addClass(HIDDEN_CLASS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default class BlobFileDropzone {
|
|
||||||
constructor(form, method) {
|
|
||||||
const formDropzone = form.find('.dropzone');
|
|
||||||
const submitButton = form.find('#submit-all');
|
|
||||||
const submitButtonLoadingIcon = submitButton.find('.js-loading-icon');
|
|
||||||
const dropzoneMessage = form.find('.dz-message');
|
|
||||||
Dropzone.autoDiscover = false;
|
|
||||||
|
|
||||||
const dropzone = formDropzone.dropzone({
|
|
||||||
autoDiscover: false,
|
|
||||||
autoProcessQueue: false,
|
|
||||||
url: form.attr('action'),
|
|
||||||
// Rails uses a hidden input field for PUT
|
|
||||||
method,
|
|
||||||
clickable: true,
|
|
||||||
uploadMultiple: false,
|
|
||||||
paramName: 'file',
|
|
||||||
maxFilesize: gon.max_file_size || 10,
|
|
||||||
parallelUploads: 1,
|
|
||||||
maxFiles: 1,
|
|
||||||
addRemoveLinks: true,
|
|
||||||
previewsContainer: '.dropzone-previews',
|
|
||||||
headers: csrf.headers,
|
|
||||||
init() {
|
|
||||||
this.on('processing', function () {
|
|
||||||
this.options.url = form.attr('action');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.on('addedfile', () => {
|
|
||||||
toggleLoading(submitButton, submitButtonLoadingIcon, false);
|
|
||||||
dropzoneMessage.addClass(HIDDEN_CLASS);
|
|
||||||
$('.dropzone-alerts').html('').hide();
|
|
||||||
});
|
|
||||||
this.on('removedfile', () => {
|
|
||||||
toggleLoading(submitButton, submitButtonLoadingIcon, false);
|
|
||||||
dropzoneMessage.removeClass(HIDDEN_CLASS);
|
|
||||||
});
|
|
||||||
this.on('success', (header, response) => {
|
|
||||||
$('#modal-upload-blob').modal('hide');
|
|
||||||
visitUrl(response.filePath);
|
|
||||||
});
|
|
||||||
this.on('maxfilesexceeded', function (file) {
|
|
||||||
dropzoneMessage.addClass(HIDDEN_CLASS);
|
|
||||||
this.removeFile(file);
|
|
||||||
});
|
|
||||||
this.on('sending', (file, xhr, formData) => {
|
|
||||||
formData.append('branch_name', form.find('.js-branch-name').val());
|
|
||||||
formData.append('create_merge_request', form.find('.js-create-merge-request').val());
|
|
||||||
formData.append('commit_message', form.find('.js-commit-message').val());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// Override behavior of adding error underneath preview
|
|
||||||
error(file, errorMessage) {
|
|
||||||
const stripped = $('<div/>').html(errorMessage).text();
|
|
||||||
$('.dropzone-alerts')
|
|
||||||
.html(sprintf(__('Error uploading file: %{stripped}'), { stripped }))
|
|
||||||
.show();
|
|
||||||
this.removeFile(file);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
submitButton.on('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
|
|
||||||
// eslint-disable-next-line no-alert
|
|
||||||
alert(__('Please select a file'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
toggleLoading(submitButton, submitButtonLoadingIcon, true);
|
|
||||||
dropzone[0].dropzone.processQueue();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,9 +3,8 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import initPopover from '~/blob/suggest_gitlab_ci_yml';
|
import initPopover from '~/blob/suggest_gitlab_ci_yml';
|
||||||
import { createAlert } from '~/flash';
|
import { createAlert } from '~/flash';
|
||||||
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
|
import { setCookie } from '~/lib/utils/common_utils';
|
||||||
import Tracking from '~/tracking';
|
import Tracking from '~/tracking';
|
||||||
import BlobFileDropzone from '../blob/blob_file_dropzone';
|
|
||||||
import NewCommitForm from '../new_commit_form';
|
import NewCommitForm from '../new_commit_form';
|
||||||
|
|
||||||
const initPopovers = () => {
|
const initPopovers = () => {
|
||||||
|
@ -38,21 +37,8 @@ const initPopovers = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initUploadForm = () => {
|
|
||||||
const uploadBlobForm = $('.js-upload-blob-form');
|
|
||||||
if (uploadBlobForm.length) {
|
|
||||||
const method = uploadBlobForm.data('method');
|
|
||||||
|
|
||||||
new BlobFileDropzone(uploadBlobForm, method);
|
|
||||||
new NewCommitForm(uploadBlobForm);
|
|
||||||
|
|
||||||
disableButtonIfEmptyField(uploadBlobForm.find('.js-commit-message'), '.btn-upload-file');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const editBlobForm = $('.js-edit-blob-form');
|
const editBlobForm = $('.js-edit-blob-form');
|
||||||
const deleteBlobForm = $('.js-delete-blob-form');
|
|
||||||
|
|
||||||
if (editBlobForm.length) {
|
if (editBlobForm.length) {
|
||||||
const urlRoot = editBlobForm.data('relativeUrlRoot');
|
const urlRoot = editBlobForm.data('relativeUrlRoot');
|
||||||
|
@ -99,10 +85,4 @@ export default () => {
|
||||||
// returning here blocks page navigation
|
// returning here blocks page navigation
|
||||||
window.onbeforeunload = () => '';
|
window.onbeforeunload = () => '';
|
||||||
}
|
}
|
||||||
|
|
||||||
initUploadForm();
|
|
||||||
|
|
||||||
if (deleteBlobForm.length) {
|
|
||||||
new NewCommitForm(deleteBlobForm);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,7 +43,7 @@ export default {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
},
|
},
|
||||||
mixins: [boardCardInner],
|
mixins: [boardCardInner],
|
||||||
inject: ['rootPath', 'scopedLabelsAvailable'],
|
inject: ['rootPath', 'scopedLabelsAvailable', 'isEpicBoard'],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -78,7 +78,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
|
...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
|
||||||
...mapGetters(['isEpicBoard', 'isProjectBoard']),
|
...mapGetters(['isProjectBoard']),
|
||||||
cappedAssignees() {
|
cappedAssignees() {
|
||||||
// e.g. maxRender is 4,
|
// e.g. maxRender is 4,
|
||||||
// Render up to all 4 assignees if there are only 4 assigness
|
// Render up to all 4 assignees if there are only 4 assigness
|
||||||
|
|
|
@ -7,12 +7,7 @@ import { s__ } from '~/locale';
|
||||||
import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
|
import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
|
||||||
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
|
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
|
||||||
import { defaultSortableOptions } from '~/sortable/constants';
|
import { defaultSortableOptions } from '~/sortable/constants';
|
||||||
import {
|
import { DraggableItemTypes, BoardType, listsQuery } from 'ee_else_ce/boards/constants';
|
||||||
DraggableItemTypes,
|
|
||||||
issuableTypes,
|
|
||||||
BoardType,
|
|
||||||
listsQuery,
|
|
||||||
} from 'ee_else_ce/boards/constants';
|
|
||||||
import BoardColumn from './board_column.vue';
|
import BoardColumn from './board_column.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -31,7 +26,15 @@ export default {
|
||||||
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
|
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
|
||||||
GlAlert,
|
GlAlert,
|
||||||
},
|
},
|
||||||
inject: ['canAdminList', 'boardType', 'fullPath', 'issuableType', 'isApolloBoard'],
|
inject: [
|
||||||
|
'canAdminList',
|
||||||
|
'boardType',
|
||||||
|
'fullPath',
|
||||||
|
'issuableType',
|
||||||
|
'isIssueBoard',
|
||||||
|
'isEpicBoard',
|
||||||
|
'isApolloBoard',
|
||||||
|
],
|
||||||
props: {
|
props: {
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -78,12 +81,6 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['boardLists', 'error', 'addColumnForm']),
|
...mapState(['boardLists', 'error', 'addColumnForm']),
|
||||||
...mapGetters(['isSwimlanesOn']),
|
...mapGetters(['isSwimlanesOn']),
|
||||||
isIssueBoard() {
|
|
||||||
return this.issuableType === issuableTypes.issue;
|
|
||||||
},
|
|
||||||
isEpicBoard() {
|
|
||||||
return this.issuableType === issuableTypes.epic;
|
|
||||||
},
|
|
||||||
addColumnFormVisible() {
|
addColumnFormVisible() {
|
||||||
return this.addColumnForm?.visible;
|
return this.addColumnForm?.visible;
|
||||||
},
|
},
|
||||||
|
|
|
@ -84,7 +84,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['error']),
|
...mapState(['error']),
|
||||||
...mapGetters(['isIssueBoard', 'isGroupBoard', 'isProjectBoard']),
|
...mapGetters(['isGroupBoard', 'isProjectBoard']),
|
||||||
isNewForm() {
|
isNewForm() {
|
||||||
return this.currentPage === formType.new;
|
return this.currentPage === formType.new;
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
|
import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
|
||||||
import Draggable from 'vuedraggable';
|
import Draggable from 'vuedraggable';
|
||||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
import { mapActions, mapState } from 'vuex';
|
||||||
import { sprintf, __ } from '~/locale';
|
import { sprintf, __ } from '~/locale';
|
||||||
import { defaultSortableOptions } from '~/sortable/constants';
|
import { defaultSortableOptions } from '~/sortable/constants';
|
||||||
import { sortableStart, sortableEnd } from '~/sortable/utils';
|
import { sortableStart, sortableEnd } from '~/sortable/utils';
|
||||||
|
@ -31,6 +31,7 @@ export default {
|
||||||
BoardCardMoveToPosition,
|
BoardCardMoveToPosition,
|
||||||
},
|
},
|
||||||
mixins: [Tracking.mixin()],
|
mixins: [Tracking.mixin()],
|
||||||
|
inject: ['isEpicBoard'],
|
||||||
props: {
|
props: {
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -69,7 +70,6 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['pageInfoByListId', 'listsFlags', 'filterParams', 'isUpdateIssueOrderInProgress']),
|
...mapState(['pageInfoByListId', 'listsFlags', 'filterParams', 'isUpdateIssueOrderInProgress']),
|
||||||
...mapGetters(['isEpicBoard']),
|
|
||||||
listItemsCount() {
|
listItemsCount() {
|
||||||
return this.isEpicBoard ? this.list.epicsCount : this.boardList?.issuesCount;
|
return this.isEpicBoard ? this.list.epicsCount : this.boardList?.issuesCount;
|
||||||
},
|
},
|
||||||
|
|
|
@ -57,6 +57,9 @@ export default {
|
||||||
canCreateEpic: {
|
canCreateEpic: {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
isEpicBoard: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
list: {
|
list: {
|
||||||
|
@ -76,7 +79,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['activeId', 'filterParams', 'boardId']),
|
...mapState(['activeId', 'filterParams', 'boardId']),
|
||||||
...mapGetters(['isEpicBoard', 'isSwimlanesOn']),
|
...mapGetters(['isSwimlanesOn']),
|
||||||
isLoggedIn() {
|
isLoggedIn() {
|
||||||
return Boolean(this.currentUserId);
|
return Boolean(this.currentUserId);
|
||||||
},
|
},
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default {
|
||||||
GlModal: GlModalDirective,
|
GlModal: GlModalDirective,
|
||||||
},
|
},
|
||||||
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
|
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
|
||||||
inject: ['canAdminList', 'scopedLabelsAvailable'],
|
inject: ['canAdminList', 'scopedLabelsAvailable', 'isIssueBoard'],
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -40,10 +40,10 @@ export default {
|
||||||
},
|
},
|
||||||
modalId: 'board-settings-sidebar-modal',
|
modalId: 'board-settings-sidebar-modal',
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['isSidebarOpen', 'isEpicBoard']),
|
...mapGetters(['isSidebarOpen']),
|
||||||
...mapState(['activeId', 'sidebarType', 'boardLists']),
|
...mapState(['activeId', 'sidebarType', 'boardLists']),
|
||||||
isWipLimitsOn() {
|
isWipLimitsOn() {
|
||||||
return this.glFeatures.wipLimits && !this.isEpicBoard;
|
return this.glFeatures.wipLimits && this.isIssueBoard;
|
||||||
},
|
},
|
||||||
activeList() {
|
activeList() {
|
||||||
return this.boardLists[this.activeId] || {};
|
return this.boardLists[this.activeId] || {};
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
|
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
|
||||||
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
|
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
|
||||||
import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue';
|
import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue';
|
||||||
|
@ -20,10 +19,7 @@ export default {
|
||||||
EpicBoardFilteredSearch: () =>
|
EpicBoardFilteredSearch: () =>
|
||||||
import('ee_component/boards/components/epic_filtered_search.vue'),
|
import('ee_component/boards/components/epic_filtered_search.vue'),
|
||||||
},
|
},
|
||||||
inject: ['swimlanesFeatureAvailable', 'canAdminList', 'isSignedIn'],
|
inject: ['swimlanesFeatureAvailable', 'canAdminList', 'isSignedIn', 'isIssueBoard'],
|
||||||
computed: {
|
|
||||||
...mapGetters(['isEpicBoard']),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -37,8 +33,8 @@ export default {
|
||||||
>
|
>
|
||||||
<boards-selector />
|
<boards-selector />
|
||||||
<new-board-button />
|
<new-board-button />
|
||||||
<epic-board-filtered-search v-if="isEpicBoard" />
|
<issue-board-filtered-search v-if="isIssueBoard" />
|
||||||
<issue-board-filtered-search v-else />
|
<epic-board-filtered-search v-else />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="filter-dropdown-container gl-md-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-align-items-flex-start"
|
class="filter-dropdown-container gl-md-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-align-items-flex-start"
|
||||||
|
|
|
@ -72,6 +72,8 @@ function mountBoardApp(el) {
|
||||||
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
|
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
|
||||||
hasMissingBoards: parseBoolean(el.dataset.hasMissingBoards),
|
hasMissingBoards: parseBoolean(el.dataset.hasMissingBoards),
|
||||||
weights: el.dataset.weights ? JSON.parse(el.dataset.weights) : [],
|
weights: el.dataset.weights ? JSON.parse(el.dataset.weights) : [],
|
||||||
|
isIssueBoard: true,
|
||||||
|
isEpicBoard: false,
|
||||||
// Permissions
|
// Permissions
|
||||||
canUpdate: parseBoolean(el.dataset.canUpdate),
|
canUpdate: parseBoolean(el.dataset.canUpdate),
|
||||||
canAdminList: parseBoolean(el.dataset.canAdminList),
|
canAdminList: parseBoolean(el.dataset.canAdminList),
|
||||||
|
|
|
@ -10,14 +10,6 @@ import { initUploadFileTrigger } from '~/projects/upload_file';
|
||||||
import initReadMore from '~/read_more';
|
import initReadMore from '~/read_more';
|
||||||
|
|
||||||
// Project show page loads different overview content based on user preferences
|
// Project show page loads different overview content based on user preferences
|
||||||
if (document.querySelector('.js-upload-blob-form')) {
|
|
||||||
import(/* webpackChunkName: 'blobBundle' */ '~/blob_edit/blob_bundle')
|
|
||||||
.then(({ initUploadForm }) => {
|
|
||||||
initUploadForm();
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.getElementById('js-tree-list')) {
|
if (document.getElementById('js-tree-list')) {
|
||||||
import(/* webpackChunkName: 'treeList' */ 'ee_else_ce/repository')
|
import(/* webpackChunkName: 'treeList' */ 'ee_else_ce/repository')
|
||||||
.then(({ default: initTree }) => {
|
.then(({ default: initTree }) => {
|
||||||
|
|
|
@ -23,7 +23,6 @@ class Projects::LearnGitlabController < Projects::ApplicationController
|
||||||
|
|
||||||
experiment(:invite_for_help_continuous_onboarding, namespace: project.namespace) do |e|
|
experiment(:invite_for_help_continuous_onboarding, namespace: project.namespace) do |e|
|
||||||
e.candidate {}
|
e.candidate {}
|
||||||
e.publish_to_database
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -338,7 +338,6 @@ class Projects::PipelinesController < Projects::ApplicationController
|
||||||
|
|
||||||
experiment(:runners_availability_section, namespace: project.root_ancestor) do |e|
|
experiment(:runners_availability_section, namespace: project.root_ancestor) do |e|
|
||||||
e.candidate {}
|
e.candidate {}
|
||||||
e.publish_to_database
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,25 +3,6 @@
|
||||||
class ApplicationExperiment < Gitlab::Experiment
|
class ApplicationExperiment < Gitlab::Experiment
|
||||||
control { nil } # provide a default control for anonymous experiments
|
control { nil } # provide a default control for anonymous experiments
|
||||||
|
|
||||||
# Documented in:
|
|
||||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/357904
|
|
||||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/345932
|
|
||||||
#
|
|
||||||
# @deprecated
|
|
||||||
def publish_to_database
|
|
||||||
ActiveSupport::Deprecation.warn('publish_to_database is deprecated and should not be used for reporting anymore')
|
|
||||||
|
|
||||||
return unless should_track?
|
|
||||||
|
|
||||||
# if the context contains a namespace, group, project, user, or actor
|
|
||||||
value = context.value
|
|
||||||
subject = value[:namespace] || value[:group] || value[:project] || value[:user] || value[:actor]
|
|
||||||
return unless ExperimentSubject.valid_subject?(subject)
|
|
||||||
|
|
||||||
variant_name = :experimental if variant&.name != 'control'
|
|
||||||
Experiment.add_subject(name, variant: variant_name || :control, subject: subject)
|
|
||||||
end
|
|
||||||
|
|
||||||
def control_behavior
|
def control_behavior
|
||||||
# define a default nil control behavior so we can omit it when not needed
|
# define a default nil control behavior so we can omit it when not needed
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,18 +12,8 @@ class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment
|
||||||
run
|
run
|
||||||
end
|
end
|
||||||
|
|
||||||
def record_conversion(namespace)
|
|
||||||
return unless should_track?
|
|
||||||
|
|
||||||
Experiment.by_name(name).record_conversion_event_for_subject(subject, namespace_id: namespace.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def subject
|
|
||||||
context.value[:user]
|
|
||||||
end
|
|
||||||
|
|
||||||
def existing_user
|
def existing_user
|
||||||
return false unless user_or_actor
|
return false unless user_or_actor
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,4 @@
|
||||||
class SecurityReportsMrWidgetPromptExperiment < ApplicationExperiment
|
class SecurityReportsMrWidgetPromptExperiment < ApplicationExperiment
|
||||||
control {}
|
control {}
|
||||||
candidate {}
|
candidate {}
|
||||||
|
|
||||||
def publish(_result = nil)
|
|
||||||
super
|
|
||||||
|
|
||||||
publish_to_database
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -306,8 +306,8 @@ class Deployment < ApplicationRecord
|
||||||
last_deployment_id = environment.last_deployment&.id
|
last_deployment_id = environment.last_deployment&.id
|
||||||
|
|
||||||
return false unless last_deployment_id.present?
|
return false unless last_deployment_id.present?
|
||||||
|
|
||||||
return false if self.id == last_deployment_id
|
return false if self.id == last_deployment_id
|
||||||
|
return false if self.sha == environment.last_deployment&.sha
|
||||||
|
|
||||||
self.id < last_deployment_id
|
self.id < last_deployment_id
|
||||||
end
|
end
|
||||||
|
|
|
@ -452,6 +452,14 @@ class User < ApplicationRecord
|
||||||
after_transition banned: :active do |user|
|
after_transition banned: :active do |user|
|
||||||
user.banned_user&.destroy
|
user.banned_user&.destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
after_transition any => :active do |user|
|
||||||
|
user.starred_projects.update_counters(star_count: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
after_transition active: any do |user|
|
||||||
|
user.starred_projects.update_counters(star_count: -1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Scopes
|
# Scopes
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class UsersStarProject < ApplicationRecord
|
class UsersStarProject < ApplicationRecord
|
||||||
include Sortable
|
include Sortable
|
||||||
|
|
||||||
belongs_to :project, counter_cache: :star_count
|
belongs_to :project
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
validates :user, presence: true
|
validates :user, presence: true
|
||||||
|
@ -12,7 +12,10 @@ class UsersStarProject < ApplicationRecord
|
||||||
|
|
||||||
alias_attribute :starred_since, :created_at
|
alias_attribute :starred_since, :created_at
|
||||||
|
|
||||||
scope :with_active_user, -> { joins(:user).merge(User.active) }
|
after_create :increment_project_star_count
|
||||||
|
after_destroy :decrement_project_star_count
|
||||||
|
|
||||||
|
scope :with_active_user, -> { joins(:user).merge(User.with_state(:active)) }
|
||||||
scope :order_user_name_asc, -> { joins(:user).merge(User.order_name_asc) }
|
scope :order_user_name_asc, -> { joins(:user).merge(User.order_name_asc) }
|
||||||
scope :order_user_name_desc, -> { joins(:user).merge(User.order_name_desc) }
|
scope :order_user_name_desc, -> { joins(:user).merge(User.order_name_desc) }
|
||||||
scope :by_project, -> (project) { where(project_id: project.id) }
|
scope :by_project, -> (project) { where(project_id: project.id) }
|
||||||
|
@ -36,4 +39,14 @@ class UsersStarProject < ApplicationRecord
|
||||||
joins(:user).merge(User.search(query, use_minimum_char_limit: false))
|
joins(:user).merge(User.search(query, use_minimum_char_limit: false))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def increment_project_star_count
|
||||||
|
Project.update_counters(project, star_count: 1) if user.active?
|
||||||
|
end
|
||||||
|
|
||||||
|
def decrement_project_star_count
|
||||||
|
Project.update_counters(project, star_count: -1) if user.active?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -851,6 +851,10 @@ class ProjectPolicy < BasePolicy
|
||||||
enable :read_incident_management_timeline_event_tag
|
enable :read_incident_management_timeline_event_tag
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rule { can?(:download_code) }.policy do
|
||||||
|
enable :read_code
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def user_is_user?
|
def user_is_user?
|
||||||
|
|
|
@ -12,8 +12,8 @@ module Projects
|
||||||
Project.transaction do
|
Project.transaction do
|
||||||
user_stars.update_all(project_id: @project.id)
|
user_stars.update_all(project_id: @project.id)
|
||||||
|
|
||||||
Project.reset_counters @project.id, :users_star_projects
|
@project.update(star_count: @project.starrers.with_state(:active).size)
|
||||||
Project.reset_counters source_project.id, :users_star_projects
|
source_project.update(star_count: source_project.starrers.with_state(:active).size)
|
||||||
|
|
||||||
success
|
success
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@ name: policy_project_updated
|
||||||
description: "This event is triggered whenever the security policy project is updated for a project."
|
description: "This event is triggered whenever the security policy project is updated for a project."
|
||||||
introduced_by_issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/377877"
|
introduced_by_issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/377877"
|
||||||
introduced_by_mr: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102154"
|
introduced_by_mr: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102154"
|
||||||
milestone: 15.6
|
milestone: "15.6"
|
||||||
group: "govern::security policies"
|
group: "govern::security policies"
|
||||||
saved_to_database: true
|
saved_to_database: true
|
||||||
streamed: false
|
streamed: false
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
- name: "`gitlab-runner register` command" # (required) The name of the feature to be deprecated
|
||||||
|
announcement_milestone: "15.6" # (required) The milestone when this feature was first announced as deprecated.
|
||||||
|
announcement_date: "2022-11-22" # (required) The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
|
||||||
|
removal_milestone: "16.0" # (required) The milestone when this feature is planned to be removed
|
||||||
|
removal_date: "2023-05-22" # (required) The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
|
||||||
|
breaking_change: true # (required) If this deprecation is a breaking change, set this value to true
|
||||||
|
reporter: pedropombeiro # (required) GitLab username of the person reporting the deprecation
|
||||||
|
stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
|
||||||
|
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/380872 # (required) Link to the deprecation issue in GitLab
|
||||||
|
body: | # (required) Do not modify this line, instead modify the lines below.
|
||||||
|
The command to [register](https://docs.gitlab.com/runner/register/) a runner, `gitlab-runner register` is deprecated. GitLab plans to introduce a new [GitLab Runner token architecture](https://docs.gitlab.com/ee/architecture/blueprints/runner_tokens/), which introduces a new method for registering runners and eliminates the legacy [runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens).
|
||||||
|
end_of_support_milestone: "16.0" # (optional) Use "XX.YY" format. The milestone when support for this feature will end.
|
||||||
|
end_of_support_date: "2023-05-22" # (optional) The date of the milestone release when support for this feature will end.
|
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class QueuePopulateProjectsStarCount < Gitlab::Database::Migration[2.0]
|
||||||
|
MIGRATION = 'PopulateProjectsStarCount'
|
||||||
|
DELAY_INTERVAL = 2.minutes
|
||||||
|
|
||||||
|
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||||
|
|
||||||
|
def up
|
||||||
|
queue_batched_background_migration(
|
||||||
|
MIGRATION,
|
||||||
|
:projects,
|
||||||
|
:id,
|
||||||
|
job_interval: DELAY_INTERVAL,
|
||||||
|
sub_batch_size: 50
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
delete_batched_background_migration(MIGRATION, :projects, :id, [])
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
186e7df4e7e81913981595a069c5c8b5fbb600ee5dcebf333bfff728c5019ab2
|
|
@ -4,7 +4,7 @@ group: Project Management
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||||
---
|
---
|
||||||
|
|
||||||
# Award emoji API **(FREE)**
|
# Award emojis API **(FREE)**
|
||||||
|
|
||||||
An [awarded emoji](../user/award_emojis.md) tells a thousand words.
|
An [awarded emoji](../user/award_emojis.md) tells a thousand words.
|
||||||
|
|
||||||
|
@ -15,11 +15,11 @@ We call GitLab objects on which you can award an emoji "awardables". You can awa
|
||||||
- [Merge requests](../user/project/merge_requests/index.md) ([API](merge_requests.md)).
|
- [Merge requests](../user/project/merge_requests/index.md) ([API](merge_requests.md)).
|
||||||
- [Snippets](../user/snippets.md) ([API](snippets.md)).
|
- [Snippets](../user/snippets.md) ([API](snippets.md)).
|
||||||
|
|
||||||
Emojis can also [be awarded](../user/award_emojis.md#award-emoji-for-comments) on comments (also known as notes). See also [Notes API](notes.md).
|
Emojis can also [be awarded](../user/award_emojis.md#award-emojis-for-comments) on comments (also known as notes). See also [Notes API](notes.md).
|
||||||
|
|
||||||
## Issues, merge requests, and snippets
|
## Issues, merge requests, and snippets
|
||||||
|
|
||||||
See [Award Emoji on Comments](#award-emoji-on-comments) for information on using these endpoints with comments.
|
See [Award emojis on comments](#award-emojis-on-comments) for information on using these endpoints with comments.
|
||||||
|
|
||||||
### List an awardable's award emojis
|
### List an awardable's award emojis
|
||||||
|
|
||||||
|
@ -201,7 +201,7 @@ Parameters:
|
||||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344"
|
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Award Emoji on Comments
|
## Award emojis on comments
|
||||||
|
|
||||||
Comments (also known as notes) are a sub-resource of issues, merge requests, and snippets.
|
Comments (also known as notes) are a sub-resource of issues, merge requests, and snippets.
|
||||||
|
|
||||||
|
@ -366,7 +366,7 @@ Parameters:
|
||||||
| `id` | integer/string | yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). |
|
| `id` | integer/string | yes | ID or [URL-encoded path of the project](index.md#namespaced-path-encoding). |
|
||||||
| `issue_iid` | integer | yes | Internal ID of an issue. |
|
| `issue_iid` | integer | yes | Internal ID of an issue. |
|
||||||
| `note_id` | integer | yes | ID of a comment (note). |
|
| `note_id` | integer | yes | ID of a comment (note). |
|
||||||
| `award_id` | integer | yes | ID of an award_emoji. |
|
| `award_id` | integer | yes | ID of an award emoji. |
|
||||||
|
|
||||||
Example request:
|
Example request:
|
||||||
|
|
||||||
|
|
|
@ -6,17 +6,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
|
|
||||||
# Use custom emojis with GraphQL **(FREE)**
|
# Use custom emojis with GraphQL **(FREE)**
|
||||||
|
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911) in GitLab 13.6
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911) in GitLab 13.6 [with a flag](../../administration/feature_flags.md) named `custom_emoji`. Disabled by default.
|
||||||
> - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
|
> - Enabled on GitLab.com in GitLab 14.0.
|
||||||
> - Enabled on GitLab.com.
|
|
||||||
> - Recommended for production use.
|
|
||||||
> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-custom-emoji-api). **(FREE SELF)**
|
|
||||||
|
|
||||||
This in-development feature might not be available for your use. There can be
|
FLAG:
|
||||||
[risks when enabling features still in development](../../administration/feature_flags.md#risks-when-enabling-features-still-in-development).
|
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `custom_emoji`.
|
||||||
Refer to this feature's version history for more details.
|
On GitLab.com, this feature is available.
|
||||||
|
This feature is ready for production use.
|
||||||
|
|
||||||
To use custom emoji in comments and descriptions, you can add them to a group using the GraphQL API.
|
To use custom emojis in comments and descriptions, you can add them to a group using the GraphQL API.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
|
||||||
|
@ -40,7 +38,7 @@ mutation CreateCustomEmoji($groupPath: ID!) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
After adding custom emoji to the group, members can use it in the same way as other emoji in the comments.
|
After adding a custom emoji to the group, members can use it in the same way as other emojis in the comments.
|
||||||
|
|
||||||
## Get custom emoji for a group
|
## Get custom emoji for a group
|
||||||
|
|
||||||
|
@ -92,22 +90,3 @@ For more information on:
|
||||||
- GraphQL specific entities, such as Fragments and Interfaces, see the official
|
- GraphQL specific entities, such as Fragments and Interfaces, see the official
|
||||||
[GraphQL documentation](https://graphql.org/learn/).
|
[GraphQL documentation](https://graphql.org/learn/).
|
||||||
- Individual attributes, see the [GraphQL API Resources](reference/index.md).
|
- Individual attributes, see the [GraphQL API Resources](reference/index.md).
|
||||||
|
|
||||||
## Enable or disable custom emoji API **(FREE SELF)**
|
|
||||||
|
|
||||||
Custom emoji is under development but ready for production use. It is
|
|
||||||
deployed behind a feature flag that is **disabled by default**.
|
|
||||||
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
|
|
||||||
can enable it.
|
|
||||||
|
|
||||||
To enable it:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
Feature.enable(:custom_emoji)
|
|
||||||
```
|
|
||||||
|
|
||||||
To disable it:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
Feature.disable(:custom_emoji)
|
|
||||||
```
|
|
||||||
|
|
|
@ -651,8 +651,8 @@ Supported attributes:
|
||||||
| `merge_commit_sha` | string | SHA of the merge request commit (set once merged). |
|
| `merge_commit_sha` | string | SHA of the merge request commit (set once merged). |
|
||||||
| `merge_error` | string | Error message due to a merge error. |
|
| `merge_error` | string | Error message due to a merge error. |
|
||||||
| `merge_user` | object | User who merged this merge request or set it to merge when pipeline succeeds. |
|
| `merge_user` | object | User who merged this merge request or set it to merge when pipeline succeeds. |
|
||||||
| `merge_status` | string | Status of the merge request. Can be `unchecked`, `checking`, `can_be_merged`, `cannot_be_merged` or `cannot_be_merged_recheck`. |
|
| `merge_status` | string | Status of the merge request. Can be `unchecked`, `checking`, `can_be_merged`, `cannot_be_merged` or `cannot_be_merged_recheck`. [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/3169#note_1162532204) in GitLab 15.6. Use `detailed_merge_status` instead. |
|
||||||
| `merge_when_pipeline_succeeds` | boolean | Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS). |
|
| `merge_when_pipeline_succeeds` | boolean | Indicates if the merge has been set to be merged when its pipeline succeeds. |
|
||||||
| `merged_at` | datetime | Timestamp of when the merge request was merged. |
|
| `merged_at` | datetime | Timestamp of when the merge request was merged. |
|
||||||
| `merged_by` | object | Deprecated: Use `merge_user` instead. User who merged this merge request or set it to merge when pipeline succeeds. |
|
| `merged_by` | object | Deprecated: Use `merge_user` instead. User who merged this merge request or set it to merge when pipeline succeeds. |
|
||||||
| `milestone` | object | Milestone of the merge request. |
|
| `milestone` | object | Milestone of the merge request. |
|
||||||
|
@ -848,14 +848,11 @@ the `approvals_before_merge` parameter:
|
||||||
|
|
||||||
### Merge status
|
### Merge status
|
||||||
|
|
||||||
> The `detailed_merge_status` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101724) in GitLab 15.6.
|
> - The `detailed_merge_status` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101724) in GitLab 15.6.
|
||||||
|
> - The `merge_status` field was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/3169#note_1162532204) in GitLab 15.6.
|
||||||
|
|
||||||
|
Use `detailed_merge_status` instead of `merge_status` to account for all potential statuses.
|
||||||
|
|
||||||
- The `merge_status` field may hold one of the following values:
|
|
||||||
- `unchecked`: This merge request has not yet been checked.
|
|
||||||
- `checking`: This merge request is currently being checked to see if it can be merged.
|
|
||||||
- `can_be_merged`: This merge request can be merged without conflict.
|
|
||||||
- `cannot_be_merged`: There are merge conflicts between the source and target branches.
|
|
||||||
- `cannot_be_merged_recheck`: Currently unchecked. Before the current changes, there were conflicts.
|
|
||||||
- The `detailed_merge_status` field may hold one of the following values:
|
- The `detailed_merge_status` field may hold one of the following values:
|
||||||
- `blocked_status`: Merge request is blocked by another merge request.
|
- `blocked_status`: Merge request is blocked by another merge request.
|
||||||
- `broken_status`: Can not merge the source into the target branch, potential conflict.
|
- `broken_status`: Can not merge the source into the target branch, potential conflict.
|
||||||
|
|
|
@ -1,50 +1,41 @@
|
||||||
---
|
---
|
||||||
stage: Verify
|
stage: Verify
|
||||||
group: Pipeline Execution
|
group: Pipeline Authoring
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
# Get started with GitLab CI/CD **(FREE)**
|
# Tutorial: Create and run your first GitLab CI/CD pipeline **(FREE)**
|
||||||
|
|
||||||
Use this document to get started with [GitLab CI/CD](../index.md).
|
This tutorial shows you how to configure and run your first CI/CD pipeline in GitLab.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
Before you start, make sure you have:
|
Before you start, make sure you have:
|
||||||
|
|
||||||
- A project in GitLab that you would like to use CI/CD for.
|
- A project in GitLab that you would like to use CI/CD for.
|
||||||
- The Maintainer or Owner role for the project.
|
- The Maintainer or Owner role for the project.
|
||||||
|
|
||||||
If you are migrating from another CI/CD tool, view this documentation:
|
If you don't have a project, you can create a public project for free on <https://gitlab.com>.
|
||||||
|
|
||||||
- [Migrate from CircleCI](../migration/circleci.md).
|
## Steps
|
||||||
- [Migrate from Jenkins](../migration/jenkins.md).
|
|
||||||
|
|
||||||
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch [First time GitLab & CI/CD](https://www.youtube.com/watch?v=kTNfi5z6Uvk&t=553s). This includes a quick introduction to GitLab, the first steps with CI/CD, building a Go project, running tests, using the CI/CD pipeline editor, detecting secrets and security vulnerabilities and offers more exercises for asynchronous practice.
|
To create and run your first pipeline:
|
||||||
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch [Intro to GitLab CI](https://www.youtube.com/watch?v=l5705U8s_nQ&t=358s). This workshop uses the Web IDE to quickly get going with building source code using CI/CD, and run unit tests.
|
|
||||||
|
|
||||||
## CI/CD process overview
|
|
||||||
|
|
||||||
To use GitLab CI/CD:
|
|
||||||
|
|
||||||
1. [Ensure you have runners available](#ensure-you-have-runners-available) to run your jobs.
|
1. [Ensure you have runners available](#ensure-you-have-runners-available) to run your jobs.
|
||||||
GitLab SaaS provides runners, so if you're using GitLab.com, you can skip this step.
|
|
||||||
|
|
||||||
If you don't have a runner, [install GitLab Runner](https://docs.gitlab.com/runner/install/)
|
If you're using GitLab.com, you can skip this step. GitLab.com provides shared runners for you.
|
||||||
and [register a runner](https://docs.gitlab.com/runner/register/) for your instance, project, or group.
|
|
||||||
1. [Create a `.gitlab-ci.yml` file](#create-a-gitlab-ciyml-file)
|
1. [Create a `.gitlab-ci.yml` file](#create-a-gitlab-ciyml-file)
|
||||||
at the root of your repository. This file is where you define your CI/CD jobs.
|
at the root of your repository. This file is where you define the CI/CD jobs.
|
||||||
|
|
||||||
When you commit the file to your repository, the runner runs your jobs.
|
When you commit the file to your repository, the runner runs your jobs.
|
||||||
The job results [are displayed in a pipeline](#view-the-status-of-your-pipeline-and-jobs).
|
The job results [are displayed in a pipeline](#view-the-status-of-your-pipeline-and-jobs).
|
||||||
|
|
||||||
### Ensure you have runners available
|
## Ensure you have runners available
|
||||||
|
|
||||||
In GitLab, runners are agents that run your CI/CD jobs.
|
In GitLab, runners are agents that run your CI/CD jobs.
|
||||||
|
|
||||||
You might already have runners available for your project, including
|
|
||||||
[shared runners](../runners/runners_scope.md), which are
|
|
||||||
available to all projects in your GitLab instance.
|
|
||||||
|
|
||||||
To view available runners:
|
To view available runners:
|
||||||
|
|
||||||
- Go to **Settings > CI/CD** and expand **Runners**.
|
- Go to **Settings > CI/CD** and expand **Runners**.
|
||||||
|
@ -52,34 +43,32 @@ To view available runners:
|
||||||
As long as you have at least one runner that's active, with a green circle next to it,
|
As long as you have at least one runner that's active, with a green circle next to it,
|
||||||
you have a runner available to process your jobs.
|
you have a runner available to process your jobs.
|
||||||
|
|
||||||
If no runners are listed on the **Runners** page in the UI, you or an administrator
|
### If you don't have a runner
|
||||||
must [install GitLab Runner](https://docs.gitlab.com/runner/install/) and
|
|
||||||
[register](https://docs.gitlab.com/runner/register/) at least one runner.
|
|
||||||
|
|
||||||
If you are testing CI/CD, you can install GitLab Runner and register runners on your local machine.
|
If you don't have a runner:
|
||||||
When your CI/CD jobs run, they run on your local machine.
|
|
||||||
|
|
||||||
### Create a `.gitlab-ci.yml` file
|
1. [Install GitLab Runner](https://docs.gitlab.com/runner/install/) on your local machine.
|
||||||
|
1. [Register the runner](https://docs.gitlab.com/runner/register/) for your project.
|
||||||
|
Choose the `shell` executor.
|
||||||
|
|
||||||
The `.gitlab-ci.yml` file is a [YAML](https://en.wikipedia.org/wiki/YAML) file where
|
When your CI/CD jobs run, in a later step, they will run on your local machine.
|
||||||
you configure specific instructions for GitLab CI/CD.
|
|
||||||
|
## Create a `.gitlab-ci.yml` file
|
||||||
|
|
||||||
|
Now create a `.gitlab-ci.yml` file. It is a [YAML](https://en.wikipedia.org/wiki/YAML) file where
|
||||||
|
you specify instructions for GitLab CI/CD.
|
||||||
|
|
||||||
In this file, you define:
|
In this file, you define:
|
||||||
|
|
||||||
- The structure and order of jobs that the runner should execute.
|
- The structure and order of jobs that the runner should execute.
|
||||||
- The decisions the runner should make when specific conditions are encountered.
|
- The decisions the runner should make when specific conditions are encountered.
|
||||||
|
|
||||||
For example, you might want to run a suite of tests when you commit to
|
|
||||||
any branch except the default branch. When you commit to the default branch, you want
|
|
||||||
to run the same suite, but also publish your application.
|
|
||||||
|
|
||||||
All of this is defined in the `.gitlab-ci.yml` file.
|
|
||||||
|
|
||||||
To create a `.gitlab-ci.yml` file:
|
To create a `.gitlab-ci.yml` file:
|
||||||
|
|
||||||
1. On the left sidebar, select **Project information > Details**.
|
1. On the left sidebar, select **Repository > Files**.
|
||||||
1. Above the file list, select the branch you want to commit to,
|
1. Above the file list, select the branch you want to commit to.
|
||||||
select the plus icon, then select **New file**:
|
If you're not sure, leave `master` or `main`.
|
||||||
|
Then select the plus icon (**{plus}**) and **New file**:
|
||||||
|
|
||||||
![New file](img/new_file_v13_6.png)
|
![New file](img/new_file_v13_6.png)
|
||||||
|
|
||||||
|
@ -112,46 +101,50 @@ To create a `.gitlab-ci.yml` file:
|
||||||
environment: production
|
environment: production
|
||||||
```
|
```
|
||||||
|
|
||||||
`$GITLAB_USER_LOGIN` and `$CI_COMMIT_BRANCH` are
|
This example shows four jobs: `build-job`, `test-job1`, `test-job2`, and `deploy-prod`.
|
||||||
[predefined variables](../variables/predefined_variables.md)
|
The comments listed in the `echo` commands are displayed in the UI when you view the jobs.
|
||||||
that populate when the job runs.
|
The values for the [predefined variables](../variables/predefined_variables.md)
|
||||||
|
`$GITLAB_USER_LOGIN` and `$CI_COMMIT_BRANCH` are populated when the jobs run.
|
||||||
|
|
||||||
1. Select **Commit changes**.
|
1. Select **Commit changes**.
|
||||||
|
|
||||||
The pipeline starts when the commit is committed.
|
The pipeline starts and runs the jobs you defined in the `.gitlab-ci.yml` file.
|
||||||
|
|
||||||
#### `.gitlab-ci.yml` tips
|
## View the status of your pipeline and jobs
|
||||||
|
|
||||||
- After you create your first `.gitlab-ci.yml` file, use the [pipeline editor](../pipeline_editor/index.md)
|
Now take a look at your pipeline and the jobs within.
|
||||||
for all future edits to the file. With the pipeline editor, you can:
|
|
||||||
- Edit the pipeline configuration with automatic syntax highlighting and validation.
|
|
||||||
- View the [CI/CD configuration visualization](../pipeline_editor/index.md#visualize-ci-configuration),
|
|
||||||
a graphical representation of your `.gitlab-ci.yml` file.
|
|
||||||
- If you want the runner to [use a Docker container to run the jobs](../docker/using_docker_images.md),
|
|
||||||
edit the `.gitlab-ci.yml` file
|
|
||||||
to include an image name:
|
|
||||||
|
|
||||||
```yaml
|
1. Go to **CI/CD > Pipelines**. A pipeline with three stages should be displayed:
|
||||||
default:
|
|
||||||
image: ruby:2.7.5
|
|
||||||
```
|
|
||||||
|
|
||||||
This command tells the runner to use a Ruby image from Docker Hub
|
![Three stages](img/three_stages_v13_6.png)
|
||||||
and to run the jobs in a container that's generated from the image.
|
|
||||||
|
|
||||||
This process is different than
|
1. View a visual representation of your pipeline by selecting the pipeline ID:
|
||||||
[building an application as a Docker container](../docker/using_docker_build.md).
|
|
||||||
Your application does not need to be built as a Docker container to
|
|
||||||
run CI/CD jobs in Docker containers.
|
|
||||||
|
|
||||||
- Each job contains scripts and stages:
|
![Pipeline graph](img/pipeline_graph_v13_6.png)
|
||||||
|
|
||||||
|
1. View details of a job by selecting the job name. For example, `deploy-prod`:
|
||||||
|
|
||||||
|
![Job details](img/job_details_v13_6.png)
|
||||||
|
|
||||||
|
You have successfully created your first CI/CD pipeline in GitLab. Congratulations!
|
||||||
|
|
||||||
|
Now you can get started customizing your `.gitlab-ci.yml` and defining more advanced jobs.
|
||||||
|
|
||||||
|
## `.gitlab-ci.yml` tips
|
||||||
|
|
||||||
|
Here are some tips to get started working with the `.gitlab-ci.yml` file.
|
||||||
|
|
||||||
|
For the complete `.gitlab-ci.yml` syntax, see [the full `.gitlab-ci.yml` keyword reference](../yaml/index.md).
|
||||||
|
|
||||||
|
- Use the [pipeline editor](../pipeline_editor/index.md) to edit your `.gitlab-ci.yml` file.
|
||||||
|
- Each job contains a script section and belongs to a stage:
|
||||||
- The [`default`](../yaml/index.md#default) keyword is for
|
- The [`default`](../yaml/index.md#default) keyword is for
|
||||||
custom defaults, for example with [`before_script`](../yaml/index.md#before_script)
|
custom defaults, for example with [`before_script`](../yaml/index.md#before_script)
|
||||||
and [`after_script`](../yaml/index.md#after_script).
|
and [`after_script`](../yaml/index.md#after_script).
|
||||||
- [`stage`](../yaml/index.md#stage) describes the sequential execution of jobs.
|
- [`stage`](../yaml/index.md#stage) describes the sequential execution of jobs.
|
||||||
Jobs in a single stage run in parallel as long as there are available runners.
|
Jobs in a single stage run in parallel as long as there are available runners.
|
||||||
- Use [Directed Acyclic Graphs (DAG)](../directed_acyclic_graph/index.md) keywords
|
- Use the [`needs` keyword](../yaml/index.md#needs) to run jobs out of stage order.
|
||||||
to run jobs out of stage order.
|
This creates a [Directed Acyclic Graph (DAG)](../directed_acyclic_graph/index.md).
|
||||||
- You can set additional configuration to customize how your jobs and stages perform:
|
- You can set additional configuration to customize how your jobs and stages perform:
|
||||||
- Use the [`rules`](../yaml/index.md#rules) keyword to specify when to run or skip jobs.
|
- Use the [`rules`](../yaml/index.md#rules) keyword to specify when to run or skip jobs.
|
||||||
The `only` and `except` legacy keywords are still supported, but can't be used
|
The `only` and `except` legacy keywords are still supported, but can't be used
|
||||||
|
@ -159,26 +152,10 @@ The pipeline starts when the commit is committed.
|
||||||
- Keep information across jobs and stages persistent in a pipeline with [`cache`](../yaml/index.md#cache)
|
- Keep information across jobs and stages persistent in a pipeline with [`cache`](../yaml/index.md#cache)
|
||||||
and [`artifacts`](../yaml/index.md#artifacts). These keywords are ways to store
|
and [`artifacts`](../yaml/index.md#artifacts). These keywords are ways to store
|
||||||
dependencies and job output, even when using ephemeral runners for each job.
|
dependencies and job output, even when using ephemeral runners for each job.
|
||||||
- For the complete `.gitlab-ci.yml` syntax, see [the full `.gitlab-ci.yml` reference topic](../yaml/index.md).
|
|
||||||
|
|
||||||
### View the status of your pipeline and jobs
|
## Related topics
|
||||||
|
|
||||||
When you committed your changes, a pipeline started.
|
- [Follow this guide to migrate from CircleCI](../migration/circleci.md).
|
||||||
|
- [Follow this guide to migrate from Jenkins](../migration/jenkins.md).
|
||||||
To view your pipeline:
|
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch [First time GitLab & CI/CD](https://www.youtube.com/watch?v=kTNfi5z6Uvk&t=553s). This includes a quick introduction to GitLab, the first steps with CI/CD, building a Go project, running tests, using the CI/CD pipeline editor, detecting secrets and security vulnerabilities and offers more exercises for asynchronous practice.
|
||||||
|
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch [Intro to GitLab CI](https://www.youtube.com/watch?v=l5705U8s_nQ&t=358s). This workshop uses the Web IDE to quickly get going with building source code using CI/CD, and run unit tests.
|
||||||
- Go to **CI/CD > Pipelines**.
|
|
||||||
|
|
||||||
A pipeline with three stages should be displayed:
|
|
||||||
|
|
||||||
![Three stages](img/three_stages_v13_6.png)
|
|
||||||
|
|
||||||
- To view a visual representation of your pipeline, select the pipeline ID.
|
|
||||||
|
|
||||||
![Pipeline graph](img/pipeline_graph_v13_6.png)
|
|
||||||
|
|
||||||
- To view details of a job, select the job name, for example, `deploy-prod`.
|
|
||||||
|
|
||||||
![Job details](img/job_details_v13_6.png)
|
|
||||||
|
|
||||||
If the job status is `stuck`, check to ensure a runner is properly configured for the project.
|
|
||||||
|
|
|
@ -47,12 +47,11 @@ If you want to know the in-depth details, here's what's really happening:
|
||||||
1. The preview URL is shown both at the job output and in the merge request
|
1. The preview URL is shown both at the job output and in the merge request
|
||||||
widget. You also get the link to the remote pipeline.
|
widget. You also get the link to the remote pipeline.
|
||||||
1. In the `gitlab-org/gitlab-docs` project, the pipeline is created and it
|
1. In the `gitlab-org/gitlab-docs` project, the pipeline is created and it
|
||||||
[skips the test jobs](https://gitlab.com/gitlab-org/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55)
|
[skips most test jobs](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/d41ca9323f762132780d2d072f845d28817a5383/.gitlab/ci/rules.gitlab-ci.yml#L101-103)
|
||||||
to lower the build time.
|
to lower the build time.
|
||||||
1. Once the docs site is built, the HTML files are uploaded as artifacts.
|
1. After the docs site is built, the HTML files are uploaded as artifacts to
|
||||||
1. A specific runner tied only to the docs project, runs the Review App job
|
a GCP bucket (see [issue `gitlab-com/gl-infra/reliability#11021`](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/11021)
|
||||||
that downloads the artifacts and uses `rsync` to transfer the files over
|
for the implementation details).
|
||||||
to a location where NGINX serves them.
|
|
||||||
|
|
||||||
The following GitLab features are used among others:
|
The following GitLab features are used among others:
|
||||||
|
|
||||||
|
@ -60,42 +59,26 @@ The following GitLab features are used among others:
|
||||||
- [Multi project pipelines](../../ci/pipelines/downstream_pipelines.md#multi-project-pipelines)
|
- [Multi project pipelines](../../ci/pipelines/downstream_pipelines.md#multi-project-pipelines)
|
||||||
- [Review Apps](../../ci/review_apps/index.md)
|
- [Review Apps](../../ci/review_apps/index.md)
|
||||||
- [Artifacts](../../ci/yaml/index.md#artifacts)
|
- [Artifacts](../../ci/yaml/index.md#artifacts)
|
||||||
- [Specific runner](../../ci/runners/runners_scope.md#prevent-a-specific-runner-from-being-enabled-for-other-projects)
|
|
||||||
- [Merge request pipelines](../../ci/pipelines/merge_request_pipelines.md)
|
- [Merge request pipelines](../../ci/pipelines/merge_request_pipelines.md)
|
||||||
|
|
||||||
## Troubleshooting review apps
|
## Troubleshooting review apps
|
||||||
|
|
||||||
### Review app returns a 404 error
|
### `NoSuchKey The specified key does not exist`
|
||||||
|
|
||||||
If the review app URL returns a 404 error, either the site is not
|
If you see the following message in a review app, either the site is not
|
||||||
yet deployed, or something went wrong with the remote pipeline. You can:
|
yet deployed, or something went wrong with the downstream pipeline in `gitlab-docs`.
|
||||||
|
|
||||||
- Wait a few minutes and it should appear online.
|
```plaintext
|
||||||
- Check the manual job's log and verify the URL. If the URL is different, try the
|
NoSuchKeyThe specified key does not exist.No such object: <URL>
|
||||||
one from the job log.
|
```
|
||||||
|
|
||||||
|
In that case, you can:
|
||||||
|
|
||||||
|
- Wait a few minutes and the review app should appear online.
|
||||||
|
- Check the `review-docs-deploy` job's log and verify the URL. If the URL shown in the merge
|
||||||
|
request UI is different than the job log, try the one from the job log.
|
||||||
- Check the status of the remote pipeline from the link in the merge request's job output.
|
- Check the status of the remote pipeline from the link in the merge request's job output.
|
||||||
If the pipeline failed or got stuck, GitLab team members can ask for help in the `#docs`
|
If the pipeline failed or got stuck, GitLab team members can ask for help in the `#docs`
|
||||||
chat channel. Contributors can ping a technical writer in the merge request.
|
internal Slack channel. Contributors can ping a
|
||||||
|
[technical writer](https://about.gitlab.com/handbook/product/ux/technical-writing/#designated-technical-writers)
|
||||||
### Not enough disk space
|
in the merge request.
|
||||||
|
|
||||||
Sometimes the review app server is full and there is no more disk space. Each review
|
|
||||||
app takes about 570MB of disk space.
|
|
||||||
|
|
||||||
A cron job to remove review apps older than 20 days runs hourly,
|
|
||||||
but the disk space still occasionally fills up. To manually free up more space,
|
|
||||||
a GitLab technical writing team member can:
|
|
||||||
|
|
||||||
1. Navigate to the [`gitlab-docs` schedules page](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules).
|
|
||||||
1. Select the play button for the `Remove old review apps from review app server`
|
|
||||||
schedule. By default, this cleans up review apps older than 14 days.
|
|
||||||
1. Navigate to the [pipelines page](https://gitlab.com/gitlab-org/gitlab-docs/-/pipelines)
|
|
||||||
and start the manual job called `clean-pages`.
|
|
||||||
|
|
||||||
If the job says no review apps were found in that period, edit the `CLEAN_REVIEW_APPS_DAYS`
|
|
||||||
variable in the schedule, and repeat the process above. Gradually decrease the variable
|
|
||||||
until the free disk space reaches an acceptable amount (for example, 3GB).
|
|
||||||
Remember to set it to 14 again when you're done.
|
|
||||||
|
|
||||||
There's an issue to [migrate from the DigitalOcean server to GCP buckets](https://gitlab.com/gitlab-org/gitlab-docs/-/issues/735)),
|
|
||||||
which should solve the disk space problem.
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ but that repository is no longer used for development.
|
||||||
|
|
||||||
## Install Workhorse
|
## Install Workhorse
|
||||||
|
|
||||||
To install GitLab Workhorse you need [Go 1.15 or newer](https://go.dev/dl) and
|
To install GitLab Workhorse you need [Go 1.18 or newer](https://go.dev/dl) and
|
||||||
[GNU Make](https://www.gnu.org/software/make/).
|
[GNU Make](https://www.gnu.org/software/make/).
|
||||||
|
|
||||||
To install into `/usr/local/bin` run `make install`.
|
To install into `/usr/local/bin` run `make install`.
|
||||||
|
|
|
@ -53,7 +53,7 @@ CI/CD pipelines are used to automatically build, test, and deploy your code.
|
||||||
|
|
||||||
| Topic | Description | Good for beginners |
|
| Topic | Description | Good for beginners |
|
||||||
|-------|-------------|--------------------|
|
|-------|-------------|--------------------|
|
||||||
| [Get started: Create a pipeline](../ci/quick_start/index.md) | Create a `.gitlab-ci.yml` file and start a pipeline. | **{star}** |
|
| [Tutorial: Create and run your first GitLab CI/CD pipeline](../ci/quick_start/index.md) | Create a `.gitlab-ci.yml` file and start a pipeline. | **{star}** |
|
||||||
| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Get started: Learn about CI/CD](https://www.youtube.com/watch?v=sIegJaLy2ug) (9m 02s) | Learn about the `.gitlab-ci.yml` file and how it's used. | **{star}** |
|
| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Get started: Learn about CI/CD](https://www.youtube.com/watch?v=sIegJaLy2ug) (9m 02s) | Learn about the `.gitlab-ci.yml` file and how it's used. | **{star}** |
|
||||||
| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [CI deep dive](https://www.youtube.com/watch?v=ZVUbmVac-m8&list=PL05JrBw4t0KorkxIFgZGnzzxjZRCGROt_&index=27) (22m 51s) | Take a closer look at pipelines and continuous integration concepts. | |
|
| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [CI deep dive](https://www.youtube.com/watch?v=ZVUbmVac-m8&list=PL05JrBw4t0KorkxIFgZGnzzxjZRCGROt_&index=27) (22m 51s) | Take a closer look at pipelines and continuous integration concepts. | |
|
||||||
| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [CD deep dive](https://www.youtube.com/watch?v=Cn0rzND-Yjw&list=PL05JrBw4t0KorkxIFgZGnzzxjZRCGROt_&index=10) (47m 54s) | Learn about deploying in GitLab. | |
|
| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [CD deep dive](https://www.youtube.com/watch?v=Cn0rzND-Yjw&list=PL05JrBw4t0KorkxIFgZGnzzxjZRCGROt_&index=10) (47m 54s) | Learn about deploying in GitLab. | |
|
||||||
|
|
|
@ -37,11 +37,11 @@ Each time you push a change, Git records it as a unique *commit*. These commits
|
||||||
the history of when and how a file changed, and who changed it.
|
the history of when and how a file changed, and who changed it.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TB
|
graph LR
|
||||||
subgraph Repository commit history
|
subgraph Repository commit history
|
||||||
A(Author: Alex<br>Date: 3 Jan at 1PM<br>Commit message: Added sales figures for January<br> Commit ID: 123abc12) ---> B
|
A(Author: Alex<br>Date: 3 Jan at 1PM<br>Commit message: Added sales figures<br> Commit ID: 123abc12) ---> B
|
||||||
B(Author: Sam<br>Date: 4 Jan at 10AM<br>Commit message: Removed outdated marketing information<br> Commit ID: aabb1122) ---> C
|
B(Author: Sam<br>Date: 4 Jan at 10AM<br>Commit message: Removed old info<br> Commit ID: aabb1122) ---> C
|
||||||
C(Author: Zhang<br>Date: 5 Jan at 3PM<br>Commit message: Added a new 'Invoices' file<br> Commit ID: ddee4455)
|
C(Author: Zhang<br>Date: 5 Jan at 3PM<br>Commit message: Added invoices<br> Commit ID: ddee4455)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -54,15 +54,14 @@ of a repository are in a default branch. To make changes, you:
|
||||||
1. When you're ready, *merge* your branch into the default branch.
|
1. When you're ready, *merge* your branch into the default branch.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart LR
|
||||||
subgraph Default branch
|
subgraph Default branch
|
||||||
A[Commit] --> B[Commit] --> C[Commit] --> D[Commit]
|
A[Commit] --> B[Commit] --> C[Commit] --> D[Commit]
|
||||||
end
|
end
|
||||||
subgraph My branch
|
subgraph My branch
|
||||||
B --1. Create my branch--> E(Commit)
|
B --1. Create my branch--> E(Commit)
|
||||||
E --2. Add my commit--> F(Commit)
|
E --2. Add my commit--> F(Commit)
|
||||||
F --2. Add my commit--> G(Commit)
|
F --3. Merge my branch to default--> D
|
||||||
G --3. Merge my branch to default--> D
|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,21 @@ and method in GitLab 16.0, and introduce a new
|
||||||
This new architecture introduces a new method for registering runners and eliminates the legacy
|
This new architecture introduces a new method for registering runners and eliminates the legacy
|
||||||
[runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens).
|
[runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens).
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deprecation removal-160 breaking-change">
|
||||||
|
|
||||||
|
### `gitlab-runner register` command
|
||||||
|
|
||||||
|
End of Support: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)<br />
|
||||||
|
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)
|
||||||
|
|
||||||
|
WARNING:
|
||||||
|
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
|
||||||
|
Review the details carefully before upgrading.
|
||||||
|
|
||||||
|
The command to [register](https://docs.gitlab.com/runner/register/) a runner, `gitlab-runner register` is deprecated. GitLab plans to introduce a new [GitLab Runner token architecture](https://docs.gitlab.com/ee/architecture/blueprints/runner_tokens/), which introduces a new method for registering runners and eliminates the legacy [runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens).
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -4,22 +4,22 @@ group: Project Management
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||||
---
|
---
|
||||||
|
|
||||||
# Award emoji **(FREE)**
|
# Award emojis **(FREE)**
|
||||||
|
|
||||||
When you're collaborating online, you get fewer opportunities for high-fives
|
When you're collaborating online, you get fewer opportunities for high-fives
|
||||||
and thumbs-ups. Emoji can be awarded to [issues](project/issues/index.md), [merge requests](project/merge_requests/index.md),
|
and thumbs-ups. Emojis can be awarded to [issues](project/issues/index.md), [merge requests](project/merge_requests/index.md),
|
||||||
[snippets](snippets.md), and anywhere you can have a thread.
|
[snippets](snippets.md), and anywhere you can have a thread.
|
||||||
|
|
||||||
![Award emoji](img/award_emoji_select_v14_6.png)
|
![Award emoji](img/award_emoji_select_v14_6.png)
|
||||||
|
|
||||||
Award emoji make it much easier to give and receive feedback without a long
|
Award emojis make it much easier to give and receive feedback without a long
|
||||||
comment thread.
|
comment thread.
|
||||||
|
|
||||||
For information on the relevant API, see [Award Emoji API](../api/award_emoji.md).
|
For information on the relevant API, see [Award Emoji API](../api/award_emoji.md).
|
||||||
|
|
||||||
## Sort issues and merge requests on vote count
|
## Sort issues and merge requests on vote count
|
||||||
|
|
||||||
You can quickly sort issues and merge requests by the number of votes they
|
You can quickly sort issues and merge requests by the number of votes ("thumbs up" and "thumbs down" emoji) they
|
||||||
have received. The sort options can be found in the dropdown list as "Most
|
have received. The sort options can be found in the dropdown list as "Most
|
||||||
popular" and "Least popular".
|
popular" and "Least popular".
|
||||||
|
|
||||||
|
@ -29,9 +29,9 @@ The total number of votes is not summed up. An issue with 18 upvotes and 5
|
||||||
downvotes is considered more popular than an issue with 17 upvotes and no
|
downvotes is considered more popular than an issue with 17 upvotes and no
|
||||||
downvotes.
|
downvotes.
|
||||||
|
|
||||||
## Award emoji for comments
|
## Award emojis for comments
|
||||||
|
|
||||||
Award emoji can also be applied to individual comments when you want to
|
Award emojis can also be applied to individual comments when you want to
|
||||||
celebrate an accomplishment or agree with an opinion.
|
celebrate an accomplishment or agree with an opinion.
|
||||||
|
|
||||||
To add an award emoji:
|
To add an award emoji:
|
||||||
|
@ -40,3 +40,11 @@ To add an award emoji:
|
||||||
1. Select an emoji from the dropdown list.
|
1. Select an emoji from the dropdown list.
|
||||||
|
|
||||||
To remove an award emoji, select the emoji again.
|
To remove an award emoji, select the emoji again.
|
||||||
|
|
||||||
|
## Custom emojis
|
||||||
|
|
||||||
|
You can upload custom emojis to a GitLab instance with the GraphQL API.
|
||||||
|
For more, visit [Use custom emojis with GraphQL](../api/graphql/custom_emoji.md).
|
||||||
|
|
||||||
|
For the list of custom emojis available for GitLab.com, visit
|
||||||
|
[the `custom_emoji` project](https://gitlab.com/custom_emoji/custom_emoji/-/tree/main/img).
|
||||||
|
|
|
@ -1019,7 +1019,7 @@ Payload example:
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
The fields `assignee_id`, and `state` are deprecated.
|
The fields `assignee_id`, `state`, `merge_status` are deprecated.
|
||||||
|
|
||||||
## Wiki page events
|
## Wiki page events
|
||||||
|
|
||||||
|
|
|
@ -6,47 +6,57 @@ module API
|
||||||
include ActionView::Helpers::NumberHelper
|
include ActionView::Helpers::NumberHelper
|
||||||
include CountHelper
|
include CountHelper
|
||||||
|
|
||||||
expose :forks do |counts|
|
expose :forks,
|
||||||
|
documentation: { type: 'integer', example: 6, desc: 'Approximate number of repo forks' } do |counts|
|
||||||
approximate_fork_count_with_delimiters(counts)
|
approximate_fork_count_with_delimiters(counts)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :issues do |counts|
|
expose :issues,
|
||||||
|
documentation: { type: 'integer', example: 121, desc: 'Approximate number of issues' } do |counts|
|
||||||
approximate_count_with_delimiters(counts, ::Issue)
|
approximate_count_with_delimiters(counts, ::Issue)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :merge_requests do |counts|
|
expose :merge_requests,
|
||||||
|
documentation: { type: 'integer', example: 49, desc: 'Approximate number of merge requests' } do |counts|
|
||||||
approximate_count_with_delimiters(counts, ::MergeRequest)
|
approximate_count_with_delimiters(counts, ::MergeRequest)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :notes do |counts|
|
expose :notes,
|
||||||
|
documentation: { type: 'integer', example: 6, desc: 'Approximate number of notes' } do |counts|
|
||||||
approximate_count_with_delimiters(counts, ::Note)
|
approximate_count_with_delimiters(counts, ::Note)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :snippets do |counts|
|
expose :snippets,
|
||||||
|
documentation: { type: 'integer', example: 4, desc: 'Approximate number of snippets' } do |counts|
|
||||||
approximate_count_with_delimiters(counts, ::Snippet)
|
approximate_count_with_delimiters(counts, ::Snippet)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :ssh_keys do |counts|
|
expose :ssh_keys,
|
||||||
|
documentation: { type: 'integer', example: 11, desc: 'Approximate number of SSH keys' } do |counts|
|
||||||
approximate_count_with_delimiters(counts, ::Key)
|
approximate_count_with_delimiters(counts, ::Key)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :milestones do |counts|
|
expose :milestones,
|
||||||
|
documentation: { type: 'integer', example: 3, desc: 'Approximate number of milestones' } do |counts|
|
||||||
approximate_count_with_delimiters(counts, ::Milestone)
|
approximate_count_with_delimiters(counts, ::Milestone)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :users do |counts|
|
expose :users, documentation: { type: 'integer', example: 22, desc: 'Approximate number of users' } do |counts|
|
||||||
approximate_count_with_delimiters(counts, ::User)
|
approximate_count_with_delimiters(counts, ::User)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :projects do |counts|
|
expose :projects,
|
||||||
|
documentation: { type: 'integer', example: 4, desc: 'Approximate number of projects' } do |counts|
|
||||||
approximate_count_with_delimiters(counts, ::Project)
|
approximate_count_with_delimiters(counts, ::Project)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :groups do |counts|
|
expose :groups,
|
||||||
|
documentation: { type: 'integer', example: 1, desc: 'Approximate number of projects' } do |counts|
|
||||||
approximate_count_with_delimiters(counts, ::Group)
|
approximate_count_with_delimiters(counts, ::Group)
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :active_users do |_|
|
expose :active_users,
|
||||||
|
documentation: { type: 'integer', example: 21, desc: 'Number of active users' } do |_|
|
||||||
number_with_delimiter(::User.active.count)
|
number_with_delimiter(::User.active.count)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ module API
|
||||||
MergeRequest, Note, Snippet, Key, Milestone].freeze
|
MergeRequest, Note, Snippet, Key, Milestone].freeze
|
||||||
|
|
||||||
desc 'Get the current application statistics' do
|
desc 'Get the current application statistics' do
|
||||||
success Entities::ApplicationStatistics
|
success code: 200, model: Entities::ApplicationStatistics
|
||||||
end
|
end
|
||||||
get "application/statistics", urgency: :low do
|
get "application/statistics", urgency: :low do
|
||||||
counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
|
counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module BackgroundMigration
|
||||||
|
# The class to populates the star counter of projects
|
||||||
|
class PopulateProjectsStarCount < BatchedMigrationJob
|
||||||
|
MAX_UPDATE_RETRIES = 3
|
||||||
|
|
||||||
|
operation_name :update_all
|
||||||
|
|
||||||
|
def perform
|
||||||
|
each_sub_batch do |sub_batch|
|
||||||
|
update_with_retry(sub_batch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# rubocop:disable Database/RescueQueryCanceled
|
||||||
|
# rubocop:disable Database/RescueStatementTimeout
|
||||||
|
def update_with_retry(sub_batch)
|
||||||
|
update_attempt = 1
|
||||||
|
|
||||||
|
begin
|
||||||
|
update_batch(sub_batch)
|
||||||
|
rescue ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled => e
|
||||||
|
update_attempt += 1
|
||||||
|
|
||||||
|
if update_attempt <= MAX_UPDATE_RETRIES
|
||||||
|
sleep(5)
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Database/RescueQueryCanceled
|
||||||
|
# rubocop:enable Database/RescueStatementTimeout
|
||||||
|
|
||||||
|
def update_batch(sub_batch)
|
||||||
|
ApplicationRecord.connection.execute <<~SQL
|
||||||
|
WITH batched_relation AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (#{sub_batch.select(:id).to_sql})
|
||||||
|
UPDATE projects
|
||||||
|
SET star_count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM users_star_projects
|
||||||
|
INNER JOIN users
|
||||||
|
ON users_star_projects.user_id = users.id
|
||||||
|
WHERE users_star_projects.project_id = batched_relation.id
|
||||||
|
AND users.state = 'active'
|
||||||
|
)
|
||||||
|
FROM batched_relation
|
||||||
|
WHERE projects.id = batched_relation.id
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -15694,9 +15694,6 @@ msgstr ""
|
||||||
msgid "Error uploading file. Please try again."
|
msgid "Error uploading file. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Error uploading file: %{stripped}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Error while loading the merge request. Please try again."
|
msgid "Error while loading the merge request. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -30557,9 +30554,6 @@ msgstr ""
|
||||||
msgid "Please select a country"
|
msgid "Please select a country"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Please select a file"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Please select a group"
|
msgid "Please select a group"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ module QA
|
||||||
include Helpers
|
include Helpers
|
||||||
|
|
||||||
QA_PATTERN = %r{^qa/}.freeze
|
QA_PATTERN = %r{^qa/}.freeze
|
||||||
SPEC_PATTERN = %r{^qa/qa/specs/features/}.freeze
|
SPEC_PATTERN = %r{^qa/qa/specs/features/\S+_spec\.rb}.freeze
|
||||||
DEPENDENCY_PATTERN = Regexp.union(
|
DEPENDENCY_PATTERN = Regexp.union(
|
||||||
/_VERSION/,
|
/_VERSION/,
|
||||||
/Gemfile\.lock/,
|
/Gemfile\.lock/,
|
||||||
|
|
|
@ -49,6 +49,14 @@ RSpec.describe QA::Tools::Ci::QaChanges do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with shared example changes" do
|
||||||
|
let(:mr_diff) { [{ path: "qa/qa/specs/features/shared_context/some_context.rb", diff: "" }] }
|
||||||
|
|
||||||
|
it ".qa_tests do not return specific specs" do
|
||||||
|
expect(qa_changes.qa_tests).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "with non qa changes" do
|
context "with non qa changes" do
|
||||||
let(:mr_diff) { [{ path: "Gemfile" }] }
|
let(:mr_diff) { [{ path: "Gemfile" }] }
|
||||||
|
|
||||||
|
|
|
@ -34,8 +34,15 @@ RSpec.describe Projects::LearnGitlabController do
|
||||||
it { is_expected.to have_gitlab_http_status(:not_found) }
|
it { is_expected.to have_gitlab_http_status(:not_found) }
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'tracks assignment and records the subject', :invite_for_help_continuous_onboarding, :namespace do
|
context 'with invite_for_help_continuous_onboarding experiment' do
|
||||||
subject { project.namespace }
|
it 'tracks the assignment', :experiment do
|
||||||
|
stub_experiments(invite_for_help_continuous_onboarding: true)
|
||||||
|
|
||||||
|
expect(experiment(:invite_for_help_continuous_onboarding))
|
||||||
|
.to track(:assignment).with_context(namespace: project.namespace).on_next_instance
|
||||||
|
|
||||||
|
action
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -299,14 +299,15 @@ RSpec.describe Projects::PipelinesController do
|
||||||
stub_application_setting(auto_devops_enabled: false)
|
stub_application_setting(auto_devops_enabled: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def action
|
context 'with runners_availability_section experiment' do
|
||||||
get :index, params: { namespace_id: project.namespace, project_id: project }
|
it 'tracks the assignment', :experiment do
|
||||||
end
|
stub_experiments(runners_availability_section: true)
|
||||||
|
|
||||||
subject { project.namespace }
|
expect(experiment(:runners_availability_section))
|
||||||
|
.to track(:assignment).with_context(namespace: project.namespace).on_next_instance
|
||||||
|
|
||||||
context 'runners_availability_section experiment' do
|
get :index, params: { namespace_id: project.namespace, project_id: project }
|
||||||
it_behaves_like 'tracks assignment and records the subject', :runners_availability_section, :namespace
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -43,72 +43,6 @@ RSpec.describe ApplicationExperiment, :experiment do
|
||||||
variant: 'control'
|
variant: 'control'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#publish_to_database' do
|
|
||||||
using RSpec::Parameterized::TableSyntax
|
|
||||||
|
|
||||||
let(:publish_to_database) { ActiveSupport::Deprecation.silence { application_experiment.publish_to_database } }
|
|
||||||
|
|
||||||
shared_examples 'does not record to the database' do
|
|
||||||
it 'does not create an experiment record' do
|
|
||||||
expect { publish_to_database }.not_to change(Experiment, :count)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not create an experiment subject record' do
|
|
||||||
expect { publish_to_database }.not_to change(ExperimentSubject, :count)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there is a usable subject' do
|
|
||||||
let(:context) { { context_key => context_value } }
|
|
||||||
|
|
||||||
where(:context_key, :context_value, :object_type) do
|
|
||||||
:namespace | build(:namespace, id: non_existing_record_id) | :namespace
|
|
||||||
:group | build(:namespace, id: non_existing_record_id) | :namespace
|
|
||||||
:project | build(:project, id: non_existing_record_id) | :project
|
|
||||||
:user | build(:user, id: non_existing_record_id) | :user
|
|
||||||
:actor | build(:user, id: non_existing_record_id) | :user
|
|
||||||
end
|
|
||||||
|
|
||||||
with_them do
|
|
||||||
it 'creates an experiment and experiment subject record' do
|
|
||||||
expect { publish_to_database }.to change(Experiment, :count).by(1)
|
|
||||||
|
|
||||||
expect(Experiment.last.name).to eq('namespaced/stub')
|
|
||||||
expect(ExperimentSubject.last.send(object_type)).to eq(context[context_key])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when experiment hasn't ran" do
|
|
||||||
let(:context) { { user: create(:user) } }
|
|
||||||
|
|
||||||
it 'sets a variant on the experiment subject' do
|
|
||||||
publish_to_database
|
|
||||||
|
|
||||||
expect(ExperimentSubject.last.variant).to eq('control')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there is not a usable subject' do
|
|
||||||
let(:context) { { context_key => context_value } }
|
|
||||||
|
|
||||||
where(:context_key, :context_value) do
|
|
||||||
:namespace | nil
|
|
||||||
:foo | :bar
|
|
||||||
end
|
|
||||||
|
|
||||||
with_them do
|
|
||||||
include_examples 'does not record to the database'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'but we should not track' do
|
|
||||||
let(:should_track) { false }
|
|
||||||
|
|
||||||
include_examples 'does not record to the database'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#track", :snowplow do
|
describe "#track", :snowplow do
|
||||||
|
|
|
@ -30,34 +30,6 @@ RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#record_conversion' do
|
|
||||||
let_it_be(:namespace) { create(:namespace) }
|
|
||||||
|
|
||||||
context 'when should_track? is false' do
|
|
||||||
before do
|
|
||||||
allow(experiment).to receive(:should_track?).and_return(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not record a conversion event' do
|
|
||||||
expect(experiment.publish_to_database).to be_nil
|
|
||||||
expect(experiment.record_conversion(namespace)).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when should_track? is true' do
|
|
||||||
before do
|
|
||||||
allow(experiment).to receive(:should_track?).and_return(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'records a conversion event' do
|
|
||||||
experiment_subject = experiment.publish_to_database
|
|
||||||
|
|
||||||
expect { experiment.record_conversion(namespace) }.to change { experiment_subject.reload.converted_at }.from(nil)
|
|
||||||
.and change { experiment_subject.context }.to include('namespace_id' => namespace.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'exclusions' do
|
describe 'exclusions' do
|
||||||
context 'when user is new' do
|
context 'when user is new' do
|
||||||
it 'is not excluded' do
|
it 'is not excluded' do
|
||||||
|
|
|
@ -6,10 +6,4 @@ RSpec.describe SecurityReportsMrWidgetPromptExperiment do
|
||||||
it "defines a control and candidate" do
|
it "defines a control and candidate" do
|
||||||
expect(subject.behaviors.keys).to match_array(%w[control candidate])
|
expect(subject.behaviors.keys).to match_array(%w[control candidate])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "publishes to the database" do
|
|
||||||
expect(subject).to receive(:publish_to_database)
|
|
||||||
|
|
||||||
subject.publish
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -86,7 +86,7 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
|
||||||
expect(page).to have_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
|
expect(page).to have_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'creating an issue for threads' do
|
context 'creating an issue for threads', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/381729' do
|
||||||
before do
|
before do
|
||||||
page.within '.mr-state-widget' do
|
page.within '.mr-state-widget' do
|
||||||
page.click_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
|
page.click_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid)
|
||||||
|
|
|
@ -52,7 +52,7 @@ describe('Board card component', () => {
|
||||||
|
|
||||||
const performSearchMock = jest.fn();
|
const performSearchMock = jest.fn();
|
||||||
|
|
||||||
const createStore = ({ isEpicBoard = false, isProjectBoard = false } = {}) => {
|
const createStore = ({ isProjectBoard = false } = {}) => {
|
||||||
store = new Vuex.Store({
|
store = new Vuex.Store({
|
||||||
...defaultStore,
|
...defaultStore,
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -65,13 +65,12 @@ describe('Board card component', () => {
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
isGroupBoard: () => true,
|
isGroupBoard: () => true,
|
||||||
isEpicBoard: () => isEpicBoard,
|
|
||||||
isProjectBoard: () => isProjectBoard,
|
isProjectBoard: () => isProjectBoard,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createWrapper = (props = {}) => {
|
const createWrapper = ({ props = {}, isEpicBoard = false } = {}) => {
|
||||||
wrapper = mountExtended(BoardCardInner, {
|
wrapper = mountExtended(BoardCardInner, {
|
||||||
store,
|
store,
|
||||||
propsData: {
|
propsData: {
|
||||||
|
@ -97,6 +96,7 @@ describe('Board card component', () => {
|
||||||
provide: {
|
provide: {
|
||||||
rootPath: '/',
|
rootPath: '/',
|
||||||
scopedLabelsAvailable: false,
|
scopedLabelsAvailable: false,
|
||||||
|
isEpicBoard,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -111,7 +111,7 @@ describe('Board card component', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
createStore();
|
createStore();
|
||||||
createWrapper({ item: issue, list });
|
createWrapper({ props: { item: issue, list } });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -146,7 +146,7 @@ describe('Board card component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the work type icon when props is passed', () => {
|
it('renders the work type icon when props is passed', () => {
|
||||||
createWrapper({ item: issue, list, showWorkItemTypeIcon: true });
|
createWrapper({ props: { item: issue, list, showWorkItemTypeIcon: true } });
|
||||||
expect(findWorkItemIcon().exists()).toBe(true);
|
expect(findWorkItemIcon().exists()).toBe(true);
|
||||||
expect(findWorkItemIcon().props('workItemType')).toBe(issue.type);
|
expect(findWorkItemIcon().props('workItemType')).toBe(issue.type);
|
||||||
});
|
});
|
||||||
|
@ -177,9 +177,11 @@ describe('Board card component', () => {
|
||||||
describe('blocked', () => {
|
describe('blocked', () => {
|
||||||
it('renders blocked icon if issue is blocked', async () => {
|
it('renders blocked icon if issue is blocked', async () => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...issue,
|
item: {
|
||||||
blocked: true,
|
...issue,
|
||||||
|
blocked: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -188,9 +190,11 @@ describe('Board card component', () => {
|
||||||
|
|
||||||
it('does not show blocked icon if issue is not blocked', () => {
|
it('does not show blocked icon if issue is not blocked', () => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...issue,
|
item: {
|
||||||
blocked: false,
|
...issue,
|
||||||
|
blocked: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -201,9 +205,11 @@ describe('Board card component', () => {
|
||||||
describe('confidential issue', () => {
|
describe('confidential issue', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...wrapper.props('item'),
|
item: {
|
||||||
confidential: true,
|
...wrapper.props('item'),
|
||||||
|
confidential: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -216,9 +222,11 @@ describe('Board card component', () => {
|
||||||
describe('hidden issue', () => {
|
describe('hidden issue', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...wrapper.props('item'),
|
item: {
|
||||||
hidden: true,
|
...wrapper.props('item'),
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -241,11 +249,13 @@ describe('Board card component', () => {
|
||||||
describe('with avatar', () => {
|
describe('with avatar', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...wrapper.props('item'),
|
item: {
|
||||||
assignees: [user],
|
...wrapper.props('item'),
|
||||||
updateData(newData) {
|
assignees: [user],
|
||||||
Object.assign(this, newData);
|
updateData(newData) {
|
||||||
|
Object.assign(this, newData);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -294,15 +304,17 @@ describe('Board card component', () => {
|
||||||
global.gon.default_avatar_url = 'default_avatar';
|
global.gon.default_avatar_url = 'default_avatar';
|
||||||
|
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...wrapper.props('item'),
|
item: {
|
||||||
assignees: [
|
...wrapper.props('item'),
|
||||||
{
|
assignees: [
|
||||||
id: 1,
|
{
|
||||||
name: 'testing 123',
|
id: 1,
|
||||||
username: 'test',
|
name: 'testing 123',
|
||||||
},
|
username: 'test',
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -323,28 +335,30 @@ describe('Board card component', () => {
|
||||||
describe('multiple assignees', () => {
|
describe('multiple assignees', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...wrapper.props('item'),
|
item: {
|
||||||
assignees: [
|
...wrapper.props('item'),
|
||||||
{
|
assignees: [
|
||||||
id: 2,
|
{
|
||||||
name: 'user2',
|
id: 2,
|
||||||
username: 'user2',
|
name: 'user2',
|
||||||
avatarUrl: 'test_image',
|
username: 'user2',
|
||||||
},
|
avatarUrl: 'test_image',
|
||||||
{
|
},
|
||||||
id: 3,
|
{
|
||||||
name: 'user3',
|
id: 3,
|
||||||
username: 'user3',
|
name: 'user3',
|
||||||
avatarUrl: 'test_image',
|
username: 'user3',
|
||||||
},
|
avatarUrl: 'test_image',
|
||||||
{
|
},
|
||||||
id: 4,
|
{
|
||||||
name: 'user4',
|
id: 4,
|
||||||
username: 'user4',
|
name: 'user4',
|
||||||
avatarUrl: 'test_image',
|
username: 'user4',
|
||||||
},
|
avatarUrl: 'test_image',
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -364,9 +378,11 @@ describe('Board card component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...wrapper.props('item'),
|
item: {
|
||||||
assignees,
|
...wrapper.props('item'),
|
||||||
|
assignees,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -390,9 +406,11 @@ describe('Board card component', () => {
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...wrapper.props('item'),
|
item: {
|
||||||
assignees,
|
...wrapper.props('item'),
|
||||||
|
assignees,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -405,7 +423,7 @@ describe('Board card component', () => {
|
||||||
|
|
||||||
describe('labels', () => {
|
describe('labels', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createWrapper({ item: { ...issue, labels: [list.label, label1] } });
|
createWrapper({ props: { item: { ...issue, labels: [list.label, label1] } } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render list label but renders all other labels', () => {
|
it('does not render list label but renders all other labels', () => {
|
||||||
|
@ -417,7 +435,7 @@ describe('Board card component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render label if label does not have an ID', async () => {
|
it('does not render label if label does not have an ID', async () => {
|
||||||
createWrapper({ item: { ...issue, labels: [label1, { title: 'closed' }] } });
|
createWrapper({ props: { item: { ...issue, labels: [label1, { title: 'closed' }] } } });
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
|
@ -429,11 +447,13 @@ describe('Board card component', () => {
|
||||||
describe('filterByLabel method', () => {
|
describe('filterByLabel method', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...issue,
|
item: {
|
||||||
labels: [label1],
|
...issue,
|
||||||
|
labels: [label1],
|
||||||
|
},
|
||||||
|
updateFilters: true,
|
||||||
},
|
},
|
||||||
updateFilters: true,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -480,9 +500,11 @@ describe('Board card component', () => {
|
||||||
describe('loading', () => {
|
describe('loading', () => {
|
||||||
it('renders loading icon', async () => {
|
it('renders loading icon', async () => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...issue,
|
item: {
|
||||||
isLoading: true,
|
...issue,
|
||||||
|
isLoading: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -504,17 +526,20 @@ describe('Board card component', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createStore({ isEpicBoard: true });
|
createStore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render if the item has issues', () => {
|
it('should render if the item has issues', () => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...issue,
|
item: {
|
||||||
descendantCounts,
|
...issue,
|
||||||
descendantWeightSum,
|
descendantCounts,
|
||||||
hasIssues: true,
|
descendantWeightSum,
|
||||||
|
hasIssues: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
isEpicBoard: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findEpicCountables().exists()).toBe(true);
|
expect(findEpicCountables().exists()).toBe(true);
|
||||||
|
@ -535,18 +560,21 @@ describe('Board card component', () => {
|
||||||
|
|
||||||
it('shows render item countBadge, weights, and progress correctly', () => {
|
it('shows render item countBadge, weights, and progress correctly', () => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...issue,
|
item: {
|
||||||
descendantCounts: {
|
...issue,
|
||||||
...descendantCounts,
|
descendantCounts: {
|
||||||
openedIssues: 1,
|
...descendantCounts,
|
||||||
|
openedIssues: 1,
|
||||||
|
},
|
||||||
|
descendantWeightSum: {
|
||||||
|
closedIssues: 10,
|
||||||
|
openedIssues: 5,
|
||||||
|
},
|
||||||
|
hasIssues: true,
|
||||||
},
|
},
|
||||||
descendantWeightSum: {
|
|
||||||
closedIssues: 10,
|
|
||||||
openedIssues: 5,
|
|
||||||
},
|
|
||||||
hasIssues: true,
|
|
||||||
},
|
},
|
||||||
|
isEpicBoard: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findEpicCountablesBadgeIssues().text()).toBe('1');
|
expect(findEpicCountablesBadgeIssues().text()).toBe('1');
|
||||||
|
@ -556,15 +584,18 @@ describe('Board card component', () => {
|
||||||
|
|
||||||
it('does not render progress when weight is zero', () => {
|
it('does not render progress when weight is zero', () => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...issue,
|
item: {
|
||||||
descendantCounts: {
|
...issue,
|
||||||
...descendantCounts,
|
descendantCounts: {
|
||||||
openedIssues: 1,
|
...descendantCounts,
|
||||||
|
openedIssues: 1,
|
||||||
|
},
|
||||||
|
descendantWeightSum,
|
||||||
|
hasIssues: true,
|
||||||
},
|
},
|
||||||
descendantWeightSum,
|
|
||||||
hasIssues: true,
|
|
||||||
},
|
},
|
||||||
|
isEpicBoard: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findEpicBadgeProgress().exists()).toBe(false);
|
expect(findEpicBadgeProgress().exists()).toBe(false);
|
||||||
|
@ -572,15 +603,18 @@ describe('Board card component', () => {
|
||||||
|
|
||||||
it('renders the tooltip with the correct data', () => {
|
it('renders the tooltip with the correct data', () => {
|
||||||
createWrapper({
|
createWrapper({
|
||||||
item: {
|
props: {
|
||||||
...issue,
|
item: {
|
||||||
descendantCounts,
|
...issue,
|
||||||
descendantWeightSum: {
|
descendantCounts,
|
||||||
closedIssues: 10,
|
descendantWeightSum: {
|
||||||
openedIssues: 5,
|
closedIssues: 10,
|
||||||
|
openedIssues: 5,
|
||||||
|
},
|
||||||
|
hasIssues: true,
|
||||||
},
|
},
|
||||||
hasIssues: true,
|
|
||||||
},
|
},
|
||||||
|
isEpicBoard: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tooltip = findEpicCountablesTotalTooltip();
|
const tooltip = findEpicCountablesTotalTooltip();
|
||||||
|
|
|
@ -101,6 +101,8 @@ export default function createComponent({
|
||||||
weightFeatureAvailable: false,
|
weightFeatureAvailable: false,
|
||||||
boardWeight: null,
|
boardWeight: null,
|
||||||
canAdminList: true,
|
canAdminList: true,
|
||||||
|
isIssueBoard: true,
|
||||||
|
isEpicBoard: false,
|
||||||
...provide,
|
...provide,
|
||||||
},
|
},
|
||||||
stubs,
|
stubs,
|
||||||
|
|
|
@ -53,11 +53,11 @@ describe('Board card layout', () => {
|
||||||
state: {
|
state: {
|
||||||
labels,
|
labels,
|
||||||
labelsLoading: false,
|
labelsLoading: false,
|
||||||
isEpicBoard: false,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
provide: {
|
provide: {
|
||||||
scopedLabelsAvailable: true,
|
scopedLabelsAvailable: true,
|
||||||
|
isEpicBoard: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -30,7 +30,6 @@ describe('Board card', () => {
|
||||||
},
|
},
|
||||||
actions: mockActions,
|
actions: mockActions,
|
||||||
getters: {
|
getters: {
|
||||||
isEpicBoard: () => false,
|
|
||||||
isProjectBoard: () => false,
|
isProjectBoard: () => false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -61,6 +60,7 @@ describe('Board card', () => {
|
||||||
groupId: null,
|
groupId: null,
|
||||||
rootPath: '/',
|
rootPath: '/',
|
||||||
scopedLabelsAvailable: false,
|
scopedLabelsAvailable: false,
|
||||||
|
isEpicBoard: false,
|
||||||
...provide,
|
...provide,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,6 +47,8 @@ describe('BoardContent', () => {
|
||||||
canAdminList = true,
|
canAdminList = true,
|
||||||
isApolloBoard = false,
|
isApolloBoard = false,
|
||||||
issuableType = 'issue',
|
issuableType = 'issue',
|
||||||
|
isIssueBoard = true,
|
||||||
|
isEpicBoard = false,
|
||||||
boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse),
|
boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse),
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
fakeApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
|
fakeApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
|
||||||
|
@ -67,6 +69,8 @@ describe('BoardContent', () => {
|
||||||
boardType: 'group',
|
boardType: 'group',
|
||||||
fullPath: 'gitlab-org/gitlab',
|
fullPath: 'gitlab-org/gitlab',
|
||||||
issuableType,
|
issuableType,
|
||||||
|
isIssueBoard,
|
||||||
|
isEpicBoard,
|
||||||
isApolloBoard,
|
isApolloBoard,
|
||||||
},
|
},
|
||||||
store,
|
store,
|
||||||
|
@ -133,7 +137,7 @@ describe('BoardContent', () => {
|
||||||
|
|
||||||
describe('when issuableType is not issue', () => {
|
describe('when issuableType is not issue', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({ issuableType: 'foo' });
|
createComponent({ issuableType: 'foo', isIssueBoard: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render BoardContentSidebar', () => {
|
it('does not render BoardContentSidebar', () => {
|
||||||
|
|
|
@ -59,7 +59,6 @@ describe('Board List Header Component', () => {
|
||||||
store = new Vuex.Store({
|
store = new Vuex.Store({
|
||||||
state: {},
|
state: {},
|
||||||
actions: { updateList: updateListSpy, toggleListCollapsed: toggleListCollapsedSpy },
|
actions: { updateList: updateListSpy, toggleListCollapsed: toggleListCollapsedSpy },
|
||||||
getters: { isEpicBoard: () => false },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fakeApollo = createMockApollo([[listQuery, listQueryHandler]]);
|
fakeApollo = createMockApollo([[listQuery, listQueryHandler]]);
|
||||||
|
@ -76,6 +75,7 @@ describe('Board List Header Component', () => {
|
||||||
boardId,
|
boardId,
|
||||||
weightFeatureAvailable: false,
|
weightFeatureAvailable: false,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
|
isEpicBoard: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,6 +45,7 @@ describe('BoardSettingsSidebar', () => {
|
||||||
provide: {
|
provide: {
|
||||||
canAdminList,
|
canAdminList,
|
||||||
scopedLabelsAvailable: false,
|
scopedLabelsAvailable: false,
|
||||||
|
isIssueBoard: true,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlModal: createMockDirective(),
|
GlModal: createMockDirective(),
|
||||||
|
|
|
@ -15,18 +15,14 @@ describe('BoardTopBar', () => {
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
const createStore = ({ mockGetters = {} } = {}) => {
|
const createStore = () => {
|
||||||
return new Vuex.Store({
|
return new Vuex.Store({
|
||||||
state: {},
|
state: {},
|
||||||
getters: {
|
|
||||||
isEpicBoard: () => false,
|
|
||||||
...mockGetters,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createComponent = ({ provide = {}, mockGetters = {} } = {}) => {
|
const createComponent = ({ provide = {} } = {}) => {
|
||||||
const store = createStore({ mockGetters });
|
const store = createStore();
|
||||||
wrapper = shallowMount(BoardTopBar, {
|
wrapper = shallowMount(BoardTopBar, {
|
||||||
store,
|
store,
|
||||||
provide: {
|
provide: {
|
||||||
|
@ -36,6 +32,7 @@ describe('BoardTopBar', () => {
|
||||||
fullPath: 'gitlab-org',
|
fullPath: 'gitlab-org',
|
||||||
boardType: 'group',
|
boardType: 'group',
|
||||||
releasesFetchPath: '/releases',
|
releasesFetchPath: '/releases',
|
||||||
|
isIssueBoard: true,
|
||||||
...provide,
|
...provide,
|
||||||
},
|
},
|
||||||
stubs: { IssueBoardFilteredSearch },
|
stubs: { IssueBoardFilteredSearch },
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::BackgroundMigration::PopulateProjectsStarCount, schema: 20221019105041 do
|
||||||
|
let(:namespaces) { table(:namespaces) }
|
||||||
|
let(:projects) { table(:projects) }
|
||||||
|
let(:users) { table(:users) }
|
||||||
|
let(:users_star_projects) { table(:users_star_projects) }
|
||||||
|
|
||||||
|
let(:namespace1) { namespaces.create!(name: 'namespace 1', path: 'namespace1') }
|
||||||
|
let(:namespace2) { namespaces.create!(name: 'namespace 2', path: 'namespace2') }
|
||||||
|
let(:namespace3) { namespaces.create!(name: 'namespace 3', path: 'namespace3') }
|
||||||
|
let(:namespace4) { namespaces.create!(name: 'namespace 4', path: 'namespace4') }
|
||||||
|
let(:namespace5) { namespaces.create!(name: 'namespace 5', path: 'namespace5') }
|
||||||
|
|
||||||
|
let(:project1) { projects.create!(namespace_id: namespace1.id, project_namespace_id: namespace1.id) }
|
||||||
|
let(:project2) { projects.create!(namespace_id: namespace2.id, project_namespace_id: namespace2.id) }
|
||||||
|
let(:project3) { projects.create!(namespace_id: namespace3.id, project_namespace_id: namespace3.id) }
|
||||||
|
let(:project4) { projects.create!(namespace_id: namespace4.id, project_namespace_id: namespace4.id) }
|
||||||
|
let(:project5) { projects.create!(namespace_id: namespace5.id, project_namespace_id: namespace5.id) }
|
||||||
|
|
||||||
|
let(:user_active) { users.create!(state: 'active', email: 'test1@example.com', projects_limit: 5) }
|
||||||
|
let(:user_blocked) { users.create!(state: 'blocked', email: 'test2@example.com', projects_limit: 5) }
|
||||||
|
|
||||||
|
let(:migration) do
|
||||||
|
described_class.new(
|
||||||
|
start_id: project1.id,
|
||||||
|
end_id: project4.id,
|
||||||
|
batch_table: :projects,
|
||||||
|
batch_column: :id,
|
||||||
|
sub_batch_size: 2,
|
||||||
|
pause_ms: 2,
|
||||||
|
connection: ApplicationRecord.connection
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:perform_migration) { migration.perform }
|
||||||
|
|
||||||
|
it 'correctly populates the star counters' do
|
||||||
|
users_star_projects.create!(project_id: project1.id, user_id: user_active.id)
|
||||||
|
users_star_projects.create!(project_id: project2.id, user_id: user_blocked.id)
|
||||||
|
users_star_projects.create!(project_id: project4.id, user_id: user_active.id)
|
||||||
|
users_star_projects.create!(project_id: project4.id, user_id: user_blocked.id)
|
||||||
|
users_star_projects.create!(project_id: project5.id, user_id: user_active.id)
|
||||||
|
|
||||||
|
perform_migration
|
||||||
|
|
||||||
|
expect(project1.reload.star_count).to eq(1)
|
||||||
|
expect(project2.reload.star_count).to eq(0)
|
||||||
|
expect(project3.reload.star_count).to eq(0)
|
||||||
|
expect(project4.reload.star_count).to eq(1)
|
||||||
|
expect(project5.reload.star_count).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when database timeouts' do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
where(error_class: [ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled])
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it 'retries on timeout error' do
|
||||||
|
expect(migration).to receive(:update_batch).exactly(3).times.and_raise(error_class)
|
||||||
|
expect(migration).to receive(:sleep).with(5).twice
|
||||||
|
|
||||||
|
expect do
|
||||||
|
perform_migration
|
||||||
|
end.to raise_error(error_class)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require_migration!
|
||||||
|
|
||||||
|
RSpec.describe QueuePopulateProjectsStarCount do
|
||||||
|
let_it_be(:batched_migration) { described_class::MIGRATION }
|
||||||
|
|
||||||
|
it 'schedules a new batched migration' do
|
||||||
|
reversible_migration do |migration|
|
||||||
|
migration.before -> {
|
||||||
|
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||||
|
}
|
||||||
|
|
||||||
|
migration.after -> {
|
||||||
|
expect(batched_migration).to have_scheduled_batched_migration(
|
||||||
|
table_name: :projects,
|
||||||
|
column_name: :id,
|
||||||
|
interval: described_class::DELAY_INTERVAL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -388,15 +388,30 @@ RSpec.describe Deployment do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when deployment is behind current deployment' do
|
context 'when deployment is behind current deployment' do
|
||||||
|
let_it_be(:commits) { project.repository.commits('master', limit: 2) }
|
||||||
|
|
||||||
|
let!(:deployment) do
|
||||||
|
create(:deployment, :success, project: project, environment: environment,
|
||||||
|
finished_at: 1.year.ago, sha: commits[0].sha)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:last_deployment) do
|
||||||
|
create(:deployment, :success, project: project, environment: environment, sha: commits[1].sha)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be_truthy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when deployment is the same sha as the current deployment' do
|
||||||
let!(:deployment) do
|
let!(:deployment) do
|
||||||
create(:deployment, :success, project: project, environment: environment, finished_at: 1.year.ago)
|
create(:deployment, :success, project: project, environment: environment, finished_at: 1.year.ago)
|
||||||
end
|
end
|
||||||
|
|
||||||
let!(:last_deployment) do
|
let!(:last_deployment) do
|
||||||
create(:deployment, :success, project: project, environment: environment)
|
create(:deployment, :success, project: project, environment: environment, sha: deployment.sha)
|
||||||
end
|
end
|
||||||
|
|
||||||
it { is_expected.to be_truthy }
|
it { is_expected.to be_falsey }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1658,6 +1658,33 @@ RSpec.describe Project, factory_default: :keep do
|
||||||
expect(project.reload.star_count).to eq(0)
|
expect(project.reload.star_count).to eq(0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not count stars from blocked users' do
|
||||||
|
user1 = create(:user)
|
||||||
|
user2 = create(:user)
|
||||||
|
project = create(:project, :public)
|
||||||
|
|
||||||
|
expect(project.star_count).to eq(0)
|
||||||
|
|
||||||
|
user1.toggle_star(project)
|
||||||
|
expect(project.reload.star_count).to eq(1)
|
||||||
|
|
||||||
|
user2.toggle_star(project)
|
||||||
|
project.reload
|
||||||
|
expect(project.reload.star_count).to eq(2)
|
||||||
|
|
||||||
|
user1.block
|
||||||
|
project.reload
|
||||||
|
expect(project.reload.star_count).to eq(1)
|
||||||
|
|
||||||
|
user2.block
|
||||||
|
project.reload
|
||||||
|
expect(project.reload.star_count).to eq(0)
|
||||||
|
|
||||||
|
user1.activate
|
||||||
|
project.reload
|
||||||
|
expect(project.reload.star_count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
it 'counts stars on the right project' do
|
it 'counts stars on the right project' do
|
||||||
user = create(:user)
|
user = create(:user)
|
||||||
project1 = create(:project, :public)
|
project1 = create(:project, :public)
|
||||||
|
|
|
@ -2482,6 +2482,30 @@ RSpec.describe User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'starred_projects' do
|
||||||
|
let_it_be(:project) { create(:project) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.toggle_star(project)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when blocking a user' do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
it 'decrements star count of project' do
|
||||||
|
expect { user.block }.to change { project.reload.star_count }.by(-1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when activating a user' do
|
||||||
|
let_it_be(:user) { create(:user, :blocked) }
|
||||||
|
|
||||||
|
it 'increments star count of project' do
|
||||||
|
expect { user.activate }.to change { project.reload.star_count }.by(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.instance_access_request_approvers_to_be_notified' do
|
describe '.instance_access_request_approvers_to_be_notified' do
|
||||||
let_it_be(:admin_issue_board_list) { create_list(:user, 12, :admin, :with_sign_ins) }
|
let_it_be(:admin_issue_board_list) { create_list(:user, 12, :admin, :with_sign_ins) }
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,14 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe UsersStarProject, type: :model do
|
RSpec.describe UsersStarProject, type: :model do
|
||||||
|
let_it_be(:project1) { create(:project) }
|
||||||
|
let_it_be(:project2) { create(:project) }
|
||||||
|
let_it_be(:user_active) { create(:user, state: 'active', name: 'user2', private_profile: true) }
|
||||||
|
let_it_be(:user_blocked) { create(:user, state: 'blocked', name: 'user1') }
|
||||||
|
|
||||||
it { is_expected.to belong_to(:project).touch(false) }
|
it { is_expected.to belong_to(:project).touch(false) }
|
||||||
|
|
||||||
describe 'scopes' do
|
describe 'scopes' do
|
||||||
let_it_be(:project1) { create(:project) }
|
|
||||||
let_it_be(:project2) { create(:project) }
|
|
||||||
let_it_be(:user_active) { create(:user, state: 'active', name: 'user2', private_profile: true) }
|
|
||||||
let_it_be(:user_blocked) { create(:user, state: 'blocked', name: 'user1') }
|
|
||||||
|
|
||||||
let_it_be(:users_star_project1) { create(:users_star_project, project: project1, user: user_active) }
|
let_it_be(:users_star_project1) { create(:users_star_project, project: project1, user: user_active) }
|
||||||
let_it_be(:users_star_project2) { create(:users_star_project, project: project2, user: user_blocked) }
|
let_it_be(:users_star_project2) { create(:users_star_project, project: project2, user: user_blocked) }
|
||||||
|
|
||||||
|
@ -50,4 +50,38 @@ RSpec.describe UsersStarProject, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'star count hooks' do
|
||||||
|
context 'on after_create' do
|
||||||
|
context 'if user is active' do
|
||||||
|
it 'increments star count of project' do
|
||||||
|
expect { user_active.toggle_star(project1) }.to change { project1.reload.star_count }.by(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'if user is not active' do
|
||||||
|
it 'does not increment star count of project' do
|
||||||
|
expect { user_blocked.toggle_star(project1) }.not_to change { project1.reload.star_count }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on after_destory' do
|
||||||
|
context 'if user is active' do
|
||||||
|
let_it_be(:users_star_project) { create(:users_star_project, project: project2, user: user_active) }
|
||||||
|
|
||||||
|
it 'decrements star count of project' do
|
||||||
|
expect { users_star_project.destroy! }.to change { project2.reload.star_count }.by(-1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'if user is not active' do
|
||||||
|
let_it_be(:users_star_project) { create(:users_star_project, project: project2, user: user_blocked) }
|
||||||
|
|
||||||
|
it 'does not decrement star count of project' do
|
||||||
|
expect { users_star_project.destroy! }.not_to change { project2.reload.star_count }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2884,6 +2884,27 @@ RSpec.describe ProjectPolicy do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'read_code' do
|
||||||
|
let(:current_user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(subject).to receive(:allowed?).and_call_original
|
||||||
|
allow(subject).to receive(:allowed?).with(:download_code).and_return(can_download_code)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the current_user can download_code' do
|
||||||
|
let(:can_download_code) { true }
|
||||||
|
|
||||||
|
it { expect_allowed(:read_code) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the current_user cannot download_code' do
|
||||||
|
let(:can_download_code) { false }
|
||||||
|
|
||||||
|
it { expect_disallowed(:read_code) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def project_subject(project_type)
|
def project_subject(project_type)
|
||||||
|
|
|
@ -15,6 +15,9 @@ RSpec.describe Projects::MoveUsersStarProjectsService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'moves the user\'s stars from one project to another' do
|
it 'moves the user\'s stars from one project to another' do
|
||||||
|
project_with_stars.reload
|
||||||
|
target_project.reload
|
||||||
|
|
||||||
expect(project_with_stars.users_star_projects.count).to eq 2
|
expect(project_with_stars.users_star_projects.count).to eq 2
|
||||||
expect(project_with_stars.star_count).to eq 2
|
expect(project_with_stars.star_count).to eq 2
|
||||||
expect(target_project.users_star_projects.count).to eq 0
|
expect(target_project.users_star_projects.count).to eq 0
|
||||||
|
@ -34,6 +37,8 @@ RSpec.describe Projects::MoveUsersStarProjectsService do
|
||||||
allow(subject).to receive(:success).and_raise(StandardError)
|
allow(subject).to receive(:success).and_raise(StandardError)
|
||||||
|
|
||||||
expect { subject.execute(project_with_stars) }.to raise_error(StandardError)
|
expect { subject.execute(project_with_stars) }.to raise_error(StandardError)
|
||||||
|
project_with_stars.reload
|
||||||
|
target_project.reload
|
||||||
|
|
||||||
expect(project_with_stars.users_star_projects.count).to eq 2
|
expect(project_with_stars.users_star_projects.count).to eq 2
|
||||||
expect(project_with_stars.star_count).to eq 2
|
expect(project_with_stars.star_count).to eq 2
|
||||||
|
|
|
@ -57,7 +57,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos
|
||||||
context "race conditions" do
|
context "race conditions" do
|
||||||
context "when #{record_class_name} migration fails and is rolled back" do
|
context "when #{record_class_name} migration fails and is rolled back" do
|
||||||
before do
|
before do
|
||||||
expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
|
allow_any_instance_of(ActiveRecord::Associations::CollectionProxy)
|
||||||
.to receive(:update_all).and_raise(ActiveRecord::StatementTimeout)
|
.to receive(:update_all).and_raise(ActiveRecord::StatementTimeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -68,6 +68,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't unblock a previously-blocked user" do
|
it "doesn't unblock a previously-blocked user" do
|
||||||
|
expect(user.starred_projects).to receive(:update_all).and_call_original
|
||||||
user.block
|
user.block
|
||||||
|
|
||||||
expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout)
|
expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout)
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
RSpec.shared_examples 'tracks assignment and records the subject' do |experiment, subject_type|
|
|
||||||
before do
|
|
||||||
stub_experiments(experiment => true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'tracks the assignment', :experiment do
|
|
||||||
expect(experiment(experiment))
|
|
||||||
.to track(:assignment)
|
|
||||||
.with_context(subject_type => subject)
|
|
||||||
.on_next_instance
|
|
||||||
|
|
||||||
action
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'records the subject' do
|
|
||||||
expect(Experiment).to receive(:add_subject).with(experiment.to_s, variant: anything, subject: subject)
|
|
||||||
|
|
||||||
action
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in New Issue