diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-05 15:08:47 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-05 15:08:47 +0300 |
commit | 5460c19548d3d3d2ff764ceff05c2b72aa2d3a32 (patch) | |
tree | 3a7fff116a7f6ff9c033e7f5e87a4341de801784 | |
parent | 2fe341d705700b3e99f82076b09b95885878ac0d (diff) |
Add latest changes from gitlab-org/gitlab@master
122 files changed, 1723 insertions, 279 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index fab33dd3c07..0fd2c01bb04 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -72,7 +72,7 @@ review-deploy: - download_chart - date - deploy || (display_deployment_debug && exit 1) - - disable_sign_ups + - disable_sign_ups || (delete_release && exit 1) # When the job is manual, review-qa-smoke is also manual and we don't want people # to have to manually start the jobs in sequence, so we do it for them. - '[ -z $CI_JOB_MANUAL ] || play_job "review-qa-smoke"' diff --git a/app/assets/javascripts/design_management/components/design_note_pin.vue b/app/assets/javascripts/design_management/components/design_note_pin.vue index 2b5e62c2870..320e0654aab 100644 --- a/app/assets/javascripts/design_management/components/design_note_pin.vue +++ b/app/assets/javascripts/design_management/components/design_note_pin.vue @@ -17,19 +17,11 @@ export default { required: false, default: null, }, - repositioning: { - type: Boolean, - required: false, - default: false, - }, }, computed: { isNewNote() { return this.label === null; }, - pinStyle() { - return this.repositioning ? { ...this.position, cursor: 'move' } : this.position; - }, pinLabel() { return this.isNewNote ? __('Comment form position') @@ -41,13 +33,13 @@ export default { <template> <button - :style="pinStyle" + :style="position" :aria-label="pinLabel" :class="{ - 'btn-transparent comment-indicator': isNewNote, + 'btn-transparent comment-indicator gl-p-0': isNewNote, 'js-image-badge badge badge-pill': !isNewNote, }" - class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0" + class="gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-font-lg gl-outline-0!" type="button" @mousedown="$emit('mousedown', $event)" @mouseup="$emit('mouseup', $event)" diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue index 5c4a3ab5f94..88f3ce0b8ea 100644 --- a/app/assets/javascripts/design_management/components/design_overlay.vue +++ b/app/assets/javascripts/design_management/components/design_overlay.vue @@ -266,7 +266,7 @@ export default { type="button" role="button" :aria-label="$options.i18n.newCommentButtonLabel" - class="gl-absolute gl-w-full gl-h-full gl-p-0 gl-top-0 gl-left-0 gl-outline-0! btn-transparent design-detail-overlay-add-comment" + class="gl-absolute gl-w-full gl-h-full gl-p-0 gl-top-0 gl-left-0 gl-outline-0! btn-transparent gl-hover-cursor-crosshair" data-qa-selector="design_image_button" @mouseup="onAddCommentMouseup" ></button> @@ -276,7 +276,6 @@ export default { v-if="resolvedDiscussionsExpanded || !note.resolved" :key="note.id" :label="note.index" - :repositioning="isMovingNote(note.id)" :position=" isMovingNote(note.id) && movingNoteNewPosition ? getNotePositionStyle(movingNoteNewPosition) @@ -290,7 +289,6 @@ export default { <design-note-pin v-if="currentCommentForm" :position="currentCommentPositionStyle" - :repositioning="isMovingCurrentComment" @mousedown.stop="onNoteMousedown" @mouseup.stop="onNoteMouseup" /> diff --git a/app/assets/javascripts/lib/utils/css_utils.js b/app/assets/javascripts/lib/utils/css_utils.js new file mode 100644 index 00000000000..90213221443 --- /dev/null +++ b/app/assets/javascripts/lib/utils/css_utils.js @@ -0,0 +1,19 @@ +export function loadCSSFile(path) { + return new Promise(resolve => { + if (document.querySelector(`link[href="${path}"]`)) { + resolve(); + } else { + const linkElement = document.createElement('link'); + linkElement.type = 'text/css'; + linkElement.rel = 'stylesheet'; + // eslint-disable-next-line @gitlab/require-i18n-strings + linkElement.media = 'screen,print'; + linkElement.onload = () => { + resolve(); + }; + linkElement.href = path; + + document.head.appendChild(linkElement); + } + }); +} diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js index 37242822e35..6a0e92bff2d 100644 --- a/app/assets/javascripts/packages/list/constants.js +++ b/app/assets/javascripts/packages/list/constants.js @@ -82,7 +82,7 @@ export const PACKAGE_REGISTRY_TABS = [ type: PackageType.NUGET, }, { - title: s__('PackageRegistry|PyPi'), + title: s__('PackageRegistry|PyPI'), type: PackageType.PYPI, }, ]; diff --git a/app/assets/javascripts/packages/shared/utils.js b/app/assets/javascripts/packages/shared/utils.js index a0c7389651d..b0807558266 100644 --- a/app/assets/javascripts/packages/shared/utils.js +++ b/app/assets/javascripts/packages/shared/utils.js @@ -18,7 +18,7 @@ export const getPackageTypeLabel = packageType => { case PackageType.NUGET: return s__('PackageType|NuGet'); case PackageType.PYPI: - return s__('PackageType|PyPi'); + return s__('PackageType|PyPI'); case PackageType.COMPOSER: return s__('PackageType|Composer'); diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index 55bc9fb8955..ecb69422287 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -3,6 +3,7 @@ import $ from 'jquery'; import 'cropper'; import { isString } from 'lodash'; +import { loadCSSFile } from '../lib/utils/css_utils'; (() => { // Matches everything but the file name @@ -180,6 +181,9 @@ import { isString } from 'lodash'; } } + const cropModal = document.querySelector('.modal-profile-crop'); + if (cropModal) loadCSSFile(cropModal.dataset.cropperCssPath); + $.fn.glCrop = function(opts) { return this.each(function() { return $(this).data('glcrop', new GitLabCrop(this, opts)); diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index cae886bf846..4b1139d2354 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -6,7 +6,6 @@ @import '@gitlab/at.js/dist/css/jquery.atwho'; @import 'dropzone/dist/basic'; @import 'select2'; -@import 'cropper'; // GitLab UI framework @import 'framework'; diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss index f198c06c2df..81f2091e915 100644 --- a/app/assets/stylesheets/components/design_management/design.scss +++ b/app/assets/stylesheets/components/design_management/design.scss @@ -1,3 +1,6 @@ +$design-pin-diameter: 28px; +$t-gray-a-16-design-pin: rgba($black, 0.16); + .layout-page.design-detail-layout { max-height: 100vh; } @@ -9,34 +12,61 @@ top: 35px; } - .design-pin { - transition: opacity $gl-transition-duration-medium $general-hover-transition-curve; - - &.inactive { - @include gl-opacity-5; - - &:hover { - @include gl-opacity-10; - } - } - } - .badge.badge-pill { display: flex; - height: 28px; - width: 28px; - background-color: $blue-400; + height: $design-pin-diameter; + width: $design-pin-diameter; + box-sizing: content-box; + background-color: $purple-500; color: $white; - border: $white 1px solid; + font-weight: $gl-font-weight-bold; border-radius: 50%; + z-index: 1; + padding: 0; &.resolved { background-color: $gray-500; } } - .design-detail-overlay-add-comment { - cursor: crosshair; + .comment-indicator { + border-radius: 50%; + } + + .comment-indicator, + .frame .badge.badge-pill { + &:active { + cursor: grabbing; + } + } + + /** + * Design pin that overlays the design + */ + .frame .badge.badge-pill { + box-shadow: 0 2px 4px $t-gray-a-08, 0 0 1px $t-gray-a-24; + border: $white 2px solid; + will-change: transform, box-shadow, opacity; + // NOTE: verbose transition property required for Safari + transition: transform $general-hover-transition-duration linear, box-shadow $general-hover-transition-duration linear, opacity $general-hover-transition-duration linear; + transform-origin: 0 0; + transform: translate(-50%, -50%); + + &:hover { + transform: scale(1.2) translate(-50%, -50%); + } + + &:active { + box-shadow: 0 0 4px $t-gray-a-16-design-pin, 0 4px 12px $t-gray-a-16-design-pin; + } + + &.inactive { + @include gl-opacity-5; + + &:hover { + @include gl-opacity-10; + } + } } } @@ -105,8 +135,8 @@ border-left: 1px solid $gray-100; position: absolute; left: 28px; - top: -18px; - height: 18px; + top: -17px; + height: 17px; } .design-note { diff --git a/app/assets/stylesheets/lazy_bundles/cropper.css b/app/assets/stylesheets/lazy_bundles/cropper.css new file mode 100644 index 00000000000..9c7fdded117 --- /dev/null +++ b/app/assets/stylesheets/lazy_bundles/cropper.css @@ -0,0 +1,378 @@ +/*! + * Cropper v2.3.0 + * https://github.com/fengyuanchen/cropper + * + * Copyright (c) 2014-2016 Fengyuan Chen and contributors + * Released under the MIT license + * + * Date: 2016-02-22T02:13:13.332Z + */ +.cropper-container { + font-size: 0; + line-height: 0; + + position: relative; + + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + direction: ltr !important; + touch-action: none; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; +} + +.cropper-container img { + display: block; + + width: 100%; + min-width: 0 !important; + max-width: none !important; + height: 100%; + min-height: 0 !important; + max-height: none !important; + + image-orientation: 0deg !important; +} + +.cropper-wrap-box, +.cropper-canvas, +.cropper-drag-box, +.cropper-crop-box, +.cropper-modal { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.cropper-wrap-box { + overflow: hidden; +} + +.cropper-drag-box { + opacity: 0; + background-color: #fff; + + filter: alpha(opacity=0); +} + +.cropper-modal { + opacity: 0.5; + background-color: #000; + + filter: alpha(opacity=50); +} + +.cropper-view-box { + display: block; + overflow: hidden; + + width: 100%; + height: 100%; + + outline: 1px solid #39f; + outline-color: rgba(51, 153, 255, 0.75); +} + +.cropper-dashed { + position: absolute; + + display: block; + + opacity: 0.5; + border: 0 dashed #eee; + + filter: alpha(opacity=50); +} + +.cropper-dashed.dashed-h { + top: 33.33333%; + left: 0; + + width: 100%; + height: 33.33333%; + + border-top-width: 1px; + border-bottom-width: 1px; +} + +.cropper-dashed.dashed-v { + top: 0; + left: 33.33333%; + + width: 33.33333%; + height: 100%; + + border-right-width: 1px; + border-left-width: 1px; +} + +.cropper-center { + position: absolute; + top: 50%; + left: 50%; + + display: block; + + width: 0; + height: 0; + + opacity: 0.75; + + filter: alpha(opacity=75); +} + +.cropper-center::before, +.cropper-center::after { + position: absolute; + + display: block; + + content: ' '; + + background-color: #eee; +} + +.cropper-center::before { + top: 0; + left: -3px; + + width: 7px; + height: 1px; +} + +.cropper-center::after { + top: -3px; + left: 0; + + width: 1px; + height: 7px; +} + +.cropper-face, +.cropper-line, +.cropper-point { + position: absolute; + + display: block; + + width: 100%; + height: 100%; + + opacity: 0.1; + + filter: alpha(opacity=10); +} + +.cropper-face { + top: 0; + left: 0; + + background-color: #fff; +} + +.cropper-line { + background-color: #39f; +} + +.cropper-line.line-e { + top: 0; + right: -3px; + + width: 5px; + + cursor: e-resize; +} + +.cropper-line.line-n { + top: -3px; + left: 0; + + height: 5px; + + cursor: n-resize; +} + +.cropper-line.line-w { + top: 0; + left: -3px; + + width: 5px; + + cursor: w-resize; +} + +.cropper-line.line-s { + bottom: -3px; + left: 0; + + height: 5px; + + cursor: s-resize; +} + +.cropper-point { + width: 5px; + height: 5px; + + opacity: 0.75; + background-color: #39f; + + filter: alpha(opacity=75); +} + +.cropper-point.point-e { + top: 50%; + right: -3px; + + margin-top: -3px; + + cursor: e-resize; +} + +.cropper-point.point-n { + top: -3px; + left: 50%; + + margin-left: -3px; + + cursor: n-resize; +} + +.cropper-point.point-w { + top: 50%; + left: -3px; + + margin-top: -3px; + + cursor: w-resize; +} + +.cropper-point.point-s { + bottom: -3px; + left: 50%; + + margin-left: -3px; + + cursor: s-resize; +} + +.cropper-point.point-ne { + top: -3px; + right: -3px; + + cursor: ne-resize; +} + +.cropper-point.point-nw { + top: -3px; + left: -3px; + + cursor: nw-resize; +} + +.cropper-point.point-sw { + bottom: -3px; + left: -3px; + + cursor: sw-resize; +} + +.cropper-point.point-se { + right: -3px; + bottom: -3px; + + width: 20px; + height: 20px; + + cursor: se-resize; + + opacity: 1; + + filter: alpha(opacity=100); +} + +.cropper-point.point-se::before { + position: absolute; + right: -50%; + bottom: -50%; + + display: block; + + width: 200%; + height: 200%; + + content: ' '; + + opacity: 0; + background-color: #39f; + + filter: alpha(opacity=0); +} + +@media (min-width: 768px) { + .cropper-point.point-se { + width: 15px; + height: 15px; + } +} + +@media (min-width: 992px) { + .cropper-point.point-se { + width: 10px; + height: 10px; + } +} + +@media (min-width: 1200px) { + .cropper-point.point-se { + width: 5px; + height: 5px; + + opacity: 0.75; + + filter: alpha(opacity=75); + } +} + +.cropper-invisible { + opacity: 0; + + filter: alpha(opacity=0); +} + +.cropper-bg { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC'); +} + +.cropper-hide { + position: absolute; + + display: block; + + width: 0; + height: 0; +} + +.cropper-hidden { + display: none !important; +} + +.cropper-move { + cursor: move; +} + +.cropper-crop { + cursor: crosshair; +} + +.cropper-disabled .cropper-drag-box, +.cropper-disabled .cropper-face, +.cropper-disabled .cropper-line, +.cropper-disabled .cropper-point { + cursor: not-allowed; +} diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index a1bbcf34f69..14de8d6c6ab 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -2,6 +2,7 @@ class HelpController < ApplicationController skip_before_action :authenticate_user!, unless: :public_visibility_restricted? + feature_category :not_owned layout 'help' diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb index 2c17f5b5542..8c0414ad5da 100644 --- a/app/controllers/ide_controller.rb +++ b/app/controllers/ide_controller.rb @@ -11,6 +11,8 @@ class IdeController < ApplicationController push_frontend_feature_flag(:schema_linting) end + feature_category :web_ide + def index Gitlab::UsageDataCounters::WebIdeCounter.increment_views_count end diff --git a/app/controllers/import/available_namespaces_controller.rb b/app/controllers/import/available_namespaces_controller.rb index 7983b4f20b5..c6211b33d28 100644 --- a/app/controllers/import/available_namespaces_controller.rb +++ b/app/controllers/import/available_namespaces_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Import::AvailableNamespacesController < ApplicationController + feature_category :importers + def index render json: NamespaceSerializer.new.represent(current_user.manageable_groups_with_routes) end diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 8a7a4c92b37..151ba46e629 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -4,6 +4,7 @@ class Import::BaseController < ApplicationController include ActionView::Helpers::SanitizeHelper before_action :import_rate_limit, only: [:create] + feature_category :importers def status respond_to do |format| diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb index 58b9f8c0fbb..cad181f0d64 100644 --- a/app/controllers/import/bulk_imports_controller.rb +++ b/app/controllers/import/bulk_imports_controller.rb @@ -4,6 +4,8 @@ class Import::BulkImportsController < ApplicationController before_action :ensure_group_import_enabled before_action :verify_blocked_uri, only: :status + feature_category :importers + def configure session[access_token_key] = params[access_token_key]&.strip session[url_key] = params[url_key] diff --git a/app/controllers/import/gitlab_groups_controller.rb b/app/controllers/import/gitlab_groups_controller.rb index 330af68385e..d8118477a80 100644 --- a/app/controllers/import/gitlab_groups_controller.rb +++ b/app/controllers/import/gitlab_groups_controller.rb @@ -6,6 +6,8 @@ class Import::GitlabGroupsController < ApplicationController before_action :ensure_group_import_enabled before_action :import_rate_limit, only: %i[create] + feature_category :importers + def create unless file_is_valid?(group_params[:file]) return redirect_back_or_default(options: { alert: s_('GroupImport|Unable to process group import file') }) diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 591ded7630c..c7b8486d1c9 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -12,6 +12,8 @@ class InvitesController < ApplicationController respond_to :html + feature_category :authentication_and_authorization + def show track_new_user_invite_experiment('opened') accept if skip_invitation_prompt? diff --git a/app/controllers/jira_connect/application_controller.rb b/app/controllers/jira_connect/application_controller.rb index a84f25998a6..9c311f92b69 100644 --- a/app/controllers/jira_connect/application_controller.rb +++ b/app/controllers/jira_connect/application_controller.rb @@ -7,6 +7,8 @@ class JiraConnect::ApplicationController < ApplicationController skip_before_action :verify_authenticity_token before_action :verify_atlassian_jwt! + feature_category :integrations + attr_reader :current_jira_installation private diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 3e7755046cd..5199bb25c8c 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -8,6 +8,8 @@ class JwtController < ApplicationController # Add this before other actions, since we want to have the user or project prepend_before_action :auth_user, :authenticate_project_or_user + feature_category :authentication_and_authorization + SERVICES = { Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService }.freeze diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb index e5d4a4bb073..7d8c035c852 100644 --- a/app/controllers/notification_settings_controller.rb +++ b/app/controllers/notification_settings_controller.rb @@ -3,6 +3,8 @@ class NotificationSettingsController < ApplicationController before_action :authenticate_user! + feature_category :users + def create return render_404 unless can_read?(resource) diff --git a/app/controllers/oauth/jira/authorizations_controller.rb b/app/controllers/oauth/jira/authorizations_controller.rb index f552b0dc10c..f23149c8544 100644 --- a/app/controllers/oauth/jira/authorizations_controller.rb +++ b/app/controllers/oauth/jira/authorizations_controller.rb @@ -8,6 +8,8 @@ class Oauth::Jira::AuthorizationsController < ApplicationController skip_before_action :authenticate_user! skip_before_action :verify_authenticity_token + feature_category :integrations + # 1. Rewire Jira OAuth initial request to our stablished OAuth authorization URL. def new session[:redirect_uri] = params['redirect_uri'] diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 2708e6669e7..c9791703413 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -11,6 +11,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true + feature_category :authentication_and_authorization + def handle_omniauth omniauth_flow(Gitlab::Auth::OAuth) end diff --git a/app/controllers/registrations/experience_levels_controller.rb b/app/controllers/registrations/experience_levels_controller.rb index 38cffff91eb..eb2b899d20e 100644 --- a/app/controllers/registrations/experience_levels_controller.rb +++ b/app/controllers/registrations/experience_levels_controller.rb @@ -7,6 +7,8 @@ module Registrations before_action :check_experiment_enabled before_action :ensure_namespace_path_param + feature_category :navigation + def update current_user.experience_level = params[:experience_level] diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 204520a3e71..99b97bca73d 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -15,6 +15,8 @@ class RegistrationsController < Devise::RegistrationsController if: -> { action_name == 'create' && Gitlab::CurrentSettings.current_application_settings.enforce_terms? } before_action :load_recaptcha, only: :new + feature_category :authentication_and_authorization + def new if experiment_enabled?(:signup_flow) track_experiment_event(:terms_opt_in, 'start') diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb index e02955433b2..de452aa69b7 100644 --- a/app/controllers/repositories/git_http_client_controller.rb +++ b/app/controllers/repositories/git_http_client_controller.rb @@ -20,6 +20,8 @@ module Repositories prepend_before_action :authenticate_user, :parse_repo_path + feature_category :source_code_management + private def download_request? diff --git a/app/controllers/runner_setup_controller.rb b/app/controllers/runner_setup_controller.rb index 2cb204b729c..e0e9c5b7c23 100644 --- a/app/controllers/runner_setup_controller.rb +++ b/app/controllers/runner_setup_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class RunnerSetupController < ApplicationController + feature_category :continuous_integration + def platforms render json: Gitlab::Ci::RunnerInstructions::OS.merge(Gitlab::Ci::RunnerInstructions::OTHER_ENVIRONMENTS) end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index b5e221a8894..5bc01cbb354 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -25,6 +25,8 @@ class SearchController < ApplicationController layout 'search' + feature_category :global_search + def show @project = search_service.project @group = search_service.group diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb index 20134de81a0..db07b212d00 100644 --- a/app/controllers/sent_notifications_controller.rb +++ b/app/controllers/sent_notifications_controller.rb @@ -3,6 +3,8 @@ class SentNotificationsController < ApplicationController skip_before_action :authenticate_user! + feature_category :users + def unsubscribe @sent_notification = SentNotification.for(params[:id]) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 318553b5e0a..ae9744b224a 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -49,6 +49,8 @@ class SessionsController < Devise::SessionsController # token mismatch. protect_from_forgery with: :exception, prepend: true, except: :destroy + feature_category :authentication_and_authorization + CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha' MAX_FAILED_LOGIN_ATTEMPTS = 5 diff --git a/app/controllers/snippets/application_controller.rb b/app/controllers/snippets/application_controller.rb index a533e46a75d..f259f4569ef 100644 --- a/app/controllers/snippets/application_controller.rb +++ b/app/controllers/snippets/application_controller.rb @@ -4,6 +4,8 @@ class Snippets::ApplicationController < ApplicationController include FindSnippet include SnippetAuthorizations + feature_category :snippets + private def authorize_read_snippet! diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb index a7e8ef0798b..8532257cb8d 100644 --- a/app/controllers/snippets/notes_controller.rb +++ b/app/controllers/snippets/notes_controller.rb @@ -8,6 +8,8 @@ class Snippets::NotesController < ApplicationController before_action :authorize_read_snippet!, only: [:show, :index] before_action :authorize_create_note!, only: [:create] + feature_category :snippets + private def note diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 9510734bc9b..6692c285335 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -25,6 +25,8 @@ class UploadsController < ApplicationController before_action :authorize_create_access!, only: [:create, :authorize] before_action :verify_workhorse_api!, only: [:authorize] + feature_category :not_owned + def uploader_class PersonalFileUploader end diff --git a/app/controllers/user_callouts_controller.rb b/app/controllers/user_callouts_controller.rb index 06f422b9d90..cfec9d6d905 100644 --- a/app/controllers/user_callouts_controller.rb +++ b/app/controllers/user_callouts_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class UserCalloutsController < ApplicationController + feature_category :navigation + def create callout = ensure_callout diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb index 231e449f733..be670658048 100644 --- a/app/controllers/users/terms_controller.rb +++ b/app/controllers/users/terms_controller.rb @@ -14,6 +14,8 @@ module Users layout 'terms' + feature_category :users + def index @redirect = redirect_path diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index e7af60beade..672f36dedc0 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -21,6 +21,8 @@ class UsersController < ApplicationController before_action :authorize_read_user_profile!, only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :starred_projects, :snippets] + feature_category :users + def show respond_to do |format| format.html diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 573818b1b7a..adb689baf6a 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -152,6 +152,13 @@ module Types field :auto_merge_enabled, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates if auto merge is enabled for the merge request' + field :approved_by, Types::UserType.connection_type, null: true, + description: 'Users who approved the merge request' + + def approved_by + object.approved_by_users + end + def diff_stats(path: nil) stats = Array.wrap(object.diff_stats&.to_a) diff --git a/app/graphql/types/package_type_enum.rb b/app/graphql/types/package_type_enum.rb index bc03b8f5f8b..fe6e4d80a1a 100644 --- a/app/graphql/types/package_type_enum.rb +++ b/app/graphql/types/package_type_enum.rb @@ -3,7 +3,7 @@ module Types class PackageTypeEnum < BaseEnum ::Packages::Package.package_types.keys.each do |package_type| - value package_type.to_s.upcase, "Packages from the #{package_type} package manager", value: package_type.to_s + value package_type.to_s.upcase, "Packages from the #{package_type.capitalize} package manager", value: package_type.to_s end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 6b4a71d4e28..21c3ab3eebe 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -782,6 +782,11 @@ module Ci end end + def has_expired_locked_archive_artifacts? + locked_artifacts? && + artifacts_expire_at.present? && artifacts_expire_at < Time.current + end + def has_expiring_archive_artifacts? has_expiring_artifacts? && job_artifacts_archive.present? end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index f9a55fa9157..c9e05b45dbd 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -27,6 +27,13 @@ module Ci sha_attribute :source_sha sha_attribute :target_sha + # Ci::CreatePipelineService returns Ci::Pipeline so this is the only place + # where we can pass additional information from the service. This accessor + # is used for storing the processed CI YAML contents for linting purposes. + # There is an open issue to address this: + # https://gitlab.com/gitlab-org/gitlab/-/issues/259010 + attr_accessor :merged_yaml + belongs_to :project, inverse_of: :all_pipelines belongs_to :user belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' diff --git a/app/models/packages/event.rb b/app/models/packages/event.rb index 730ce267273..42b92f3d671 100644 --- a/app/models/packages/event.rb +++ b/app/models/packages/event.rb @@ -4,7 +4,7 @@ class Packages::Event < ApplicationRecord belongs_to :package, optional: true # FIXME: Remove debian: 9 from here when it's added to the types in package.rb model - EVENT_SCOPES = ::Packages::Package.package_types.merge(debian: 9, container: 1000, tag: 1001).freeze + EVENT_SCOPES = ::Packages::Package.package_types.merge(container: 1000, tag: 1001).freeze enum event_scope: EVENT_SCOPES diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index caf2522e3dd..64f79b0ad0e 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -47,7 +47,7 @@ class Packages::Package < ApplicationRecord format: { with: Gitlab::Regex.generic_package_version_regex }, if: :generic? - enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6, generic: 7, golang: 8 } + enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6, generic: 7, golang: 8, debian: 9 } scope :with_name, ->(name) { where(name: name) } scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) } diff --git a/app/presenters/packages/pypi/package_presenter.rb b/app/presenters/packages/pypi/package_presenter.rb index 4192e974645..1cb11c7be1a 100644 --- a/app/presenters/packages/pypi/package_presenter.rb +++ b/app/presenters/packages/pypi/package_presenter.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Display package version data acording to PyPi +# Display package version data acording to PyPI # Simple API: https://warehouse.pypa.io/api-reference/legacy/#simple-project-api module Packages module Pypi @@ -12,7 +12,7 @@ module Packages @project = project end - # Returns the HTML body for PyPi simple API. + # Returns the HTML body for PyPI simple API. # Basically a list of package download links for a specific # package def body diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 2b8522539b4..6da3261a4a1 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -35,7 +35,7 @@ class BuildDetailsEntity < JobEntity browse_project_job_artifacts_path(project, build) end - expose :keep_path, if: -> (*) { (build.locked_artifacts? || build.has_expiring_archive_artifacts?) && can?(current_user, :update_build, build) } do |build| + expose :keep_path, if: -> (*) { (build.has_expired_locked_archive_artifacts? || build.has_expiring_archive_artifacts?) && can?(current_user, :update_build, build) } do |build| keep_project_job_artifacts_path(project, build) end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index 64958b06f1d..2d278f0e30d 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -82,7 +82,9 @@ class PipelineEntity < Grape::Entity end expose :failed_builds, if: -> (*) { can_retry? }, using: JobEntity do |pipeline| - pipeline.failed_builds + pipeline.failed_builds.each do |build| + build.project = pipeline.project + end end private diff --git a/app/services/ci/build_report_result_service.rb b/app/services/ci/build_report_result_service.rb index ca66ad8249d..3fc070f5d96 100644 --- a/app/services/ci/build_report_result_service.rb +++ b/app/services/ci/build_report_result_service.rb @@ -2,12 +2,20 @@ module Ci class BuildReportResultService + include Gitlab::Utils::UsageData + + EVENT_NAME = 'i_testing_test_case_parsed' + def execute(build) return unless build.has_test_reports? + test_suite = generate_test_suite_report(build) + + track_test_cases(build, test_suite) + build.report_results.create!( project_id: build.project_id, - data: tests_params(build) + data: tests_params(test_suite) ) end @@ -17,9 +25,7 @@ module Ci build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) end - def tests_params(build) - test_suite = generate_test_suite_report(build) - + def tests_params(test_suite) { tests: { name: test_suite.name, @@ -31,5 +37,20 @@ module Ci } } end + + def track_test_cases(build, test_suite) + return if Feature.disabled?(:track_unique_test_cases_parsed, build.project) + + track_usage_event(EVENT_NAME, test_case_hashes(build, test_suite)) + end + + def test_case_hashes(build, test_suite) + [].tap do |hashes| + test_suite.each_test_case do |test_case| + key = "#{build.project_id}-#{test_suite.name}-#{test_case.key}" + hashes << Digest::SHA256.hexdigest(key) + end + end + end end end diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb index 784bd6b9699..7a73af0a81a 100644 --- a/app/services/system_notes/issuables_service.rb +++ b/app/services/system_notes/issuables_service.rb @@ -13,6 +13,8 @@ module SystemNotes def relate_issue(noteable_ref) body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}" + issue_activity_counter.track_issue_related_action(author: author) if noteable.is_a?(Issue) + create_note(NoteSummary.new(noteable, project, author, body, action: 'relate')) end @@ -27,6 +29,8 @@ module SystemNotes def unrelate_issue(noteable_ref) body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}" + issue_activity_counter.track_issue_unrelated_action(author: author) if noteable.is_a?(Issue) + create_note(NoteSummary.new(noteable, project, author, body, action: 'unrelate')) end @@ -174,6 +178,8 @@ module SystemNotes if noteable.is_a?(ExternalIssue) noteable.project.external_issue_tracker.create_cross_reference_note(noteable, mentioner, author) else + issue_activity_counter.track_issue_cross_referenced_action(author: author) if noteable.is_a?(Issue) + create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference')) end end @@ -208,6 +214,8 @@ module SystemNotes status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE body = "marked the task **#{new_task.source}** as #{status_label}" + issue_activity_counter.track_issue_description_changed_action(author: author) if noteable.is_a?(Issue) + create_note(NoteSummary.new(noteable, project, author, body, action: 'task')) end @@ -229,6 +237,8 @@ module SystemNotes cross_reference = noteable_ref.to_reference(project) body = "moved #{direction} #{cross_reference}" + issue_activity_counter.track_issue_moved_action(author: author) if noteable.is_a?(Issue) + create_note(NoteSummary.new(noteable, project, author, body, action: 'moved')) end @@ -299,6 +309,9 @@ module SystemNotes # Returns the created Note object def mark_duplicate_issue(canonical_issue) body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" + + issue_activity_counter.track_issue_marked_as_duplicate_action(author: author) if noteable.is_a?(Issue) + create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) end @@ -322,6 +335,14 @@ module SystemNotes action = noteable.discussion_locked? ? 'locked' : 'unlocked' body = "#{action} this #{noteable.class.to_s.titleize.downcase}" + if noteable.is_a?(Issue) + if action == 'locked' + issue_activity_counter.track_issue_locked_action(author: author) + else + issue_activity_counter.track_issue_unlocked_action(author: author) + end + end + create_note(NoteSummary.new(noteable, project, author, body, action: action)) end diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 1eb3a14525f..86474ea699e 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -122,7 +122,7 @@ = f.submit s_("Profiles|Update profile settings"), class: 'btn btn-success' = link_to _("Cancel"), user_path(current_user), class: 'btn btn-cancel' -.modal.modal-profile-crop +.modal.modal-profile-crop{ data: { cropper_css_path: ActionController::Base.helpers.stylesheet_path('lazy_bundles/cropper.css') } } .modal-dialog .modal-content .modal-header diff --git a/changelogs/unreleased/219951-improve-styling-of-design-pins.yml b/changelogs/unreleased/219951-improve-styling-of-design-pins.yml new file mode 100644 index 00000000000..e81e2e7409b --- /dev/null +++ b/changelogs/unreleased/219951-improve-styling-of-design-pins.yml @@ -0,0 +1,5 @@ +--- +title: Update styling of design comment pins +merge_request: 39797 +author: +type: changed diff --git a/changelogs/unreleased/229918-track-additional-issue-change-events.yml b/changelogs/unreleased/229918-track-additional-issue-change-events.yml new file mode 100644 index 00000000000..0e34439be06 --- /dev/null +++ b/changelogs/unreleased/229918-track-additional-issue-change-events.yml @@ -0,0 +1,5 @@ +--- +title: Add more issue change events to usage ping +merge_request: 43828 +author: +type: other diff --git a/changelogs/unreleased/231467-reduce-cached-query-jobscontroller-show.yml b/changelogs/unreleased/231467-reduce-cached-query-jobscontroller-show.yml new file mode 100644 index 00000000000..8a6e84f3a0b --- /dev/null +++ b/changelogs/unreleased/231467-reduce-cached-query-jobscontroller-show.yml @@ -0,0 +1,5 @@ +--- +title: Reduce cached SQL for JobsController#show +merge_request: 43559 +author: +type: performance diff --git a/changelogs/unreleased/242285-approve-graphql-ce.yml b/changelogs/unreleased/242285-approve-graphql-ce.yml new file mode 100644 index 00000000000..12415795839 --- /dev/null +++ b/changelogs/unreleased/242285-approve-graphql-ce.yml @@ -0,0 +1,5 @@ +--- +title: Allow get approvals on merge request by GraphQL in CE +author: Pavel Kuznetsov +merge_request: 43325 +type: add diff --git a/changelogs/unreleased/251113-create_framework_model.yml b/changelogs/unreleased/251113-create_framework_model.yml new file mode 100644 index 00000000000..7db07459e9e --- /dev/null +++ b/changelogs/unreleased/251113-create_framework_model.yml @@ -0,0 +1,5 @@ +--- +title: Create ComplianceManagement::Framework Model +merge_request: 43301 +author: +type: changed diff --git a/changelogs/unreleased/251935-add-default_branch_name-column-to-namespace_settings-table.yml b/changelogs/unreleased/251935-add-default_branch_name-column-to-namespace_settings-table.yml new file mode 100644 index 00000000000..6a6139c3c45 --- /dev/null +++ b/changelogs/unreleased/251935-add-default_branch_name-column-to-namespace_settings-table.yml @@ -0,0 +1,5 @@ +--- +title: Add :default_branch_name column to namespace_settings +merge_request: 42778 +author: +type: added diff --git a/changelogs/unreleased/eb-unit-tests-parsed-usage-ping.yml b/changelogs/unreleased/eb-unit-tests-parsed-usage-ping.yml new file mode 100644 index 00000000000..0ac94f0cafb --- /dev/null +++ b/changelogs/unreleased/eb-unit-tests-parsed-usage-ping.yml @@ -0,0 +1,5 @@ +--- +title: Track unique number of test cases parsed +merge_request: 41918 +author: +type: added diff --git a/changelogs/unreleased/fix-spelling-of-pypi.yml b/changelogs/unreleased/fix-spelling-of-pypi.yml new file mode 100644 index 00000000000..3101367cbc6 --- /dev/null +++ b/changelogs/unreleased/fix-spelling-of-pypi.yml @@ -0,0 +1,5 @@ +--- +title: Fix spelling of PyPI +merge_request: 44058 +author: Peter Bittner (@bittner) +type: other diff --git a/changelogs/unreleased/mc-feature-fix-keep-path-artifact-locking.yml b/changelogs/unreleased/mc-feature-fix-keep-path-artifact-locking.yml new file mode 100644 index 00000000000..bdd19afb0b7 --- /dev/null +++ b/changelogs/unreleased/mc-feature-fix-keep-path-artifact-locking.yml @@ -0,0 +1,5 @@ +--- +title: Show keep path for expired locked artifacts. +merge_request: 43866 +author: +type: changed diff --git a/changelogs/unreleased/mc-feature-project-ci-lint-api.yml b/changelogs/unreleased/mc-feature-project-ci-lint-api.yml new file mode 100644 index 00000000000..db24843e65f --- /dev/null +++ b/changelogs/unreleased/mc-feature-project-ci-lint-api.yml @@ -0,0 +1,5 @@ +--- +title: Add project scoped CI lint API endpoint. +merge_request: 42998 +author: +type: added diff --git a/changelogs/unreleased/tz-lazy-load-cropper.yml b/changelogs/unreleased/tz-lazy-load-cropper.yml new file mode 100644 index 00000000000..41f9e01940a --- /dev/null +++ b/changelogs/unreleased/tz-lazy-load-cropper.yml @@ -0,0 +1,5 @@ +--- +title: Loads cropper css only when needed +merge_request: 44137 +author: +type: performance diff --git a/config/application.rb b/config/application.rb index f5d7b2600ba..1d9dfd256de 100644 --- a/config/application.rb +++ b/config/application.rb @@ -194,6 +194,7 @@ module Gitlab config.assets.precompile << "page_bundles/milestone.css" config.assets.precompile << "page_bundles/todos.css" config.assets.precompile << "page_bundles/xterm.css" + config.assets.precompile << "lazy_bundles/cropper.css" config.assets.precompile << "performance_bar.css" config.assets.precompile << "lib/ace.js" config.assets.precompile << "disable_animations.css" diff --git a/config/feature_flags/development/track_unique_test_cases_parsed.yml b/config/feature_flags/development/track_unique_test_cases_parsed.yml new file mode 100644 index 00000000000..98ae38e1cb0 --- /dev/null +++ b/config/feature_flags/development/track_unique_test_cases_parsed.yml @@ -0,0 +1,7 @@ +--- +name: track_unique_test_cases_parsed +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41918 +rollout_issue_url: +group: group::testing +type: development +default_enabled: false diff --git a/config/feature_flags/development/usage_data_i_testing_test_case_parsed.yml b/config/feature_flags/development/usage_data_i_testing_test_case_parsed.yml new file mode 100644 index 00000000000..095010da56b --- /dev/null +++ b/config/feature_flags/development/usage_data_i_testing_test_case_parsed.yml @@ -0,0 +1,7 @@ +--- +name: usage_data_i_testing_test_case_parsed +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41918 +rollout_issue_url: +group: group::testing +type: development +default_enabled: true diff --git a/db/migrate/20200919200318_add_default_branch_name_to_namespace_settings.rb b/db/migrate/20200919200318_add_default_branch_name_to_namespace_settings.rb new file mode 100644 index 00000000000..c8c856c7533 --- /dev/null +++ b/db/migrate/20200919200318_add_default_branch_name_to_namespace_settings.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddDefaultBranchNameToNamespaceSettings < ActiveRecord::Migration[6.0] + DOWNTIME = false + + # rubocop:disable Migration/AddLimitToTextColumns + + # limit is added in 20200919204155_add_text_limit_to_namespace_settings_default_branch_name + # + def change + add_column :namespace_settings, :default_branch_name, :text + end + + # rubocop:enable Migration/AddLimitToTextColumns +end diff --git a/db/migrate/20200919204155_add_text_limit_to_namespace_settings_default_branch_name.rb b/db/migrate/20200919204155_add_text_limit_to_namespace_settings_default_branch_name.rb new file mode 100644 index 00000000000..174a1a9c556 --- /dev/null +++ b/db/migrate/20200919204155_add_text_limit_to_namespace_settings_default_branch_name.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddTextLimitToNamespaceSettingsDefaultBranchName < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_text_limit :namespace_settings, :default_branch_name, 255 + end + + def down + # Down is required as `add_text_limit` is not reversible + # + remove_text_limit :namespace_settings, :default_branch_name + end +end diff --git a/db/migrate/20200922075244_add_compliance_framework_model.rb b/db/migrate/20200922075244_add_compliance_framework_model.rb new file mode 100644 index 00000000000..376482d9005 --- /dev/null +++ b/db/migrate/20200922075244_add_compliance_framework_model.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class AddComplianceFrameworkModel < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless table_exists?(:compliance_management_frameworks) + with_lock_retries do + create_table :compliance_management_frameworks do |t| + t.references :group, foreign_key: { to_table: :namespaces, on_delete: :cascade }, null: false, index: false + t.text :name, null: false + t.text :description, null: false + t.text :color, null: false + t.index [:group_id, :name], unique: true + end + end + end + + add_text_limit :compliance_management_frameworks, :name, 255 + add_text_limit :compliance_management_frameworks, :description, 255 + add_text_limit :compliance_management_frameworks, :color, 10 + end + + def down + with_lock_retries do + drop_table :compliance_management_frameworks + end + end +end diff --git a/db/schema_migrations/20200919200318 b/db/schema_migrations/20200919200318 new file mode 100644 index 00000000000..29721a3ef2b --- /dev/null +++ b/db/schema_migrations/20200919200318 @@ -0,0 +1 @@ +f33c66297ca7848c576778dc275e42801f5f9d7cdcf8c4d2fb205d4eb9770937
\ No newline at end of file diff --git a/db/schema_migrations/20200919204155 b/db/schema_migrations/20200919204155 new file mode 100644 index 00000000000..39b608deed4 --- /dev/null +++ b/db/schema_migrations/20200919204155 @@ -0,0 +1 @@ +097cb7a36fdc831045f3a33a047f8bda6474b8165ef5fc95dbdeea45d0ac04a3
\ No newline at end of file diff --git a/db/schema_migrations/20200922075244 b/db/schema_migrations/20200922075244 new file mode 100644 index 00000000000..82a2fc5f304 --- /dev/null +++ b/db/schema_migrations/20200922075244 @@ -0,0 +1 @@ +a8450c6c21b1182afd06c88af18c0d9be92b0e7fdc73505c07d4ab3fddd39abf
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index bde64076670..cc330feeab1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11087,6 +11087,26 @@ CREATE SEQUENCE commit_user_mentions_id_seq ALTER SEQUENCE commit_user_mentions_id_seq OWNED BY commit_user_mentions.id; +CREATE TABLE compliance_management_frameworks ( + id bigint NOT NULL, + group_id bigint NOT NULL, + name text NOT NULL, + description text NOT NULL, + color text NOT NULL, + CONSTRAINT check_08cd34b2c2 CHECK ((char_length(color) <= 10)), + CONSTRAINT check_1617e0b87e CHECK ((char_length(description) <= 255)), + CONSTRAINT check_ab00bc2193 CHECK ((char_length(name) <= 255)) +); + +CREATE SEQUENCE compliance_management_frameworks_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE compliance_management_frameworks_id_seq OWNED BY compliance_management_frameworks.id; + CREATE TABLE container_expiration_policies ( project_id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -13609,7 +13629,9 @@ CREATE TABLE namespace_settings ( updated_at timestamp with time zone NOT NULL, namespace_id integer NOT NULL, prevent_forking_outside_group boolean DEFAULT false NOT NULL, - allow_mfa_for_subgroups boolean DEFAULT true NOT NULL + allow_mfa_for_subgroups boolean DEFAULT true NOT NULL, + default_branch_name text, + CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)) ); CREATE TABLE namespace_statistics ( @@ -17344,6 +17366,8 @@ ALTER TABLE ONLY clusters_kubernetes_namespaces ALTER COLUMN id SET DEFAULT next ALTER TABLE ONLY commit_user_mentions ALTER COLUMN id SET DEFAULT nextval('commit_user_mentions_id_seq'::regclass); +ALTER TABLE ONLY compliance_management_frameworks ALTER COLUMN id SET DEFAULT nextval('compliance_management_frameworks_id_seq'::regclass); + ALTER TABLE ONLY container_repositories ALTER COLUMN id SET DEFAULT nextval('container_repositories_id_seq'::regclass); ALTER TABLE ONLY conversational_development_index_metrics ALTER COLUMN id SET DEFAULT nextval('conversational_development_index_metrics_id_seq'::regclass); @@ -18377,6 +18401,9 @@ ALTER TABLE ONLY clusters ALTER TABLE ONLY commit_user_mentions ADD CONSTRAINT commit_user_mentions_pkey PRIMARY KEY (id); +ALTER TABLE ONLY compliance_management_frameworks + ADD CONSTRAINT compliance_management_frameworks_pkey PRIMARY KEY (id); + ALTER TABLE ONLY container_expiration_policies ADD CONSTRAINT container_expiration_policies_pkey PRIMARY KEY (project_id); @@ -19445,6 +19472,8 @@ CREATE UNIQUE INDEX idx_jira_connect_subscriptions_on_installation_id_namespace_ CREATE INDEX idx_members_created_at_user_id_invite_token ON members USING btree (created_at) WHERE ((invite_token IS NOT NULL) AND (user_id IS NULL)); +CREATE INDEX idx_merge_requests_on_id_and_merge_jid ON merge_requests USING btree (id, merge_jid) WHERE ((merge_jid IS NOT NULL) AND (state_id = 4)); + CREATE INDEX idx_merge_requests_on_source_project_and_branch_state_opened ON merge_requests USING btree (source_project_id, source_branch) WHERE (state_id = 1); CREATE INDEX idx_merge_requests_on_state_id_and_merge_status ON merge_requests USING btree (state_id, merge_status) WHERE ((state_id = 1) AND ((merge_status)::text = 'can_be_merged'::text)); @@ -19987,6 +20016,8 @@ CREATE INDEX index_clusters_on_user_id ON clusters USING btree (user_id); CREATE UNIQUE INDEX index_commit_user_mentions_on_note_id ON commit_user_mentions USING btree (note_id); +CREATE UNIQUE INDEX index_compliance_management_frameworks_on_group_id_and_name ON compliance_management_frameworks USING btree (group_id, name); + CREATE INDEX index_container_expiration_policies_on_next_run_at_and_enabled ON container_expiration_policies USING btree (next_run_at, enabled); CREATE INDEX index_container_repositories_on_project_id ON container_repositories USING btree (project_id); @@ -23668,6 +23699,9 @@ ALTER TABLE ONLY requirements_management_test_reports ALTER TABLE ONLY pool_repositories ADD CONSTRAINT fk_rails_d2711daad4 FOREIGN KEY (source_project_id) REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE ONLY compliance_management_frameworks + ADD CONSTRAINT fk_rails_d3240d6339 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY group_group_links ADD CONSTRAINT fk_rails_d3a0488427 FOREIGN KEY (shared_group_id) REFERENCES namespaces(id) ON DELETE CASCADE; diff --git a/doc/administration/geo/replication/datatypes.md b/doc/administration/geo/replication/datatypes.md index 52a2b1521a9..18c788d63bc 100644 --- a/doc/administration/geo/replication/datatypes.md +++ b/doc/administration/geo/replication/datatypes.md @@ -181,7 +181,7 @@ successfully, you must replicate their data using some other means. | [Maven Repository](../../../user/packages/maven_repository/index.md) | **Yes** (13.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default | | [Conan Repository](../../../user/packages/conan_repository/index.md) | **Yes** (13.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default | | [NuGet Repository](../../../user/packages/nuget_repository/index.md) | **Yes** (13.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default | -| [PyPi Repository](../../../user/packages/pypi_repository/index.md) | **Yes** (13.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default | +| [PyPI Repository](../../../user/packages/pypi_repository/index.md) | **Yes** (13.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default | | [Composer Repository](../../../user/packages/composer_repository/index.md) | **Yes** (13.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default | | [Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_terraform_state_version_replication`, enabled by default | | [External merge request diffs](../../merge_request_diffs.md) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/33817) | No | Via Object Storage provider if supported. Native Geo support (Beta). | | diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 750e6aab687..7178db3be6d 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -317,7 +317,7 @@ disable enforcement. For more information, see the documentation on configuring ``` 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). -1. Run `sudo /opt/gitlab/embedded/service/gitlab-shell/bin/check -config /opt/gitlab/embedded/service/gitlab-shell/config.yml` +1. Run `sudo /opt/gitlab/embedded/bin/gitaly-hooks check /var/opt/gitlab/gitaly/config.toml` to confirm that Gitaly can perform callbacks to the GitLab internal API. **For installations from source** @@ -364,7 +364,7 @@ disable enforcement. For more information, see the documentation on configuring ``` 1. Save the files and [restart GitLab](../restart_gitlab.md#installations-from-source). -1. Run `sudo -u git /home/git/gitlab-shell/bin/check -config /home/git/gitlab-shell/config.yml` +1. Run `sudo -u git /home/git/gitaly/gitaly-hooks check /home/git/gitaly/config.toml` to confirm that Gitaly can perform callbacks to the GitLab internal API. ### Configure Gitaly clients @@ -711,6 +711,15 @@ Gitaly Go process. Some examples of things that are implemented in `gitaly-ruby` - RPCs that deal with wikis. - RPCs that create commits on behalf of a user, such as merge commits. +We recommend: + +- At least 300MB memory per worker. +- No more than one worker per core. + +NOTE: **Note:** +`gitaly-ruby` is planned to be eventually removed. To track progress, see the +[Remove the Gitaly-Ruby sidecar](https://gitlab.com/groups/gitlab-org/-/epics/2862) epic. + ### Configure number of `gitaly-ruby` workers `gitaly-ruby` has much less capacity than Gitaly implemented in Go. If your Gitaly server has to handle lots of diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index 45c077cded1..5a96c489083 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -873,10 +873,10 @@ Particular attention should be shown to: gitlab-ctl reconfigure ``` -1. Verify each `gitlab-shell` on each Gitaly node can reach GitLab. On each Gitaly node run: +1. Verify on each Gitaly node the Git Hooks can reach GitLab. On each Gitaly node run: ```shell - /opt/gitlab/embedded/service/gitlab-shell/bin/check -config /opt/gitlab/embedded/service/gitlab-shell/config.yml + /opt/gitlab/embedded/bin/gitaly-hooks check /var/opt/gitlab/gitaly/config.toml ``` 1. Verify that GitLab can reach Praefect: diff --git a/doc/administration/packages/index.md b/doc/administration/packages/index.md index 6b9e9ae3d5f..6d841d47caf 100644 --- a/doc/administration/packages/index.md +++ b/doc/administration/packages/index.md @@ -14,7 +14,7 @@ The Packages feature allows GitLab to act as a repository for the following: | Software repository | Description | Available in GitLab version | | ------------------- | ----------- | --------------------------- | -| [PyPi Repository](../../user/packages/pypi_repository/index.md) | The GitLab PyPi Repository enables every project in GitLab to have its own space to store [PyPi](https://pypi.org/) packages. | 12.10+ | +| [PyPI Repository](../../user/packages/pypi_repository/index.md) | The GitLab PyPI Repository enables every project in GitLab to have its own space to store [PyPI](https://pypi.org/) packages. | 12.10+ | | [Composer Repository](../../user/packages/composer_repository/index.md) | The GitLab Composer Repository enables every project in GitLab to have its own space to store [Composer](https://getcomposer.org/) packages. | 13.1+ | | [NuGet Repository](../../user/packages/nuget_repository/index.md) | The GitLab NuGet Repository enables every project in GitLab to have its own space to store [NuGet](https://www.nuget.org/) packages. | 12.8+ | | [Conan Repository](../../user/packages/conan_repository/index.md) | The GitLab Conan Repository enables every project in GitLab to have its own space to store [Conan](https://conan.io/) packages. | 12.4+ | diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md index 2f32cf9fb04..e4ddca4e5f9 100644 --- a/doc/administration/reference_architectures/10k_users.md +++ b/doc/administration/reference_architectures/10k_users.md @@ -1879,7 +1879,7 @@ On each node perform the following: 1. Optionally, from the Gitaly servers, confirm that Gitaly can perform callbacks to the internal API: ```shell - sudo /opt/gitlab/embedded/service/gitlab-shell/bin/check -config /opt/gitlab/embedded/service/gitlab-shell/config.yml + sudo /opt/gitlab/embedded/bin/gitaly-hooks check /var/opt/gitlab/gitaly/config.toml ``` NOTE: **Note:** diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md index 55de38645d7..cdec6c4ee9e 100644 --- a/doc/administration/reference_architectures/25k_users.md +++ b/doc/administration/reference_architectures/25k_users.md @@ -1879,7 +1879,7 @@ On each node perform the following: 1. Optionally, from the Gitaly servers, confirm that Gitaly can perform callbacks to the internal API: ```shell - sudo /opt/gitlab/embedded/service/gitlab-shell/bin/check -config /opt/gitlab/embedded/service/gitlab-shell/config.yml + sudo /opt/gitlab/embedded/bin/gitaly-hooks check /var/opt/gitlab/gitaly/config.toml ``` NOTE: **Note:** diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md index 1c174fc6e03..427815519ab 100644 --- a/doc/administration/reference_architectures/2k_users.md +++ b/doc/administration/reference_architectures/2k_users.md @@ -466,7 +466,7 @@ To configure the Gitaly server: 1. Confirm that Gitaly can perform callbacks to the internal API: ```shell - sudo /opt/gitlab/embedded/service/gitlab-shell/bin/check -config /opt/gitlab/embedded/service/gitlab-shell/config.yml + sudo /opt/gitlab/embedded/bin/gitaly-hooks check /var/opt/gitlab/gitaly/config.toml ``` ### Gitaly TLS support diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md index f69e19ffef8..2a4156fee6b 100644 --- a/doc/administration/reference_architectures/3k_users.md +++ b/doc/administration/reference_architectures/3k_users.md @@ -1223,7 +1223,7 @@ On each node: 1. Confirm that Gitaly can perform callbacks to the internal API: ```shell - sudo /opt/gitlab/embedded/service/gitlab-shell/bin/check -config /opt/gitlab/embedded/service/gitlab-shell/config.yml + sudo /opt/gitlab/embedded/bin/gitaly-hooks check /var/opt/gitlab/gitaly/config.toml ``` 1. Verify the GitLab services are running: diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md index 7ea571108fb..126725421d4 100644 --- a/doc/administration/reference_architectures/50k_users.md +++ b/doc/administration/reference_architectures/50k_users.md @@ -1879,7 +1879,7 @@ On each node perform the following: 1. Optionally, from the Gitaly servers, confirm that Gitaly can perform callbacks to the internal API: ```shell - sudo /opt/gitlab/embedded/service/gitlab-shell/bin/check -config /opt/gitlab/embedded/service/gitlab-shell/config.yml + sudo /opt/gitlab/embedded/bin/gitaly-hooks check /var/opt/gitlab/gitaly/config.toml ``` NOTE: **Note:** diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md index b32560d7055..960ad2b7368 100644 --- a/doc/administration/reference_architectures/5k_users.md +++ b/doc/administration/reference_architectures/5k_users.md @@ -1222,7 +1222,7 @@ On each node: 1. Confirm that Gitaly can perform callbacks to the internal API: ```shell - sudo /opt/gitlab/embedded/service/gitlab-shell/bin/check -config /opt/gitlab/embedded/service/gitlab-shell/config.yml + sudo /opt/gitlab/embedded/bin/gitaly-hooks check /var/opt/gitlab/gitaly/config.toml ``` 1. Verify the GitLab services are running: diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 84ac8d30660..1dc91105add 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -11619,6 +11619,7 @@ type Mutation { toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload @deprecated(reason: "Use awardEmojiToggle. Deprecated in 13.2") updateAlertStatus(input: UpdateAlertStatusInput!): UpdateAlertStatusPayload updateBoard(input: UpdateBoardInput!): UpdateBoardPayload + updateBoardEpicUserPreferences(input: UpdateBoardEpicUserPreferencesInput!): UpdateBoardEpicUserPreferencesPayload updateBoardList(input: UpdateBoardListInput!): UpdateBoardListPayload updateContainerExpirationPolicy(input: UpdateContainerExpirationPolicyInput!): UpdateContainerExpirationPolicyPayload updateEpic(input: UpdateEpicInput!): UpdateEpicPayload @@ -12257,42 +12258,47 @@ type PackageFileRegistryEdge { enum PackageTypeEnum { """ - Packages from the composer package manager + Packages from the Composer package manager """ COMPOSER """ - Packages from the conan package manager + Packages from the Conan package manager """ CONAN """ - Packages from the generic package manager + Packages from the Debian package manager + """ + DEBIAN + + """ + Packages from the Generic package manager """ GENERIC """ - Packages from the golang package manager + Packages from the Golang package manager """ GOLANG """ - Packages from the maven package manager + Packages from the Maven package manager """ MAVEN """ - Packages from the npm package manager + Packages from the Npm package manager """ NPM """ - Packages from the nuget package manager + Packages from the Nuget package manager """ NUGET """ - Packages from the pypi package manager + Packages from the Pypi package manager """ PYPI } @@ -18471,6 +18477,51 @@ type UpdateAlertStatusPayload { } """ +Autogenerated input type of UpdateBoardEpicUserPreferences +""" +input UpdateBoardEpicUserPreferencesInput { + """ + The board global ID + """ + boardId: BoardID! + + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Whether the epic should be collapsed in the board + """ + collapsed: Boolean! + + """ + ID of an epic to set preferences for + """ + epicId: EpicID! +} + +""" +Autogenerated return type of UpdateBoardEpicUserPreferences +""" +type UpdateBoardEpicUserPreferencesPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + User preferences for the epic in the board after mutation + """ + epicUserPreferences: BoardEpicUserPreferences + + """ + Errors encountered during execution of the mutation. + """ + errors: [String!]! +} + +""" Autogenerated input type of UpdateBoard """ input UpdateBoardInput { diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 15aa205bed3..f245fe4f04b 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -34110,6 +34110,33 @@ "deprecationReason": null }, { + "name": "updateBoardEpicUserPreferences", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateBoardEpicUserPreferencesInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UpdateBoardEpicUserPreferencesPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "updateBoardList", "description": null, "args": [ @@ -36318,49 +36345,55 @@ "enumValues": [ { "name": "MAVEN", - "description": "Packages from the maven package manager", + "description": "Packages from the Maven package manager", "isDeprecated": false, "deprecationReason": null }, { "name": "NPM", - "description": "Packages from the npm package manager", + "description": "Packages from the Npm package manager", "isDeprecated": false, "deprecationReason": null }, { "name": "CONAN", - "description": "Packages from the conan package manager", + "description": "Packages from the Conan package manager", "isDeprecated": false, "deprecationReason": null }, { "name": "NUGET", - "description": "Packages from the nuget package manager", + "description": "Packages from the Nuget package manager", "isDeprecated": false, "deprecationReason": null }, { "name": "PYPI", - "description": "Packages from the pypi package manager", + "description": "Packages from the Pypi package manager", "isDeprecated": false, "deprecationReason": null }, { "name": "COMPOSER", - "description": "Packages from the composer package manager", + "description": "Packages from the Composer package manager", "isDeprecated": false, "deprecationReason": null }, { "name": "GENERIC", - "description": "Packages from the generic package manager", + "description": "Packages from the Generic package manager", "isDeprecated": false, "deprecationReason": null }, { "name": "GOLANG", - "description": "Packages from the golang package manager", + "description": "Packages from the Golang package manager", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "DEBIAN", + "description": "Packages from the Debian package manager", "isDeprecated": false, "deprecationReason": null } @@ -54021,6 +54054,136 @@ }, { "kind": "INPUT_OBJECT", + "name": "UpdateBoardEpicUserPreferencesInput", + "description": "Autogenerated input type of UpdateBoardEpicUserPreferences", + "fields": null, + "inputFields": [ + { + "name": "boardId", + "description": "The board global ID", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "BoardID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "epicId", + "description": "ID of an epic to set preferences for", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "EpicID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "collapsed", + "description": "Whether the epic should be collapsed in the board", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UpdateBoardEpicUserPreferencesPayload", + "description": "Autogenerated return type of UpdateBoardEpicUserPreferences", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "epicUserPreferences", + "description": "User preferences for the epic in the board after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "BoardEpicUserPreferences", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Errors encountered during execution of the mutation.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", "name": "UpdateBoardInput", "description": "Autogenerated input type of UpdateBoard", "fields": null, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 4a3675df15d..350a1b6c0f3 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2623,6 +2623,16 @@ Autogenerated return type of UpdateAlertStatus. | `issue` | Issue | The issue created after mutation | | `todo` | Todo | The todo after mutation | +### UpdateBoardEpicUserPreferencesPayload + +Autogenerated return type of UpdateBoardEpicUserPreferences. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `epicUserPreferences` | BoardEpicUserPreferences | User preferences for the epic in the board after mutation | +| `errors` | String! => Array | Errors encountered during execution of the mutation. | + ### UpdateBoardListPayload Autogenerated return type of UpdateBoardList. @@ -3416,14 +3426,15 @@ Values for sorting projects. | Value | Description | | ----- | ----------- | -| `COMPOSER` | Packages from the composer package manager | -| `CONAN` | Packages from the conan package manager | -| `GENERIC` | Packages from the generic package manager | -| `GOLANG` | Packages from the golang package manager | -| `MAVEN` | Packages from the maven package manager | -| `NPM` | Packages from the npm package manager | -| `NUGET` | Packages from the nuget package manager | -| `PYPI` | Packages from the pypi package manager | +| `COMPOSER` | Packages from the Composer package manager | +| `CONAN` | Packages from the Conan package manager | +| `DEBIAN` | Packages from the Debian package manager | +| `GENERIC` | Packages from the Generic package manager | +| `GOLANG` | Packages from the Golang package manager | +| `MAVEN` | Packages from the Maven package manager | +| `NPM` | Packages from the Npm package manager | +| `NUGET` | Packages from the Nuget package manager | +| `PYPI` | Packages from the Pypi package manager | ### PipelineConfigSourceEnum diff --git a/doc/api/lint.md b/doc/api/lint.md index df7198ac5c6..652a5289f13 100644 --- a/doc/api/lint.md +++ b/doc/api/lint.md @@ -94,3 +94,51 @@ Example response: "merged_config": "---\n:another_test:\n :stage: test\n :script: echo 2\n:test:\n :stage: test\n :script: echo 1\n" } ``` + +## Validate a project's CI configuration + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/231352) in GitLab 13.5. + +Checks if a project's latest (`HEAD` of the project's default branch) +`.gitlab-ci.yml` configuration is valid. This endpoint uses all namespace +specific data available, including variables, local includes, and so on. + +```plaintext +GET /projects/:id/ci/lint +``` + +| Attribute | Type | Required | Description | +| ---------- | ------- | -------- | -------- | +| `dry_run` | boolean | no | Run pipeline creation simulation, or only do static check. | + +Example request: + +```shell +curl "https://gitlab.example.com/api/v4/projects/:id/ci/lint" +``` + +Example responses: + +- Valid config: + +```json +{ + "valid": true, + "merged_yaml": "---\n:test_job:\n :script: echo 1\n", + "errors": [], + "warnings": [] +} +``` + +- Invalid config: + +```json +{ + "valid": false, + "merged_yaml": "---\n:test_job:\n :script: echo 1\n", + "errors": [ + "jobs config should contain at least one visible job" + ], + "warnings": [] +} +``` diff --git a/doc/development/internal_api.md b/doc/development/internal_api.md index 4b46787c2c3..5e74be9e439 100644 --- a/doc/development/internal_api.md +++ b/doc/development/internal_api.md @@ -223,6 +223,7 @@ Example response: - GitLab Geo - GitLab Shell's `bin/check` +- Gitaly ## Get new 2FA recovery codes using an SSH key diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index 54375db1831..b0be9b20c12 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -441,7 +441,7 @@ The following are some available Rake tasks: | [`sudo gitlab-rake gitlab:elastic:index`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Enables Elasticsearch indexing and run `gitlab:elastic:create_empty_index`, `gitlab:elastic:clear_index_status`, `gitlab:elastic:index_projects`, and `gitlab:elastic:index_snippets`. | | [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Iterates over all projects and queues Sidekiq jobs to index them in the background. | | [`sudo gitlab-rake gitlab:elastic:index_projects_status`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Determines the overall status of the indexing. It is done by counting the total number of indexed projects, dividing by a count of the total number of projects, then multiplying by 100. | -| [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Deletes all instances of IndexStatus for all projects. | +| [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Deletes all instances of IndexStatus for all projects. Note that this command will result in a complete wipe of the index, and it should be used with caution. | | [`sudo gitlab-rake gitlab:elastic:create_empty_index[<TARGET_NAME>]`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Generates an empty index and assigns an alias for it on the Elasticsearch side only if it doesn't already exist. | | [`sudo gitlab-rake gitlab:elastic:delete_index[<TARGET_NAME>]`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Removes the GitLab index and alias (if exists) on the Elasticsearch instance. | | [`sudo gitlab-rake gitlab:elastic:recreate_index[<TARGET_NAME>]`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Wrapper task for `gitlab:elastic:delete_index[<TARGET_NAME>]` and `gitlab:elastic:create_empty_index[<TARGET_NAME>]`. | diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index c050931ce3b..efef6136485 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -914,7 +914,7 @@ restore: sudo gitlab-backup restore BACKUP=11493107454_2018_04_25_10.6.4-ce ``` -Users of GitLab 12.1 and earlier should use the command `gitlab-rake gitlab:backup:create` instead. +Users of GitLab 12.1 and earlier should use the command `gitlab-rake gitlab:backup:restore` instead. CAUTION: **Warning:** `gitlab-rake gitlab:backup:restore` doesn't set the correct file system diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md index 7d79da7d79b..e6148a9f892 100644 --- a/doc/user/packages/pypi_repository/index.md +++ b/doc/user/packages/pypi_repository/index.md @@ -4,14 +4,14 @@ group: Package info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers --- -# GitLab PyPi Repository +# GitLab PyPI Repository > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208747) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.10. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3. -With the GitLab PyPi Repository, every project can have its own space to store PyPi packages. +With the GitLab PyPI Repository, every project can have its own space to store PyPI packages. -The GitLab PyPi Repository works with: +The GitLab PyPI Repository works with: - [pip](https://pypi.org/project/pip/) - [twine](https://pypi.org/project/twine/) @@ -20,13 +20,13 @@ The GitLab PyPi Repository works with: You need a recent version of [pip](https://pypi.org/project/pip/) and [twine](https://pypi.org/project/twine/). -## Enabling the PyPi Repository +## Enabling the PyPI Repository NOTE: **Note:** This option is available only if your GitLab administrator has [enabled support for the Package Registry](../../../administration/packages/index.md). -After the PyPi Repository is enabled, it is available for all new projects +After the PyPI Repository is enabled, it is available for all new projects by default. To enable it for existing projects, or if you want to disable it: 1. Navigate to your project's **Settings > General > Visibility, project features, permissions**. @@ -37,8 +37,8 @@ You should then be able to see the **Packages & Registries** section on the left ## Getting started -This section covers creating a new example PyPi package to upload. This is a -quickstart to test out the **GitLab PyPi Registry**. If you already understand how +This section covers creating a new example PyPI package to upload. This is a +quickstart to test out the **GitLab PyPI Registry**. If you already understand how to build and publish your own packages, move on to the [next section](#adding-the-gitlab-pypi-repository-as-a-source). ### Create a project @@ -152,10 +152,10 @@ And confirm your output matches the below: mypypipackage-0.0.1-py3-none-any.whl mypypipackage-0.0.1.tar.gz ``` -Our package is now all set up and ready to be uploaded to the **GitLab PyPi +Our package is now all set up and ready to be uploaded to the **GitLab PyPI Package Registry**. Before we do so, we next need to set up authentication. -## Adding the GitLab PyPi Repository as a source +## Adding the GitLab PyPI Repository as a source ### Authenticating with a personal access token @@ -256,7 +256,7 @@ TWINE_PASSWORD=<personal_access_token or deploy_token> TWINE_USERNAME=<username ``` If you did not follow the guide above, then you need to ensure your package -has been properly built and you [created a PyPi package with `setuptools`](https://packaging.python.org/tutorials/packaging-projects/). +has been properly built and you [created a PyPI package with `setuptools`](https://packaging.python.org/tutorials/packaging-projects/). You can then upload your package using the following command: diff --git a/lib/api/entities/ci/lint/result.rb b/lib/api/entities/ci/lint/result.rb new file mode 100644 index 00000000000..0e4aa238ba2 --- /dev/null +++ b/lib/api/entities/ci/lint/result.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module Lint + class Result < Grape::Entity + expose :valid?, as: :valid + expose :errors + expose :warnings + expose :merged_yaml + end + end + end + end +end diff --git a/lib/api/lint.rb b/lib/api/lint.rb index 51a87f9433c..3f071ef4a91 100644 --- a/lib/api/lint.rb +++ b/lib/api/lint.rb @@ -25,5 +25,24 @@ module API end end end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Validation of .gitlab-ci.yml content' do + detail 'This feature was introduced in GitLab 13.5.' + end + params do + optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check.' + end + get ':id/ci/lint' do + authorize! :download_code, user_project + + content = user_project.repository.gitlab_ci_yml_for(user_project.commit.id, user_project.ci_config_path_or_default) + result = Gitlab::Ci::Lint + .new(project: user_project, current_user: current_user) + .validate(content, dry_run: params[:dry_run]) + + present result, with: Entities::Ci::Lint::Result, current_user: current_user + end + end end end diff --git a/lib/gitlab/ci/lint.rb b/lib/gitlab/ci/lint.rb index 86a9ebfa451..44f2ac23ce3 100644 --- a/lib/gitlab/ci/lint.rb +++ b/lib/gitlab/ci/lint.rb @@ -4,10 +4,11 @@ module Gitlab module Ci class Lint class Result - attr_reader :jobs, :errors, :warnings + attr_reader :jobs, :merged_yaml, :errors, :warnings - def initialize(jobs:, errors:, warnings:) + def initialize(jobs:, merged_yaml:, errors:, warnings:) @jobs = jobs + @merged_yaml = merged_yaml @errors = errors @warnings = warnings end @@ -39,6 +40,7 @@ module Gitlab Result.new( jobs: dry_run_convert_to_jobs(pipeline.stages), + merged_yaml: pipeline.merged_yaml, errors: pipeline.error_messages.map(&:content), warnings: pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content) ) @@ -54,6 +56,7 @@ module Gitlab Result.new( jobs: static_validation_convert_to_jobs(result), + merged_yaml: result.merged_yaml, errors: result.errors, warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord ) diff --git a/lib/gitlab/ci/pipeline/chain/config/process.rb b/lib/gitlab/ci/pipeline/chain/config/process.rb index 8ccb33ffd34..c3fbd0c9e24 100644 --- a/lib/gitlab/ci/pipeline/chain/config/process.rb +++ b/lib/gitlab/ci/pipeline/chain/config/process.rb @@ -28,6 +28,8 @@ module Gitlab error(result.errors.first, config_error: true) end + @pipeline.merged_yaml = result.merged_yaml + rescue => ex Gitlab::ErrorTracking.track_exception(ex, project_id: project.id, diff --git a/lib/gitlab/ci/reports/test_case.rb b/lib/gitlab/ci/reports/test_case.rb index 15a3c862c9e..59c058eeebe 100644 --- a/lib/gitlab/ci/reports/test_case.rb +++ b/lib/gitlab/ci/reports/test_case.rb @@ -23,7 +23,7 @@ module Gitlab @attachment = params.fetch(:attachment, nil) @job = params.fetch(:job, nil) - @key = sanitize_key_name("#{classname}_#{name}") + @key = hash_key("#{classname}_#{name}") end def has_attachment? @@ -42,8 +42,8 @@ module Gitlab private - def sanitize_key_name(key) - key.gsub(/[^0-9A-Za-z]/, '-') + def hash_key(key) + Digest::SHA256.hexdigest(key) end end end diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb index e9b78b841e4..00920dfbd54 100644 --- a/lib/gitlab/ci/reports/test_suite.rb +++ b/lib/gitlab/ci/reports/test_suite.rb @@ -12,18 +12,24 @@ module Gitlab def initialize(name = nil) @name = name @test_cases = {} + @all_test_cases = [] @total_time = 0.0 - @duplicate_cases = [] end def add_test_case(test_case) - @duplicate_cases << test_case if existing_key?(test_case) - @test_cases[test_case.status] ||= {} @test_cases[test_case.status][test_case.key] = test_case @total_time += test_case.execution_time end + def each_test_case + @test_cases.each do |status, test_cases| + test_cases.values.each do |test_case| + yield test_case + end + end + end + # rubocop: disable CodeReuse/ActiveRecord def total_count return 0 if suite_error @@ -86,10 +92,6 @@ module Gitlab private - def existing_key?(test_case) - @test_cases[test_case.status]&.key?(test_case.key) - end - def sort_by_status @test_cases = @test_cases.sort_by { |status, _| Gitlab::Ci::Reports::TestCase::STATUS_TYPES.index(status) }.to_h end diff --git a/lib/gitlab/redis/hll.rb b/lib/gitlab/redis/hll.rb index 496018c88cb..010a6b59da5 100644 --- a/lib/gitlab/redis/hll.rb +++ b/lib/gitlab/redis/hll.rb @@ -3,6 +3,7 @@ module Gitlab module Redis class HLL + BATCH_SIZE = 300 KEY_REGEX = %r{\A(\w|-|:)*\{\w*\}(\w|-|:)*\z}.freeze KeyFormatError = Class.new(StandardError) @@ -29,17 +30,24 @@ module Gitlab # 2020-216-{project_action} # i_{analytics}_dev_ops_score-2020-32 def add(key:, value:, expiry:) - unless KEY_REGEX.match?(key) - raise KeyFormatError.new("Invalid key format. #{key} key should have changeable parts in curly braces. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands") - end + validate_key!(key) Gitlab::Redis::SharedState.with do |redis| redis.multi do |multi| - multi.pfadd(key, value) + Array.wrap(value).each_slice(BATCH_SIZE) { |batch| multi.pfadd(key, batch) } + multi.expire(key, expiry) end end end + + private + + def validate_key!(key) + return if KEY_REGEX.match?(key) + + raise KeyFormatError.new("Invalid key format. #{key} key should have changeable parts in curly braces. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands") + end end end end diff --git a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb index b1ce46a1ff5..c37fbc47276 100644 --- a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb +++ b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb @@ -17,6 +17,13 @@ module Gitlab ISSUE_REOPENED = 'g_project_management_issue_reopened' ISSUE_TITLE_CHANGED = 'g_project_management_issue_title_changed' ISSUE_WEIGHT_CHANGED = 'g_project_management_issue_weight_changed' + ISSUE_CROSS_REFERENCED = 'g_project_management_issue_cross_referenced' + ISSUE_MOVED = 'g_project_management_issue_moved' + ISSUE_RELATED = 'g_project_management_issue_related' + ISSUE_UNRELATED = 'g_project_management_issue_unrelated' + ISSUE_MARKED_AS_DUPLICATE = 'g_project_management_issue_marked_as_duplicate' + ISSUE_LOCKED = 'g_project_management_issue_locked' + ISSUE_UNLOCKED = 'g_project_management_issue_unlocked' class << self def track_issue_created_action(author:, time: Time.zone.now) @@ -67,6 +74,34 @@ module Gitlab track_unique_action(ISSUE_WEIGHT_CHANGED, author, time) end + def track_issue_cross_referenced_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_CROSS_REFERENCED, author, time) + end + + def track_issue_moved_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_MOVED, author, time) + end + + def track_issue_related_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_RELATED, author, time) + end + + def track_issue_unrelated_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_UNRELATED, author, time) + end + + def track_issue_marked_as_duplicate_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_MARKED_AS_DUPLICATE, author, time) + end + + def track_issue_locked_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_LOCKED, author, time) + end + + def track_issue_unlocked_action(author:, time: Time.zone.now) + track_unique_action(ISSUE_UNLOCKED, author, time) + end + private def track_unique_action(action, author, time) diff --git a/lib/gitlab/usage_data_counters/known_events.yml b/lib/gitlab/usage_data_counters/known_events.yml index 9bbf804d87c..d91fa6ba926 100644 --- a/lib/gitlab/usage_data_counters/known_events.yml +++ b/lib/gitlab/usage_data_counters/known_events.yml @@ -185,6 +185,11 @@ redis_slot: incident_management category: incident_management aggregation: weekly +# Testing category +- name: i_testing_test_case_parsed + category: testing + redis_slot: testing + aggregation: weekly # Project Management group - name: g_project_management_issue_title_changed category: issues_edit @@ -234,3 +239,31 @@ category: issues_edit redis_slot: project_management aggregation: daily +- name: g_project_management_issue_cross_referenced + category: issues_edit + redis_slot: project_management + aggregation: daily +- name: g_project_management_issue_moved + category: issues_edit + redis_slot: project_management + aggregation: daily +- name: g_project_management_issue_related + category: issues_edit + redis_slot: project_management + aggregation: daily +- name: g_project_management_issue_unrelated + category: issues_edit + redis_slot: project_management + aggregation: daily +- name: g_project_management_issue_marked_as_duplicate + category: issues_edit + redis_slot: project_management + aggregation: daily +- name: g_project_management_issue_locked + category: issues_edit + redis_slot: project_management + aggregation: daily +- name: g_project_management_issue_unlocked + category: issues_edit + redis_slot: project_management + aggregation: daily diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8fdea2bcb15..a2e425f43a0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18305,7 +18305,7 @@ msgstr "" msgid "PackageRegistry|Published to the %{project} Package Registry %{datetime}" msgstr "" -msgid "PackageRegistry|PyPi" +msgid "PackageRegistry|PyPI" msgstr "" msgid "PackageRegistry|Recipe: %{recipe}" @@ -18389,7 +18389,7 @@ msgstr "" msgid "PackageType|NuGet" msgstr "" -msgid "PackageType|PyPi" +msgid "PackageType|PyPI" msgstr "" msgid "Packages" diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 1a1cfbbd5f6..b8cbe625e5b 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -137,7 +137,7 @@ function run_task() { local ruby_cmd="${1}" local task_runner_pod=$(get_pod "task-runner") - kubectl exec -it --namespace "${namespace}" "${task_runner_pod}" -- gitlab-rails runner "${ruby_cmd}" + kubectl exec --namespace "${namespace}" "${task_runner_pod}" -- gitlab-rails runner "${ruby_cmd}" } function disable_sign_ups() { @@ -150,7 +150,7 @@ function disable_sign_ups() { # Create the root token local ruby_cmd="token = User.find_by_username('root').personal_access_tokens.create(scopes: [:api], name: 'Token to disable sign-ups'); token.set_token('${REVIEW_APPS_ROOT_TOKEN}'); begin; token.save!; rescue(ActiveRecord::RecordNotUnique); end" - run_task "${ruby_cmd}" + retry "run_task \"${ruby_cmd}\"" # Disable sign-ups local signup_enabled=$(retry 'curl --silent --show-error --request PUT --header "PRIVATE-TOKEN: ${REVIEW_APPS_ROOT_TOKEN}" "${CI_ENVIRONMENT_URL}/api/v4/application/settings?signup_enabled=false" | jq ".signup_enabled"') diff --git a/spec/controllers/every_controller_spec.rb b/spec/controllers/every_controller_spec.rb index d333c98ccf7..7a7b17ae1f0 100644 --- a/spec/controllers/every_controller_spec.rb +++ b/spec/controllers/every_controller_spec.rb @@ -24,19 +24,19 @@ RSpec.describe "Every controller" do let_it_be(:routes_without_category) do controller_actions.map do |controller, action| next if controller.feature_category_for_action(action) - next unless controller.to_s.start_with?('B', 'C', 'D', 'E', 'F', 'Projects::MergeRequestsController') + + next unless controller.to_s.start_with?('B', 'C', 'D', 'E', 'F', + 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z', + 'Projects::MergeRequestsController') "#{controller}##{action}" end.compact end it "has feature categories" do - routes_without_category.map { |x| x.split('#') }.group_by(&:first).each do |controller, actions| - puts controller - puts actions.map { |x| ":#{x.last}" }.sort.join(', ') - puts '' - end - expect(routes_without_category).to be_empty, "#{routes_without_category} did not have a category" end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index d7c22d46e90..eff98ab65a6 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -197,16 +197,40 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do context 'with not expiry date' do let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } - it 'exposes needed information' do - get_show_json + context 'when artifacts are unlocked' do + before do + job.pipeline.unlocked! + end - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('job/job_details') - expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) - expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) - expect(json_response['artifact']).not_to have_key('keep_path') - expect(json_response['artifact']).not_to have_key('expired') - expect(json_response['artifact']).not_to have_key('expired_at') + it 'exposes needed information' do + get_show_json + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) + expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) + expect(json_response['artifact']).not_to have_key('keep_path') + expect(json_response['artifact']).not_to have_key('expired') + expect(json_response['artifact']).not_to have_key('expired_at') + end + end + + context 'when artifacts are locked' do + before do + job.pipeline.artifacts_locked! + end + + it 'exposes needed information' do + get_show_json + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) + expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) + expect(json_response['artifact']).not_to have_key('keep_path') + expect(json_response['artifact']).not_to have_key('expired') + expect(json_response['artifact']).not_to have_key('expired_at') + end end end diff --git a/spec/factories/packages.rb b/spec/factories/packages.rb index 3562f57654e..e2c5b000988 100644 --- a/spec/factories/packages.rb +++ b/spec/factories/packages.rb @@ -21,6 +21,10 @@ FactoryBot.define do end end + factory :debian_package do + package_type { :debian } + end + factory :npm_package do sequence(:name) { |n| "@#{project.root_namespace.path}/package-#{n}"} version { '1.0.0' } diff --git a/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap index 62a0f675cff..ed8ed3254ba 100644 --- a/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap +++ b/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap @@ -1,23 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Design note pin component should match the snapshot of note when repositioning 1`] = ` -<button - aria-label="Comment form position" - class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 btn-transparent comment-indicator" - style="left: 10px; top: 10px; cursor: move;" - type="button" -> - <gl-icon-stub - name="image-comment-dark" - size="24" - /> -</button> -`; - exports[`Design note pin component should match the snapshot of note with index 1`] = ` <button aria-label="Comment '1' position" - class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 js-image-badge badge badge-pill" + class="gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-font-lg gl-outline-0! js-image-badge badge badge-pill" style="left: 10px; top: 10px;" type="button" > @@ -30,7 +16,7 @@ exports[`Design note pin component should match the snapshot of note with index exports[`Design note pin component should match the snapshot of note without index 1`] = ` <button aria-label="Comment form position" - class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 btn-transparent comment-indicator" + class="gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-font-lg gl-outline-0! btn-transparent comment-indicator gl-p-0" style="left: 10px; top: 10px;" type="button" > diff --git a/spec/frontend/design_management/components/design_note_pin_spec.js b/spec/frontend/design_management/components/design_note_pin_spec.js index 4e045b58a35..a6219923aca 100644 --- a/spec/frontend/design_management/components/design_note_pin_spec.js +++ b/spec/frontend/design_management/components/design_note_pin_spec.js @@ -29,21 +29,4 @@ describe('Design note pin component', () => { createComponent({ label: 1 }); expect(wrapper.element).toMatchSnapshot(); }); - - it('should match the snapshot of note when repositioning', () => { - createComponent({ repositioning: true }); - expect(wrapper.element).toMatchSnapshot(); - }); - - describe('pinStyle', () => { - it('sets cursor to `move` when repositioning = true', () => { - createComponent({ repositioning: true }); - expect(wrapper.vm.pinStyle.cursor).toBe('move'); - }); - - it('does not set cursor when repositioning = false', () => { - createComponent(); - expect(wrapper.vm.pinStyle.cursor).toBe(undefined); - }); - }); }); diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js index 673a09320e5..4ef067e3f5e 100644 --- a/spec/frontend/design_management/components/design_overlay_spec.js +++ b/spec/frontend/design_management/components/design_overlay_spec.js @@ -202,7 +202,7 @@ describe('Design overlay component', () => { { x: position.x, y: position.y }, { x: 20, y: 20 }, ).then(() => { - expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 20px; cursor: move;'); + expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 20px;'); }); }); @@ -300,9 +300,7 @@ describe('Design overlay component', () => { { x: position.x, y: position.y }, { x: 20, y: 20 }, ).then(() => { - expect(findCommentBadge().attributes().style).toBe( - 'left: 20px; top: 20px; cursor: move;', - ); + expect(findCommentBadge().attributes().style).toBe('left: 20px; top: 20px;'); }); }); diff --git a/spec/frontend/packages/details/store/getters_spec.js b/spec/frontend/packages/details/store/getters_spec.js index 06e5950eb5d..cb164a426c8 100644 --- a/spec/frontend/packages/details/store/getters_spec.js +++ b/spec/frontend/packages/details/store/getters_spec.js @@ -101,7 +101,7 @@ describe('Getters PackageDetails Store', () => { ${packageWithoutBuildInfo} | ${'Maven'} ${npmPackage} | ${'NPM'} ${nugetPackage} | ${'NuGet'} - ${pypiPackage} | ${'PyPi'} + ${pypiPackage} | ${'PyPI'} `(`package type`, ({ packageEntity, expectedResult }) => { beforeEach(() => setupState({ packageEntity })); diff --git a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap index 794e583a487..9a52531ae8d 100644 --- a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap +++ b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap @@ -385,7 +385,7 @@ exports[`packages_list_app renders 1`] = ` </b-tab-stub> <b-tab-stub tag="div" - title="PyPi" + title="PyPI" titlelinkclass="gl-tab-nav-item" > <template> @@ -400,7 +400,7 @@ exports[`packages_list_app renders 1`] = ` class="svg-250 svg-content" > <img - alt="There are no PyPi packages yet" + alt="There are no PyPI packages yet" class="gl-max-w-full" src="helpSvg" /> @@ -416,7 +416,7 @@ exports[`packages_list_app renders 1`] = ` <h1 class="h4" > - There are no PyPi packages yet + There are no PyPI packages yet </h1> <p> diff --git a/spec/frontend/packages/shared/utils_spec.js b/spec/frontend/packages/shared/utils_spec.js index 1fe90a4827f..3e4ce8eb323 100644 --- a/spec/frontend/packages/shared/utils_spec.js +++ b/spec/frontend/packages/shared/utils_spec.js @@ -37,7 +37,7 @@ describe('Packages shared utils', () => { ${'maven'} | ${'Maven'} ${'npm'} | ${'NPM'} ${'nuget'} | ${'NuGet'} - ${'pypi'} | ${'PyPi'} + ${'pypi'} | ${'PyPI'} ${'composer'} | ${'Composer'} ${'foo'} | ${null} `(`package type`, ({ packageType, expectedResult }) => { diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb index 46aebbdabeb..1bad9f8c627 100644 --- a/spec/graphql/types/merge_request_type_spec.rb +++ b/spec/graphql/types/merge_request_type_spec.rb @@ -27,14 +27,13 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do upvotes downvotes head_pipeline pipelines task_completion_status milestone assignees participants subscribed labels discussion_locked time_estimate total_time_spent reference author merged_at commit_count current_user_todos - conflicts auto_merge_enabled + conflicts auto_merge_enabled approved_by ] if Gitlab.ee? expected_fields << 'approved' expected_fields << 'approvals_left' expected_fields << 'approvals_required' - expected_fields << 'approved_by' end expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/package_type_enum_spec.rb b/spec/graphql/types/package_type_enum_spec.rb index 638f8ccbaee..407d5786f65 100644 --- a/spec/graphql/types/package_type_enum_spec.rb +++ b/spec/graphql/types/package_type_enum_spec.rb @@ -4,6 +4,6 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['PackageTypeEnum'] do it 'exposes all package types' do - expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG]) + expect(described_class.values.keys).to contain_exactly(*%w[MAVEN NPM CONAN NUGET PYPI COMPOSER GENERIC GOLANG DEBIAN]) end end diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index 077c0fd3162..c67f8464123 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Lint do - let_it_be(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } let(:lint) { described_class.new(project: project, current_user: user) } @@ -61,6 +61,43 @@ RSpec.describe Gitlab::Ci::Lint do end end + shared_examples 'sets merged yaml' do + let(:content) do + <<~YAML + :include: + :local: another-gitlab-ci.yml + :test_job: + :stage: test + :script: echo + YAML + end + + let(:included_content) do + <<~YAML + :another_job: + :script: echo + YAML + end + + before do + project.repository.create_file( + project.creator, + 'another-gitlab-ci.yml', + included_content, + message: 'Automatically created another-gitlab-ci.yml', + branch_name: 'master' + ) + end + + it 'sets merged_config' do + root_config = YAML.safe_load(content, [Symbol]) + included_config = YAML.safe_load(included_content, [Symbol]) + expected_config = included_config.merge(root_config).except(:include) + + expect(subject.merged_yaml).to eq(expected_config.to_yaml) + end + end + shared_examples 'content with errors and warnings' do context 'when content has errors' do let(:content) do @@ -173,6 +210,8 @@ RSpec.describe Gitlab::Ci::Lint do end end + it_behaves_like 'sets merged yaml' + include_context 'advanced validations' do it 'does not catch advanced logical errors' do expect(subject).to be_valid @@ -203,6 +242,8 @@ RSpec.describe Gitlab::Ci::Lint do end end + it_behaves_like 'sets merged yaml' + include_context 'advanced validations' do it 'runs advanced logical validations' do expect(subject).not_to be_valid diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb index 15fa78444e5..50d1595da73 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb @@ -229,6 +229,20 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do end end + describe '#each_test_case' do + before do + test_suite.add_test_case(test_case_success) + test_suite.add_test_case(test_case_failed) + test_suite.add_test_case(test_case_skipped) + test_suite.add_test_case(test_case_error) + end + + it 'yields each test case to given block' do + expect { |b| test_suite.each_test_case(&b) } + .to yield_successive_args(test_case_success, test_case_failed, test_case_skipped, test_case_error) + end + end + Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type| describe "##{status_type}_count" do subject { test_suite.public_send("#{status_type}_count") } diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index f2bc6390032..37349c30224 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -3,18 +3,16 @@ require 'spec_helper' RSpec.describe Gitlab::ClosingIssueExtractor do - let(:project) { create(:project) } - let(:project2) { create(:project) } - let(:forked_project) { Projects::ForkService.new(project, project2.creator).execute } - let(:issue) { create(:issue, project: project) } - let(:issue2) { create(:issue, project: project2) } + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:project2) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:issue2) { create(:issue, project: project2) } let(:reference) { issue.to_reference } let(:cross_reference) { issue2.to_reference(project) } - let(:fork_cross_reference) { issue.to_reference(forked_project) } subject { described_class.new(project, project.creator) } - before do + before_all do project.add_developer(project.creator) project.add_developer(project2.creator) project2.add_maintainer(project.creator) @@ -325,6 +323,9 @@ RSpec.describe Gitlab::ClosingIssueExtractor do end context "with a cross-project fork reference" do + let(:forked_project) { Projects::ForkService.new(project, project2.creator).execute } + let(:fork_cross_reference) { issue.to_reference(forked_project) } + subject { described_class.new(forked_project, forked_project.creator) } it do @@ -348,8 +349,8 @@ RSpec.describe Gitlab::ClosingIssueExtractor do end context 'with multiple references' do - let(:other_issue) { create(:issue, project: project) } - let(:third_issue) { create(:issue, project: project) } + let_it_be(:other_issue) { create(:issue, project: project) } + let_it_be(:third_issue) { create(:issue, project: project) } let(:reference2) { other_issue.to_reference } let(:reference3) { third_issue.to_reference } diff --git a/spec/lib/gitlab/redis/hll_spec.rb b/spec/lib/gitlab/redis/hll_spec.rb index cbf78f23036..e452e5b2f52 100644 --- a/spec/lib/gitlab/redis/hll_spec.rb +++ b/spec/lib/gitlab/redis/hll_spec.rb @@ -39,6 +39,24 @@ RSpec.describe Gitlab::Redis::HLL, :clean_gitlab_redis_shared_state do end end end + + context 'when adding entries' do + let(:metric) { 'test-{metric}' } + + it 'supports single value' do + track_event(metric, 1) + + expect(count_unique_events([metric])).to eq(1) + end + + it 'supports multiple values' do + stub_const("#{described_class.name}::HLL_BATCH_SIZE", 2) + + track_event(metric, [1, 2, 3, 4, 5]) + + expect(count_unique_events([metric])).to eq(5) + end + end end describe '.count' do @@ -94,13 +112,13 @@ RSpec.describe Gitlab::Redis::HLL, :clean_gitlab_redis_shared_state do expect(unique_counts).to eq(4) end + end - def track_event(key, value, expiry = 1.day) - described_class.add(key: key, value: value, expiry: expiry) - end + def track_event(key, value, expiry = 1.day) + described_class.add(key: key, value: value, expiry: expiry) + end - def count_unique_events(keys) - described_class.count(keys: keys) - end + def count_unique_events(keys) + described_class.count(keys: keys) end end diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index 3255e3616b2..e84c3c17274 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s describe '.categories' do it 'gets all unique category names' do - expect(described_class.categories).to contain_exactly('analytics', 'compliance', 'ide_edit', 'search', 'source_code', 'incident_management', 'issues_edit') + expect(described_class.categories).to contain_exactly('analytics', 'compliance', 'ide_edit', 'search', 'source_code', 'incident_management', 'issues_edit', 'testing') end end diff --git a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb index 4cc85c86de1..bd3c8024215 100644 --- a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb @@ -132,6 +132,76 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end end + context 'for Issue cross-referenced actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_CROSS_REFERENCED } + + def track_action(params) + described_class.track_issue_cross_referenced_action(**params) + end + end + end + + context 'for Issue moved actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_MOVED } + + def track_action(params) + described_class.track_issue_moved_action(**params) + end + end + end + + context 'for Issue relate actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_RELATED } + + def track_action(params) + described_class.track_issue_related_action(**params) + end + end + end + + context 'for Issue unrelate actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_UNRELATED } + + def track_action(params) + described_class.track_issue_unrelated_action(**params) + end + end + end + + context 'for Issue marked as duplicate actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_MARKED_AS_DUPLICATE } + + def track_action(params) + described_class.track_issue_marked_as_duplicate_action(**params) + end + end + end + + context 'for Issue locked actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_LOCKED } + + def track_action(params) + described_class.track_issue_locked_action(**params) + end + end + end + + context 'for Issue unlocked actions' do + it_behaves_like 'tracks and counts action' do + let(:action) { described_class::ISSUE_UNLOCKED } + + def track_action(params) + described_class.track_issue_unlocked_action(**params) + end + end + end + it 'can return the count of actions per user deduplicated', :aggregate_failures do described_class.track_issue_title_changed_action(author: user1) described_class.track_issue_description_changed_action(author: user1) diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 984fd4c08e6..09728539499 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1182,9 +1182,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do subject { described_class.redis_hll_counters } let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } - let(:ineligible_total_categories) { ['source_code'] } + let(:ineligible_total_categories) { %w[source_code testing] } - it 'has all know_events' do + it 'has all known_events' do expect(subject).to have_key(:redis_hll_counters) expect(subject[:redis_hll_counters].keys).to match_array(categories) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 49731757593..ab22a203d06 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2307,6 +2307,54 @@ RSpec.describe Ci::Build do end end + describe '#has_expired_locked_archive_artifacts?' do + subject { build.has_expired_locked_archive_artifacts? } + + context 'when build does not have artifacts' do + it { is_expected.to eq(nil) } + end + + context 'when build has artifacts' do + before do + create(:ci_job_artifact, :archive, job: build) + end + + context 'when artifacts are unlocked' do + before do + build.pipeline.unlocked! + end + + it { is_expected.to eq(false) } + end + + context 'when artifacts are locked' do + before do + build.pipeline.artifacts_locked! + end + + context 'when artifacts do not expire' do + it { is_expected.to eq(false) } + end + + context 'when artifacts expire in the future' do + before do + build.update!(artifacts_expire_at: 1.day.from_now) + end + + it { is_expected.to eq(false) } + end + + context 'when artifacts expired in the past' do + before do + build.update!(artifacts_expire_at: 1.day.ago) + end + + it { is_expected.to eq(true) } + end + end + end + end + describe '#has_expiring_archive_artifacts?' do context 'when artifacts have expiration date set' do before do diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index dfce1e12dd5..40e5e1d5a84 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -173,6 +173,28 @@ RSpec.describe 'getting merge request listings nested in a project' do it_behaves_like 'searching with parameters' end + context 'when requesting `approved_by`' do + let(:search_params) { { iids: [merge_request_a.iid.to_s, merge_request_b.iid.to_s] } } + let(:extra_iid_for_second_query) { merge_request_c.iid.to_s } + let(:requested_fields) { query_graphql_field(:approved_by, nil, query_graphql_field(:nodes, nil, [:username])) } + + def execute_query + query = query_merge_requests(requested_fields) + post_graphql(query, current_user: current_user) + end + + it 'exposes approver username' do + merge_request_a.approved_by_users << current_user + + execute_query + + user_data = { 'username' => current_user.username } + expect(results).to include(a_hash_including('approvedBy' => { 'nodes' => array_including(user_data) })) + end + + include_examples 'N+1 query check' + end + describe 'fields' do let(:requested_fields) { nil } let(:extra_iid_for_second_query) { merge_request_c.iid.to_s } diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb index 6b5a4b6436a..3892df273bd 100644 --- a/spec/requests/api/lint_spec.rb +++ b/spec/requests/api/lint_spec.rb @@ -75,4 +75,115 @@ RSpec.describe API::Lint do end end end + + describe 'GET /projects/:id/ci/lint' do + subject(:ci_lint) { get api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run } } + + let_it_be(:api_user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:dry_run) { nil } + + RSpec.shared_examples 'valid config' do + it 'passes validation' do + ci_lint + + included_config = YAML.safe_load(included_content, [Symbol]) + root_config = YAML.safe_load(yaml_content, [Symbol]) + expected_yaml = included_config.merge(root_config).except(:include).to_yaml + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Hash + expect(json_response['merged_yaml']).to eq(expected_yaml) + expect(json_response['valid']).to eq(true) + expect(json_response['errors']).to eq([]) + end + end + + RSpec.shared_examples 'invalid config' do + it 'responds with errors about invalid configuration' do + ci_lint + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['merged_yaml']).to eq(yaml_content) + expect(json_response['valid']).to eq(false) + expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) + end + end + + context 'when unauthenticated' do + it 'returns authentication error' do + ci_lint + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when authenticated as project member' do + before do + project.add_developer(api_user) + end + + context 'with valid .gitlab-ci.yml content' do + let(:yaml_content) do + { include: { local: 'another-gitlab-ci.yml' }, test: { stage: 'test', script: 'echo 1' } }.to_yaml + end + + let(:included_content) do + { another_test: { stage: 'test', script: 'echo 1' } }.to_yaml + end + + before do + project.repository.create_file( + project.creator, + '.gitlab-ci.yml', + yaml_content, + message: 'Automatically created .gitlab-ci.yml', + branch_name: 'master' + ) + + project.repository.create_file( + project.creator, + 'another-gitlab-ci.yml', + included_content, + message: 'Automatically created another-gitlab-ci.yml', + branch_name: 'master' + ) + end + + context 'when running as dry run' do + let(:dry_run) { true } + + it_behaves_like 'valid config' + end + + context 'when running static validation' do + let(:dry_run) { false } + + it_behaves_like 'valid config' + end + end + + context 'with invalid .gitlab-ci.yml content' do + let(:yaml_content) do + { image: 'ruby:2.7', services: ['postgres'] }.to_yaml + end + + before do + stub_ci_pipeline_yaml_file(yaml_content) + end + + context 'when running as dry run' do + let(:dry_run) { true } + + it_behaves_like 'invalid config' + end + + context 'when running static validation' do + let(:dry_run) { false } + + it_behaves_like 'invalid config' + end + end + end + end end diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb index e72ac002f6b..de5dea449f8 100644 --- a/spec/requests/api/pypi_packages_spec.rb +++ b/spec/requests/api/pypi_packages_spec.rb @@ -23,24 +23,24 @@ RSpec.describe API::PypiPackages do using RSpec::Parameterized::TableSyntax where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'PyPi package versions' | :success - 'PUBLIC' | :guest | true | true | 'PyPi package versions' | :success - 'PUBLIC' | :developer | true | false | 'PyPi package versions' | :success - 'PUBLIC' | :guest | true | false | 'PyPi package versions' | :success - 'PUBLIC' | :developer | false | true | 'PyPi package versions' | :success - 'PUBLIC' | :guest | false | true | 'PyPi package versions' | :success - 'PUBLIC' | :developer | false | false | 'PyPi package versions' | :success - 'PUBLIC' | :guest | false | false | 'PyPi package versions' | :success - 'PUBLIC' | :anonymous | false | true | 'PyPi package versions' | :success - 'PRIVATE' | :developer | true | true | 'PyPi package versions' | :success - 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden - 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :developer | true | true | 'PyPI package versions' | :success + 'PUBLIC' | :guest | true | true | 'PyPI package versions' | :success + 'PUBLIC' | :developer | true | false | 'PyPI package versions' | :success + 'PUBLIC' | :guest | true | false | 'PyPI package versions' | :success + 'PUBLIC' | :developer | false | true | 'PyPI package versions' | :success + 'PUBLIC' | :guest | false | true | 'PyPI package versions' | :success + 'PUBLIC' | :developer | false | false | 'PyPI package versions' | :success + 'PUBLIC' | :guest | false | false | 'PyPI package versions' | :success + 'PUBLIC' | :anonymous | false | true | 'PyPI package versions' | :success + 'PRIVATE' | :developer | true | true | 'PyPI package versions' | :success + 'PRIVATE' | :guest | true | true | 'process PyPI api request' | :forbidden + 'PRIVATE' | :developer | true | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :guest | true | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :developer | false | true | 'process PyPI api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process PyPI api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :guest | false | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'process PyPI api request' | :unauthorized end with_them do @@ -76,24 +76,24 @@ RSpec.describe API::PypiPackages do using RSpec::Parameterized::TableSyntax where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process PyPi api request' | :success - 'PUBLIC' | :guest | true | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :developer | true | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :guest | true | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :developer | false | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :guest | false | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :developer | false | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :guest | false | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :developer | true | true | 'process PyPi api request' | :success - 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden - 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :developer | true | true | 'process PyPI api request' | :success + 'PUBLIC' | :guest | true | true | 'process PyPI api request' | :forbidden + 'PUBLIC' | :developer | true | false | 'process PyPI api request' | :unauthorized + 'PUBLIC' | :guest | true | false | 'process PyPI api request' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process PyPI api request' | :forbidden + 'PUBLIC' | :guest | false | true | 'process PyPI api request' | :forbidden + 'PUBLIC' | :developer | false | false | 'process PyPI api request' | :unauthorized + 'PUBLIC' | :guest | false | false | 'process PyPI api request' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process PyPI api request' | :success + 'PRIVATE' | :guest | true | true | 'process PyPI api request' | :forbidden + 'PRIVATE' | :developer | true | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :guest | true | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :developer | false | true | 'process PyPI api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process PyPI api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :guest | false | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'process PyPI api request' | :unauthorized end with_them do @@ -142,24 +142,24 @@ RSpec.describe API::PypiPackages do using RSpec::Parameterized::TableSyntax where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'PyPi package creation' | :created - 'PUBLIC' | :guest | true | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :developer | true | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :guest | true | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :developer | false | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :guest | false | true | 'process PyPi api request' | :forbidden - 'PUBLIC' | :developer | false | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :guest | false | false | 'process PyPi api request' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :developer | true | true | 'process PyPi api request' | :created - 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden - 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found - 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :developer | true | true | 'PyPI package creation' | :created + 'PUBLIC' | :guest | true | true | 'process PyPI api request' | :forbidden + 'PUBLIC' | :developer | true | false | 'process PyPI api request' | :unauthorized + 'PUBLIC' | :guest | true | false | 'process PyPI api request' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process PyPI api request' | :forbidden + 'PUBLIC' | :guest | false | true | 'process PyPI api request' | :forbidden + 'PUBLIC' | :developer | false | false | 'process PyPI api request' | :unauthorized + 'PUBLIC' | :guest | false | false | 'process PyPI api request' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process PyPI api request' | :created + 'PRIVATE' | :guest | true | true | 'process PyPI api request' | :forbidden + 'PRIVATE' | :developer | true | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :guest | true | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :developer | false | true | 'process PyPI api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process PyPI api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :guest | false | false | 'process PyPI api request' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'process PyPI api request' | :unauthorized end with_them do @@ -185,7 +185,7 @@ RSpec.describe API::PypiPackages do project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) end - it_behaves_like 'process PyPi api request', :developer, :bad_request, true + it_behaves_like 'process PyPI api request', :developer, :bad_request, true end context 'with an invalid package' do @@ -232,24 +232,24 @@ RSpec.describe API::PypiPackages do using RSpec::Parameterized::TableSyntax where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'PyPi package download' | :success - 'PUBLIC' | :guest | true | true | 'PyPi package download' | :success - 'PUBLIC' | :developer | true | false | 'PyPi package download' | :success - 'PUBLIC' | :guest | true | false | 'PyPi package download' | :success - 'PUBLIC' | :developer | false | true | 'PyPi package download' | :success - 'PUBLIC' | :guest | false | true | 'PyPi package download' | :success - 'PUBLIC' | :developer | false | false | 'PyPi package download' | :success - 'PUBLIC' | :guest | false | false | 'PyPi package download' | :success - 'PUBLIC' | :anonymous | false | true | 'PyPi package download' | :success - 'PRIVATE' | :developer | true | true | 'PyPi package download' | :success - 'PRIVATE' | :guest | true | true | 'PyPi package download' | :success - 'PRIVATE' | :developer | true | false | 'PyPi package download' | :success - 'PRIVATE' | :guest | true | false | 'PyPi package download' | :success - 'PRIVATE' | :developer | false | true | 'PyPi package download' | :success - 'PRIVATE' | :guest | false | true | 'PyPi package download' | :success - 'PRIVATE' | :developer | false | false | 'PyPi package download' | :success - 'PRIVATE' | :guest | false | false | 'PyPi package download' | :success - 'PRIVATE' | :anonymous | false | true | 'PyPi package download' | :success + 'PUBLIC' | :developer | true | true | 'PyPI package download' | :success + 'PUBLIC' | :guest | true | true | 'PyPI package download' | :success + 'PUBLIC' | :developer | true | false | 'PyPI package download' | :success + 'PUBLIC' | :guest | true | false | 'PyPI package download' | :success + 'PUBLIC' | :developer | false | true | 'PyPI package download' | :success + 'PUBLIC' | :guest | false | true | 'PyPI package download' | :success + 'PUBLIC' | :developer | false | false | 'PyPI package download' | :success + 'PUBLIC' | :guest | false | false | 'PyPI package download' | :success + 'PUBLIC' | :anonymous | false | true | 'PyPI package download' | :success + 'PRIVATE' | :developer | true | true | 'PyPI package download' | :success + 'PRIVATE' | :guest | true | true | 'PyPI package download' | :success + 'PRIVATE' | :developer | true | false | 'PyPI package download' | :success + 'PRIVATE' | :guest | true | false | 'PyPI package download' | :success + 'PRIVATE' | :developer | false | true | 'PyPI package download' | :success + 'PRIVATE' | :guest | false | true | 'PyPI package download' | :success + 'PRIVATE' | :developer | false | false | 'PyPI package download' | :success + 'PRIVATE' | :guest | false | false | 'PyPI package download' | :success + 'PRIVATE' | :anonymous | false | true | 'PyPI package download' | :success end with_them do diff --git a/spec/services/ci/build_report_result_service_spec.rb b/spec/services/ci/build_report_result_service_spec.rb index 70bcf74ba43..134b662a72a 100644 --- a/spec/services/ci/build_report_result_service_spec.rb +++ b/spec/services/ci/build_report_result_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Ci::BuildReportResultService do - describe "#execute" do + describe '#execute', :clean_gitlab_redis_shared_state do subject(:build_report_result) { described_class.new.execute(build) } context 'when build is finished' do @@ -17,6 +17,25 @@ RSpec.describe Ci::BuildReportResultService do expect(build_report_result.tests_skipped).to eq(0) expect(build_report_result.tests_duration).to eq(0.010284) expect(Ci::BuildReportResult.count).to eq(1) + + unique_test_cases_parsed = Gitlab::UsageDataCounters::HLLRedisCounter.unique_events( + event_names: described_class::EVENT_NAME, + start_date: 2.weeks.ago, + end_date: 2.weeks.from_now + ) + expect(unique_test_cases_parsed).to eq(4) + end + + context 'when feature flag for tracking is disabled' do + before do + stub_feature_flags(track_unique_test_cases_parsed: false) + end + + it 'creates the report but does not track the event' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + expect(build_report_result.tests_name).to eq("test") + expect(Ci::BuildReportResult.count).to eq(1) + end end context 'when data has already been persisted' do diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index 40bedc84366..bbcf856350d 100644 --- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member = true| +RSpec.shared_examples 'PyPI package creation' do |user_type, status, add_member = true| RSpec.shared_examples 'creating pypi package files' do it 'creates package files' do expect { subject } @@ -106,7 +106,7 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member end end -RSpec.shared_examples 'PyPi package versions' do |user_type, status, add_member = true| +RSpec.shared_examples 'PyPI package versions' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do project.send("add_#{user_type}", user) if add_member && user_type != :anonymous @@ -123,7 +123,7 @@ RSpec.shared_examples 'PyPi package versions' do |user_type, status, add_member end end -RSpec.shared_examples 'PyPi package download' do |user_type, status, add_member = true| +RSpec.shared_examples 'PyPI package download' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do project.send("add_#{user_type}", user) if add_member && user_type != :anonymous @@ -140,7 +140,7 @@ RSpec.shared_examples 'PyPi package download' do |user_type, status, add_member end end -RSpec.shared_examples 'process PyPi api request' do |user_type, status, add_member = true| +RSpec.shared_examples 'process PyPI api request' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do project.send("add_#{user_type}", user) if add_member && user_type != :anonymous @@ -155,13 +155,13 @@ RSpec.shared_examples 'rejects PyPI access with unknown project id' do let(:project) { OpenStruct.new(id: 1234567890) } context 'as anonymous' do - it_behaves_like 'process PyPi api request', :anonymous, :not_found + it_behaves_like 'process PyPI api request', :anonymous, :not_found end context 'as authenticated user' do subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } - it_behaves_like 'process PyPi api request', :anonymous, :not_found + it_behaves_like 'process PyPI api request', :anonymous, :not_found end end end diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index c00a087311c..65f4b3b5513 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -171,6 +171,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false| let_it_be(:package6) { create(:composer_package, project: project) } let_it_be(:package7) { create(:generic_package, project: project) } let_it_be(:package8) { create(:golang_package, project: project) } + let_it_be(:package9) { create(:debian_package, project: project) } Packages::Package.package_types.keys.each do |package_type| context "for package type #{package_type}" do |