diff options
67 files changed, 731 insertions, 649 deletions
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md index a9f96a61d29..40ee1c125da 100644 --- a/.gitlab/issue_templates/Feature Flag Roll Out.md +++ b/.gitlab/issue_templates/Feature Flag Roll Out.md @@ -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. diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js deleted file mode 100644 index 387d6043315..00000000000 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ /dev/null @@ -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; - }); - } -} diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 1c9c99dcc2f..4741dd53708 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -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); - } }; diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue index 1c67a2d9f7f..05c786ca61d 100644 --- a/app/assets/javascripts/boards/components/board_card_inner.vue +++ b/app/assets/javascripts/boards/components/board_card_inner.vue @@ -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 diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index 9230513ff93..150378f7a7d 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -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; }, diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index eb889344c1e..fcf026bbe00 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -84,7 +84,7 @@ export default { }, computed: { ...mapState(['error']), - ...mapGetters(['isIssueBoard', 'isGroupBoard', 'isProjectBoard']), + ...mapGetters(['isGroupBoard', 'isProjectBoard']), isNewForm() { return this.currentPage === formType.new; }, diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index ebf19c32e5e..6a5e2801de5 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -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; }, diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index 230fa4e1e0f..cb2f68df0d1 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -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); }, diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index e93edad675c..c0c2699b63d 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -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] || {}; diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue index 54a6e3000a4..368feba9a44 100644 --- a/app/assets/javascripts/boards/components/board_top_bar.vue +++ b/app/assets/javascripts/boards/components/board_top_bar.vue @@ -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" diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 9590a3f963e..f8bd81e6b98 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -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), diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index eff39a744ad..1de36f4a0fb 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -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 }) => { diff --git a/app/controllers/projects/learn_gitlab_controller.rb b/app/controllers/projects/learn_gitlab_controller.rb index 61e4a1812ba..6fe009c8a28 100644 --- a/app/controllers/projects/learn_gitlab_controller.rb +++ b/app/controllers/projects/learn_gitlab_controller.rb @@ -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 diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index dd3e9886239..7d1a75ae449 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -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 diff --git a/app/experiments/application_experiment.rb b/app/experiments/application_experiment.rb index 863a2c41d4c..60bf47c2f12 100644 --- a/app/experiments/application_experiment.rb +++ b/app/experiments/application_experiment.rb @@ -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 diff --git a/app/experiments/require_verification_for_namespace_creation_experiment.rb b/app/experiments/require_verification_for_namespace_creation_experiment.rb index cb667c6ae60..914c5c4a29e 100644 --- a/app/experiments/require_verification_for_namespace_creation_experiment.rb +++ b/app/experiments/require_verification_for_namespace_creation_experiment.rb @@ -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 diff --git a/app/experiments/security_reports_mr_widget_prompt_experiment.rb b/app/experiments/security_reports_mr_widget_prompt_experiment.rb index 1bf3e15ba3b..0a5778950fa 100644 --- a/app/experiments/security_reports_mr_widget_prompt_experiment.rb +++ b/app/experiments/security_reports_mr_widget_prompt_experiment.rb @@ -3,10 +3,4 @@ class SecurityReportsMrWidgetPromptExperiment < ApplicationExperiment control {} candidate {} - - def publish(_result = nil) - super - - publish_to_database - end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 7116827f153..ea92b978d3a 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index b6dce68616f..ad55fec0158 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb index 9998e9b561f..6cffc97822d 100644 --- a/app/models/users_star_project.rb +++ b/app/models/users_star_project.rb @@ -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 diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 06c3b6f96b9..9a2208b8adb 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -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? diff --git a/app/services/projects/move_users_star_projects_service.rb b/app/services/projects/move_users_star_projects_service.rb index 5490448553f..4f1580c5f53 100644 --- a/app/services/projects/move_users_star_projects_service.rb +++ b/app/services/projects/move_users_star_projects_service.rb @@ -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 diff --git a/config/audit_events/types/policy_project_updated.yml b/config/audit_events/types/policy_project_updated.yml index 8692486b5cb..6fffc7f6b10 100644 --- a/config/audit_events/types/policy_project_updated.yml +++ b/config/audit_events/types/policy_project_updated.yml @@ -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 diff --git a/data/deprecations/15-6-deprecate-runner-register-command.yml b/data/deprecations/15-6-deprecate-runner-register-command.yml new file mode 100644 index 00000000000..bed02088897 --- /dev/null +++ b/data/deprecations/15-6-deprecate-runner-register-command.yml @@ -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. diff --git a/db/post_migrate/20221019105041_queue_populate_projects_star_count.rb b/db/post_migrate/20221019105041_queue_populate_projects_star_count.rb new file mode 100644 index 00000000000..768e0c7826f --- /dev/null +++ b/db/post_migrate/20221019105041_queue_populate_projects_star_count.rb @@ -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 diff --git a/db/schema_migrations/20221019105041 b/db/schema_migrations/20221019105041 new file mode 100644 index 00000000000..0dff355b300 --- /dev/null +++ b/db/schema_migrations/20221019105041 @@ -0,0 +1 @@ +186e7df4e7e81913981595a069c5c8b5fbb600ee5dcebf333bfff728c5019ab2
\ No newline at end of file diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index ca6761ed6be..9d0b8945c53 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -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: diff --git a/doc/api/graphql/custom_emoji.md b/doc/api/graphql/custom_emoji.md index f3759c15f5f..e2e8bce4290 100644 --- a/doc/api/graphql/custom_emoji.md +++ b/doc/api/graphql/custom_emoji.md @@ -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) -``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index a09ca5a548c..c8611616cba 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -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. diff --git a/doc/ci/quick_start/index.md b/doc/ci/quick_start/index.md index ad41e4fa88a..f4e74774062 100644 --- a/doc/ci/quick_start/index.md +++ b/doc/ci/quick_start/index.md @@ -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: - -- [Migrate from CircleCI](../migration/circleci.md). -- [Migrate from Jenkins](../migration/jenkins.md). +If you don't have a project, you can create a public project for free on <https://gitlab.com>. -> - <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. +## Steps -## 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 don't have a runner: + +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. -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. +When your CI/CD jobs run, in a later step, they will run on your local machine. -### Create a `.gitlab-ci.yml` file +## Create a `.gitlab-ci.yml` file -The `.gitlab-ci.yml` file is a [YAML](https://en.wikipedia.org/wiki/YAML) file where -you configure specific instructions for GitLab CI/CD. +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. + +## View the status of your pipeline and jobs + +Now take a look at your pipeline and the jobs within. + +1. Go to **CI/CD > Pipelines**. A pipeline with three stages should be displayed: + + ![Three stages](img/three_stages_v13_6.png) -#### `.gitlab-ci.yml` tips +1. View a visual representation of your pipeline by selecting the pipeline ID: -- 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: + ![Pipeline graph](img/pipeline_graph_v13_6.png) - ```yaml - default: - image: ruby:2.7.5 - ``` +1. View details of a job by selecting the job name. For example, `deploy-prod`: - 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. + ![Job details](img/job_details_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. +You have successfully created your first CI/CD pipeline in GitLab. Congratulations! -- Each job contains scripts and stages: +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 - -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) +## Related topics -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. diff --git a/doc/development/documentation/review_apps.md b/doc/development/documentation/review_apps.md index cb04f0909c1..3cf77fda22b 100644 --- a/doc/development/documentation/review_apps.md +++ b/doc/development/documentation/review_apps.md @@ -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. -- 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. +```plaintext +NoSuchKeyThe specified key does not exist.No such object: <URL> +``` -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: +In that case, you 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. +- 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` + 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. diff --git a/doc/development/workhorse/index.md b/doc/development/workhorse/index.md index 0f4e55a002a..91795336f78 100644 --- a/doc/development/workhorse/index.md +++ b/doc/development/workhorse/index.md @@ -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`. diff --git a/doc/tutorials/index.md b/doc/tutorials/index.md index 0ed6335c89a..f9be21b3887 100644 --- a/doc/tutorials/index.md +++ b/doc/tutorials/index.md @@ -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. | | diff --git a/doc/tutorials/make_your_first_git_commit.md b/doc/tutorials/make_your_first_git_commit.md index 98dc0ba86b4..78905a73258 100644 --- a/doc/tutorials/make_your_first_git_commit.md +++ b/doc/tutorials/make_your_first_git_commit.md @@ -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 ``` diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index 7320f3b0eb0..5ce46cbd364 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -67,6 +67,21 @@ This new architecture introduces a new method for registering runners and elimin [runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens). </div> + +<div class="deprecation removal-160 breaking-change"> + +### `gitlab-runner register` command + +End of Support: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)<br /> +Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22) + +WARNING: +This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/). +Review the details carefully before upgrading. + +The command to [register](https://docs.gitlab.com/runner/register/) a runner, `gitlab-runner register` is deprecated. GitLab plans to introduce a new [GitLab Runner token architecture](https://docs.gitlab.com/ee/architecture/blueprints/runner_tokens/), which introduces a new method for registering runners and eliminates the legacy [runner registration token](https://docs.gitlab.com/ee/security/token_overview.html#runner-registration-tokens). + +</div> </div> <div class="announcement-milestone"> diff --git a/doc/user/award_emojis.md b/doc/user/award_emojis.md index 601e2a928c1..ea631b0e251 100644 --- a/doc/user/award_emojis.md +++ b/doc/user/award_emojis.md @@ -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). diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md index 4514bba5d50..e43190230a6 100644 --- a/doc/user/project/integrations/webhook_events.md +++ b/doc/user/project/integrations/webhook_events.md @@ -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 diff --git a/lib/api/entities/application_statistics.rb b/lib/api/entities/application_statistics.rb index 4bcba1da464..7e75ef23675 100644 --- a/lib/api/entities/application_statistics.rb +++ b/lib/api/entities/application_statistics.rb @@ -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 diff --git a/lib/api/statistics.rb b/lib/api/statistics.rb index a12a2ed08d7..1af83c0737a 100644 --- a/lib/api/statistics.rb +++ b/lib/api/statistics.rb @@ -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) diff --git a/lib/gitlab/background_migration/populate_projects_star_count.rb b/lib/gitlab/background_migration/populate_projects_star_count.rb new file mode 100644 index 00000000000..085d576637e --- /dev/null +++ b/lib/gitlab/background_migration/populate_projects_star_count.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 86d33d31994..3a26183da38 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/qa/qa/tools/ci/qa_changes.rb b/qa/qa/tools/ci/qa_changes.rb index 784923714d6..1ab93b6dfbf 100644 --- a/qa/qa/tools/ci/qa_changes.rb +++ b/qa/qa/tools/ci/qa_changes.rb @@ -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/, diff --git a/qa/spec/tools/ci/qa_changes_spec.rb b/qa/spec/tools/ci/qa_changes_spec.rb index d93d3cd9258..778a0ad33bb 100644 --- a/qa/spec/tools/ci/qa_changes_spec.rb +++ b/qa/spec/tools/ci/qa_changes_spec.rb @@ -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" }] } diff --git a/spec/controllers/projects/learn_gitlab_controller_spec.rb b/spec/controllers/projects/learn_gitlab_controller_spec.rb index 2d00fcbccf3..a93da82d948 100644 --- a/spec/controllers/projects/learn_gitlab_controller_spec.rb +++ b/spec/controllers/projects/learn_gitlab_controller_spec.rb @@ -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 diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 527cba9e618..f66e4b133ca 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -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 diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb index b144e6f77d2..7aca5e492f4 100644 --- a/spec/experiments/application_experiment_spec.rb +++ b/spec/experiments/application_experiment_spec.rb @@ -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 diff --git a/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb index 269b6222020..c91a8f1950e 100644 --- a/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb +++ b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb @@ -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 diff --git a/spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb b/spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb index 4328ff12d42..ee02fa5f1f2 100644 --- a/spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb +++ b/spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb @@ -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 diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb index a385e8a5fd0..c9efda7822d 100644 --- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb @@ -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) diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index 3b77c999a9e..d05e057095d 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -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, - }, - descendantWeightSum: { - closedIssues: 10, - openedIssues: 5, + props: { + item: { + ...issue, + descendantCounts: { + ...descendantCounts, + openedIssues: 1, + }, + descendantWeightSum: { + closedIssues: 10, + openedIssues: 5, + }, + hasIssues: true, }, - 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(); diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js index 65a41c49e7f..c5c3faf1712 100644 --- a/spec/frontend/boards/board_list_helper.js +++ b/spec/frontend/boards/board_list_helper.js @@ -101,6 +101,8 @@ export default function createComponent({ weightFeatureAvailable: false, boardWeight: null, canAdminList: true, + isIssueBoard: true, + isEpicBoard: false, ...provide, }, stubs, diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js index 5fae1c4359f..a3b2988ce75 100644 --- a/spec/frontend/boards/components/board_add_new_column_spec.js +++ b/spec/frontend/boards/components/board_add_new_column_spec.js @@ -53,11 +53,11 @@ describe('Board card layout', () => { state: { labels, labelsLoading: false, - isEpicBoard: false, }, }), provide: { scopedLabelsAvailable: true, + isEpicBoard: false, }, }), ); diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 2feaa5dff8c..38b79e2e3f3 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -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, }, }); diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index bf491029c41..b2138700602 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -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', () => { diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js index 50901f3fe84..4633612891c 100644 --- a/spec/frontend/boards/components/board_list_header_spec.js +++ b/spec/frontend/boards/components/board_list_header_spec.js @@ -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, }, }), ); diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js index 4171a6236de..7d602042685 100644 --- a/spec/frontend/boards/components/board_settings_sidebar_spec.js +++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js @@ -45,6 +45,7 @@ describe('BoardSettingsSidebar', () => { provide: { canAdminList, scopedLabelsAvailable: false, + isIssueBoard: true, }, directives: { GlModal: createMockDirective(), diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js index 997768a0cc7..08b5042f70f 100644 --- a/spec/frontend/boards/components/board_top_bar_spec.js +++ b/spec/frontend/boards/components/board_top_bar_spec.js @@ -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 }, diff --git a/spec/lib/gitlab/background_migration/populate_projects_star_count_spec.rb b/spec/lib/gitlab/background_migration/populate_projects_star_count_spec.rb new file mode 100644 index 00000000000..74f674e052d --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_projects_star_count_spec.rb @@ -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 diff --git a/spec/migrations/queue_populate_projects_star_count_spec.rb b/spec/migrations/queue_populate_projects_star_count_spec.rb new file mode 100644 index 00000000000..848136d8005 --- /dev/null +++ b/spec/migrations/queue_populate_projects_star_count_spec.rb @@ -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 diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 7d068235f18..daa65f528e9 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -388,16 +388,31 @@ 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) + 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) + 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, sha: deployment.sha) + end + + it { is_expected.to be_falsey } + end end describe '#success?' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index c596748b0a5..736e70d1efc 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -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) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d91eaae7caf..7207ee0b172 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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) } diff --git a/spec/models/users_star_project_spec.rb b/spec/models/users_star_project_spec.rb index 8b66fd9c187..60ec108f77d 100644 --- a/spec/models/users_star_project_spec.rb +++ b/spec/models/users_star_project_spec.rb @@ -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 diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 785fb0fc352..09fed665479 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -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) diff --git a/spec/services/projects/move_users_star_projects_service_spec.rb b/spec/services/projects/move_users_star_projects_service_spec.rb index 0f766ebd0ec..b580d3d8772 100644 --- a/spec/services/projects/move_users_star_projects_service_spec.rb +++ b/spec/services/projects/move_users_star_projects_service_spec.rb @@ -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 diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb index 1e291a90163..ae98ce689e3 100644 --- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb +++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb @@ -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) diff --git a/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb deleted file mode 100644 index fdca326dbea..00000000000 --- a/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb +++ /dev/null @@ -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 |