diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-10 21:09:14 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-10 21:09:14 +0300 |
commit | d18b7dc5eea84db5008986c6879a24ad7f6462a6 (patch) | |
tree | 98d6e8635ac32f210f15fcfb3dc583a6295e0b9a | |
parent | 6ebe886c82111e1ab9e71d4c02a888d2312898bc (diff) |
Add latest changes from gitlab-org/gitlab@master
66 files changed, 1133 insertions, 431 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index cdf11c7f4d7..0993e77fdf7 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -f6ca4c932139d0e6b4407f2ec6251858479382f0 +0206458c621892e0bc247098eb6f3a21d2424477 @@ -302,6 +302,9 @@ gem 'rack-attack', '~> 6.3.0' # Sentry integration gem 'sentry-raven', '~> 3.1' +gem 'sentry-ruby', '~> 5.1.1' +gem 'sentry-rails', '~> 5.1.1' +gem 'sentry-sidekiq', '~> 5.1.1' # PostgreSQL query parsing # diff --git a/Gemfile.lock b/Gemfile.lock index 49ec246a727..6fe4cdc9726 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1174,8 +1174,19 @@ GEM selenium-webdriver (3.142.7) childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) + sentry-rails (5.1.1) + railties (>= 5.0) + sentry-ruby-core (~> 5.1.1) sentry-raven (3.1.2) faraday (>= 1.0) + sentry-ruby (5.1.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + sentry-ruby-core (= 5.1.1) + sentry-ruby-core (5.1.1) + concurrent-ruby + sentry-sidekiq (5.1.1) + sentry-ruby-core (~> 5.1.1) + sidekiq (>= 3.0) set (1.0.1) settingslogic (2.0.9) sexp_processor (4.15.1) @@ -1627,7 +1638,10 @@ DEPENDENCIES sd_notify (~> 0.1.0) seed-fu (~> 2.3.7) selenium-webdriver (~> 3.142) + sentry-rails (~> 5.1.1) sentry-raven (~> 3.1) + sentry-ruby (~> 5.1.1) + sentry-sidekiq (~> 5.1.1) settingslogic (~> 2.0.9) shoulda-matchers (~> 4.0.1) sidekiq (~> 6.4) diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue index 6030af9d2c3..ae2d5f4fbc5 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue @@ -13,7 +13,6 @@ import { REMOVE_INFO_TEXT, EXPIRATION_SCHEDULE_LABEL, NAME_REGEX_LABEL, - NAME_REGEX_PLACEHOLDER, NAME_REGEX_DESCRIPTION, CADENCE_LABEL, EXPIRATION_POLICY_FOOTER_NOTE, @@ -68,7 +67,6 @@ export default { REMOVE_INFO_TEXT, EXPIRATION_SCHEDULE_LABEL, NAME_REGEX_LABEL, - NAME_REGEX_PLACEHOLDER, NAME_REGEX_DESCRIPTION, CADENCE_LABEL, EXPIRATION_POLICY_FOOTER_NOTE, @@ -141,6 +139,17 @@ export default { [model]: state, }; }, + encapsulateError(path, message) { + return { + graphQLErrors: [ + { + extensions: { + problems: [{ path: [path], message }], + }, + }, + ], + }; + }, submit() { this.track('submit_form'); this.apiErrors = {}; @@ -156,7 +165,8 @@ export default { .then(({ data }) => { const errorMessage = data?.updateContainerExpirationPolicy?.errors[0]; if (errorMessage) { - this.$toast.show(errorMessage); + const customError = this.encapsulateError('nameRegex', errorMessage); + throw customError; } else { this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE); } @@ -273,7 +283,6 @@ export default { :error="apiErrors.nameRegex" :disabled="isFieldDisabled" :label="$options.i18n.NAME_REGEX_LABEL" - :placeholder="$options.i18n.NAME_REGEX_PLACEHOLDER" :description="$options.i18n.NAME_REGEX_DESCRIPTION" name="remove-regex" data-testid="remove-regex-input" diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js index 4d477fbd05d..841585c5646 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js +++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js @@ -32,7 +32,6 @@ export const REMOVE_INFO_TEXT = s__( ); export const EXPIRATION_SCHEDULE_LABEL = s__('ContainerRegistry|Remove tags older than:'); export const NAME_REGEX_LABEL = s__('ContainerRegistry|Remove tags matching:'); -export const NAME_REGEX_PLACEHOLDER = '.*'; export const NAME_REGEX_DESCRIPTION = s__( 'ContainerRegistry|Tags with names that match this regex pattern are removed. %{linkStart}View regex examples.%{linkEnd}', ); diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/utils/cache_update.js b/app/assets/javascripts/packages_and_registries/settings/project/graphql/utils/cache_update.js index c4b2af13862..5e0be3834cb 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/graphql/utils/cache_update.js +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/utils/cache_update.js @@ -10,6 +10,7 @@ export const updateContainerExpirationPolicy = (projectPath) => (client, { data: const data = produce(sourceData, (draftState) => { draftState.project.containerExpirationPolicy = { + ...draftState.project.containerExpirationPolicy, ...updatedData.updateContainerExpirationPolicy.containerExpirationPolicy, }; }); diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue index adae97c6b6f..67962d69fa5 100644 --- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue +++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue @@ -27,11 +27,6 @@ export default { required: true, type: Object, }, - inviteMembers: { - type: Boolean, - required: false, - default: false, - }, project: { required: true, type: Object, @@ -54,7 +49,7 @@ export default { }, }, mounted() { - if (this.inviteMembers && this.getCookieForInviteMembers()) { + if (this.getCookieForInviteMembers()) { this.openInviteMembersModal('celebrate'); } diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js index c62cab1a425..63357ea9c72 100644 --- a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js +++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; -import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import LearnGitlab from '../components/learn_gitlab.vue'; function initLearnGitlab() { @@ -13,13 +13,12 @@ function initLearnGitlab() { const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions)); const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections)); const project = convertObjectPropsToCamelCase(JSON.parse(el.dataset.project)); - const { inviteMembers } = el.dataset; return new Vue({ el, render(createElement) { return createElement(LearnGitlab, { - props: { actions, sections, project, inviteMembers: parseBoolean(inviteMembers) }, + props: { actions, sections, project }, }); }, }); diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue index 65114ee066e..f27dae8249d 100644 --- a/app/assets/javascripts/search/topbar/components/app.vue +++ b/app/assets/javascripts/search/topbar/components/app.vue @@ -1,17 +1,20 @@ <script> -import { GlForm, GlSearchBoxByType, GlButton } from '@gitlab/ui'; +import { GlSearchBoxByClick } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; +import { s__ } from '~/locale'; import GroupFilter from './group_filter.vue'; import ProjectFilter from './project_filter.vue'; export default { name: 'GlobalSearchTopbar', + i18n: { + searchPlaceholder: s__(`GlobalSearch|Search for projects, issues, etc.`), + searchLabel: s__(`GlobalSearch|What are you searching for?`), + }, components: { - GlForm, - GlSearchBoxByType, + GlSearchBoxByClick, GroupFilter, ProjectFilter, - GlButton, }, props: { groupInitialData: { @@ -49,28 +52,24 @@ export default { </script> <template> - <gl-form class="search-page-form" @submit.prevent="applyQuery"> - <section class="gl-lg-display-flex gl-align-items-flex-end"> - <div class="gl-flex-grow-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2"> - <label>{{ __('What are you searching for?') }}</label> - <gl-search-box-by-type - id="dashboard_search" - v-model="search" - name="search" - :placeholder="__(`Search for projects, issues, etc.`)" - /> - </div> - <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2"> - <label class="gl-display-block">{{ __('Group') }}</label> - <group-filter :initial-data="groupInitialData" /> - </div> - <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2"> - <label class="gl-display-block">{{ __('Project') }}</label> - <project-filter :initial-data="projectInitialData" /> - </div> - <gl-button class="btn-search gl-lg-ml-2" category="primary" variant="confirm" type="submit" - >{{ __('Search') }} - </gl-button> - </section> - </gl-form> + <section class="search-page-form gl-lg-display-flex gl-align-items-flex-end"> + <div class="gl-flex-grow-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2"> + <label>{{ $options.i18n.searchLabel }}</label> + <gl-search-box-by-click + id="dashboard_search" + v-model="search" + name="search" + :placeholder="$options.i18n.searchPlaceholder" + @submit="applyQuery" + /> + </div> + <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2"> + <label class="gl-display-block">{{ __('Group') }}</label> + <group-filter :initial-data="groupInitialData" /> + </div> + <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2"> + <label class="gl-display-block">{{ __('Project') }}</label> + <project-filter :initial-data="projectInitialData" /> + </div> + </section> </template> diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index 81d222438e3..11e092d8eb4 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -281,3 +281,14 @@ export const featureToMutationMap = { export const AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY = 'security_configuration_auto_devops_enabled_dismissed_projects'; + +// Fetch the svg path from the GraphQL query once this issue is resolved +// https://gitlab.com/gitlab-org/gitlab/-/issues/346899 +export const TEMP_PROVIDER_LOGOS = { + Kontra: { + svg: '/assets/illustrations/vulnerability/kontra-logo.svg', + }, + [__('Secure Code Warrior')]: { + svg: '/assets/illustrations/vulnerability/scw-logo.svg', + }, +}; diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue index bfdabc10227..ea3eedc6e90 100644 --- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue +++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue @@ -24,6 +24,7 @@ import { updateSecurityTrainingCache, updateSecurityTrainingOptimisticResponse, } from '~/security_configuration/graphql/cache_utils'; +import { TEMP_PROVIDER_LOGOS } from './constants'; const i18n = { providerQueryErrorMessage: __( @@ -38,17 +39,6 @@ const i18n = { ), }; -// Fetch the svg path from the GraphQL query once this issue is resolved -// https://gitlab.com/gitlab-org/gitlab/-/issues/346899 -const TEMP_PROVIDER_LOGOS = { - 'gid://gitlab/Security::TrainingProvider/1': { - svg: '/assets/illustrations/vulnerability/kontra-logo.svg', - }, - 'gid://gitlab/Security::TrainingProvider/2': { - svg: '/assets/illustrations/vulnerability/scw-logo.svg', - }, -}; - export default { components: { GlAlert, @@ -242,8 +232,12 @@ export default { label-position="hidden" @change="toggleProvider(provider)" /> - <div v-if="$options.TEMP_PROVIDER_LOGOS[provider.id]" class="gl-ml-4"> - <img :src="$options.TEMP_PROVIDER_LOGOS[provider.id].svg" width="18" /> + <div v-if="$options.TEMP_PROVIDER_LOGOS[provider.name]" class="gl-ml-4"> + <img + :src="$options.TEMP_PROVIDER_LOGOS[provider.name].svg" + width="18" + role="presentation" + /> </div> <div class="gl-ml-3"> <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ provider.name }}</h3> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index efb99eb0d94..56fae425d1a 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -1,30 +1,33 @@ <script> /* This is a re-usable vue component for rendering a user avatar that - does not need to link to the user's profile. The image and an optional - tooltip can be configured by props passed to this component. + does not need to link to the user's profile. The image and an optional + tooltip can be configured by props passed to this component. - Sample configuration: + Sample configuration: - <user-avatar-image - :lazy="true" - :img-src="userAvatarSrc" - :img-alt="tooltipText" - :tooltip-text="tooltipText" - tooltip-placement="top" - /> + <user-avatar-image + lazy + :img-src="userAvatarSrc" + :img-alt="tooltipText" + :tooltip-text="tooltipText" + tooltip-placement="top" + /> -*/ + */ -import { GlTooltip } from '@gitlab/ui'; +import { GlTooltip, GlAvatar } from '@gitlab/ui'; import defaultAvatarUrl from 'images/no_avatar.png'; import { __ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { placeholderImage } from '../../../lazy_loader'; export default { name: 'UserAvatarImage', components: { GlTooltip, + GlAvatar, }, + mixins: [glFeatureFlagMixin()], props: { lazy: { type: Boolean, @@ -85,7 +88,20 @@ export default { <template> <span> + <gl-avatar + v-if="glFeatures.glAvatarForAllUserAvatars" + ref="userAvatarImage" + :class="{ + lazy: lazy, + [cssClasses]: true, + }" + :src="resultantSrcAttribute" + :data-src="sanitizedSource" + :size="size" + :alt="imgAlt" + /> <img + v-else ref="userAvatarImage" :class="{ lazy: lazy, @@ -100,11 +116,9 @@ export default { class="avatar" /> <gl-tooltip - v-if="tooltipText || $slots.default" :target="() => $refs.userAvatarImage" :placement="tooltipPlacement" boundary="window" - class="js-user-avatar-image-tooltip" > <slot> {{ tooltipText }} </slot> </gl-tooltip> diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 56c61c1bea2..fa03d73646d 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -66,7 +66,7 @@ class ContainerRepository < ApplicationRecord # feature flag since it is only accessed in this query. # https://gitlab.com/gitlab-org/gitlab/-/issues/350543 tracks the rollout and # removal of this feature flag. - joins(:project).where( + joins(project: [:namespace]).where( migration_state: [:default], created_at: ...ContainerRegistry::Migration.created_before ).with_target_import_tier @@ -76,7 +76,7 @@ class ContainerRepository < ApplicationRecord FROM feature_gates WHERE feature_gates.feature_key = 'container_registry_phase_2_deny_list' AND feature_gates.key = 'actors' - AND feature_gates.value = concat('Group:', projects.namespace_id) + AND feature_gates.value = concat('Group:', namespaces.traversal_ids[1]) )" ) end diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb index fc7c348dfdb..ad8140ac684 100644 --- a/app/models/packages/package_file.rb +++ b/app/models/packages/package_file.rb @@ -49,6 +49,7 @@ class Packages::PackageFile < ApplicationRecord scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) } scope :preload_debian_file_metadata, -> { preload(:debian_file_metadatum) } scope :preload_helm_file_metadata, -> { preload(:helm_file_metadatum) } + scope :order_id_asc, -> { order(id: :asc) } scope :for_rubygem_with_file_name, ->(project, file_name) do joins(:package).merge(project.packages.rubygems).with_file_name(file_name) diff --git a/app/services/ci/job_artifacts/destroy_associations_service.rb b/app/services/ci/job_artifacts/destroy_associations_service.rb index 794d24eadf2..08d7f7f6f02 100644 --- a/app/services/ci/job_artifacts/destroy_associations_service.rb +++ b/app/services/ci/job_artifacts/destroy_associations_service.rb @@ -12,7 +12,7 @@ module Ci def destroy_records @job_artifacts_relation.each_batch(of: BATCH_SIZE) do |relation| - service = Ci::JobArtifacts::DestroyBatchService.new(relation, pick_up_at: Time.current) + service = Ci::JobArtifacts::DestroyBatchService.new(relation, pick_up_at: Time.current, fix_expire_at: false) result = service.execute(update_stats: false) updates = result[:statistics_updates] diff --git a/app/services/ci/job_artifacts/destroy_batch_service.rb b/app/services/ci/job_artifacts/destroy_batch_service.rb index 866b40c32d8..d5a0a2dd885 100644 --- a/app/services/ci/job_artifacts/destroy_batch_service.rb +++ b/app/services/ci/job_artifacts/destroy_batch_service.rb @@ -17,13 +17,18 @@ module Ci # +pick_up_at+:: When to pick up for deletion of files # Returns: # +Hash+:: A hash with status and destroyed_artifacts_count keys - def initialize(job_artifacts, pick_up_at: nil) + def initialize(job_artifacts, pick_up_at: nil, fix_expire_at: fix_expire_at?) @job_artifacts = job_artifacts.with_destroy_preloads.to_a @pick_up_at = pick_up_at + @fix_expire_at = fix_expire_at end # rubocop: disable CodeReuse/ActiveRecord def execute(update_stats: true) + # Detect and fix artifacts that had `expire_at` wrongly backfilled by migration + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47723 + detect_and_fix_wrongly_expired_artifacts + return success(destroyed_artifacts_count: 0, statistics_updates: {}) if @job_artifacts.empty? destroy_related_records(@job_artifacts) @@ -89,6 +94,55 @@ module Ci @job_artifacts.sum { |artifact| artifact.try(:size) || 0 } end end + + # This detects and fixes job artifacts that have `expire_at` wrongly backfilled by the migration + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47723. + # These job artifacts will not be deleted and will have their `expire_at` removed. + # + # The migration would have backfilled `expire_at` + # to midnight on the 22nd of the month of the local timezone, + # storing it as UTC time in the database. + # + # If the timezone setting has changed since the migration, + # the `expire_at` stored in the database could have changed to a different local time other than midnight. + # For example: + # - changing timezone from UTC+02:00 to UTC+02:30 would change the `expire_at` in local time 00:00:00 to 00:30:00. + # - changing timezone from UTC+00:00 to UTC-01:00 would change the `expire_at` in local time 00:00:00 to 23:00:00 on the previous day (21st). + # + # Therefore job artifacts that have `expire_at` exactly on the 00, 30 or 45 minute mark + # on the dates 21, 22, 23 of the month will not be deleted. + # https://en.wikipedia.org/wiki/List_of_UTC_time_offsets + def detect_and_fix_wrongly_expired_artifacts + return unless @fix_expire_at + + wrongly_expired_artifacts, @job_artifacts = @job_artifacts.partition { |artifact| wrongly_expired?(artifact) } + + remove_expire_at(wrongly_expired_artifacts) + end + + def fix_expire_at? + Feature.enabled?(:ci_detect_wrongly_expired_artifacts, default_enabled: :yaml) + end + + def wrongly_expired?(artifact) + return false unless artifact.expire_at.present? + + match_date?(artifact.expire_at) && match_time?(artifact.expire_at) + end + + def match_date?(expire_at) + [21, 22, 23].include?(expire_at.day) + end + + def match_time?(expire_at) + %w[00:00.000 30:00.000 45:00.000].include?(expire_at.strftime('%M:%S.%L')) + end + + def remove_expire_at(artifacts) + Ci::JobArtifact.id_in(artifacts).update_all(expire_at: nil) + + Gitlab::AppLogger.info(message: "Fixed expire_at from artifacts.", fixed_artifacts_expire_at_count: artifacts.count) + end end end end diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 665f02ab63d..6a54eedf6c8 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,5 +1,6 @@ - @content_class = "limit-container-width" unless fluid_layout - default_branch_name = @project.default_branch_or_main +- escaped_default_branch_name = default_branch_name.shellescape - @skip_current_level_breadcrumb = true = render partial: 'flash_messages', locals: { project: @project } @@ -42,25 +43,25 @@ :preserve git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} cd #{h @project.path} - git switch -c #{h default_branch_name} + git switch -c #{h escaped_default_branch_name} touch README.md git add README.md git commit -m "add README" - if @project.can_current_user_push_to_default_branch? %span>< - git push -u origin #{h default_branch_name } + git push -u origin #{h escaped_default_branch_name } %h5= _('Push an existing folder') %pre.bg-light :preserve cd existing_folder - git init --initial-branch=#{h default_branch_name} + git init --initial-branch=#{h escaped_default_branch_name} git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} git add . git commit -m "Initial commit" - if @project.can_current_user_push_to_default_branch? %span>< - git push -u origin #{h default_branch_name } + git push -u origin #{h escaped_default_branch_name } %h5= _('Push an existing Git repository') %pre.bg-light diff --git a/app/views/projects/learn_gitlab/index.html.haml b/app/views/projects/learn_gitlab/index.html.haml index 9924b172875..0e950c26d34 100644 --- a/app/views/projects/learn_gitlab/index.html.haml +++ b/app/views/projects/learn_gitlab/index.html.haml @@ -5,8 +5,4 @@ = render 'projects/invite_members_modal', project: @project -- experiment(:confetti_post_signup, actor: current_user) do |e| - - e.control do - #js-learn-gitlab-app{ data: data } - - e.candidate do - #js-learn-gitlab-app{ data: data.merge(invite_members: 'true') } +#js-learn-gitlab-app{ data: data } diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index c70e153ae41..66a1cbb4649 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -94,7 +94,7 @@ .input-group-text / %p.form-text.text-muted = html_escape(_('The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable.')) % { regex: '<code>\(\d+.\d+%\)</code>'.html_safe } - = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'add-test-coverage-results-to-a-merge-request-deprecated'), target: '_blank', rel: 'noopener noreferrer' + = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'add-test-coverage-results-using-project-settings-deprecated'), target: '_blank', rel: 'noopener noreferrer' = f.submit _('Save changes'), class: "btn gl-button btn-confirm", data: { qa_selector: 'save_general_pipelines_changes_button' } diff --git a/config/feature_flags/development/ci_detect_wrongly_expired_artifacts.yml b/config/feature_flags/development/ci_detect_wrongly_expired_artifacts.yml new file mode 100644 index 00000000000..8340b347a96 --- /dev/null +++ b/config/feature_flags/development/ci_detect_wrongly_expired_artifacts.yml @@ -0,0 +1,8 @@ +--- +name: ci_detect_wrongly_expired_artifacts +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82084 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354955 +milestone: '14.9' +type: development +group: group::pipeline insights +default_enabled: false diff --git a/config/feature_flags/development/enable_new_sentry_integration.yml b/config/feature_flags/development/enable_new_sentry_integration.yml new file mode 100644 index 00000000000..00665f80ed6 --- /dev/null +++ b/config/feature_flags/development/enable_new_sentry_integration.yml @@ -0,0 +1,8 @@ +--- +name: enable_new_sentry_integration +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72428 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344832 +milestone: '14.9' +type: development +group: group::pipeline execution +default_enabled: false diff --git a/config/feature_flags/development/enable_old_sentry_integration.yml b/config/feature_flags/development/enable_old_sentry_integration.yml new file mode 100644 index 00000000000..4911dbfdc78 --- /dev/null +++ b/config/feature_flags/development/enable_old_sentry_integration.yml @@ -0,0 +1,8 @@ +--- +name: enable_old_sentry_integration +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72428 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344832 +milestone: '14.9' +type: development +group: group::pipeline execution +default_enabled: true diff --git a/config/feature_flags/development/fips_mode.yml b/config/feature_flags/development/fips_mode.yml deleted file mode 100644 index cade948b886..00000000000 --- a/config/feature_flags/development/fips_mode.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: fips_mode -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81418/diffs?view=inline -rollout_issue_url: -milestone: '14.9' -type: development -group: group::source code -default_enabled: false diff --git a/config/feature_flags/experiment/confetti_post_signup.yml b/config/feature_flags/development/gl_avatar_for_all_user_avatars.yml index 9f677bf252a..a3fee67a7f6 100644 --- a/config/feature_flags/experiment/confetti_post_signup.yml +++ b/config/feature_flags/development/gl_avatar_for_all_user_avatars.yml @@ -1,8 +1,8 @@ --- -name: confetti_post_signup -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70011 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339890 -milestone: '14.5' -type: experiment -group: group::expansion +name: gl_avatar_for_all_user_avatars +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81437 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353477 +milestone: '14.9' +type: development +group: group::foundations default_enabled: false diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md index 4ae4aabbf5a..373244acbb9 100644 --- a/doc/ci/pipelines/settings.md +++ b/doc/ci/pipelines/settings.md @@ -195,13 +195,7 @@ Jobs that exceed the timeout are marked as failed. You can override this value [for individual runners](../runners/configure_runners.md#set-maximum-job-timeout-for-a-runner). -## Add test coverage results to a merge request (DEPRECATED) - -> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/17633) in GitLab 14.9. Replaced by [`coverage` keyword](../yaml/index.md#coverage). - -WARNING: -This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/17633) -for use in GitLab 14.9, and is planned for [removal](https://gitlab.com/gitlab-org/gitlab/-/issues/17633) in GitLab 15.0. +## Merge request test coverage results If you use test coverage in your code, you can use a regular expression to find coverage results in the job log. You can then include these results @@ -215,27 +209,38 @@ averaged. ![Build status coverage](img/pipelines_test_coverage_build.png) -To define a coverage-parsing regular expression: +### Add test coverage results using `coverage` keyword + +To add test coverage results to a merge request using the project's `.gitlab-ci.yml` file, provide a regular expression +using the [`coverage`](../yaml/index.md#coverage) keyword. + +Setting the regular expression this way takes precedence over project settings. -- Using the project's `.gitlab-ci.yml`, provide a regular expression using the [`coverage`](../yaml/index.md#coverage) - keyword. Setting the regular expression this way takes precedence over the project's CI/CD settings. +### Add test coverage results using project settings (DEPRECATED) -- Using the Project's CI/CD settings: - - Set using the GitLab UI: +> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/17633) in GitLab 14.9. Replaced by [`coverage` keyword](../yaml/index.md#coverage). + +WARNING: +This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/17633) +for use in GitLab 14.9, and is planned for [removal](https://gitlab.com/gitlab-org/gitlab/-/issues/17633) in GitLab 15.0. + +You can add test coverage results to merge requests using the Project's CI/CD settings: - 1. On the top bar, select **Menu > Projects** and find your project. - 1. On the left sidebar, select **Settings > CI/CD**. - 1. Expand **General pipelines**. - 1. In the **Test coverage parsing** field, enter a regular expression. Leave blank to disable this feature. +- Set using the GitLab UI: - - Set when [editing a project](../../api/projects.md#edit-project) or [creating a project](../../api/projects.md#create-project) - using the GitLab API with the `build_coverage_regex` attribute: + 1. On the top bar, select **Menu > Projects** and find your project. + 1. On the left sidebar, select **Settings > CI/CD**. + 1. Expand **General pipelines**. + 1. In the **Test coverage parsing** field, enter a regular expression. Leave blank to disable this feature. - ```shell - curl --request PUT --header "PRIVATE-TOKEN: <your-token>" \ - --url 'https://gitlab.com/api/v4/projects/<your-project-ID>' \ - --data "build_coverage_regex=<your-regular-expression>" - ``` +- Set when [editing a project](../../api/projects.md#edit-project) or [creating a project](../../api/projects.md#create-project) + using the GitLab API with the `build_coverage_regex` attribute: + + ```shell + curl --request PUT --header "PRIVATE-TOKEN: <your-token>" \ + --url 'https://gitlab.com/api/v4/projects/<your-project-ID>' \ + --data "build_coverage_regex=<your-regular-expression>" + ``` You can use <https://rubular.com> to test your regular expression. The regular expression returns the **last** match found in the output. @@ -372,9 +377,8 @@ https://gitlab.example.com/<namespace>/<project>/badges/<branch>/pipeline.svg?ig ### Test coverage report badge -You can define the regular expression for the [coverage report](#add-test-coverage-results-to-a-merge-request-deprecated) -that each job log is matched against. This means that each job in the -pipeline can have the test coverage percentage value defined. +You can define the regular expression for the [coverage report](#merge-request-test-coverage-results) that each job log +is matched against. This means that each job in the pipeline can have the test coverage percentage value defined. To access the test coverage badge, use the following link: diff --git a/doc/ci/unit_test_reports.md b/doc/ci/unit_test_reports.md index e2de47c6c62..14a2a1c810f 100644 --- a/doc/ci/unit_test_reports.md +++ b/doc/ci/unit_test_reports.md @@ -231,7 +231,7 @@ The [JunitXML.TestLogger](https://www.nuget.org/packages/JunitXml.TestLogger/) N package can generate test reports for .Net Framework and .Net Core applications. The following example expects a solution in the root folder of the repository, with one or more project files in sub-folders. One result file is produced per test project, and each file -is placed in a new artifacts folder. This example includes optional formatting arguments, which +is placed in the artifacts folder. This example includes optional formatting arguments, which improve the readability of test data in the test widget. A full .Net Core [example is available](https://gitlab.com/Siphonophora/dot-net-cicd-test-logging-demo). diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index d9ea5498b63..e754e7081b9 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -1349,7 +1349,7 @@ In this example: **Additional details**: - Coverage regular expressions set in `gitlab-ci.yml` take precedence over coverage regular expression set in the - [GitLab UI](../pipelines/settings.md#add-test-coverage-results-to-a-merge-request-deprecated). + [GitLab UI](../pipelines/settings.md#add-test-coverage-results-using-project-settings-deprecated). - If there is more than one matched line in the job output, the last line is used (the first result of reverse search). - If there are multiple matches in a single line, the last match is searched diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index 43ef16e14d6..8dae7f16974 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -301,6 +301,21 @@ We follow the [PostgreSQL versions shipped with Omnibus GitLab](../administratio | PG11 | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | | PG13 | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | +## Redis versions testing + +Our test suite runs against Redis 6 as GitLab.com runs on Redis 6 and +[Omnibus defaults to Redis 6 for new installs and upgrades](https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/master/config/software/redis.rb). + +We do run our test suite against Redis 5 on `nightly` scheduled pipelines, specifically when running backward-compatible and forward-compatible PostgreSQL jobs. + +### Current versions testing + +| Where? | Redis version | +| ------ | ------------------ | +| MRs | 6 | +| `default branch` (non-scheduled pipelines) | 6 | +| `nightly` scheduled pipelines | 5 | + ## Pipelines types for merge requests In general, pipelines for an MR fall into one of the following types (from shorter to longer), depending on the changes made in the MR: @@ -541,6 +556,8 @@ that are scoped to a single [configuration keyword](../ci/yaml/index.md#job-keyw | `.use-pg11-ee` | Same as `.use-pg11` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). | | `.use-pg12` | Allows a job to use the `postgres` 12 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). | | `.use-pg12-ee` | Same as `.use-pg12` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). | +| `.use-pg13` | Allows a job to use the `postgres` 13 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). | +| `.use-pg13-ee` | Same as `.use-pg13` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). | | `.use-kaniko` | Allows a job to use the `kaniko` tool to build Docker images. | | `.as-if-foss` | Simulate the FOSS project by setting the `FOSS_ONLY='1'` CI/CD variable. | | `.use-docker-in-docker` | Allows a job to use Docker in Docker. | diff --git a/doc/user/infrastructure/clusters/connect/new_gke_cluster.md b/doc/user/infrastructure/clusters/connect/new_gke_cluster.md index 55fea5890b3..1ed8b0ef350 100644 --- a/doc/user/infrastructure/clusters/connect/new_gke_cluster.md +++ b/doc/user/infrastructure/clusters/connect/new_gke_cluster.md @@ -4,65 +4,59 @@ group: Configure 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/#assignments --- -# New GKE cluster through IaC (DEPRECATED) - -> [Deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8) in GitLab 14.5. - -WARNING: -The process described on this page uses cluster certificates to connect the -new cluster to GitLab, [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8) in GitLab 14.5. -You can still create a cluster and then connect it to GitLab through the [agent](../index.md). -[An issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/343660) -to migrate this functionality to the [agent](../index.md). +# Create a Google GKE cluster Learn how to create a new cluster on Google Kubernetes Engine (GKE) through -[Infrastructure as Code (IaC)](../../index.md). - -This process combines the GitLab Terraform and Google Terraform providers -with Kubernetes to help you create GKE clusters and deploy them through -GitLab. - -This document describes how to set up a [group-level cluster](../../../group/clusters/index.md) on GKE by importing an example project to get you started. -You can then modify the project files according to your needs. +[Infrastructure as Code (IaC)](../../index.md). This process uses the Google +and Kubernetes Terraform providers create GKE clusters. You connect the clusters to GitLab +by using the GitLab agent for Kubernetes. **Prerequisites:** -- A GitLab group. -- A GitLab user with the Maintainer role in the group. -- A [GitLab personal access token](../../../profile/personal_access_tokens.md) with `api` access, created by a user with at least the Maintainer role in the group. - A [Google Cloud Platform (GCP) service account](https://cloud.google.com/docs/authentication/getting-started). +- [A runner](https://docs.gitlab.com/runner/install/) you can use to run the GitLab CI/CD pipeline. **Steps:** 1. [Import the example project](#import-the-example-project). -1. [Create your GCP and GitLab credentials](#create-your-gcp-and-gitlab-credentials). +1. [Register the agent for Kubernetes](#register-the-agent). +1. [Create your GCP credentials](#create-your-gcp-credentials). 1. [Configure your project](#configure-your-project). -1. [Deploy your cluster](#deploy-your-cluster). +1. [Provision your cluster](#provision-your-cluster). ## Import the example project -To create a new group-level cluster from GitLab using Infrastructure as Code, it is necessary -to create a project to manage the cluster from. In this tutorial, we import a pre-configured -sample project to help you get started. +To create a cluster from GitLab using Infrastructure as Code, you must +create a project to manage the cluster from. In this tutorial, you start with +a sample project and modify it according to your needs. -Start by [importing the example project by URL](../../../project/import/repo_by_url.md). Use `https://gitlab.com/gitlab-org/configure/examples/gitlab-terraform-gke.git` as URL. +Start by [importing the example project by URL](../../../project/import/repo_by_url.md). -This project provides you with the following resources: +To import the project: + +1. On the top bar, select **Menu > Create new project**. +1. Select **Import project**. +1. Select **Repo by URL**. +1. For the **Git repository URL**, enter `https://gitlab.com/gitlab-org/configure/examples/gitlab-terraform-gke.git`. +1. Complete the fields and select **Create project**. + +This project provides you with: - A [cluster on Google Cloud Platform (GCP)](https://gitlab.com/gitlab-org/configure/examples/gitlab-terraform-gke/-/blob/master/gke.tf) with defaults for name, location, node count, and Kubernetes version. -- A [`gitlab-admin` K8s service account](https://gitlab.com/gitlab-org/configure/examples/gitlab-terraform-gke/-/blob/master/gitlab-admin.tf) with `cluster-admin` privileges. -- The new group-level cluster connected to GitLab. -- Pre-configures Terraform files: - - ```plaintext - ├── backend.tf # State file Location Configuration - ├── gke.tf # Google GKE Configuration - ├── gitlab-admin.tf # Adding kubernetes service account - └── group_cluster.tf # Registering kubernetes cluster to GitLab `apps` Group - ``` +- The [GitLab agent for Kubernetes](https://gitlab.com/gitlab-org/configure/examples/gitlab-terraform-gke/-/blob/master/agent.tf) installed in the cluster. -## Create your GCP and GitLab credentials +## Register the agent + +To create a GitLab agent for Kubernetes: + +1. On the left sidebar, select **Infrastructure > Kubernetes clusters**. +1. Select **Actions**. +1. From the **Select an agent** dropdown list, select `gke-agent` and select **Register an agent**. +1. GitLab generates a registration token for the agent. Securely store this secret token, as you will need it later. +1. GitLab provides an address for the agent server (KAS), which you will also need later. + +## Create your GCP credentials To set up your project to communicate to GCP and the GitLab API: @@ -85,18 +79,14 @@ The Admin role creates a service account in the `kube-system` namespace. ## Configure your project -**Required configuration:** - -Use CI/CD environment variables to configure your project as detailed below. +Use CI/CD environment variables to configure your project. **Required configuration:** 1. On the left sidebar, select **Settings > CI/CD**. 1. Expand **Variables**. -1. Set the variable `TF_VAR_gitlab_token` to the GitLab personal access token you just created. 1. Set the variable `BASE64_GOOGLE_CREDENTIALS` to the `base64` encoded JSON file you just created. 1. Set the variable `TF_VAR_gcp_project` to your GCP's `project` name. -1. Set the variable `TF_VAR_gitlab_group` to the name of the group you want to connect your cluster to. If your group's URL is `https://gitlab.example.com/my-example-group`, `my-example-group` is your group's name. **Optional configuration:** @@ -105,22 +95,57 @@ contains other variables that you can override according to your needs: - `TF_VAR_gcp_region`: Set your cluster's region. - `TF_VAR_cluster_name`: Set your cluster's name. -- `TF_VAR_machine_type`: Set the machine type for the Kubernetes nodes. - `TF_VAR_cluster_description`: Set a description for the cluster. We recommend setting this to `$CI_PROJECT_URL` to create a reference to your GitLab project on your GCP cluster detail page. This way you know which project was responsible for provisioning the cluster you see on the GCP dashboard. -- `TF_VAR_base_domain`: Set to the base domain to provision resources under. -- `TF_VAR_environment_scope`: Set to the environment scope for your cluster. +- `TF_VAR_machine_type`: Set the machine type for the Kubernetes nodes. +- `TF_VAR_node_count`: Set the number of Kubernetes nodes. +- `TF_VAR_agent_version`: Set the version of the GitLab agent. +- `TF_VAR_agent_namespace`: Set the Kubernetes namespace for the GitLab agent. -Refer to the [GitLab Terraform provider](https://registry.terraform.io/providers/gitlabhq/gitlab/latest/docs) and the [Google Terraform provider](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference) documentation for further resource options. +Refer to the [Google Terraform provider](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference) and the [Kubernetes Terraform provider](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs) documentation for further resource options. -## Deploy your cluster +## Provision your cluster -After configuring your project, manually trigger the deployment of your cluster. In GitLab: +After configuring your project, manually trigger the provisioning of your cluster. In GitLab: -1. From your project's sidebar, go to **CI/CD > Pipelines**. -1. Select the dropdown icon (**{angle-down}**) next to the play icon (**{play}**). -1. Select **deploy** to manually trigger the deployment job. +1. On the left sidebar, go to **CI/CD > Pipelines**. +1. Next to **Play** (**{play}**), select the dropdown icon (**{angle-down}**). +1. Select **Deploy** to manually trigger the deployment job. When the pipeline finishes successfully, you can see your new cluster: - In GCP: on your [GCP console's Kubernetes list](https://console.cloud.google.com/kubernetes/list). - In GitLab: from your project's sidebar, select **Infrastructure > Kubernetes clusters**. + +## Use your cluster + +After you provision the cluster, it is connected to GitLab and is ready for deployments. To check the connection: + +1. On the left sidebar, select **Infrastructure > Kubernetes clusters**. +1. In the list, view the **Connection status** column. + +For more information about the capabilities of the connection, see [the GitLab agent for Kubernetes documentation](../index.md). + +## Remove the cluster + +A cleanup job is not included in your pipeline by default. To remove all created resources, you +must modify your GitLab CI/CD template before running the cleanup job. + +To remove all resources: + +1. Add the following to your `.gitlab-ci.yml` file: + + ```yaml + stages: + - init + - validate + - build + - deploy + - cleanup + + destroy: + extends: .destroy + needs: [] + ``` + +1. On the left sidebar, select **CI/CD > Pipelines** and select the most recent pipeline. +1. For the `destroy` job, select **Play** (**{play}**). diff --git a/doc/user/infrastructure/iac/index.md b/doc/user/infrastructure/iac/index.md index 90014d81848..3bc7495d4be 100644 --- a/doc/user/infrastructure/iac/index.md +++ b/doc/user/infrastructure/iac/index.md @@ -108,14 +108,7 @@ is available as part of the official Terraform provider documentation. ## Create a new cluster through IaC - Learn how to [create a new cluster on Amazon Elastic Kubernetes Service (EKS)](../clusters/connect/new_eks_cluster.md). -- Learn how to [create a new cluster on Google Kubernetes Engine (GKE)](../clusters/connect/new_gke_cluster.md) (DEPRECATED). - -NOTE: -The linked GKE tutorial connects the cluster to GitLab through cluster certificates, -and this method was [deprecated](https://gitlab.com/groups/gitlab-org/configure/-/epics/8) -in GitLab 14.5. You can still create a cluster through IaC and then connect it to GitLab -through the [agent](../../clusters/agent/index.md), the default and fully supported -method to connect clusters to GitLab. +- Learn how to [create a new cluster on Google Kubernetes Engine (GKE)](../clusters/connect/new_gke_cluster.md). ## Troubleshooting diff --git a/doc/user/project/merge_requests/test_coverage_visualization.md b/doc/user/project/merge_requests/test_coverage_visualization.md index d51c6e6359e..16c5dbe9199 100644 --- a/doc/user/project/merge_requests/test_coverage_visualization.md +++ b/doc/user/project/merge_requests/test_coverage_visualization.md @@ -52,7 +52,12 @@ from any job in any stage in the pipeline. The coverage displays for each line: Hovering over the coverage bar provides further information, such as the number of times the line was checked by tests. -Uploading a test coverage report does not enable [test coverage results in Merge Requests](../../../ci/pipelines/settings.md#add-test-coverage-results-to-a-merge-request-deprecated) or [code coverage history](../../../ci/pipelines/settings.md#view-code-coverage-history). You must configure these separately. +Uploading a test coverage report does not enable: + +- [Test coverage results in merge requests](../../../ci/pipelines/settings.md#merge-request-test-coverage-results). +- [Code coverage history](../../../ci/pipelines/settings.md#view-code-coverage-history). + +You must configure these separately. ### Limits diff --git a/lib/api/package_files.rb b/lib/api/package_files.rb index e80355e80c7..4861c0c740e 100644 --- a/lib/api/package_files.rb +++ b/lib/api/package_files.rb @@ -29,7 +29,7 @@ module API .new(user_project, params[:package_id]).execute package_files = package.installable_package_files - .preload_pipelines + .preload_pipelines.order_id_asc present paginate(package_files), with: ::API::Entities::PackageFile end diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml index d5ca93a0a3b..f3d2e293c86 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - AUTO_BUILD_IMAGE_VERSION: 'v1.5.0' + AUTO_BUILD_IMAGE_VERSION: 'v1.9.1' build: stage: build @@ -7,7 +7,7 @@ build: variables: DOCKER_TLS_CERTDIR: '' services: - - name: 'docker:20.10.6-dind' + - name: 'docker:20.10.12-dind' command: ['--tls=false', '--host=tcp://0.0.0.0:2375'] script: - | diff --git a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml index d5ca93a0a3b..f3d2e293c86 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - AUTO_BUILD_IMAGE_VERSION: 'v1.5.0' + AUTO_BUILD_IMAGE_VERSION: 'v1.9.1' build: stage: build @@ -7,7 +7,7 @@ build: variables: DOCKER_TLS_CERTDIR: '' services: - - name: 'docker:20.10.6-dind' + - name: 'docker:20.10.12-dind' command: ['--tls=false', '--host=tcp://0.0.0.0:2375'] script: - | diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb index 6a637306225..259b430a73c 100644 --- a/lib/gitlab/error_tracking.rb +++ b/lib/gitlab/error_tracking.rb @@ -23,7 +23,12 @@ module Gitlab ].freeze class << self - def configure + def configure(&block) + configure_raven(&block) + configure_sentry(&block) + end + + def configure_raven Raven.configure do |config| config.dsn = sentry_dsn config.release = Gitlab.revision @@ -34,7 +39,20 @@ module Gitlab # Sanitize authentication headers config.sanitize_http_headers = %w[Authorization Private-Token] - config.before_send = method(:before_send) + config.before_send = method(:before_send_raven) + + yield config if block_given? + end + end + + def configure_sentry + Sentry.init do |config| + config.dsn = new_sentry_dsn + config.release = Gitlab.revision + config.environment = new_sentry_environment + config.before_send = method(:before_send_sentry) + config.background_worker_threads = 0 + config.send_default_pii = true yield config if block_given? end @@ -96,6 +114,18 @@ module Gitlab private + def before_send_raven(event, hint) + return unless Feature.enabled?(:enable_old_sentry_integration, default_enabled: :yaml) + + before_send(event, hint) + end + + def before_send_sentry(event, hint) + return unless Feature.enabled?(:enable_new_sentry_integration, default_enabled: :yaml) + + before_send(event, hint) + end + def before_send(event, hint) inject_context_for_exception(event, hint[:exception]) custom_fingerprinting(event, hint[:exception]) @@ -112,6 +142,13 @@ module Gitlab Raven.capture_exception(exception, **context_payload) end + # There is a possibility that this method is called before Sentry is + # configured. Since Sentry 4.0, some methods of Sentry are forwarded to + # to `nil`, hence we have to check the client as well. + if sentry && ::Sentry.get_current_client && ::Sentry.configuration.dsn + ::Sentry.capture_exception(exception, **context_payload) + end + if logging formatter = Gitlab::ErrorTracking::LogFormatter.new log_hash = formatter.generate_log(exception, context_payload) @@ -121,12 +158,30 @@ module Gitlab end def sentry_dsn - return unless Rails.env.production? || Rails.env.development? + return unless sentry_configurable? return unless Gitlab.config.sentry.enabled Gitlab.config.sentry.dsn end + def new_sentry_dsn + return unless sentry_configurable? + return unless Gitlab::CurrentSettings.respond_to?(:sentry_enabled?) + return unless Gitlab::CurrentSettings.sentry_enabled? + + Gitlab::CurrentSettings.sentry_dsn + end + + def new_sentry_environment + return unless Gitlab::CurrentSettings.respond_to?(:sentry_environment) + + Gitlab::CurrentSettings.sentry_environment + end + + def sentry_configurable? + Rails.env.production? || Rails.env.development? + end + def should_raise_for_dev? Rails.env.development? || Rails.env.test? end diff --git a/lib/gitlab/error_tracking/processor/grpc_error_processor.rb b/lib/gitlab/error_tracking/processor/grpc_error_processor.rb index e835deeea2c..045a18f4110 100644 --- a/lib/gitlab/error_tracking/processor/grpc_error_processor.rb +++ b/lib/gitlab/error_tracking/processor/grpc_error_processor.rb @@ -18,7 +18,7 @@ module Gitlab # only the first one since that's what is used for grouping. def process_first_exception_value(event) # Better in new version, will be event.exception.values - exceptions = event.instance_variable_get(:@interfaces)[:exception]&.values + exceptions = extract_exceptions_from(event) return unless exceptions.is_a?(Array) @@ -37,7 +37,13 @@ module Gitlab # instance variable if message.present? exceptions.each do |exception| - exception.value = message if valid_exception?(exception) + next unless valid_exception?(exception) + + if exception.respond_to?(:value=) + exception.value = message + else + exception.instance_variable_set(:@value, message) + end end end @@ -55,6 +61,14 @@ module Gitlab private + def extract_exceptions_from(event) + if event.is_a?(Raven::Event) + event.instance_variable_get(:@interfaces)[:exception]&.values + else + event.exception&.instance_variable_get(:@values) + end + end + def custom_grpc_fingerprint?(fingerprint) fingerprint.is_a?(Array) && fingerprint.length == 2 && fingerprint[0].start_with?('GRPC::') end @@ -71,7 +85,7 @@ module Gitlab def valid_exception?(exception) case exception - when Raven::SingleExceptionInterface + when Raven::SingleExceptionInterface, Sentry::SingleExceptionInterface exception&.value else false diff --git a/lib/gitlab/fips.rb b/lib/gitlab/fips.rb index 3b3eac6e5e2..1dd363ceb17 100644 --- a/lib/gitlab/fips.rb +++ b/lib/gitlab/fips.rb @@ -10,7 +10,13 @@ module Gitlab # # @return [Boolean] def enabled? - Feature.enabled?(:fips_mode, default_enabled: :yaml) + # Attempt to auto-detect FIPS mode from OpenSSL + return true if OpenSSL.fips_mode + + # Otherwise allow it to be set manually via the env vars + return true if ENV["FIPS_MODE"] == "true" + + false end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index f5d74ad1563..c76e006bae4 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -59,6 +59,7 @@ module Gitlab push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml) push_frontend_feature_flag(:sandboxed_mermaid, default_enabled: :yaml) push_frontend_feature_flag(:source_editor_toolbar, default_enabled: :yaml) + push_frontend_feature_flag(:gl_avatar_for_all_user_avatars, default_enabled: :yaml) end # Exposes the state of a feature flag to the frontend code. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 44df1e5363b..257a69adfd6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16840,6 +16840,9 @@ msgstr "" msgid "GlobalSearch|Search GitLab" msgstr "" +msgid "GlobalSearch|Search for projects, issues, etc." +msgstr "" + msgid "GlobalSearch|Search results are loading" msgstr "" @@ -16852,6 +16855,9 @@ msgstr "" msgid "GlobalSearch|Type for new suggestions to appear below." msgstr "" +msgid "GlobalSearch|What are you searching for?" +msgstr "" + msgid "GlobalSearch|in all GitLab" msgstr "" @@ -32377,6 +32383,9 @@ msgstr "" msgid "Secret token" msgstr "" +msgid "Secure Code Warrior" +msgstr "" + msgid "Secure Files" msgstr "" @@ -41416,9 +41425,6 @@ msgstr "" msgid "What are project audit events?" msgstr "" -msgid "What are you searching for?" -msgstr "" - msgid "What does this command do?" msgstr "" diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index fa01304ffe0..5dd627f3b76 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -23,6 +23,7 @@ RSpec.describe 'Project issue boards', :js do project.add_maintainer(user2) sign_in(user) + stub_feature_flags(gl_avatar_for_all_user_avatars: false) set_cookie('sidebar_collapsed', 'true') end diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb index 33c5a936b8d..fca40dc7edc 100644 --- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb +++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb @@ -25,6 +25,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do before do project.add_maintainer(user) sign_in user + stub_feature_flags(gl_avatar_for_all_user_avatars: false) set_cookie('sidebar_collapsed', 'true') end diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb index c04a4493a9b..a0016f82f0a 100644 --- a/spec/features/search/user_searches_for_code_spec.rb +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -42,7 +42,7 @@ RSpec.describe 'User searches for code' do it 'finds code and links to blob' do fill_in('dashboard_search', with: 'rspec') - find('.btn-search').click + find('.gl-search-box-by-click-search-button').click expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions') @@ -52,7 +52,7 @@ RSpec.describe 'User searches for code' do it 'finds code and links to blame' do fill_in('dashboard_search', with: 'rspec') - find('.btn-search').click + find('.gl-search-box-by-click-search-button').click expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions') @@ -65,7 +65,7 @@ RSpec.describe 'User searches for code' do search = 'for naming files' fill_in('dashboard_search', with: search) - find('.btn-search').click + find('.gl-search-box-by-click-search-button').click expect(page).to have_selector('.results', text: expected_result) @@ -94,7 +94,7 @@ RSpec.describe 'User searches for code' do expect(find('.js-project-refs-dropdown')).to have_text(ref_name) end it 'persists branch name across search' do - find('.btn-search').click + find('.gl-search-box-by-click-search-button').click expect(find('.js-project-refs-dropdown')).to have_text(ref_name) end diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb index b0902096770..c23a54594d4 100644 --- a/spec/features/search/user_searches_for_issues_spec.rb +++ b/spec/features/search/user_searches_for_issues_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'User searches for issues', :js do def search_for_issue(search) fill_in('dashboard_search', with: search) - find('.btn-search').click + find('.gl-search-box-by-click-search-button').click select_search_scope('Issues') end diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb index d7f490ba9bc..61c61d793db 100644 --- a/spec/features/search/user_searches_for_merge_requests_spec.rb +++ b/spec/features/search/user_searches_for_merge_requests_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'User searches for merge requests', :js do def search_for_mr(search) fill_in('dashboard_search', with: search) - find('.btn-search').click + find('.gl-search-box-by-click-search-button').click select_search_scope('Merge requests') end diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb index 7a1ec16385c..61f2e8e0c8f 100644 --- a/spec/features/search/user_searches_for_milestones_spec.rb +++ b/spec/features/search/user_searches_for_milestones_spec.rb @@ -20,7 +20,7 @@ RSpec.describe 'User searches for milestones', :js do it 'finds a milestone' do fill_in('dashboard_search', with: milestone1.title) - find('.btn-search').click + find('.gl-search-box-by-click-search-button').click select_search_scope('Milestones') page.within('.results') do @@ -40,7 +40,7 @@ RSpec.describe 'User searches for milestones', :js do end fill_in('dashboard_search', with: milestone1.title) - find('.btn-search').click + find('.gl-search-box-by-click-search-button').click select_search_scope('Milestones') page.within('.results') do diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb index 06545d8640f..9808383adb7 100644 --- a/spec/features/search/user_searches_for_wiki_pages_spec.rb +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -28,7 +28,7 @@ RSpec.describe 'User searches for wiki pages', :js do end fill_in('dashboard_search', with: search_term) - find('.btn-search').click + find('.gl-search-box-by-click-search-button').click select_search_scope('Wiki') page.within('.results') do diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap index 9938357ed24..841a9bf8290 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap @@ -58,7 +58,7 @@ exports[`Settings Form Remove regex matches snapshot 1`] = ` error="" label="Remove tags matching:" name="remove-regex" - placeholder=".*" + placeholder="" value="asdasdssssdfdf" /> `; diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js index 625aa37fc0f..266f953c3e0 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js @@ -49,6 +49,11 @@ describe('Settings Form', () => { const findOlderThanDropdown = () => wrapper.find('[data-testid="older-than-dropdown"]'); const findRemoveRegexInput = () => wrapper.find('[data-testid="remove-regex-input"]'); + const submitForm = async () => { + findForm().trigger('submit'); + return waitForPromises(); + }; + const mountComponent = ({ props = defaultProps, data, @@ -318,27 +323,24 @@ describe('Settings Form', () => { mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), }); - findForm().trigger('submit'); - await waitForPromises(); - await nextTick(); + await submitForm(); expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE); }); describe('when submit fails', () => { describe('user recoverable errors', () => { - it('when there is an error is shown in a toast', async () => { + it('when there is an error is shown in the nameRegex field t', async () => { mountComponentWithApollo({ mutationResolver: jest .fn() .mockResolvedValue(expirationPolicyMutationPayload({ errors: ['foo'] })), }); - findForm().trigger('submit'); - await waitForPromises(); - await nextTick(); + await submitForm(); - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('foo'); + expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE); + expect(findRemoveRegexInput().props('error')).toBe('foo'); }); }); @@ -348,9 +350,7 @@ describe('Settings Form', () => { mutationResolver: jest.fn().mockRejectedValue(expirationPolicyMutationPayload()), }); - findForm().trigger('submit'); - await waitForPromises(); - await nextTick(); + await submitForm(); expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE); }); @@ -367,9 +367,7 @@ describe('Settings Form', () => { }); mountComponent({ mocks: { $apollo: { mutate } } }); - findForm().trigger('submit'); - await waitForPromises(); - await nextTick(); + await submitForm(); expect(findKeepRegexInput().props('error')).toEqual('baz'); }); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/graphql/cache_updated_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/graphql/cache_updated_spec.js index 4d6bd65bd93..76d5f8a6659 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/graphql/cache_updated_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/graphql/cache_updated_spec.js @@ -4,15 +4,15 @@ import { updateContainerExpirationPolicy } from '~/packages_and_registries/setti describe('Registry settings cache update', () => { let client; - const payload = { + const payload = (value) => ({ data: { updateContainerExpirationPolicy: { containerExpirationPolicy: { - enabled: true, + ...value, }, }, }, - }; + }); const cacheMock = { project: { @@ -35,12 +35,12 @@ describe('Registry settings cache update', () => { }); describe('Registry settings cache update', () => { it('calls readQuery', () => { - updateContainerExpirationPolicy('foo')(client, payload); + updateContainerExpirationPolicy('foo')(client, payload({ enabled: true })); expect(client.readQuery).toHaveBeenCalledWith(queryAndVariables); }); it('writes the correct result in the cache', () => { - updateContainerExpirationPolicy('foo')(client, payload); + updateContainerExpirationPolicy('foo')(client, payload({ enabled: true })); expect(client.writeQuery).toHaveBeenCalledWith({ ...queryAndVariables, data: { @@ -52,5 +52,20 @@ describe('Registry settings cache update', () => { }, }); }); + + it('with an empty update preserves the state', () => { + updateContainerExpirationPolicy('foo')(client, payload()); + + expect(client.writeQuery).toHaveBeenCalledWith({ + ...queryAndVariables, + data: { + project: { + containerExpirationPolicy: { + enabled: false, + }, + }, + }, + }); + }); }); }); diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js index ee682b18af3..5f1aff99578 100644 --- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js +++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js @@ -9,7 +9,6 @@ import { testActions, testSections, testProject } from './mock_data'; describe('Learn GitLab', () => { let wrapper; let sidebar; - let inviteMembers = false; const createWrapper = () => { wrapper = mount(LearnGitlab, { @@ -17,7 +16,6 @@ describe('Learn GitLab', () => { actions: testActions, sections: testSections, project: testProject, - inviteMembers, }, }); }; @@ -38,7 +36,6 @@ describe('Learn GitLab', () => { afterEach(() => { wrapper.destroy(); wrapper = null; - inviteMembers = false; sidebar.remove(); }); @@ -73,7 +70,6 @@ describe('Learn GitLab', () => { }); it('emits openModal', () => { - inviteMembers = true; Cookies.set(INVITE_MODAL_OPEN_COOKIE, true); createWrapper(); @@ -86,19 +82,11 @@ describe('Learn GitLab', () => { }); it('does not emit openModal when cookie is not set', () => { - inviteMembers = true; - createWrapper(); expect(spy).not.toHaveBeenCalled(); expect(cookieSpy).toHaveBeenCalledWith(INVITE_MODAL_OPEN_COOKIE); }); - - it('does not emit openModal when inviteMembers is false', () => { - createWrapper(); - - expect(spy).not.toHaveBeenCalled(); - }); }); describe('when the showSuccessfulInvitationsAlert event is fired', () => { diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js index 7ce5efb3c52..0a44688bfe0 100644 --- a/spec/frontend/search/topbar/components/app_spec.js +++ b/spec/frontend/search/topbar/components/app_spec.js @@ -1,4 +1,4 @@ -import { GlForm, GlSearchBoxByType, GlButton } from '@gitlab/ui'; +import { GlSearchBoxByClick } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; @@ -36,40 +36,19 @@ describe('GlobalSearchTopbar', () => { wrapper.destroy(); }); - const findTopbarForm = () => wrapper.find(GlForm); - const findGlSearchBox = () => wrapper.find(GlSearchBoxByType); + const findGlSearchBox = () => wrapper.find(GlSearchBoxByClick); const findGroupFilter = () => wrapper.find(GroupFilter); const findProjectFilter = () => wrapper.find(ProjectFilter); - const findSearchButton = () => wrapper.find(GlButton); describe('template', () => { beforeEach(() => { createComponent(); }); - it('renders Topbar Form always', () => { - expect(findTopbarForm().exists()).toBe(true); - }); - describe('Search box', () => { it('renders always', () => { expect(findGlSearchBox().exists()).toBe(true); }); - - describe('onSearch', () => { - const testSearch = 'test search'; - - beforeEach(() => { - findGlSearchBox().vm.$emit('input', testSearch); - }); - - it('calls setQuery when input event is fired from GlSearchBoxByType', () => { - expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), { - key: 'search', - value: testSearch, - }); - }); - }); }); describe.each` @@ -92,10 +71,6 @@ describe('GlobalSearchTopbar', () => { expect(findProjectFilter().exists()).toBe(showFilters); }); }); - - it('renders SearchButton always', () => { - expect(findSearchButton().exists()).toBe(true); - }); }); describe('actions', () => { @@ -103,8 +78,8 @@ describe('GlobalSearchTopbar', () => { createComponent(); }); - it('clicking SearchButton calls applyQuery', () => { - findTopbarForm().vm.$emit('submit', { preventDefault: () => {} }); + it('clicking search button inside search box calls applyQuery', () => { + findGlSearchBox().vm.$emit('submit', { preventDefault: () => {} }); expect(actionSpies.applyQuery).toHaveBeenCalled(); }); diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js index 08ba4bcbf69..5e2efa2425c 100644 --- a/spec/frontend/security_configuration/components/training_provider_list_spec.js +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -26,6 +26,7 @@ import { updateSecurityTrainingProvidersErrorResponse, testProjectPath, testProviderIds, + testProviderName, tempProviderLogos, } from '../mock_data'; @@ -207,9 +208,13 @@ describe('TrainingProviderList component', () => { expect(findLogos().at(provider).attributes('width')).toBe('18'); }); + it.each(providerIndexArray)('has a11y decorative attribute for provider %s', (provider) => { + expect(findLogos().at(provider).attributes('role')).toBe('presentation'); + }); + it.each(providerIndexArray)('displays the correct svg path for provider %s', (provider) => { expect(findLogos().at(provider).attributes('src')).toBe( - tempProviderLogos[testProviderIds[provider]].svg, + tempProviderLogos[testProviderName[provider]].svg, ); }); }); diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js index 588fac11987..3bad687740c 100644 --- a/spec/frontend/security_configuration/mock_data.js +++ b/spec/frontend/security_configuration/mock_data.js @@ -1,11 +1,11 @@ export const testProjectPath = 'foo/bar'; - export const testProviderIds = [101, 102, 103]; +export const testProviderName = ['Vendor Name 1', 'Vendor Name 2', 'Vendor Name 3']; const createSecurityTrainingProviders = ({ providerOverrides = {} }) => [ { id: testProviderIds[0], - name: 'Vendor Name 1', + name: testProviderName[0], description: 'Interactive developer security education', url: 'https://www.example.org/security/training', isEnabled: false, @@ -14,7 +14,7 @@ const createSecurityTrainingProviders = ({ providerOverrides = {} }) => [ }, { id: testProviderIds[1], - name: 'Vendor Name 2', + name: testProviderName[1], description: 'Security training with guide and learning pathways.', url: 'https://www.vendornametwo.com/', isEnabled: false, @@ -23,7 +23,7 @@ const createSecurityTrainingProviders = ({ providerOverrides = {} }) => [ }, { id: testProviderIds[2], - name: 'Vendor Name 3', + name: testProviderName[2], description: 'Security training for the everyday developer.', url: 'https://www.vendornamethree.com/', isEnabled: false, @@ -99,10 +99,10 @@ export const updateSecurityTrainingProvidersErrorResponse = { // Will remove once this issue is resolved where the svg path will be available in the GraphQL query // https://gitlab.com/gitlab-org/gitlab/-/issues/346899 export const tempProviderLogos = { - [testProviderIds[0]]: { + [testProviderName[0]]: { svg: '/assets/illustrations/vulnerability/vendor-1.svg', }, - [testProviderIds[1]]: { + [testProviderName[1]]: { svg: '/assets/illustrations/vulnerability/vendor-2.svg', }, }; diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js index 2c3fc70e116..64ce210b6c8 100644 --- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js @@ -1,12 +1,13 @@ import { shallowMount } from '@vue/test-utils'; +import { GlAvatar, GlTooltip } from '@gitlab/ui'; import defaultAvatarUrl from 'images/no_avatar.png'; import { placeholderImage } from '~/lazy_loader'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; jest.mock('images/no_avatar.png', () => 'default-avatar-url'); -const DEFAULT_PROPS = { - size: 99, +const PROVIDED_PROPS = { + size: 32, imgSrc: 'myavatarurl.com', imgAlt: 'mydisplayname', cssClasses: 'myextraavatarclass', @@ -14,6 +15,10 @@ const DEFAULT_PROPS = { tooltipPlacement: 'bottom', }; +const DEFAULT_PROPS = { + size: 20, +}; + describe('User Avatar Image Component', () => { let wrapper; @@ -21,64 +26,149 @@ describe('User Avatar Image Component', () => { wrapper.destroy(); }); - describe('Initialization', () => { - beforeEach(() => { - wrapper = shallowMount(UserAvatarImage, { - propsData: { - ...DEFAULT_PROPS, - }, + describe('`glAvatarForAllUserAvatars` feature flag enabled', () => { + describe('Initialization', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...PROVIDED_PROPS, + }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars: true, + }, + }, + }); + }); + + it('should render `GlAvatar` and provide correct properties to it', () => { + const avatar = wrapper.findComponent(GlAvatar); + + expect(avatar.attributes('data-src')).toBe( + `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + ); + expect(avatar.props()).toMatchObject({ + src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + alt: PROVIDED_PROPS.imgAlt, + }); + }); + + it('should add correct CSS classes', () => { + const classes = wrapper.findComponent(GlAvatar).classes(); + expect(classes).toContain(PROVIDED_PROPS.cssClasses); + expect(classes).not.toContain('lazy'); }); }); - it('should have <img> as a child element', () => { - const imageElement = wrapper.find('img'); + describe('Initialization when lazy', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...PROVIDED_PROPS, + lazy: true, + }, + provide: { + glFeatures: { + glAvatarForAllUserAvatars: true, + }, + }, + }); + }); + + it('should add lazy attributes', () => { + const avatar = wrapper.findComponent(GlAvatar); - expect(imageElement.exists()).toBe(true); - expect(imageElement.attributes('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - expect(imageElement.attributes('alt')).toBe(DEFAULT_PROPS.imgAlt); + expect(avatar.classes()).toContain('lazy'); + expect(avatar.attributes()).toMatchObject({ + src: placeholderImage, + 'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + }); + }); }); - it('should properly render img css', () => { - const classes = wrapper.find('img').classes(); - expect(classes).toEqual(expect.arrayContaining(['avatar', 's99', DEFAULT_PROPS.cssClasses])); - expect(classes).not.toContain('lazy'); + describe('Initialization without src', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage); + }); + + it('should have default avatar image', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.attributes('src')).toBe( + `${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`, + ); + }); }); }); - describe('Initialization when lazy', () => { - beforeEach(() => { - wrapper = shallowMount(UserAvatarImage, { - propsData: { - ...DEFAULT_PROPS, - lazy: true, - }, + describe('`glAvatarForAllUserAvatars` feature flag disabled', () => { + describe('Initialization', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...PROVIDED_PROPS, + }, + }); }); - }); - it('should add lazy attributes', () => { - const imageElement = wrapper.find('img'); + it('should have <img> as a child element', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.exists()).toBe(true); + expect(imageElement.attributes('src')).toBe( + `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + ); + expect(imageElement.attributes('data-src')).toBe( + `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + ); + expect(imageElement.attributes('alt')).toBe(PROVIDED_PROPS.imgAlt); + }); - expect(imageElement.classes()).toContain('lazy'); - expect(imageElement.attributes('src')).toBe(placeholderImage); - expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + it('should properly render img css', () => { + const classes = wrapper.find('img').classes(); + expect(classes).toEqual(['avatar', 's32', PROVIDED_PROPS.cssClasses]); + expect(classes).not.toContain('lazy'); + }); }); - }); - describe('Initialization without src', () => { - beforeEach(() => { - wrapper = shallowMount(UserAvatarImage); + describe('Initialization when lazy', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...PROVIDED_PROPS, + lazy: true, + }, + }); + }); + + it('should add lazy attributes', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.classes()).toContain('lazy'); + expect(imageElement.attributes('src')).toBe(placeholderImage); + expect(imageElement.attributes('data-src')).toBe( + `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`, + ); + }); }); - it('should have default avatar image', () => { - const imageElement = wrapper.find('img'); + describe('Initialization without src', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage); + }); + + it('should have default avatar image', () => { + const imageElement = wrapper.find('img'); - expect(imageElement.attributes('src')).toBe(`${defaultAvatarUrl}?width=20`); + expect(imageElement.attributes('src')).toBe( + `${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`, + ); + }); }); }); describe('dynamic tooltip content', () => { - const props = DEFAULT_PROPS; + const props = PROVIDED_PROPS; const slots = { default: ['Action!'], }; @@ -91,11 +181,11 @@ describe('User Avatar Image Component', () => { }); it('renders the tooltip slot', () => { - expect(wrapper.find('.js-user-avatar-image-tooltip').exists()).toBe(true); + expect(wrapper.findComponent(GlTooltip).exists()).toBe(true); }); it('renders the tooltip content', () => { - expect(wrapper.find('.js-user-avatar-image-tooltip').text()).toContain(slots.default[0]); + expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]); }); it('does not render tooltip data attributes for on avatar image', () => { diff --git a/spec/lib/api/entities/ci/job_artifact_file_spec.rb b/spec/lib/api/entities/ci/job_artifact_file_spec.rb new file mode 100644 index 00000000000..9e4ec272518 --- /dev/null +++ b/spec/lib/api/entities/ci/job_artifact_file_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Ci::JobArtifactFile do + let(:artifact_file) { instance_double(JobArtifactUploader, filename: 'ci_build_artifacts.zip', cached_size: 42) } + let(:entity) { described_class.new(artifact_file) } + + subject { entity.as_json } + + it 'returns the filename' do + expect(subject[:filename]).to eq('ci_build_artifacts.zip') + end + + it 'returns the size' do + expect(subject[:size]).to eq(42) + end +end diff --git a/spec/lib/api/entities/ci/job_request/dependency_spec.rb b/spec/lib/api/entities/ci/job_request/dependency_spec.rb new file mode 100644 index 00000000000..fa5f3da554c --- /dev/null +++ b/spec/lib/api/entities/ci/job_request/dependency_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Ci::JobRequest::Dependency do + let(:job) { create(:ci_build, :artifacts) } + let(:entity) { described_class.new(job) } + + subject { entity.as_json } + + it 'returns the dependency id' do + expect(subject[:id]).to eq(job.id) + end + + it 'returns the dependency name' do + expect(subject[:name]).to eq(job.name) + end + + it 'returns the dependency token' do + expect(subject[:token]).to eq(job.token) + end + + it 'returns the dependency artifacts_file', :aggregate_failures do + expect(subject[:artifacts_file][:filename]).to eq('ci_build_artifacts.zip') + expect(subject[:artifacts_file][:size]).to eq(job.artifacts_size) + end +end diff --git a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb index 9acc7fd04be..33d322d0d44 100644 --- a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb +++ b/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor do +RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor, :sentry do describe '.call' do - let(:required_options) do + let(:raven_required_options) do { configuration: Raven.configuration, context: Raven.context, @@ -12,7 +12,15 @@ RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor do } end - let(:event) { Raven::Event.from_exception(exception, required_options.merge(data)) } + let(:raven_event) do + Raven::Event + .from_exception(exception, raven_required_options.merge(data)) + end + + let(:sentry_event) do + Sentry.get_current_client.event_from_exception(exception) + end + let(:result_hash) { described_class.call(event).to_hash } let(:data) do @@ -27,36 +35,43 @@ RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor do } end + before do + Sentry.get_current_scope.update_from_options(**data) + Sentry.get_current_scope.apply_to_event(sentry_event) + end + + after do + Sentry.get_current_scope.clear + end + context 'when there is no GRPC exception' do let(:exception) { RuntimeError.new } let(:data) { { fingerprint: ['ArgumentError', 'Missing arguments'] } } - it 'leaves data unchanged' do - expect(result_hash).to include(data) + shared_examples 'leaves data unchanged' do + it { expect(result_hash).to include(data) } end - end - context 'when there is a GRPC exception with a debug string' do - let(:exception) { GRPC::DeadlineExceeded.new('Deadline Exceeded', {}, '{"hello":1}') } + context 'with Raven event' do + let(:event) { raven_event } - it 'removes the debug error string and stores it as an extra field' do - expect(result_hash[:fingerprint]) - .to eq(['GRPC::DeadlineExceeded', '4:Deadline Exceeded.']) + it_behaves_like 'leaves data unchanged' + end - expect(result_hash[:exception][:values].first) - .to include(type: 'GRPC::DeadlineExceeded', value: '4:Deadline Exceeded.') + context 'with Sentry event' do + let(:event) { sentry_event } - expect(result_hash[:extra]) - .to include(caller: 'test', grpc_debug_error_string: '{"hello":1}') + it_behaves_like 'leaves data unchanged' end + end - context 'with no custom fingerprint' do - let(:data) do - { extra: { caller: 'test' } } - end + context 'when there is a GRPC exception with a debug string' do + let(:exception) { GRPC::DeadlineExceeded.new('Deadline Exceeded', {}, '{"hello":1}') } + shared_examples 'processes the exception' do it 'removes the debug error string and stores it as an extra field' do - expect(result_hash).not_to include(:fingerprint) + expect(result_hash[:fingerprint]) + .to eq(['GRPC::DeadlineExceeded', '4:Deadline Exceeded.']) expect(result_hash[:exception][:values].first) .to include(type: 'GRPC::DeadlineExceeded', value: '4:Deadline Exceeded.') @@ -64,11 +79,42 @@ RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor do expect(result_hash[:extra]) .to include(caller: 'test', grpc_debug_error_string: '{"hello":1}') end + + context 'with no custom fingerprint' do + let(:data) do + { extra: { caller: 'test' } } + end + + it 'removes the debug error string and stores it as an extra field' do + expect(result_hash[:fingerprint]).to be_blank + + expect(result_hash[:exception][:values].first) + .to include(type: 'GRPC::DeadlineExceeded', value: '4:Deadline Exceeded.') + + expect(result_hash[:extra]) + .to include(caller: 'test', grpc_debug_error_string: '{"hello":1}') + end + end + end + + context 'with Raven event' do + let(:event) { raven_event } + + it_behaves_like 'processes the exception' + end + + context 'with Sentry event' do + let(:event) { sentry_event } + + it_behaves_like 'processes the exception' end end context 'when there is a wrapped GRPC exception with a debug string' do - let(:inner_exception) { GRPC::DeadlineExceeded.new('Deadline Exceeded', {}, '{"hello":1}') } + let(:inner_exception) do + GRPC::DeadlineExceeded.new('Deadline Exceeded', {}, '{"hello":1}') + end + let(:exception) do begin raise inner_exception @@ -79,27 +125,10 @@ RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor do e end - it 'removes the debug error string and stores it as an extra field' do - expect(result_hash[:fingerprint]) - .to eq(['GRPC::DeadlineExceeded', '4:Deadline Exceeded.']) - - expect(result_hash[:exception][:values].first) - .to include(type: 'GRPC::DeadlineExceeded', value: '4:Deadline Exceeded.') - - expect(result_hash[:exception][:values].second) - .to include(type: 'StandardError', value: '4:Deadline Exceeded.') - - expect(result_hash[:extra]) - .to include(caller: 'test', grpc_debug_error_string: '{"hello":1}') - end - - context 'with no custom fingerprint' do - let(:data) do - { extra: { caller: 'test' } } - end - + shared_examples 'processes the exception' do it 'removes the debug error string and stores it as an extra field' do - expect(result_hash).not_to include(:fingerprint) + expect(result_hash[:fingerprint]) + .to eq(['GRPC::DeadlineExceeded', '4:Deadline Exceeded.']) expect(result_hash[:exception][:values].first) .to include(type: 'GRPC::DeadlineExceeded', value: '4:Deadline Exceeded.') @@ -110,6 +139,37 @@ RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor do expect(result_hash[:extra]) .to include(caller: 'test', grpc_debug_error_string: '{"hello":1}') end + + context 'with no custom fingerprint' do + let(:data) do + { extra: { caller: 'test' } } + end + + it 'removes the debug error string and stores it as an extra field' do + expect(result_hash[:fingerprint]).to be_blank + + expect(result_hash[:exception][:values].first) + .to include(type: 'GRPC::DeadlineExceeded', value: '4:Deadline Exceeded.') + + expect(result_hash[:exception][:values].second) + .to include(type: 'StandardError', value: '4:Deadline Exceeded.') + + expect(result_hash[:extra]) + .to include(caller: 'test', grpc_debug_error_string: '{"hello":1}') + end + end + end + + context 'with Raven event' do + let(:event) { raven_event } + + it_behaves_like 'processes the exception' + end + + context 'with Sentry event' do + let(:event) { sentry_event } + + it_behaves_like 'processes the exception' end end end diff --git a/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb index 3febc10831a..d33f8393904 100644 --- a/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb +++ b/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require 'rspec-parameterized' -RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do +RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor, :sentry do after do if described_class.instance_variable_defined?(:@permitted_arguments_for_worker) described_class.remove_instance_variable(:@permitted_arguments_for_worker) @@ -95,7 +95,9 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do end describe '.call' do - let(:required_options) do + let(:exception) { StandardError.new('Test exception') } + + let(:raven_required_options) do { configuration: Raven.configuration, context: Raven.context, @@ -103,9 +105,25 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do } end - let(:event) { Raven::Event.new(required_options.merge(wrapped_value)) } + let(:raven_event) do + Raven::Event.new(raven_required_options.merge(wrapped_value)) + end + + let(:sentry_event) do + Sentry.get_current_client.event_from_exception(exception) + end + let(:result_hash) { described_class.call(event).to_hash } + before do + Sentry.get_current_scope.update_from_options(**wrapped_value) + Sentry.get_current_scope.apply_to_event(sentry_event) + end + + after do + Sentry.get_current_scope.clear + end + context 'when there is Sidekiq data' do let(:wrapped_value) { { extra: { sidekiq: value } } } @@ -140,42 +158,90 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do end context 'when processing via the default error handler' do - include_examples 'Sidekiq arguments', args_in_job_hash: true + context 'with Raven events' do + let(:event) { raven_event} + + include_examples 'Sidekiq arguments', args_in_job_hash: true + end + + context 'with Sentry events' do + let(:event) { sentry_event} + + include_examples 'Sidekiq arguments', args_in_job_hash: true + end end context 'when processing via Gitlab::ErrorTracking' do - include_examples 'Sidekiq arguments', args_in_job_hash: false - end + context 'with Raven events' do + let(:event) { raven_event} - context 'when a jobstr field is present' do - let(:value) do - { - job: { 'args' => [1] }, - jobstr: { 'args' => [1] }.to_json - } + include_examples 'Sidekiq arguments', args_in_job_hash: false end - it 'removes the jobstr' do - expect(result_hash.dig(:extra, :sidekiq)).to eq(value.except(:jobstr)) + context 'with Sentry events' do + let(:event) { sentry_event} + + include_examples 'Sidekiq arguments', args_in_job_hash: false end end - context 'when no jobstr value is present' do - let(:value) { { job: { 'args' => [1] } } } + shared_examples 'handles jobstr fields' do + context 'when a jobstr field is present' do + let(:value) do + { + job: { 'args' => [1] }, + jobstr: { 'args' => [1] }.to_json + } + end + + it 'removes the jobstr' do + expect(result_hash.dig(:extra, :sidekiq)).to eq(value.except(:jobstr)) + end + end + + context 'when no jobstr value is present' do + let(:value) { { job: { 'args' => [1] } } } - it 'does nothing' do - expect(result_hash.dig(:extra, :sidekiq)).to eq(value) + it 'does nothing' do + expect(result_hash.dig(:extra, :sidekiq)).to eq(value) + end end end + + context 'with Raven events' do + let(:event) { raven_event} + + it_behaves_like 'handles jobstr fields' + end + + context 'with Sentry events' do + let(:event) { sentry_event} + + it_behaves_like 'handles jobstr fields' + end end context 'when there is no Sidekiq data' do let(:value) { { tags: { foo: 'bar', baz: 'quux' } } } let(:wrapped_value) { value } - it 'does nothing' do - expect(result_hash).to include(value) - expect(result_hash.dig(:extra, :sidekiq)).to be_nil + shared_examples 'does nothing' do + it 'does nothing' do + expect(result_hash).to include(value) + expect(result_hash.dig(:extra, :sidekiq)).to be_nil + end + end + + context 'with Raven events' do + let(:event) { raven_event} + + it_behaves_like 'does nothing' + end + + context 'with Sentry events' do + let(:event) { sentry_event} + + it_behaves_like 'does nothing' end end @@ -183,8 +249,22 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do let(:value) { { other: 'foo' } } let(:wrapped_value) { { extra: { sidekiq: value } } } - it 'does nothing' do - expect(result_hash.dig(:extra, :sidekiq)).to eq(value) + shared_examples 'does nothing' do + it 'does nothing' do + expect(result_hash.dig(:extra, :sidekiq)).to eq(value) + end + end + + context 'with Raven events' do + let(:event) { raven_event} + + it_behaves_like 'does nothing' + end + + context 'with Sentry events' do + let(:event) { sentry_event} + + it_behaves_like 'does nothing' end end end diff --git a/spec/lib/gitlab/error_tracking_spec.rb b/spec/lib/gitlab/error_tracking_spec.rb index a5d44963f4b..936954fc1b6 100644 --- a/spec/lib/gitlab/error_tracking_spec.rb +++ b/spec/lib/gitlab/error_tracking_spec.rb @@ -3,13 +3,14 @@ require 'spec_helper' require 'raven/transports/dummy' +require 'sentry/transport/dummy_transport' RSpec.describe Gitlab::ErrorTracking do let(:exception) { RuntimeError.new('boom') } let(:issue_url) { 'http://gitlab.com/gitlab-org/gitlab-foss/issues/1' } let(:extra) { { issue_url: issue_url, some_other_info: 'info' } } - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } let(:sentry_payload) do { @@ -43,17 +44,28 @@ RSpec.describe Gitlab::ErrorTracking do } end - let(:sentry_event) { Gitlab::Json.parse(Raven.client.transport.events.last[1]) } + let(:raven_event) do + event = Raven.client.transport.events.last[1] + Gitlab::Json.parse(event) + end + + let(:sentry_event) do + Sentry.get_current_client.transport.events.last + end before do + stub_feature_flags(enable_old_sentry_integration: true) + stub_feature_flags(enable_new_sentry_integration: true) stub_sentry_settings - allow(described_class).to receive(:sentry_dsn).and_return(Gitlab.config.sentry.dsn) + allow(described_class).to receive(:sentry_configurable?) { true } + allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid') allow(I18n).to receive(:locale).and_return('en') described_class.configure do |config| - config.encoding = 'json' + config.encoding = 'json' if config.respond_to?(:encoding=) + config.transport.transport_class = Sentry::DummyTransport if config.respond_to?(:transport) end end @@ -63,6 +75,10 @@ RSpec.describe Gitlab::ErrorTracking do end end + after do + Sentry.get_current_scope.clear + end + describe '.track_and_raise_for_dev_exception' do context 'when exceptions for dev should be raised' do before do @@ -71,6 +87,7 @@ RSpec.describe Gitlab::ErrorTracking do it 'raises the exception' do expect(Raven).to receive(:capture_exception).with(exception, sentry_payload) + expect(Sentry).to receive(:capture_exception).with(exception, sentry_payload) expect do described_class.track_and_raise_for_dev_exception( @@ -89,6 +106,7 @@ RSpec.describe Gitlab::ErrorTracking do it 'logs the exception with all attributes passed' do expect(Raven).to receive(:capture_exception).with(exception, sentry_payload) + expect(Sentry).to receive(:capture_exception).with(exception, sentry_payload) described_class.track_and_raise_for_dev_exception( exception, @@ -112,6 +130,7 @@ RSpec.describe Gitlab::ErrorTracking do describe '.track_and_raise_exception' do it 'always raises the exception' do expect(Raven).to receive(:capture_exception).with(exception, sentry_payload) + expect(Sentry).to receive(:capture_exception).with(exception, sentry_payload) expect do described_class.track_and_raise_for_dev_exception( @@ -136,20 +155,24 @@ RSpec.describe Gitlab::ErrorTracking do end describe '.track_exception' do - subject(:track_exception) { described_class.track_exception(exception, extra) } + subject(:track_exception) do + described_class.track_exception(exception, extra) + end before do allow(Raven).to receive(:capture_exception).and_call_original + allow(Sentry).to receive(:capture_exception).and_call_original allow(Gitlab::ErrorTracking::Logger).to receive(:error) end it 'calls Raven.capture_exception' do track_exception - expect(Raven).to have_received(:capture_exception).with( - exception, - sentry_payload - ) + expect(Raven) + .to have_received(:capture_exception).with(exception, sentry_payload) + + expect(Sentry) + .to have_received(:capture_exception).with(exception, sentry_payload) end it 'calls Gitlab::ErrorTracking::Logger.error with formatted payload' do @@ -172,7 +195,10 @@ RSpec.describe Gitlab::ErrorTracking do context 'the exception implements :sentry_extra_data' do let(:extra_info) { { event: 'explosion', size: :massive } } - let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info, backtrace: caller, cause: nil) } + + before do + allow(exception).to receive(:sentry_extra_data).and_return(extra_info) + end it 'includes the extra data from the exception in the tracking information' do track_exception @@ -180,29 +206,30 @@ RSpec.describe Gitlab::ErrorTracking do expect(Raven).to have_received(:capture_exception).with( exception, a_hash_including(extra: a_hash_including(extra_info)) ) + + expect(Sentry).to have_received(:capture_exception).with( + exception, a_hash_including(extra: a_hash_including(extra_info)) + ) end end context 'the exception implements :sentry_extra_data, which returns nil' do - let(:exception) { double(message: 'bang!', sentry_extra_data: nil, backtrace: caller, cause: nil) } let(:extra) { { issue_url: issue_url } } + before do + allow(exception).to receive(:sentry_extra_data).and_return(nil) + end + it 'just includes the other extra info' do track_exception expect(Raven).to have_received(:capture_exception).with( exception, a_hash_including(extra: a_hash_including(extra)) ) - end - end - - context 'when the error is kind of an `ActiveRecord::StatementInvalid`' do - let(:exception) { ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1') } - it 'injects the normalized sql query into extra' do - track_exception - - expect(sentry_event.dig('extra', 'sql')).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1') + expect(Sentry).to have_received(:capture_exception).with( + exception, a_hash_including(extra: a_hash_including(extra)) + ) end end end @@ -212,32 +239,65 @@ RSpec.describe Gitlab::ErrorTracking do before do allow(Raven).to receive(:capture_exception).and_call_original + allow(Sentry).to receive(:capture_exception).and_call_original allow(Gitlab::ErrorTracking::Logger).to receive(:error) end context 'custom GitLab context when using Raven.capture_exception directly' do - subject(:raven_capture_exception) { Raven.capture_exception(exception) } + subject(:track_exception) { Raven.capture_exception(exception) } it 'merges a default set of tags into the existing tags' do allow(Raven.context).to receive(:tags).and_return(foo: 'bar') - raven_capture_exception + track_exception - expect(sentry_event['tags']).to include('correlation_id', 'feature_category', 'foo', 'locale', 'program') + expect(raven_event['tags']).to include('correlation_id', 'feature_category', 'foo', 'locale', 'program') end it 'merges the current user information into the existing user information' do Raven.user_context(id: -1) - raven_capture_exception + track_exception - expect(sentry_event['user']).to eq('id' => -1, 'username' => user.username) + expect(raven_event['user']).to eq('id' => -1, 'username' => user.username) + end + end + + context 'custom GitLab context when using Sentry.capture_exception directly' do + subject(:track_exception) { Sentry.capture_exception(exception) } + + it 'merges a default set of tags into the existing tags' do + Sentry.set_tags(foo: 'bar') + + track_exception + + expect(sentry_event.tags).to include(:correlation_id, :feature_category, :foo, :locale, :program) + end + + it 'merges the current user information into the existing user information' do + Sentry.set_user(id: -1) + + track_exception + + expect(sentry_event.user).to eq(id: -1, username: user.username) end end context 'with sidekiq args' do context 'when the args does not have anything sensitive' do - let(:extra) { { sidekiq: { 'class' => 'PostReceive', 'args' => [1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value'] } } } + let(:extra) do + { + sidekiq: { + 'class' => 'PostReceive', + 'args' => [ + 1, + { 'id' => 2, 'name' => 'hello' }, + 'some-value', + 'another-value' + ] + } + } + end it 'ensures extra.sidekiq.args is a string' do track_exception @@ -254,8 +314,10 @@ RSpec.describe Gitlab::ErrorTracking do it 'does not filter parameters when sending to Sentry' do track_exception + expected_data = [1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value'] - expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq([1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value']) + expect(raven_event.dig('extra', 'sidekiq', 'args')).to eq(expected_data) + expect(sentry_event.extra[:sidekiq]['args']).to eq(expected_data) end end @@ -265,7 +327,8 @@ RSpec.describe Gitlab::ErrorTracking do it 'filters sensitive arguments before sending and logging' do track_exception - expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq(['[FILTERED]', 1, 2]) + expect(raven_event.dig('extra', 'sidekiq', 'args')).to eq(['[FILTERED]', 1, 2]) + expect(sentry_event.extra[:sidekiq]['args']).to eq(['[FILTERED]', 1, 2]) expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with( hash_including( 'extra.sidekiq' => { @@ -285,8 +348,10 @@ RSpec.describe Gitlab::ErrorTracking do it 'sets the GRPC debug error string in the Sentry event and adds a custom fingerprint' do track_exception - expect(sentry_event.dig('extra', 'grpc_debug_error_string')).to eq('{"hello":1}') - expect(sentry_event['fingerprint']).to eq(['GRPC::DeadlineExceeded', '4:unknown cause.']) + expect(raven_event.dig('extra', 'grpc_debug_error_string')).to eq('{"hello":1}') + expect(raven_event['fingerprint']).to eq(['GRPC::DeadlineExceeded', '4:unknown cause.']) + expect(sentry_event.extra[:grpc_debug_error_string]).to eq('{"hello":1}') + expect(sentry_event.fingerprint).to eq(['GRPC::DeadlineExceeded', '4:unknown cause.']) end end @@ -296,8 +361,10 @@ RSpec.describe Gitlab::ErrorTracking do it 'does not do any processing on the event' do track_exception - expect(sentry_event['extra']).not_to include('grpc_debug_error_string') - expect(sentry_event['fingerprint']).to eq(['GRPC::DeadlineExceeded', '4:unknown cause']) + expect(raven_event['extra']).not_to include('grpc_debug_error_string') + expect(raven_event['fingerprint']).to eq(['GRPC::DeadlineExceeded', '4:unknown cause']) + expect(sentry_event.extra).not_to include(:grpc_debug_error_string) + expect(sentry_event.fingerprint).to eq(['GRPC::DeadlineExceeded', '4:unknown cause']) end end end diff --git a/spec/lib/gitlab/fips_spec.rb b/spec/lib/gitlab/fips_spec.rb index 2ede2e3adf3..4d19a44f617 100644 --- a/spec/lib/gitlab/fips_spec.rb +++ b/spec/lib/gitlab/fips_spec.rb @@ -6,16 +6,46 @@ RSpec.describe Gitlab::FIPS do describe ".enabled?" do subject { described_class.enabled? } - context "feature flag is enabled" do - it { is_expected.to be_truthy } + let(:openssl_fips_mode) { false } + let(:fips_mode_env_var) { nil } + + before do + expect(OpenSSL).to receive(:fips_mode).and_return(openssl_fips_mode) + stub_env("FIPS_MODE", fips_mode_env_var) + end + + describe "OpenSSL auto-detection" do + context "OpenSSL is in FIPS mode" do + let(:openssl_fips_mode) { true } + + it { is_expected.to be_truthy } + end + + context "OpenSSL is not in FIPS mode" do + let(:openssl_fips_mode) { false } + + it { is_expected.to be_falsey } + end end - context "feature flag is disabled" do - before do - stub_feature_flags(fips_mode: false) + describe "manual configuration via env var" do + context "env var is not set" do + let(:fips_mode_env_var) { nil } + + it { is_expected.to be_falsey } end - it { is_expected.to be_falsey } + context "env var is set to true" do + let(:fips_mode_env_var) { "true" } + + it { is_expected.to be_truthy } + end + + context "env var is set to false" do + let(:fips_mode_env_var) { "false" } + + it { is_expected.to be_falsey } + end end end end diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb index e27fee9286b..c8d86edc55f 100644 --- a/spec/models/container_repository_spec.rb +++ b/spec/models/container_repository_spec.rb @@ -1255,7 +1255,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do subject { described_class.ready_for_import } before do - stub_application_setting(container_registry_import_target_plan: project.namespace.actual_plan_name) + stub_application_setting(container_registry_import_target_plan: root_group.actual_plan_name) end it 'works' do diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb index 0e7230c042e..67d664a617b 100644 --- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb @@ -102,5 +102,81 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do is_expected.to eq(destroyed_artifacts_count: 0, statistics_updates: {}, status: :success) end end + + context 'with artifacts that has backfilled expire_at' do + let!(:created_on_00_30_45_minutes_on_21_22_23) do + [ + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 00:00:00.000')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 01:30:00.000')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-22 12:00:00.000')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-22 12:30:00.000')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-23 23:00:00.000')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-23 23:30:00.000')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-23 06:45:00.000')) + ] + end + + let!(:created_close_to_00_or_30_minutes) do + [ + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 00:00:00.001')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 00:30:00.999')) + ] + end + + let!(:created_on_00_or_30_minutes_on_other_dates) do + [ + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-01 00:00:00.000')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-19 12:00:00.000')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-24 23:30:00.000')) + ] + end + + let!(:created_at_other_times) do + [ + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-19 00:00:00.000')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-19 00:30:00.000')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-24 00:00:00.000')), + create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-24 00:30:00.000')) + ] + end + + let(:artifacts_to_keep) { created_on_00_30_45_minutes_on_21_22_23 } + let(:artifacts_to_delete) { created_close_to_00_or_30_minutes + created_on_00_or_30_minutes_on_other_dates + created_at_other_times } + let(:all_artifacts) { artifacts_to_keep + artifacts_to_delete } + + let(:artifacts) { Ci::JobArtifact.where(id: all_artifacts.map(&:id)) } + + it 'deletes job artifacts that do not have expire_at on 00, 30 or 45 minute of 21, 22, 23 of the month' do + expect { subject }.to change { Ci::JobArtifact.count }.by(artifacts_to_delete.size * -1) + end + + it 'keeps job artifacts that have expire_at on 00, 30 or 45 minute of 21, 22, 23 of the month' do + expect { subject }.not_to change { Ci::JobArtifact.where(id: artifacts_to_keep.map(&:id)).count } + end + + it 'removes expire_at on job artifacts that have expire_at on 00, 30 or 45 minute of 21, 22, 23 of the month' do + subject + + expect(artifacts_to_keep.all? { |artifact| artifact.reload.expire_at.nil? }).to be(true) + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ci_detect_wrongly_expired_artifacts: false) + end + + it 'deletes all job artifacts' do + expect { subject }.to change { Ci::JobArtifact.count }.by(all_artifacts.size * -1) + end + end + + context 'when fix_expire_at is false' do + let(:service) { described_class.new(artifacts, pick_up_at: Time.current, fix_expire_at: false) } + + it 'deletes all job artifacts' do + expect { subject }.to change { Ci::JobArtifact.count }.by(all_artifacts.size * -1) + end + end + end end end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index 8c60dc30cdb..20f46396424 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -90,10 +90,18 @@ module StubConfiguration allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages)) end - def stub_sentry_settings - allow(Gitlab.config.sentry).to receive(:enabled).and_return(true) - allow(Gitlab.config.sentry).to receive(:dsn).and_return('dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/42') - allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return('dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/43') + def stub_sentry_settings(enabled: true) + allow(Gitlab.config.sentry).to receive(:enabled) { enabled } + allow(Gitlab::CurrentSettings).to receive(:sentry_enabled?) { enabled } + + dsn = 'dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/42' + allow(Gitlab.config.sentry).to receive(:dsn) { dsn } + allow(Gitlab::CurrentSettings).to receive(:sentry_dsn) { dsn } + + clientside_dsn = 'dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/43' + allow(Gitlab.config.sentry).to receive(:clientside_dsn) { clientside_dsn } + allow(Gitlab::CurrentSettings) + .to receive(:sentry_clientside_dsn) { clientside_dsn } end def stub_kerberos_setting(messages) diff --git a/spec/support/sentry.rb b/spec/support/sentry.rb new file mode 100644 index 00000000000..c439b6c0fd9 --- /dev/null +++ b/spec/support/sentry.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.around(:example, :sentry) do |example| + dsn = Sentry.get_current_client.configuration.dsn + Sentry.get_current_client.configuration.dsn = 'dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/42' + begin + example.run + ensure + Sentry.get_current_client.configuration.dsn = dsn.to_s.presence + end + end +end diff --git a/spec/support/shared_contexts/container_repositories_shared_context.rb b/spec/support/shared_contexts/container_repositories_shared_context.rb index 7f61631dce0..9a9f80a3cbd 100644 --- a/spec/support/shared_contexts/container_repositories_shared_context.rb +++ b/spec/support/shared_contexts/container_repositories_shared_context.rb @@ -1,13 +1,16 @@ # frozen_string_literal: true RSpec.shared_context 'importable repositories' do - let_it_be(:project) { create(:project) } + let_it_be(:root_group) { create(:group) } + let_it_be(:group) { create(:group, parent_id: root_group.id) } + let_it_be(:project) { create(:project, namespace: group) } let_it_be(:valid_container_repository) { create(:container_repository, project: project, created_at: 2.days.ago) } let_it_be(:valid_container_repository2) { create(:container_repository, project: project, created_at: 1.year.ago) } let_it_be(:importing_container_repository) { create(:container_repository, :importing, project: project, created_at: 2.days.ago) } let_it_be(:new_container_repository) { create(:container_repository, project: project) } - let_it_be(:denied_group) { create(:group) } + let_it_be(:denied_root_group) { create(:group) } + let_it_be(:denied_group) { create(:group, parent_id: denied_root_group.id) } let_it_be(:denied_project) { create(:project, group: denied_group) } let_it_be(:denied_container_repository) { create(:container_repository, project: denied_project, created_at: 2.days.ago) } @@ -21,7 +24,7 @@ RSpec.shared_context 'importable repositories' do Feature::FlipperGate.create!( feature_key: 'container_registry_phase_2_deny_list', key: 'actors', - value: "Group:#{denied_group.id}" + value: "Group:#{denied_root_group.id}" ) end end diff --git a/spec/views/projects/empty.html.haml_spec.rb b/spec/views/projects/empty.html.haml_spec.rb index 416dfc10174..6077dda3c98 100644 --- a/spec/views/projects/empty.html.haml_spec.rb +++ b/spec/views/projects/empty.html.haml_spec.rb @@ -25,6 +25,21 @@ RSpec.describe 'projects/empty' do expect(rendered).to have_content("git clone") end + + context 'when default branch name contains special shell characters' do + let(:branch_name) { ';rm -rf /' } + + before do + allow(project).to receive(:default_branch_or_main).and_return(branch_name) + end + + it 'escapes the default branch name' do + render + + expect(rendered).not_to have_content(branch_name) + expect(rendered).to have_content(branch_name.shellescape) + end + end end context 'when user can not push code on the project' do |