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 -->
|
||||
|
||||
<!--
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
<!-- 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.
|
||||
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)).
|
||||
- [ ] 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.
|
||||
- [ ] 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`
|
||||
- Enable the feature globally on production environment.
|
||||
- [ ] `/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).
|
||||
|
||||
### (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>`
|
||||
- [ ] 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`
|
||||
- [ ] 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).
|
||||
- [ ] (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>`".
|
||||
|
@ -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),
|
||||
the feature can be officially announced in a release blog post.
|
||||
- [ ] `/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:
|
||||
- [ ] `/chatops run feature delete <feature-flag-name> --dev --staging --staging-ref --production`
|
||||
- [ ] 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 initPopover from '~/blob/suggest_gitlab_ci_yml';
|
||||
import { createAlert } from '~/flash';
|
||||
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
|
||||
import { setCookie } from '~/lib/utils/common_utils';
|
||||
import Tracking from '~/tracking';
|
||||
import BlobFileDropzone from '../blob/blob_file_dropzone';
|
||||
import NewCommitForm from '../new_commit_form';
|
||||
|
||||
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 () => {
|
||||
const editBlobForm = $('.js-edit-blob-form');
|
||||
const deleteBlobForm = $('.js-delete-blob-form');
|
||||
|
||||
if (editBlobForm.length) {
|
||||
const urlRoot = editBlobForm.data('relativeUrlRoot');
|
||||
|
@ -99,10 +85,4 @@ export default () => {
|
|||
// returning here blocks page navigation
|
||||
window.onbeforeunload = () => '';
|
||||
}
|
||||
|
||||
initUploadForm();
|
||||
|
||||
if (deleteBlobForm.length) {
|
||||
new NewCommitForm(deleteBlobForm);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -43,7 +43,7 @@ export default {
|
|||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [boardCardInner],
|
||||
inject: ['rootPath', 'scopedLabelsAvailable'],
|
||||
inject: ['rootPath', 'scopedLabelsAvailable', 'isEpicBoard'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
@ -78,7 +78,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
|
||||
...mapGetters(['isEpicBoard', 'isProjectBoard']),
|
||||
...mapGetters(['isProjectBoard']),
|
||||
cappedAssignees() {
|
||||
// e.g. maxRender is 4,
|
||||
// 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 BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
|
||||
import { defaultSortableOptions } from '~/sortable/constants';
|
||||
import {
|
||||
DraggableItemTypes,
|
||||
issuableTypes,
|
||||
BoardType,
|
||||
listsQuery,
|
||||
} from 'ee_else_ce/boards/constants';
|
||||
import { DraggableItemTypes, BoardType, listsQuery } from 'ee_else_ce/boards/constants';
|
||||
import BoardColumn from './board_column.vue';
|
||||
|
||||
export default {
|
||||
|
@ -31,7 +26,15 @@ export default {
|
|||
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
|
||||
GlAlert,
|
||||
},
|
||||
inject: ['canAdminList', 'boardType', 'fullPath', 'issuableType', 'isApolloBoard'],
|
||||
inject: [
|
||||
'canAdminList',
|
||||
'boardType',
|
||||
'fullPath',
|
||||
'issuableType',
|
||||
'isIssueBoard',
|
||||
'isEpicBoard',
|
||||
'isApolloBoard',
|
||||
],
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
|
@ -78,12 +81,6 @@ export default {
|
|||
computed: {
|
||||
...mapState(['boardLists', 'error', 'addColumnForm']),
|
||||
...mapGetters(['isSwimlanesOn']),
|
||||
isIssueBoard() {
|
||||
return this.issuableType === issuableTypes.issue;
|
||||
},
|
||||
isEpicBoard() {
|
||||
return this.issuableType === issuableTypes.epic;
|
||||
},
|
||||
addColumnFormVisible() {
|
||||
return this.addColumnForm?.visible;
|
||||
},
|
||||
|
|
|
@ -84,7 +84,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(['error']),
|
||||
...mapGetters(['isIssueBoard', 'isGroupBoard', 'isProjectBoard']),
|
||||
...mapGetters(['isGroupBoard', 'isProjectBoard']),
|
||||
isNewForm() {
|
||||
return this.currentPage === formType.new;
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
|
||||
import Draggable from 'vuedraggable';
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import { defaultSortableOptions } from '~/sortable/constants';
|
||||
import { sortableStart, sortableEnd } from '~/sortable/utils';
|
||||
|
@ -31,6 +31,7 @@ export default {
|
|||
BoardCardMoveToPosition,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
inject: ['isEpicBoard'],
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
|
@ -69,7 +70,6 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(['pageInfoByListId', 'listsFlags', 'filterParams', 'isUpdateIssueOrderInProgress']),
|
||||
...mapGetters(['isEpicBoard']),
|
||||
listItemsCount() {
|
||||
return this.isEpicBoard ? this.list.epicsCount : this.boardList?.issuesCount;
|
||||
},
|
||||
|
|
|
@ -57,6 +57,9 @@ export default {
|
|||
canCreateEpic: {
|
||||
default: false,
|
||||
},
|
||||
isEpicBoard: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
|
@ -76,7 +79,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(['activeId', 'filterParams', 'boardId']),
|
||||
...mapGetters(['isEpicBoard', 'isSwimlanesOn']),
|
||||
...mapGetters(['isSwimlanesOn']),
|
||||
isLoggedIn() {
|
||||
return Boolean(this.currentUserId);
|
||||
},
|
||||
|
|
|
@ -31,7 +31,7 @@ export default {
|
|||
GlModal: GlModalDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
|
||||
inject: ['canAdminList', 'scopedLabelsAvailable'],
|
||||
inject: ['canAdminList', 'scopedLabelsAvailable', 'isIssueBoard'],
|
||||
inheritAttrs: false,
|
||||
data() {
|
||||
return {
|
||||
|
@ -40,10 +40,10 @@ export default {
|
|||
},
|
||||
modalId: 'board-settings-sidebar-modal',
|
||||
computed: {
|
||||
...mapGetters(['isSidebarOpen', 'isEpicBoard']),
|
||||
...mapGetters(['isSidebarOpen']),
|
||||
...mapState(['activeId', 'sidebarType', 'boardLists']),
|
||||
isWipLimitsOn() {
|
||||
return this.glFeatures.wipLimits && !this.isEpicBoard;
|
||||
return this.glFeatures.wipLimits && this.isIssueBoard;
|
||||
},
|
||||
activeList() {
|
||||
return this.boardLists[this.activeId] || {};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.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';
|
||||
|
@ -20,10 +19,7 @@ export default {
|
|||
EpicBoardFilteredSearch: () =>
|
||||
import('ee_component/boards/components/epic_filtered_search.vue'),
|
||||
},
|
||||
inject: ['swimlanesFeatureAvailable', 'canAdminList', 'isSignedIn'],
|
||||
computed: {
|
||||
...mapGetters(['isEpicBoard']),
|
||||
},
|
||||
inject: ['swimlanesFeatureAvailable', 'canAdminList', 'isSignedIn', 'isIssueBoard'],
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -37,8 +33,8 @@ export default {
|
|||
>
|
||||
<boards-selector />
|
||||
<new-board-button />
|
||||
<epic-board-filtered-search v-if="isEpicBoard" />
|
||||
<issue-board-filtered-search v-else />
|
||||
<issue-board-filtered-search v-if="isIssueBoard" />
|
||||
<epic-board-filtered-search v-else />
|
||||
</div>
|
||||
<div
|
||||
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),
|
||||
hasMissingBoards: parseBoolean(el.dataset.hasMissingBoards),
|
||||
weights: el.dataset.weights ? JSON.parse(el.dataset.weights) : [],
|
||||
isIssueBoard: true,
|
||||
isEpicBoard: false,
|
||||
// Permissions
|
||||
canUpdate: parseBoolean(el.dataset.canUpdate),
|
||||
canAdminList: parseBoolean(el.dataset.canAdminList),
|
||||
|
|
|
@ -10,14 +10,6 @@ import { initUploadFileTrigger } from '~/projects/upload_file';
|
|||
import initReadMore from '~/read_more';
|
||||
|
||||
// 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')) {
|
||||
import(/* webpackChunkName: 'treeList' */ 'ee_else_ce/repository')
|
||||
.then(({ default: initTree }) => {
|
||||
|
|
|
@ -23,7 +23,6 @@ class Projects::LearnGitlabController < Projects::ApplicationController
|
|||
|
||||
experiment(:invite_for_help_continuous_onboarding, namespace: project.namespace) do |e|
|
||||
e.candidate {}
|
||||
e.publish_to_database
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -338,7 +338,6 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
|
||||
experiment(:runners_availability_section, namespace: project.root_ancestor) do |e|
|
||||
e.candidate {}
|
||||
e.publish_to_database
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,25 +3,6 @@
|
|||
class ApplicationExperiment < Gitlab::Experiment
|
||||
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
|
||||
# define a default nil control behavior so we can omit it when not needed
|
||||
end
|
||||
|
|
|
@ -12,18 +12,8 @@ class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment
|
|||
run
|
||||
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
|
||||
|
||||
def subject
|
||||
context.value[:user]
|
||||
end
|
||||
|
||||
def existing_user
|
||||
return false unless user_or_actor
|
||||
|
||||
|
|
|
@ -3,10 +3,4 @@
|
|||
class SecurityReportsMrWidgetPromptExperiment < ApplicationExperiment
|
||||
control {}
|
||||
candidate {}
|
||||
|
||||
def publish(_result = nil)
|
||||
super
|
||||
|
||||
publish_to_database
|
||||
end
|
||||
end
|
||||
|
|
|
@ -306,8 +306,8 @@ class Deployment < ApplicationRecord
|
|||
last_deployment_id = environment.last_deployment&.id
|
||||
|
||||
return false unless last_deployment_id.present?
|
||||
|
||||
return false if self.id == last_deployment_id
|
||||
return false if self.sha == environment.last_deployment&.sha
|
||||
|
||||
self.id < last_deployment_id
|
||||
end
|
||||
|
|
|
@ -452,6 +452,14 @@ class User < ApplicationRecord
|
|||
after_transition banned: :active do |user|
|
||||
user.banned_user&.destroy
|
||||
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
|
||||
|
||||
# Scopes
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class UsersStarProject < ApplicationRecord
|
||||
include Sortable
|
||||
|
||||
belongs_to :project, counter_cache: :star_count
|
||||
belongs_to :project
|
||||
belongs_to :user
|
||||
|
||||
validates :user, presence: true
|
||||
|
@ -12,7 +12,10 @@ class UsersStarProject < ApplicationRecord
|
|||
|
||||
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_desc, -> { joins(:user).merge(User.order_name_desc) }
|
||||
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))
|
||||
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
|
||||
|
|
|
@ -851,6 +851,10 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_incident_management_timeline_event_tag
|
||||
end
|
||||
|
||||
rule { can?(:download_code) }.policy do
|
||||
enable :read_code
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_is_user?
|
||||
|
|
|
@ -12,8 +12,8 @@ module Projects
|
|||
Project.transaction do
|
||||
user_stars.update_all(project_id: @project.id)
|
||||
|
||||
Project.reset_counters @project.id, :users_star_projects
|
||||
Project.reset_counters source_project.id, :users_star_projects
|
||||
@project.update(star_count: @project.starrers.with_state(:active).size)
|
||||
source_project.update(star_count: source_project.starrers.with_state(:active).size)
|
||||
|
||||
success
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@ name: policy_project_updated
|
|||
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_mr: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102154"
|
||||
milestone: 15.6
|
||||
milestone: "15.6"
|
||||
group: "govern::security policies"
|
||||
saved_to_database: true
|
||||
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
|
||||
---
|
||||
|
||||
# Award emoji API **(FREE)**
|
||||
# Award emojis API **(FREE)**
|
||||
|
||||
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)).
|
||||
- [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
|
||||
|
||||
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
|
||||
|
||||
|
@ -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"
|
||||
```
|
||||
|
||||
## Award Emoji on Comments
|
||||
## Award emojis on comments
|
||||
|
||||
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). |
|
||||
| `issue_iid` | integer | yes | Internal ID of an issue. |
|
||||
| `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:
|
||||
|
||||
|
|
|
@ -6,17 +6,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Use custom emojis with GraphQL **(FREE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911) in GitLab 13.6
|
||||
> - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
|
||||
> - 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)**
|
||||
> - [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.
|
||||
> - Enabled on GitLab.com in GitLab 14.0.
|
||||
|
||||
This in-development feature might not be available for your use. There can be
|
||||
[risks when enabling features still in development](../../administration/feature_flags.md#risks-when-enabling-features-still-in-development).
|
||||
Refer to this feature's version history for more details.
|
||||
FLAG:
|
||||
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`.
|
||||
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:
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -92,22 +90,3 @@ For more information on:
|
|||
- GraphQL specific entities, such as Fragments and Interfaces, see the official
|
||||
[GraphQL documentation](https://graphql.org/learn/).
|
||||
- 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_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_status` | string | Status of the merge request. Can be `unchecked`, `checking`, `can_be_merged`, `cannot_be_merged` or `cannot_be_merged_recheck`. |
|
||||
| `merge_when_pipeline_succeeds` | boolean | Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS). |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `milestone` | object | Milestone of the merge request. |
|
||||
|
@ -848,14 +848,11 @@ the `approvals_before_merge` parameter:
|
|||
|
||||
### 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:
|
||||
- `blocked_status`: Merge request is blocked by another merge request.
|
||||
- `broken_status`: Can not merge the source into the target branch, potential conflict.
|
||||
|
|
|
@ -1,50 +1,41 @@
|
|||
---
|
||||
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
|
||||
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:
|
||||
|
||||
- A project in GitLab that you would like to use CI/CD for.
|
||||
- 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).
|
||||
- [Migrate from Jenkins](../migration/jenkins.md).
|
||||
## Steps
|
||||
|
||||
> - <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.
|
||||
|
||||
## CI/CD process overview
|
||||
|
||||
To use GitLab CI/CD:
|
||||
To create and run your first pipeline:
|
||||
|
||||
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/)
|
||||
and [register a runner](https://docs.gitlab.com/runner/register/) for your instance, project, or group.
|
||||
If you're using GitLab.com, you can skip this step. GitLab.com provides shared runners for you.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
- 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,
|
||||
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
|
||||
must [install GitLab Runner](https://docs.gitlab.com/runner/install/) and
|
||||
[register](https://docs.gitlab.com/runner/register/) at least one runner.
|
||||
### If you don't have a runner
|
||||
|
||||
If you are testing CI/CD, you can install GitLab Runner and register runners on your local machine.
|
||||
When your CI/CD jobs run, they run on your local machine.
|
||||
If you don't have a runner:
|
||||
|
||||
### 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
|
||||
you configure specific instructions for GitLab CI/CD.
|
||||
When your CI/CD jobs run, in a later step, they will run on your local machine.
|
||||
|
||||
## 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:
|
||||
|
||||
- The structure and order of jobs that the runner should execute.
|
||||
- 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:
|
||||
|
||||
1. On the left sidebar, select **Project information > Details**.
|
||||
1. Above the file list, select the branch you want to commit to,
|
||||
select the plus icon, then select **New file**:
|
||||
1. On the left sidebar, select **Repository > Files**.
|
||||
1. Above the file list, select the branch you want to commit to.
|
||||
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)
|
||||
|
||||
|
@ -112,46 +101,50 @@ To create a `.gitlab-ci.yml` file:
|
|||
environment: production
|
||||
```
|
||||
|
||||
`$GITLAB_USER_LOGIN` and `$CI_COMMIT_BRANCH` are
|
||||
[predefined variables](../variables/predefined_variables.md)
|
||||
that populate when the job runs.
|
||||
This example shows four jobs: `build-job`, `test-job1`, `test-job2`, and `deploy-prod`.
|
||||
The comments listed in the `echo` commands are displayed in the UI when you view the jobs.
|
||||
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**.
|
||||
|
||||
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)
|
||||
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:
|
||||
Now take a look at your pipeline and the jobs within.
|
||||
|
||||
```yaml
|
||||
default:
|
||||
image: ruby:2.7.5
|
||||
```
|
||||
1. Go to **CI/CD > Pipelines**. A pipeline with three stages should be displayed:
|
||||
|
||||
This command tells the runner to use a Ruby image from Docker Hub
|
||||
and to run the jobs in a container that's generated from the image.
|
||||
![Three stages](img/three_stages_v13_6.png)
|
||||
|
||||
This process is different than
|
||||
[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.
|
||||
1. View a visual representation of your pipeline by selecting the pipeline ID:
|
||||
|
||||
- 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
|
||||
custom defaults, for example with [`before_script`](../yaml/index.md#before_script)
|
||||
and [`after_script`](../yaml/index.md#after_script).
|
||||
- [`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.
|
||||
- Use [Directed Acyclic Graphs (DAG)](../directed_acyclic_graph/index.md) keywords
|
||||
to run jobs out of stage order.
|
||||
- Use the [`needs` keyword](../yaml/index.md#needs) 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:
|
||||
- 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
|
||||
|
@ -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)
|
||||
and [`artifacts`](../yaml/index.md#artifacts). These keywords are ways to store
|
||||
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.
|
||||
|
||||
To view your pipeline:
|
||||
|
||||
- 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.
|
||||
- [Follow this guide to migrate from CircleCI](../migration/circleci.md).
|
||||
- [Follow this guide to 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.
|
||||
- <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.
|
||||
|
|
|
@ -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
|
||||
widget. You also get the link to the remote pipeline.
|
||||
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.
|
||||
1. Once the docs site is built, the HTML files are uploaded as artifacts.
|
||||
1. A specific runner tied only to the docs project, runs the Review App job
|
||||
that downloads the artifacts and uses `rsync` to transfer the files over
|
||||
to a location where NGINX serves them.
|
||||
1. After the docs site is built, the HTML files are uploaded as artifacts to
|
||||
a GCP bucket (see [issue `gitlab-com/gl-infra/reliability#11021`](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/11021)
|
||||
for the implementation details).
|
||||
|
||||
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)
|
||||
- [Review Apps](../../ci/review_apps/index.md)
|
||||
- [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)
|
||||
|
||||
## 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
|
||||
yet deployed, or something went wrong with the remote pipeline. You can:
|
||||
If you see the following message in a review app, either the site is not
|
||||
yet deployed, or something went wrong with the downstream pipeline in `gitlab-docs`.
|
||||
|
||||
- Wait a few minutes and it should appear online.
|
||||
- Check the manual job's log and verify the URL. If the URL is different, try the
|
||||
one from the job log.
|
||||
```plaintext
|
||||
NoSuchKeyThe specified key does not exist.No such object: <URL>
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### Not enough disk space
|
||||
|
||||
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.
|
||||
internal Slack channel. Contributors can ping a
|
||||
[technical writer](https://about.gitlab.com/handbook/product/ux/technical-writing/#designated-technical-writers)
|
||||
in the merge request.
|
||||
|
|
|
@ -21,7 +21,7 @@ but that repository is no longer used for development.
|
|||
|
||||
## 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/).
|
||||
|
||||
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 |
|
||||
|-------|-------------|--------------------|
|
||||
| [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> [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. | |
|
||||
|
|
|
@ -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.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
graph LR
|
||||
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
|
||||
B(Author: Sam<br>Date: 4 Jan at 10AM<br>Commit message: Removed outdated marketing information<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)
|
||||
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 old info<br> Commit ID: aabb1122) ---> C
|
||||
C(Author: Zhang<br>Date: 5 Jan at 3PM<br>Commit message: Added invoices<br> Commit ID: ddee4455)
|
||||
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.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
flowchart LR
|
||||
subgraph Default branch
|
||||
A[Commit] --> B[Commit] --> C[Commit] --> D[Commit]
|
||||
end
|
||||
subgraph My branch
|
||||
B --1. Create my branch--> E(Commit)
|
||||
E --2. Add my commit--> F(Commit)
|
||||
F --2. Add my commit--> G(Commit)
|
||||
G --3. Merge my branch to default--> D
|
||||
F --3. Merge my branch to default--> D
|
||||
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
|
||||
[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>
|
||||
|
||||
|
|
|
@ -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
|
||||
---
|
||||
|
||||
# Award emoji **(FREE)**
|
||||
# Award emojis **(FREE)**
|
||||
|
||||
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.
|
||||
|
||||
![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.
|
||||
|
||||
For information on the relevant API, see [Award Emoji API](../api/award_emoji.md).
|
||||
|
||||
## 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
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
To add an award emoji:
|
||||
|
@ -40,3 +40,11 @@ To add an award emoji:
|
|||
1. Select an emoji from the dropdown list.
|
||||
|
||||
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:
|
||||
The fields `assignee_id`, and `state` are deprecated.
|
||||
The fields `assignee_id`, `state`, `merge_status` are deprecated.
|
||||
|
||||
## Wiki page events
|
||||
|
||||
|
|
|
@ -6,47 +6,57 @@ module API
|
|||
include ActionView::Helpers::NumberHelper
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ module API
|
|||
MergeRequest, Note, Snippet, Key, Milestone].freeze
|
||||
|
||||
desc 'Get the current application statistics' do
|
||||
success Entities::ApplicationStatistics
|
||||
success code: 200, model: Entities::ApplicationStatistics
|
||||
end
|
||||
get "application/statistics", urgency: :low do
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
msgid "Error uploading file: %{stripped}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error while loading the merge request. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -30557,9 +30554,6 @@ msgstr ""
|
|||
msgid "Please select a country"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please select a file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please select a group"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ module QA
|
|||
include Helpers
|
||||
|
||||
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(
|
||||
/_VERSION/,
|
||||
/Gemfile\.lock/,
|
||||
|
|
|
@ -49,6 +49,14 @@ RSpec.describe QA::Tools::Ci::QaChanges do
|
|||
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
|
||||
let(:mr_diff) { [{ path: "Gemfile" }] }
|
||||
|
||||
|
|
|
@ -34,8 +34,15 @@ RSpec.describe Projects::LearnGitlabController do
|
|||
it { is_expected.to have_gitlab_http_status(:not_found) }
|
||||
end
|
||||
|
||||
it_behaves_like 'tracks assignment and records the subject', :invite_for_help_continuous_onboarding, :namespace do
|
||||
subject { project.namespace }
|
||||
context 'with invite_for_help_continuous_onboarding experiment' do
|
||||
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
|
||||
|
|
|
@ -299,14 +299,15 @@ RSpec.describe Projects::PipelinesController do
|
|||
stub_application_setting(auto_devops_enabled: false)
|
||||
end
|
||||
|
||||
def action
|
||||
get :index, params: { namespace_id: project.namespace, project_id: project }
|
||||
end
|
||||
context 'with runners_availability_section experiment' do
|
||||
it 'tracks the assignment', :experiment do
|
||||
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
|
||||
it_behaves_like 'tracks assignment and records the subject', :runners_availability_section, :namespace
|
||||
get :index, params: { namespace_id: project.namespace, project_id: project }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -43,72 +43,6 @@ RSpec.describe ApplicationExperiment, :experiment do
|
|||
variant: 'control'
|
||||
)
|
||||
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
|
||||
|
||||
describe "#track", :snowplow do
|
||||
|
|
|
@ -30,34 +30,6 @@ RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
|
|||
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
|
||||
context 'when user is new' do
|
||||
it 'is not excluded' do
|
||||
|
|
|
@ -6,10 +6,4 @@ RSpec.describe SecurityReportsMrWidgetPromptExperiment do
|
|||
it "defines a control and candidate" do
|
||||
expect(subject.behaviors.keys).to match_array(%w[control candidate])
|
||||
end
|
||||
|
||||
it "publishes to the database" do
|
||||
expect(subject).to receive(:publish_to_database)
|
||||
|
||||
subject.publish
|
||||
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)
|
||||
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
|
||||
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)
|
||||
|
|
|
@ -52,7 +52,7 @@ describe('Board card component', () => {
|
|||
|
||||
const performSearchMock = jest.fn();
|
||||
|
||||
const createStore = ({ isEpicBoard = false, isProjectBoard = false } = {}) => {
|
||||
const createStore = ({ isProjectBoard = false } = {}) => {
|
||||
store = new Vuex.Store({
|
||||
...defaultStore,
|
||||
actions: {
|
||||
|
@ -65,13 +65,12 @@ describe('Board card component', () => {
|
|||
},
|
||||
getters: {
|
||||
isGroupBoard: () => true,
|
||||
isEpicBoard: () => isEpicBoard,
|
||||
isProjectBoard: () => isProjectBoard,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
const createWrapper = ({ props = {}, isEpicBoard = false } = {}) => {
|
||||
wrapper = mountExtended(BoardCardInner, {
|
||||
store,
|
||||
propsData: {
|
||||
|
@ -97,6 +96,7 @@ describe('Board card component', () => {
|
|||
provide: {
|
||||
rootPath: '/',
|
||||
scopedLabelsAvailable: false,
|
||||
isEpicBoard,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -111,7 +111,7 @@ describe('Board card component', () => {
|
|||
};
|
||||
|
||||
createStore();
|
||||
createWrapper({ item: issue, list });
|
||||
createWrapper({ props: { item: issue, list } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -146,7 +146,7 @@ describe('Board card component', () => {
|
|||
});
|
||||
|
||||
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().props('workItemType')).toBe(issue.type);
|
||||
});
|
||||
|
@ -177,9 +177,11 @@ describe('Board card component', () => {
|
|||
describe('blocked', () => {
|
||||
it('renders blocked icon if issue is blocked', async () => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...issue,
|
||||
blocked: true,
|
||||
props: {
|
||||
item: {
|
||||
...issue,
|
||||
blocked: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -188,9 +190,11 @@ describe('Board card component', () => {
|
|||
|
||||
it('does not show blocked icon if issue is not blocked', () => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...issue,
|
||||
blocked: false,
|
||||
props: {
|
||||
item: {
|
||||
...issue,
|
||||
blocked: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -201,9 +205,11 @@ describe('Board card component', () => {
|
|||
describe('confidential issue', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
confidential: true,
|
||||
props: {
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
confidential: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -216,9 +222,11 @@ describe('Board card component', () => {
|
|||
describe('hidden issue', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
hidden: true,
|
||||
props: {
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -241,11 +249,13 @@ describe('Board card component', () => {
|
|||
describe('with avatar', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees: [user],
|
||||
updateData(newData) {
|
||||
Object.assign(this, newData);
|
||||
props: {
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees: [user],
|
||||
updateData(newData) {
|
||||
Object.assign(this, newData);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -294,15 +304,17 @@ describe('Board card component', () => {
|
|||
global.gon.default_avatar_url = 'default_avatar';
|
||||
|
||||
createWrapper({
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'testing 123',
|
||||
username: 'test',
|
||||
},
|
||||
],
|
||||
props: {
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'testing 123',
|
||||
username: 'test',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -323,28 +335,30 @@ describe('Board card component', () => {
|
|||
describe('multiple assignees', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'user2',
|
||||
username: 'user2',
|
||||
avatarUrl: 'test_image',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'user3',
|
||||
username: 'user3',
|
||||
avatarUrl: 'test_image',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'user4',
|
||||
username: 'user4',
|
||||
avatarUrl: 'test_image',
|
||||
},
|
||||
],
|
||||
props: {
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'user2',
|
||||
username: 'user2',
|
||||
avatarUrl: 'test_image',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'user3',
|
||||
username: 'user3',
|
||||
avatarUrl: 'test_image',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'user4',
|
||||
username: 'user4',
|
||||
avatarUrl: 'test_image',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -364,9 +378,11 @@ describe('Board card component', () => {
|
|||
});
|
||||
|
||||
createWrapper({
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees,
|
||||
props: {
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -390,9 +406,11 @@ describe('Board card component', () => {
|
|||
})),
|
||||
];
|
||||
createWrapper({
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees,
|
||||
props: {
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -405,7 +423,7 @@ describe('Board card component', () => {
|
|||
|
||||
describe('labels', () => {
|
||||
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', () => {
|
||||
|
@ -417,7 +435,7 @@ describe('Board card component', () => {
|
|||
});
|
||||
|
||||
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();
|
||||
|
||||
|
@ -429,11 +447,13 @@ describe('Board card component', () => {
|
|||
describe('filterByLabel method', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...issue,
|
||||
labels: [label1],
|
||||
props: {
|
||||
item: {
|
||||
...issue,
|
||||
labels: [label1],
|
||||
},
|
||||
updateFilters: true,
|
||||
},
|
||||
updateFilters: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -480,9 +500,11 @@ describe('Board card component', () => {
|
|||
describe('loading', () => {
|
||||
it('renders loading icon', async () => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...issue,
|
||||
isLoading: true,
|
||||
props: {
|
||||
item: {
|
||||
...issue,
|
||||
isLoading: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -504,17 +526,20 @@ describe('Board card component', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createStore({ isEpicBoard: true });
|
||||
createStore();
|
||||
});
|
||||
|
||||
it('should render if the item has issues', () => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...issue,
|
||||
descendantCounts,
|
||||
descendantWeightSum,
|
||||
hasIssues: true,
|
||||
props: {
|
||||
item: {
|
||||
...issue,
|
||||
descendantCounts,
|
||||
descendantWeightSum,
|
||||
hasIssues: true,
|
||||
},
|
||||
},
|
||||
isEpicBoard: true,
|
||||
});
|
||||
|
||||
expect(findEpicCountables().exists()).toBe(true);
|
||||
|
@ -535,18 +560,21 @@ describe('Board card component', () => {
|
|||
|
||||
it('shows render item countBadge, weights, and progress correctly', () => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...issue,
|
||||
descendantCounts: {
|
||||
...descendantCounts,
|
||||
openedIssues: 1,
|
||||
props: {
|
||||
item: {
|
||||
...issue,
|
||||
descendantCounts: {
|
||||
...descendantCounts,
|
||||
openedIssues: 1,
|
||||
},
|
||||
descendantWeightSum: {
|
||||
closedIssues: 10,
|
||||
openedIssues: 5,
|
||||
},
|
||||
hasIssues: true,
|
||||
},
|
||||
descendantWeightSum: {
|
||||
closedIssues: 10,
|
||||
openedIssues: 5,
|
||||
},
|
||||
hasIssues: true,
|
||||
},
|
||||
isEpicBoard: true,
|
||||
});
|
||||
|
||||
expect(findEpicCountablesBadgeIssues().text()).toBe('1');
|
||||
|
@ -556,15 +584,18 @@ describe('Board card component', () => {
|
|||
|
||||
it('does not render progress when weight is zero', () => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...issue,
|
||||
descendantCounts: {
|
||||
...descendantCounts,
|
||||
openedIssues: 1,
|
||||
props: {
|
||||
item: {
|
||||
...issue,
|
||||
descendantCounts: {
|
||||
...descendantCounts,
|
||||
openedIssues: 1,
|
||||
},
|
||||
descendantWeightSum,
|
||||
hasIssues: true,
|
||||
},
|
||||
descendantWeightSum,
|
||||
hasIssues: true,
|
||||
},
|
||||
isEpicBoard: true,
|
||||
});
|
||||
|
||||
expect(findEpicBadgeProgress().exists()).toBe(false);
|
||||
|
@ -572,15 +603,18 @@ describe('Board card component', () => {
|
|||
|
||||
it('renders the tooltip with the correct data', () => {
|
||||
createWrapper({
|
||||
item: {
|
||||
...issue,
|
||||
descendantCounts,
|
||||
descendantWeightSum: {
|
||||
closedIssues: 10,
|
||||
openedIssues: 5,
|
||||
props: {
|
||||
item: {
|
||||
...issue,
|
||||
descendantCounts,
|
||||
descendantWeightSum: {
|
||||
closedIssues: 10,
|
||||
openedIssues: 5,
|
||||
},
|
||||
hasIssues: true,
|
||||
},
|
||||
hasIssues: true,
|
||||
},
|
||||
isEpicBoard: true,
|
||||
});
|
||||
|
||||
const tooltip = findEpicCountablesTotalTooltip();
|
||||
|
|
|
@ -101,6 +101,8 @@ export default function createComponent({
|
|||
weightFeatureAvailable: false,
|
||||
boardWeight: null,
|
||||
canAdminList: true,
|
||||
isIssueBoard: true,
|
||||
isEpicBoard: false,
|
||||
...provide,
|
||||
},
|
||||
stubs,
|
||||
|
|
|
@ -53,11 +53,11 @@ describe('Board card layout', () => {
|
|||
state: {
|
||||
labels,
|
||||
labelsLoading: false,
|
||||
isEpicBoard: false,
|
||||
},
|
||||
}),
|
||||
provide: {
|
||||
scopedLabelsAvailable: true,
|
||||
isEpicBoard: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -30,7 +30,6 @@ describe('Board card', () => {
|
|||
},
|
||||
actions: mockActions,
|
||||
getters: {
|
||||
isEpicBoard: () => false,
|
||||
isProjectBoard: () => false,
|
||||
},
|
||||
});
|
||||
|
@ -61,6 +60,7 @@ describe('Board card', () => {
|
|||
groupId: null,
|
||||
rootPath: '/',
|
||||
scopedLabelsAvailable: false,
|
||||
isEpicBoard: false,
|
||||
...provide,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -47,6 +47,8 @@ describe('BoardContent', () => {
|
|||
canAdminList = true,
|
||||
isApolloBoard = false,
|
||||
issuableType = 'issue',
|
||||
isIssueBoard = true,
|
||||
isEpicBoard = false,
|
||||
boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse),
|
||||
} = {}) => {
|
||||
fakeApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
|
||||
|
@ -67,6 +69,8 @@ describe('BoardContent', () => {
|
|||
boardType: 'group',
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
issuableType,
|
||||
isIssueBoard,
|
||||
isEpicBoard,
|
||||
isApolloBoard,
|
||||
},
|
||||
store,
|
||||
|
@ -133,7 +137,7 @@ describe('BoardContent', () => {
|
|||
|
||||
describe('when issuableType is not issue', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ issuableType: 'foo' });
|
||||
createComponent({ issuableType: 'foo', isIssueBoard: false });
|
||||
});
|
||||
|
||||
it('does not render BoardContentSidebar', () => {
|
||||
|
|
|
@ -59,7 +59,6 @@ describe('Board List Header Component', () => {
|
|||
store = new Vuex.Store({
|
||||
state: {},
|
||||
actions: { updateList: updateListSpy, toggleListCollapsed: toggleListCollapsedSpy },
|
||||
getters: { isEpicBoard: () => false },
|
||||
});
|
||||
|
||||
fakeApollo = createMockApollo([[listQuery, listQueryHandler]]);
|
||||
|
@ -76,6 +75,7 @@ describe('Board List Header Component', () => {
|
|||
boardId,
|
||||
weightFeatureAvailable: false,
|
||||
currentUserId,
|
||||
isEpicBoard: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -45,6 +45,7 @@ describe('BoardSettingsSidebar', () => {
|
|||
provide: {
|
||||
canAdminList,
|
||||
scopedLabelsAvailable: false,
|
||||
isIssueBoard: true,
|
||||
},
|
||||
directives: {
|
||||
GlModal: createMockDirective(),
|
||||
|
|
|
@ -15,18 +15,14 @@ describe('BoardTopBar', () => {
|
|||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const createStore = ({ mockGetters = {} } = {}) => {
|
||||
const createStore = () => {
|
||||
return new Vuex.Store({
|
||||
state: {},
|
||||
getters: {
|
||||
isEpicBoard: () => false,
|
||||
...mockGetters,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createComponent = ({ provide = {}, mockGetters = {} } = {}) => {
|
||||
const store = createStore({ mockGetters });
|
||||
const createComponent = ({ provide = {} } = {}) => {
|
||||
const store = createStore();
|
||||
wrapper = shallowMount(BoardTopBar, {
|
||||
store,
|
||||
provide: {
|
||||
|
@ -36,6 +32,7 @@ describe('BoardTopBar', () => {
|
|||
fullPath: 'gitlab-org',
|
||||
boardType: 'group',
|
||||
releasesFetchPath: '/releases',
|
||||
isIssueBoard: true,
|
||||
...provide,
|
||||
},
|
||||
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
|
||||
|
||||
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
|
||||
create(:deployment, :success, project: project, environment: environment, finished_at: 1.year.ago)
|
||||
end
|
||||
|
||||
let!(:last_deployment) do
|
||||
create(:deployment, :success, project: project, environment: environment)
|
||||
create(:deployment, :success, project: project, environment: environment, sha: deployment.sha)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1658,6 +1658,33 @@ RSpec.describe Project, factory_default: :keep do
|
|||
expect(project.reload.star_count).to eq(0)
|
||||
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
|
||||
user = create(:user)
|
||||
project1 = create(:project, :public)
|
||||
|
|
|
@ -2482,6 +2482,30 @@ RSpec.describe User do
|
|||
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
|
||||
let_it_be(:admin_issue_board_list) { create_list(:user, 12, :admin, :with_sign_ins) }
|
||||
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
require 'spec_helper'
|
||||
|
||||
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) }
|
||||
|
||||
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_project2) { create(:users_star_project, project: project2, user: user_blocked) }
|
||||
|
||||
|
@ -50,4 +50,38 @@ RSpec.describe UsersStarProject, type: :model do
|
|||
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
|
||||
|
|
|
@ -2884,6 +2884,27 @@ RSpec.describe ProjectPolicy do
|
|||
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
|
||||
|
||||
def project_subject(project_type)
|
||||
|
|
|
@ -15,6 +15,9 @@ RSpec.describe Projects::MoveUsersStarProjectsService do
|
|||
end
|
||||
|
||||
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.star_count).to eq 2
|
||||
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)
|
||||
|
||||
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.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 "when #{record_class_name} migration fails and is rolled back" 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)
|
||||
end
|
||||
|
||||
|
@ -68,6 +68,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos
|
|||
end
|
||||
|
||||
it "doesn't unblock a previously-blocked user" do
|
||||
expect(user.starred_projects).to receive(:update_all).and_call_original
|
||||
user.block
|
||||
|
||||
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