diff options
92 files changed, 847 insertions, 1025 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 0fa805d2ff5..4008a63e5c9 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -ac72695adc90343b7255869818376f505bde8315 +6944c95243e2b6ed8f783ae903cb9b03ad50e0f9 diff --git a/app/assets/javascripts/jobs/bridge/app.vue b/app/assets/javascripts/jobs/bridge/app.vue deleted file mode 100644 index c639e49083b..00000000000 --- a/app/assets/javascripts/jobs/bridge/app.vue +++ /dev/null @@ -1,118 +0,0 @@ -<script> -import { GlLoadingIcon } from '@gitlab/ui'; -import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { __, sprintf } from '~/locale'; -import CiHeader from '~/vue_shared/components/header_ci_component.vue'; -import getPipelineQuery from './graphql/queries/pipeline.query.graphql'; -import BridgeEmptyState from './components/empty_state.vue'; -import BridgeSidebar from './components/sidebar.vue'; -import { SIDEBAR_COLLAPSE_BREAKPOINTS } from './components/constants'; - -export default { - name: 'BridgePageApp', - components: { - BridgeEmptyState, - BridgeSidebar, - CiHeader, - GlLoadingIcon, - }, - inject: ['buildId', 'projectFullPath', 'pipelineIid'], - apollo: { - pipeline: { - query: getPipelineQuery, - variables() { - return { - fullPath: this.projectFullPath, - iid: this.pipelineIid, - }; - }, - update(data) { - if (!data?.project?.pipeline) { - return null; - } - - const { pipeline } = data.project; - const stages = pipeline?.stages.edges.map((edge) => edge.node) || []; - const jobs = stages.map((stage) => stage.jobs.nodes).flat(); - - return { - ...pipeline, - commit: { - ...pipeline.commit, - commit_path: pipeline.commit.webPath, - short_id: pipeline.commit.shortId, - }, - id: getIdFromGraphQLId(pipeline.id), - jobs, - stages, - }; - }, - }, - }, - data() { - return { - isSidebarExpanded: true, - pipeline: {}, - }; - }, - computed: { - bridgeJob() { - return ( - this.pipeline.jobs?.filter( - (job) => getIdFromGraphQLId(job.id) === Number(this.buildId), - )[0] || {} - ); - }, - bridgeName() { - return sprintf(__('Job %{jobName}'), { jobName: this.bridgeJob.name }); - }, - isPipelineLoading() { - return this.$apollo.queries.pipeline.loading; - }, - }, - created() { - window.addEventListener('resize', this.onResize); - }, - mounted() { - this.onResize(); - }, - methods: { - toggleSidebar() { - this.isSidebarExpanded = !this.isSidebarExpanded; - }, - onResize() { - const breakpoint = bp.getBreakpointSize(); - if (SIDEBAR_COLLAPSE_BREAKPOINTS.includes(breakpoint)) { - this.isSidebarExpanded = false; - } else if (!this.isSidebarExpanded) { - this.isSidebarExpanded = true; - } - }, - }, -}; -</script> -<template> - <div> - <gl-loading-icon v-if="isPipelineLoading" size="lg" class="gl-mt-4" /> - <div v-else> - <ci-header - class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" - :status="bridgeJob.detailedStatus" - :time="bridgeJob.createdAt" - :user="pipeline.user" - :has-sidebar-button="true" - :item-name="bridgeName" - @clickedSidebarButton="toggleSidebar" - /> - <bridge-empty-state :downstream-pipeline-path="bridgeJob.downstreamPipeline.path" /> - <bridge-sidebar - v-if="isSidebarExpanded" - :bridge-job="bridgeJob" - :commit="pipeline.commit" - :is-sidebar-expanded="isSidebarExpanded" - @toggleSidebar="toggleSidebar" - /> - </div> - </div> -</template> diff --git a/app/assets/javascripts/jobs/bridge/components/constants.js b/app/assets/javascripts/jobs/bridge/components/constants.js deleted file mode 100644 index 33310b3157a..00000000000 --- a/app/assets/javascripts/jobs/bridge/components/constants.js +++ /dev/null @@ -1 +0,0 @@ -export const SIDEBAR_COLLAPSE_BREAKPOINTS = ['xs', 'sm']; diff --git a/app/assets/javascripts/jobs/bridge/components/empty_state.vue b/app/assets/javascripts/jobs/bridge/components/empty_state.vue deleted file mode 100644 index bd07d863719..00000000000 --- a/app/assets/javascripts/jobs/bridge/components/empty_state.vue +++ /dev/null @@ -1,45 +0,0 @@ -<script> -import { GlButton } from '@gitlab/ui'; -import { __ } from '~/locale'; - -export default { - name: 'BridgeEmptyState', - i18n: { - title: __('This job triggers a downstream pipeline'), - linkBtnText: __('View downstream pipeline'), - }, - components: { - GlButton, - }, - inject: { - emptyStateIllustrationPath: { - type: String, - require: true, - }, - }, - props: { - downstreamPipelinePath: { - type: String, - required: false, - default: undefined, - }, - }, -}; -</script> - -<template> - <div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11"> - <img :src="emptyStateIllustrationPath" /> - <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1> - <gl-button - v-if="downstreamPipelinePath" - class="gl-mt-3" - category="secondary" - variant="confirm" - size="medium" - :href="downstreamPipelinePath" - > - {{ $options.i18n.linkBtnText }} - </gl-button> - </div> -</template> diff --git a/app/assets/javascripts/jobs/bridge/components/sidebar.vue b/app/assets/javascripts/jobs/bridge/components/sidebar.vue deleted file mode 100644 index 3ba07cf55d1..00000000000 --- a/app/assets/javascripts/jobs/bridge/components/sidebar.vue +++ /dev/null @@ -1,105 +0,0 @@ -<script> -import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { __ } from '~/locale'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; -import { JOB_SIDEBAR } from '../../constants'; -import CommitBlock from '../../components/commit_block.vue'; - -export default { - styles: { - width: '290px', - }, - name: 'BridgeSidebar', - i18n: { - ...JOB_SIDEBAR, - retryButton: __('Retry'), - retryTriggerJob: __('Retry the trigger job'), - retryDownstreamPipeline: __('Retry the downstream pipeline'), - }, - sectionClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100', 'gl-py-5'], - components: { - CommitBlock, - GlButton, - GlDropdown, - GlDropdownItem, - TooltipOnTruncate, - }, - mixins: [glFeatureFlagsMixin()], - props: { - bridgeJob: { - type: Object, - required: true, - }, - commit: { - type: Object, - required: true, - }, - }, - data() { - return { - topPosition: 0, - }; - }, - computed: { - rootStyle() { - return { ...this.$options.styles, top: `${this.topPosition}px` }; - }, - }, - mounted() { - this.setTopPosition(); - }, - methods: { - onSidebarButtonClick() { - this.$emit('toggleSidebar'); - }, - setTopPosition() { - const navbarEl = document.querySelector('.js-navbar'); - - if (navbarEl) { - this.topPosition = navbarEl.getBoundingClientRect().bottom; - } - }, - }, -}; -</script> -<template> - <aside - class="gl-fixed gl-right-0 gl-px-5 gl-bg-gray-10 gl-h-full gl-border-l-solid gl-border-1 gl-border-gray-100 gl-z-index-200 gl-overflow-hidden" - :style="rootStyle" - > - <div class="gl-py-5 gl-display-flex gl-align-items-center"> - <tooltip-on-truncate :title="bridgeJob.name" truncate-target="child" - ><h4 class="gl-mb-0 gl-mr-2 gl-text-truncate"> - {{ bridgeJob.name }} - </h4> - </tooltip-on-truncate> - <!-- TODO: implement retry actions --> - <div - v-if="glFeatures.triggerJobRetryAction" - class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right" - > - <gl-dropdown - :text="$options.i18n.retryButton" - category="primary" - variant="confirm" - right - size="medium" - > - <gl-dropdown-item>{{ $options.i18n.retryTriggerJob }}</gl-dropdown-item> - <gl-dropdown-item>{{ $options.i18n.retryDownstreamPipeline }}</gl-dropdown-item> - </gl-dropdown> - </div> - <gl-button - :aria-label="$options.i18n.toggleSidebar" - data-testid="sidebar-expansion-toggle" - category="tertiary" - class="gl-md-display-none gl-ml-2" - icon="chevron-double-lg-right" - @click="onSidebarButtonClick" - /> - </div> - <commit-block :commit="commit" :class="$options.sectionClass" /> - <!-- TODO: show stage dropdown, jobs list --> - </aside> -</template> diff --git a/app/assets/javascripts/jobs/bridge/graphql/queries/pipeline.query.graphql b/app/assets/javascripts/jobs/bridge/graphql/queries/pipeline.query.graphql deleted file mode 100644 index 338ca9f16c7..00000000000 --- a/app/assets/javascripts/jobs/bridge/graphql/queries/pipeline.query.graphql +++ /dev/null @@ -1,70 +0,0 @@ -query getPipelineData($fullPath: ID!, $iid: ID!) { - project(fullPath: $fullPath) { - id - pipeline(iid: $iid) { - id - iid - path - sha - ref - refPath - commit { - id - shortId - title - webPath - } - detailedStatus { - id - icon - group - } - stages { - edges { - node { - id - name - jobs { - nodes { - id - createdAt - name - scheduledAt - startedAt - status - triggered - detailedStatus { - id - detailsPath - icon - group - text - tooltip - } - downstreamPipeline { - id - path - } - stage { - id - name - } - } - } - } - } - } - user { - id - avatarUrl - name - username - webPath - webUrl - status { - message - } - } - } - } -} diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index 26dd38bbe08..8fb4c480ef9 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -1,7 +1,4 @@ import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createDefaultClient from '~/lib/graphql'; -import BridgeApp from './bridge/app.vue'; import JobApp from './components/job_app.vue'; import createStore from './store'; @@ -51,43 +48,7 @@ const initializeJobPage = (element) => { }); }; -const initializeBridgePage = (el) => { - const { - buildId, - downstreamPipelinePath, - emptyStateIllustrationPath, - pipelineIid, - projectFullPath, - } = el.dataset; - - Vue.use(VueApollo); - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), - }); - - return new Vue({ - el, - apolloProvider, - provide: { - buildId, - downstreamPipelinePath, - emptyStateIllustrationPath, - pipelineIid, - projectFullPath, - }, - render(h) { - return h(BridgeApp); - }, - }); -}; - export default () => { const jobElement = document.getElementById('js-job-page'); - const bridgeElement = document.getElementById('js-bridge-page'); - - if (jobElement) { - initializeJobPage(jobElement); - } else { - initializeBridgePage(bridgeElement); - } + initializeJobPage(jobElement); }; diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index 35e7554aee2..016301368af 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -14,6 +14,8 @@ export default { LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'), TimeToRestoreServiceCharts: () => import('ee_component/dora/components/time_to_restore_service_charts.vue'), + ChangeFailureRateCharts: () => + import('ee_component/dora/components/change_failure_rate_charts.vue'), ProjectQualitySummary: () => import('ee_component/project_quality_summary/app.vue'), }, piplelinesTabEvent: 'p_analytics_ci_cd_pipelines', @@ -40,7 +42,12 @@ export default { const chartsToShow = ['pipelines']; if (this.shouldRenderDoraCharts) { - chartsToShow.push('deployment-frequency', 'lead-time', 'time-to-restore-service'); + chartsToShow.push( + 'deployment-frequency', + 'lead-time', + 'time-to-restore-service', + 'change-failure-rate', + ); } if (this.shouldRenderQualitySummary) { @@ -105,6 +112,12 @@ export default { > <time-to-restore-service-charts /> </gl-tab> + <gl-tab + :title="s__('DORA4Metrics|Change failure rate')" + data-testid="change-failure-rate-tab" + > + <change-failure-rate-charts /> + </gl-tab> </template> <gl-tab v-if="shouldRenderQualitySummary" :title="s__('QualitySummary|Project quality')"> <project-quality-summary /> diff --git a/app/assets/javascripts/surveys/merge_request_experience/app.vue b/app/assets/javascripts/surveys/merge_request_experience/app.vue index 5b1d9be9563..85eed6ae82a 100644 --- a/app/assets/javascripts/surveys/merge_request_experience/app.vue +++ b/app/assets/javascripts/surveys/merge_request_experience/app.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlSprintf, GlSafeHtmlDirective } from '@gitlab/ui'; +import { GlButton, GlSprintf, GlSafeHtmlDirective, GlTooltipDirective } from '@gitlab/ui'; import gitlabLogo from '@gitlab/svgs/dist/illustrations/gitlab_logo.svg'; import { s__, __ } from '~/locale'; import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; @@ -29,6 +29,7 @@ export default { }, directives: { safeHtml: GlSafeHtmlDirective, + tooltip: GlTooltipDirective, }, mixins: [Tracking.mixin()], i18n: { @@ -92,7 +93,7 @@ export default { > <template #default="{ dismiss }"> <aside - class="gl-fixed gl-bottom-0 gl-right-0 gl-z-index-9999 gl-p-5" + class="mr-experience-survey-wrapper gl-fixed gl-bottom-0 gl-right-0 gl-p-5" :aria-label="$options.i18n.survey" > <transition name="survey-slide-up"> @@ -101,6 +102,7 @@ export default { class="mr-experience-survey-body gl-relative gl-display-flex gl-flex-direction-column gl-bg-white gl-p-5 gl-border gl-rounded-base" > <gl-button + v-tooltip="$options.i18n.close" :aria-label="$options.i18n.close" variant="default" category="tertiary" diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index ebf3c81d9d3..96fe6caeea2 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -350,6 +350,13 @@ $comparison-empty-state-height: 62px; } } +.mr-experience-survey-wrapper { + // setting this explicitly because: + // diff-files-holder has z-index 203 + // z-index 9999 utility class breaks tooltips + z-index: 210; +} + .mr-experience-survey-body { width: 300px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index 2eae13b5265..1031bf3e60a 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -22,10 +22,6 @@ class Projects::JobsController < Projects::ApplicationController before_action :push_jobs_table_vue_search, only: [:index] before_action :reject_if_build_artifacts_size_refreshing!, only: [:erase] - before_action do - push_frontend_feature_flag(:trigger_job_retry_action, @project) - end - layout 'project' feature_category :continuous_integration diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 5ca417455fb..8ecf0c158e0 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -436,7 +436,7 @@ class IssuableFinder elsif not_params.filter_by_started_milestone? items.joins(:milestone).merge(Milestone.not_started) else - items.without_particular_milestone(not_params[:milestone_title]) + items.without_particular_milestones(not_params[:milestone_title]) end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index dc4e2e9709a..54733fa9101 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -116,19 +116,16 @@ module EmailsHelper end end - # "You are receiving this email because #{reason} on #{gitlab_host}." - def notification_reason_text(reason) - gitlab_host = Gitlab.config.gitlab.host - - case reason - when NotificationReason::OWN_ACTIVITY - _("You're receiving this email because of your activity on %{host}.") % { host: gitlab_host } - when NotificationReason::ASSIGNED - _("You're receiving this email because you have been assigned an item on %{host}.") % { host: gitlab_host } - when NotificationReason::MENTIONED - _("You're receiving this email because you have been mentioned on %{host}.") % { host: gitlab_host } + # "You are receiving this email because ... on #{host}. ..." + def notification_reason_text(reason: nil, show_manage_notifications_link: false, show_help_link: false, manage_label_subscriptions_url: nil, unsubscribe_url: nil, format: :text) + if unsubscribe_url && show_manage_notifications_link && show_help_link + notification_reason_text_with_unsubscribe_and_manage_notifications_and_help_links(reason: reason, unsubscribe_url: unsubscribe_url, format: format) + elsif !reason && manage_label_subscriptions_url && show_help_link + notification_reason_text_with_manage_label_subscriptions_and_help_links(manage_label_subscriptions_url: manage_label_subscriptions_url, format: format) + elsif show_manage_notifications_link && show_help_link + notification_reason_text_with_manage_notifications_and_help_links(reason: reason, format: format) else - _("You're receiving this email because of your account on %{host}.") % { host: gitlab_host } + notification_reason_text_without_links(reason: reason, format: format) end end @@ -259,9 +256,7 @@ module EmailsHelper end def instance_access_request_text(user, format: nil) - gitlab_host = Gitlab.config.gitlab.host - - _('%{username} has asked for a GitLab account on your instance %{host}:') % { username: sanitize_name(user.name), host: gitlab_host } + _('%{username} has asked for a GitLab account on your instance %{host}:').html_safe % { username: sanitize_name(user.name), host: gitlab_host_link(format) } end def instance_access_request_link(user, format: nil) @@ -325,6 +320,75 @@ module EmailsHelper def email_header_and_footer_enabled? current_appearance&.email_header_and_footer_enabled? end + + def gitlab_host_link(format) + case format + when :html + generate_link(Gitlab.config.gitlab.host, Gitlab.config.gitlab.url) + when :text + Gitlab.config.gitlab.host + end + end + + def notification_reason_text_with_unsubscribe_and_manage_notifications_and_help_links(reason:, unsubscribe_url:, format:) + unsubscribe_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: unsubscribe_url } + unsubscribe_link_end = '</a>'.html_safe + + manage_notifications_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="mng-notif-link">'.html_safe % { url: profile_notifications_url } + manage_notifications_link_end = '</a>'.html_safe + + help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="help-link">'.html_safe % { url: help_url } + help_link_end = '</a>'.html_safe + + case reason + when NotificationReason::OWN_ACTIVITY + _("You're receiving this email because of your activity on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), unsubscribe_link_start: unsubscribe_link_start, unsubscribe_link_end: unsubscribe_link_end, manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end } + when NotificationReason::ASSIGNED + _("You're receiving this email because you have been assigned an item on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), unsubscribe_link_start: unsubscribe_link_start, unsubscribe_link_end: unsubscribe_link_end, manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end } + when NotificationReason::MENTIONED + _("You're receiving this email because you have been mentioned on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), unsubscribe_link_start: unsubscribe_link_start, unsubscribe_link_end: unsubscribe_link_end, manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end } + else + _("You're receiving this email because of your account on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), unsubscribe_link_start: unsubscribe_link_start, unsubscribe_link_end: unsubscribe_link_end, manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end } + end + end + + def notification_reason_text_with_manage_label_subscriptions_and_help_links(manage_label_subscriptions_url:, format:) + manage_label_subscriptions_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="mng-notif-link">'.html_safe % { url: manage_label_subscriptions_url } + manage_label_subscriptions_link_end = '</a>'.html_safe + + help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="help-link">'.html_safe % { url: help_url } + help_link_end = '</a>'.html_safe + + _("You're receiving this email because of your account on %{host}. %{manage_label_subscriptions_link_start}Manage label subscriptions%{manage_label_subscriptions_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), manage_label_subscriptions_link_start: manage_label_subscriptions_link_start, manage_label_subscriptions_link_end: manage_label_subscriptions_link_end, help_link_start: help_link_start, help_link_end: help_link_end } + end + + def notification_reason_text_with_manage_notifications_and_help_links(reason:, format:) + manage_notifications_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="mng-notif-link">'.html_safe % { url: profile_notifications_url } + manage_notifications_link_end = '</a>'.html_safe + + help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="help-link">'.html_safe % { url: help_url } + help_link_end = '</a>'.html_safe + + case reason + when NotificationReason::MENTIONED + _("You're receiving this email because you have been mentioned on %{host}. %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end } + else + _("You're receiving this email because of your account on %{host}. %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end } + end + end + + def notification_reason_text_without_links(reason:, format:) + case reason + when NotificationReason::OWN_ACTIVITY + _("You're receiving this email because of your activity on %{host}.").html_safe % { host: gitlab_host_link(format) } + when NotificationReason::ASSIGNED + _("You're receiving this email because you have been assigned an item on %{host}.").html_safe % { host: gitlab_host_link(format) } + when NotificationReason::MENTIONED + _("You're receiving this email because you have been mentioned on %{host}.").html_safe % { host: gitlab_host_link(format) } + else + _("You're receiving this email because of your account on %{host}.").html_safe % { host: gitlab_host_link(format) } + end + end end EmailsHelper.prepend_mod_with('EmailsHelper') diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb index 12041b103f6..14c54d99ef3 100644 --- a/app/models/concerns/milestoneable.rb +++ b/app/models/concerns/milestoneable.rb @@ -16,7 +16,7 @@ module Milestoneable scope :any_milestone, -> { where.not(milestone_id: nil) } scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } - scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) } + scope :without_particular_milestones, ->(titles) { left_outer_joins(:milestone).where("milestones.title NOT IN (?) OR milestone_id IS NULL", titles) } scope :any_release, -> { joins_milestone_releases } scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) } scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not(milestones: { releases: { tag: tag, project_id: project_id } }) } diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb index a5c392f6a36..36902e43a77 100644 --- a/app/models/merge_request_diff_file.rb +++ b/app/models/merge_request_diff_file.rb @@ -15,7 +15,12 @@ class MergeRequestDiffFile < ApplicationRecord end def utf8_diff - fetched_diff = diff + fetched_diff = if Feature.enabled?(:externally_stored_diffs_caching_export) && + merge_request_diff&.stored_externally? + diff_export + else + diff + end return '' if fetched_diff.blank? @@ -46,14 +51,13 @@ class MergeRequestDiffFile < ApplicationRecord end end + private + # This method is meant to be used during Project Export. - # It is identical to the behaviour in #diff & #utf8_diff with the only + # It is identical to the behaviour in #diff with the only # difference of caching externally stored diffs on local disk in # temp storage location in order to improve diff export performance. def diff_export - return utf8_diff unless Feature.enabled?(:externally_stored_diffs_caching_export) - return utf8_diff unless merge_request_diff&.stored_externally? - content = merge_request_diff.cached_external_diff do |file| file.seek(external_diff_offset) @@ -69,14 +73,17 @@ class MergeRequestDiffFile < ApplicationRecord end end - if content.respond_to?(:encoding) - content = encode_utf8(content) - end + content + rescue StandardError => e + log_payload = { + message: 'Cached external diff export failed', + merge_request_diff_file_id: id, + merge_request_diff_id: merge_request_diff&.id + } - return '' if content.blank? + Gitlab::ExceptionLogFormatter.format!(e, log_payload) + Gitlab::AppLogger.warn(log_payload) - content - rescue StandardError - utf8_diff + diff end end diff --git a/app/presenters/terraform/module_version_presenter.rb b/app/presenters/terraform/module_version_presenter.rb new file mode 100644 index 00000000000..776a4d8ab82 --- /dev/null +++ b/app/presenters/terraform/module_version_presenter.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Terraform + class ModuleVersionPresenter < Gitlab::View::Presenter::Simple + attr_accessor :package, :system + + def initialize(package, system) + @package = package + @system = system + end + + def name + package.name + end + + def provider + system + end + + def providers + [ + provider + ] + end + + def root + { + 'dependencies' => [] + } + end + + def source + package&.project&.web_url + end + + def submodules + [] + end + + def version + package.version + end + + def versions + [ + version + ] + end + end +end diff --git a/app/views/admin/application_settings/_eks.html.haml b/app/views/admin/application_settings/_eks.html.haml index 370d3cea07c..68eb33d6552 100644 --- a/app/views/admin/application_settings/_eks.html.haml +++ b/app/views/admin/application_settings/_eks.html.haml @@ -1,7 +1,7 @@ - expanded = integration_expanded?('eks_') %section.settings.as-eks.no-animate#js-eks-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Amazon EKS') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml index 4d0faf69958..f287dba9866 100644 --- a/app/views/admin/application_settings/_external_authorization_service_form.html.haml +++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml @@ -1,6 +1,6 @@ %section.settings.as-external-auth.no-animate#js-external-auth-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = s_('ExternalAuthorization|External authorization') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/app/views/admin/application_settings/_floc.html.haml b/app/views/admin/application_settings/_floc.html.haml index b5a63aa0847..d63eb2bd09d 100644 --- a/app/views/admin/application_settings/_floc.html.haml +++ b/app/views/admin/application_settings/_floc.html.haml @@ -2,7 +2,7 @@ %section.settings.no-animate#js-floc-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = s_('FloC|Federated Learning of Cohorts') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml index cd4493d8016..cc1e3f968cb 100644 --- a/app/views/admin/application_settings/_gitpod.html.haml +++ b/app/views/admin/application_settings/_gitpod.html.haml @@ -2,7 +2,7 @@ %section.settings.no-animate#js-gitpod-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Gitpod') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/app/views/admin/application_settings/_jira_connect_application_key.html.haml b/app/views/admin/application_settings/_jira_connect_application_key.html.haml index e395741dcaa..e3df408cd4c 100644 --- a/app/views/admin/application_settings/_jira_connect_application_key.html.haml +++ b/app/views/admin/application_settings/_jira_connect_application_key.html.haml @@ -2,7 +2,7 @@ %section.settings.no-animate#js-jira_connect-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = s_('JiraConnect|GitLab for Jira App') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/app/views/admin/application_settings/_kroki.html.haml b/app/views/admin/application_settings/_kroki.html.haml index b1dd8a282ec..c0ac924407f 100644 --- a/app/views/admin/application_settings/_kroki.html.haml +++ b/app/views/admin/application_settings/_kroki.html.haml @@ -1,7 +1,7 @@ - expanded = integration_expanded?('kroki_') %section.settings.as-kroki.no-animate#js-kroki-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Kroki') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/app/views/admin/application_settings/_mailgun.html.haml b/app/views/admin/application_settings/_mailgun.html.haml index 1239d9178ae..cbe7e1c5bb6 100644 --- a/app/views/admin/application_settings/_mailgun.html.haml +++ b/app/views/admin/application_settings/_mailgun.html.haml @@ -1,7 +1,7 @@ - expanded = integration_expanded?('mailgun_') %section.settings.as-mailgun.no-animate#js-mailgun-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Mailgun') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml index 9e22efce453..8be37ff1dda 100644 --- a/app/views/admin/application_settings/_plantuml.html.haml +++ b/app/views/admin/application_settings/_plantuml.html.haml @@ -1,7 +1,7 @@ - expanded = integration_expanded?('plantuml_') %section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('PlantUML') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml index e9387ab3f26..8684b909853 100644 --- a/app/views/admin/application_settings/_snowplow.html.haml +++ b/app/views/admin/application_settings/_snowplow.html.haml @@ -1,7 +1,7 @@ - expanded = integration_expanded?('snowplow_') %section.settings.as-snowplow.no-animate#js-snowplow-settings{ class: ('expanded' if expanded), data: { qa_selector: 'snowplow_settings_content' } } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Snowplow') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/app/views/admin/application_settings/_sourcegraph.html.haml b/app/views/admin/application_settings/_sourcegraph.html.haml index a0cbbecb943..43ff2bc02f5 100644 --- a/app/views/admin/application_settings/_sourcegraph.html.haml +++ b/app/views/admin/application_settings/_sourcegraph.html.haml @@ -3,7 +3,7 @@ %section.settings.as-sourcegraph.no-animate#js-sourcegraph-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Sourcegraph') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/app/views/admin/application_settings/_third_party_offers.html.haml b/app/views/admin/application_settings/_third_party_offers.html.haml index a868a397992..397b47eefaa 100644 --- a/app/views/admin/application_settings/_third_party_offers.html.haml +++ b/app/views/admin/application_settings/_third_party_offers.html.haml @@ -1,7 +1,7 @@ - expanded = integration_expanded?('hide_third_party_') %section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Customer experience improvement and third-party offers') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml index 52d39172556..ce679fb3d77 100644 --- a/app/views/admin/application_settings/general.html.haml +++ b/app/views/admin/application_settings/general.html.haml @@ -4,7 +4,7 @@ %section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded_by_default?) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Visibility and access controls') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded_by_default? ? _('Collapse') : _('Expand') @@ -15,7 +15,7 @@ %section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'account_and_limit_settings_content' } } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Account and limit') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded_by_default? ? _('Collapse') : _('Expand') @@ -26,7 +26,7 @@ %section.settings.as-diff-limits.no-animate#js-merge-request-settings{ class: ('expanded' if expanded_by_default?) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Diff limits') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded_by_default? ? _('Collapse') : _('Expand') @@ -37,7 +37,7 @@ %section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'sign_up_restrictions_settings_content' } } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Sign-up restrictions') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded_by_default? ? _('Collapse') : _('Expand') @@ -48,7 +48,7 @@ %section.settings.as-signin.no-animate#js-signin-settings{ class: ('expanded' if expanded_by_default?) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Sign-in restrictions') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded_by_default? ? _('Collapse') : _('Expand') @@ -60,7 +60,7 @@ %section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded_by_default?) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Terms of Service and Privacy Policy') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded_by_default? ? _('Collapse') : _('Expand') @@ -74,7 +74,7 @@ %section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded_by_default?) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Web terminal') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded_by_default? ? _('Collapse') : _('Expand') @@ -86,7 +86,7 @@ %section.settings.no-animate#js-web-ide-settings{ class: ('expanded' if expanded_by_default?) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = _('Web IDE') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded_by_default? ? _('Collapse') : _('Expand') diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 580b8e67a3c..8452f0d9976 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -3,8 +3,6 @@ %td %img.footer-logo{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png') } %div - - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, class: 'mng-notif-link') - - help_link = link_to(_("Help"), help_url, class: 'help-link') - = _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link } + = notification_reason_text(show_manage_notifications_link: true, show_help_link: true, format: :html) = render 'layouts/mailer' diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb index 1a06ea68bcd..24553734e49 100644 --- a/app/views/layouts/mailer.text.erb +++ b/app/views/layouts/mailer.text.erb @@ -3,7 +3,7 @@ <%= yield -%> -- <%# signature marker %> -<%= _("You're receiving this email because of your account on %{host}.") % { host: Gitlab.config.gitlab.host } %> +<%= notification_reason_text %> <%= render_if_exists 'layouts/mailer/additional_text' %> <%= text_footer_message %> diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index d9f16a89fbc..d05b6951fbf 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -26,16 +26,7 @@ - else #{link_to _("View it on GitLab"), @target_url}. %br - -# Don't link the host in the line below, one link in the email is easier to quickly click than two. - = notification_reason_text(@reason) - If you'd like to receive fewer emails, you can - - if @labels_url - adjust your #{link_to 'label subscriptions', @labels_url}. - - else - - if @unsubscribe_url - = link_to "unsubscribe", @unsubscribe_url - from this thread or - adjust your notification settings. + = notification_reason_text(reason: @reason, show_manage_notifications_link: !@labels_url, show_help_link: true, manage_label_subscriptions_url: @labels_url, unsubscribe_url: @unsubscribe_url, format: :html) = email_action @target_url diff --git a/app/views/layouts/notify.text.erb b/app/views/layouts/notify.text.erb index 49ad0b5abc5..4eae96dc376 100644 --- a/app/views/layouts/notify.text.erb +++ b/app/views/layouts/notify.text.erb @@ -11,7 +11,7 @@ <% end -%> <% end -%> -<%= notification_reason_text(@reason) %> +<%= notification_reason_text(reason: @reason) %> <%= render_if_exists 'layouts/mailer/additional_text' %> <%= text_footer_message -%> diff --git a/app/views/notify/approved_merge_request_email.html.haml b/app/views/notify/approved_merge_request_email.html.haml index c51fe02370d..28da1182d49 100644 --- a/app/views/notify/approved_merge_request_email.html.haml +++ b/app/views/notify/approved_merge_request_email.html.haml @@ -152,6 +152,4 @@ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } %img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }/ %div - - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;") - - help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;") - = _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link } + = notification_reason_text(show_manage_notifications_link: true, show_help_link: true, format: :html) diff --git a/app/views/notify/merge_when_pipeline_succeeds_email.html.haml b/app/views/notify/merge_when_pipeline_succeeds_email.html.haml index 550d386c843..f6b517d6e34 100644 --- a/app/views/notify/merge_when_pipeline_succeeds_email.html.haml +++ b/app/views/notify/merge_when_pipeline_succeeds_email.html.haml @@ -148,6 +148,4 @@ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } %img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" } %div - - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;") - - help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;") - = _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link } + = notification_reason_text(show_manage_notifications_link: true, show_help_link: true, format: :html) diff --git a/app/views/notify/unapproved_merge_request_email.html.haml b/app/views/notify/unapproved_merge_request_email.html.haml index ae58ccd3995..0b8fbe14228 100644 --- a/app/views/notify/unapproved_merge_request_email.html.haml +++ b/app/views/notify/unapproved_merge_request_email.html.haml @@ -151,6 +151,4 @@ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } %img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }/ %div - - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;") - - help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;") - = _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link } + = notification_reason_text(show_manage_notifications_link: true, show_help_link: true, format: :html) diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index fedc1291a92..5f249f693ff 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -7,7 +7,4 @@ = render_if_exists "shared/shared_runners_minutes_limit_flash_message" -- if @build.is_a? ::Ci::Build - #js-job-page{ data: jobs_data } -- else - #js-bridge-page{ data: bridge_data(@build, @project) } +#js-job-page{ data: jobs_data } diff --git a/app/views/pwa/offline.html.haml b/app/views/pwa/offline.html.haml index 5eae546bea9..cd1fca5d2b5 100644 --- a/app/views/pwa/offline.html.haml +++ b/app/views/pwa/offline.html.haml @@ -1,5 +1,5 @@ = link_to root_path do - = render 'shared/logo.svg' + = render partial: 'shared/logo', formats: :svg %h1= _('Offline') .container %h3= _('You are currently offline, or the GitLab instance is not reachable.') diff --git a/config/feature_flags/development/ci_docker_image_pull_policy.yml b/config/feature_flags/development/ci_docker_image_pull_policy.yml index 09e01fb5232..5bdcdf03d27 100644 --- a/config/feature_flags/development/ci_docker_image_pull_policy.yml +++ b/config/feature_flags/development/ci_docker_image_pull_policy.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363186 milestone: '15.1' type: development group: group::pipeline authoring -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/trigger_job_retry_action.yml b/config/feature_flags/development/trigger_job_retry_action.yml deleted file mode 100644 index 79a8593fd05..00000000000 --- a/config/feature_flags/development/trigger_job_retry_action.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: trigger_job_retry_action -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77951 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349966 -milestone: '14.7' -type: development -group: group::pipeline authoring -default_enabled: false diff --git a/db/docs/namespace_bans.yml b/db/docs/namespace_bans.yml new file mode 100644 index 00000000000..7e11738ab81 --- /dev/null +++ b/db/docs/namespace_bans.yml @@ -0,0 +1,9 @@ +--- +table_name: namespace_bans +classes: + - NamespaceBan +feature_categories: + - instance_resiliency +description: Contains users banned from namespaces +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91271 +milestone: "15.2" diff --git a/db/migrate/20220628120708_create_namespace_bans.rb b/db/migrate/20220628120708_create_namespace_bans.rb new file mode 100644 index 00000000000..657d13f6448 --- /dev/null +++ b/db/migrate/20220628120708_create_namespace_bans.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateNamespaceBans < Gitlab::Database::Migration[2.0] + UNIQUE_INDEX_NAME = 'index_namespace_bans_on_namespace_id_and_user_id' + + def change + create_table :namespace_bans do |t| + t.bigint :namespace_id, null: false + t.bigint :user_id, null: false, index: true + t.timestamps_with_timezone + + t.index [:namespace_id, :user_id], unique: true, name: UNIQUE_INDEX_NAME + end + end +end diff --git a/db/migrate/20220628121644_add_namespace_bans_namespace_id_foreign_key.rb b/db/migrate/20220628121644_add_namespace_bans_namespace_id_foreign_key.rb new file mode 100644 index 00000000000..30928123618 --- /dev/null +++ b/db/migrate/20220628121644_add_namespace_bans_namespace_id_foreign_key.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddNamespaceBansNamespaceIdForeignKey < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :namespace_bans, :namespaces, column: :namespace_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :namespace_bans, column: :namespace_id + end + end +end diff --git a/db/migrate/20220628121712_add_namespace_bans_user_id_foreign_key.rb b/db/migrate/20220628121712_add_namespace_bans_user_id_foreign_key.rb new file mode 100644 index 00000000000..16a73c29cae --- /dev/null +++ b/db/migrate/20220628121712_add_namespace_bans_user_id_foreign_key.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddNamespaceBansUserIdForeignKey < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :namespace_bans, :users, column: :user_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :namespace_bans, column: :user_id + end + end +end diff --git a/db/migrate/20220629220129_increase_webauthn_xid_length.rb b/db/migrate/20220629220129_increase_webauthn_xid_length.rb new file mode 100644 index 00000000000..c5b107ce3f6 --- /dev/null +++ b/db/migrate/20220629220129_increase_webauthn_xid_length.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class IncreaseWebauthnXidLength < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + def up + new_constraint_name = check_constraint_name(:webauthn_registrations, :credential_xid, 'max_length_v3') + add_text_limit :webauthn_registrations, :credential_xid, 1364, constraint_name: new_constraint_name + + prev_constraint_name = check_constraint_name(:webauthn_registrations, :credential_xid, 'max_length_v2') + remove_text_limit :webauthn_registrations, :credential_xid, constraint_name: prev_constraint_name + end + + def down + # no-op: Danger of failling if there are records with length(credential_xid) > 1364 + end +end diff --git a/db/post_migrate/20220706132238_add_indices_on_security_scans_info_column.rb b/db/post_migrate/20220706132238_add_indices_on_security_scans_info_column.rb new file mode 100644 index 00000000000..48dc5b5d84a --- /dev/null +++ b/db/post_migrate/20220706132238_add_indices_on_security_scans_info_column.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class AddIndicesOnSecurityScansInfoColumn < Gitlab::Database::Migration[2.0] + INDEX_NAME_ON_ERRORS = :index_security_scans_on_length_of_errors + INDEX_NAME_ON_WARNINGS = :index_security_scans_on_length_of_warnings + + disable_ddl_transaction! + + def up + add_concurrent_index( + :security_scans, + "pipeline_id, jsonb_array_length(COALESCE((security_scans.info -> 'errors'::text), '[]'::jsonb))", + name: INDEX_NAME_ON_ERRORS + ) + + add_concurrent_index( + :security_scans, + "pipeline_id, jsonb_array_length(COALESCE((security_scans.info -> 'warnings'::text), '[]'::jsonb))", + name: INDEX_NAME_ON_WARNINGS + ) + end + + def down + remove_concurrent_index_by_name :security_scans, INDEX_NAME_ON_ERRORS + remove_concurrent_index_by_name :security_scans, INDEX_NAME_ON_WARNINGS + end +end diff --git a/db/schema_migrations/20220628120708 b/db/schema_migrations/20220628120708 new file mode 100644 index 00000000000..b6961491c93 --- /dev/null +++ b/db/schema_migrations/20220628120708 @@ -0,0 +1 @@ +75027a5b09491b156837707af20406b2672d5ee3ce2272ecf1496e98da2861bf
\ No newline at end of file diff --git a/db/schema_migrations/20220628121644 b/db/schema_migrations/20220628121644 new file mode 100644 index 00000000000..cac9b9d9a0b --- /dev/null +++ b/db/schema_migrations/20220628121644 @@ -0,0 +1 @@ +90b9b47ef3671b73117205264589f895a083b0d00db00e684b25e60673d2e840
\ No newline at end of file diff --git a/db/schema_migrations/20220628121712 b/db/schema_migrations/20220628121712 new file mode 100644 index 00000000000..e1c0ed37cff --- /dev/null +++ b/db/schema_migrations/20220628121712 @@ -0,0 +1 @@ +d64a9c41376bbb3bc2c9df846668b1a67b0bed1b1410d97dba17c19a2f322b38
\ No newline at end of file diff --git a/db/schema_migrations/20220629220129 b/db/schema_migrations/20220629220129 new file mode 100644 index 00000000000..580da9df82a --- /dev/null +++ b/db/schema_migrations/20220629220129 @@ -0,0 +1 @@ +2f5e08212b2f733ce5812d7154879768532e31e642b647648d1c03fd4ddf8b13
\ No newline at end of file diff --git a/db/schema_migrations/20220706132238 b/db/schema_migrations/20220706132238 new file mode 100644 index 00000000000..32ae901bfb9 --- /dev/null +++ b/db/schema_migrations/20220706132238 @@ -0,0 +1 @@ +ea387b35bfb7f15a036aca9413b8fd15ede6b16048fa9e9be5a62b9e21ca362d
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index fb278f182b7..5bdd509b107 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17493,6 +17493,23 @@ CREATE TABLE namespace_aggregation_schedules ( namespace_id integer NOT NULL ); +CREATE TABLE namespace_bans ( + id bigint NOT NULL, + namespace_id bigint NOT NULL, + user_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE namespace_bans_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE namespace_bans_id_seq OWNED BY namespace_bans.id; + CREATE TABLE namespace_ci_cd_settings ( namespace_id bigint NOT NULL, allow_stale_runner_pruning boolean DEFAULT false NOT NULL @@ -22447,7 +22464,7 @@ CREATE TABLE webauthn_registrations ( public_key text NOT NULL, u2f_registration_id integer, CONSTRAINT check_2f02e74321 CHECK ((char_length(name) <= 255)), - CONSTRAINT check_e54008d9ce CHECK ((char_length(credential_xid) <= 340)) + CONSTRAINT check_f5ab2b551a CHECK ((char_length(credential_xid) <= 1364)) ); CREATE SEQUENCE webauthn_registrations_id_seq @@ -23223,6 +23240,8 @@ ALTER TABLE ONLY milestones ALTER COLUMN id SET DEFAULT nextval('milestones_id_s ALTER TABLE ONLY namespace_admin_notes ALTER COLUMN id SET DEFAULT nextval('namespace_admin_notes_id_seq'::regclass); +ALTER TABLE ONLY namespace_bans ALTER COLUMN id SET DEFAULT nextval('namespace_bans_id_seq'::regclass); + ALTER TABLE ONLY namespace_statistics ALTER COLUMN id SET DEFAULT nextval('namespace_statistics_id_seq'::regclass); ALTER TABLE ONLY namespaces ALTER COLUMN id SET DEFAULT nextval('namespaces_id_seq'::regclass); @@ -25234,6 +25253,9 @@ ALTER TABLE ONLY namespace_admin_notes ALTER TABLE ONLY namespace_aggregation_schedules ADD CONSTRAINT namespace_aggregation_schedules_pkey PRIMARY KEY (namespace_id); +ALTER TABLE ONLY namespace_bans + ADD CONSTRAINT namespace_bans_pkey PRIMARY KEY (id); + ALTER TABLE ONLY namespace_ci_cd_settings ADD CONSTRAINT namespace_ci_cd_settings_pkey PRIMARY KEY (namespace_id); @@ -28780,6 +28802,10 @@ CREATE INDEX index_namespace_admin_notes_on_namespace_id ON namespace_admin_note CREATE UNIQUE INDEX index_namespace_aggregation_schedules_on_namespace_id ON namespace_aggregation_schedules USING btree (namespace_id); +CREATE UNIQUE INDEX index_namespace_bans_on_namespace_id_and_user_id ON namespace_bans USING btree (namespace_id, user_id); + +CREATE INDEX index_namespace_bans_on_user_id ON namespace_bans USING btree (user_id); + CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON namespace_root_storage_statistics USING btree (namespace_id); CREATE UNIQUE INDEX index_namespace_statistics_on_namespace_id ON namespace_statistics USING btree (namespace_id); @@ -29540,6 +29566,10 @@ CREATE INDEX index_security_scans_on_created_at ON security_scans USING btree (c CREATE INDEX index_security_scans_on_date_created_at_and_id ON security_scans USING btree (date(timezone('UTC'::text, created_at)), id); +CREATE INDEX index_security_scans_on_length_of_errors ON security_scans USING btree (pipeline_id, jsonb_array_length(COALESCE((info -> 'errors'::text), '[]'::jsonb))); + +CREATE INDEX index_security_scans_on_length_of_warnings ON security_scans USING btree (pipeline_id, jsonb_array_length(COALESCE((info -> 'warnings'::text), '[]'::jsonb))); + CREATE INDEX index_security_scans_on_pipeline_id ON security_scans USING btree (pipeline_id); CREATE INDEX index_security_scans_on_project_id ON security_scans USING btree (project_id); @@ -31777,6 +31807,9 @@ ALTER TABLE ONLY protected_environment_approval_rules ALTER TABLE ONLY ci_pipeline_schedule_variables ADD CONSTRAINT fk_41c35fda51 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE CASCADE; +ALTER TABLE ONLY namespace_bans + ADD CONSTRAINT fk_4275fbb1d7 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ALTER TABLE ONLY geo_event_log ADD CONSTRAINT fk_42c3b54bed FOREIGN KEY (cache_invalidation_event_id) REFERENCES geo_cache_invalidation_events(id) ON DELETE CASCADE; @@ -32164,6 +32197,9 @@ ALTER TABLE ONLY deployments ALTER TABLE ONLY routes ADD CONSTRAINT fk_bb2e5b8968 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY namespace_bans + ADD CONSTRAINT fk_bcc024eef2 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY gitlab_subscriptions ADD CONSTRAINT fk_bd0c4019c3 FOREIGN KEY (hosted_plan_id) REFERENCES plans(id) ON DELETE CASCADE; diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 4f8fbd0c07e..b19b342c665 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -347,6 +347,7 @@ Some basic Ruby runtime metrics are available: |:---------------------------------------- |:--------- |:----- |:----------- | | `ruby_gc_duration_seconds` | Counter | 11.1 | Time spent by Ruby in GC | | `ruby_gc_stat_...` | Gauge | 11.1 | Various metrics from [GC.stat](https://ruby-doc.org/core-2.6.5/GC.html#method-c-stat) | +| `ruby_gc_stat_ext_heap_fragmentation` | Gauge | 15.2 | Degree of Ruby heap fragmentation as live objects versus eden slots (range 0 to 1) | | `ruby_file_descriptors` | Gauge | 11.1 | File descriptors per process | | `ruby_sampler_duration_seconds` | Counter | 11.1 | Time spent collecting stats | | `ruby_process_cpu_seconds_total` | Gauge | 12.0 | Total amount of CPU time per process | diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index ad600761f75..2a7236e460b 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3530,12 +3530,12 @@ in that container. #### `service:pull_policy` > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21619) in GitLab 15.1 [with a flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`. Disabled by default. +> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/363186) in GitLab 15.2. > - Requires GitLab Runner 15.1 or later. FLAG: -On self-managed GitLab, by default this feature is not available. To make it available, -ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`. -The feature is not ready for production use. +On self-managed GitLab, by default this feature is available. To hide the feature, +ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`. The pull policy that the runner uses to fetch the Docker image. diff --git a/doc/development/integrations/secure.md b/doc/development/integrations/secure.md index a68c0630b18..af2566934d1 100644 --- a/doc/development/integrations/secure.md +++ b/doc/development/integrations/secure.md @@ -360,6 +360,41 @@ Ongoing improvements to report validation are tracked [in this epic](https://git In the meantime, you can see which versions are supported in the [source code](https://gitlab.com/gitlab-org/gitlab/-/blob/08dd756429731a0cca1e27ca9d59eea226398a7d/lib/gitlab/ci/parsers/security/validators/schema_validator.rb#L9-27). +#### Validate locally + +Before running your analyzer in GitLab, you should validate the report produced by your analyzer to +ensure it complies with the declared schema version. + +Use the script below to validate JSON files against a given schema. + +```ruby +require 'bundler/inline' + +gemfile do + source 'https://rubygems.org' + gem 'json_schemer' +end + +require 'json' +require 'pathname' + +raise 'Usage: ruby script.rb <security schema file name> <report file name>' unless ARGV.size == 2 + +schema = JSONSchemer.schema(Pathname.new(ARGV[0])) +report = JSON.parse(File.open(ARGV[1]).read) +schema_validation_errors = schema.validate(report).map { |error| JSONSchemer::Errors.pretty(error) } +puts(schema_validation_errors) +``` + +1. Download the appropriate schema that matches your report type and declared version. For + example, you can find version `14.0.6` of the `container_scanning` report schema at + `https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/v14.0.6/dist/container-scanning-report-format.json?inline=false`. +1. Save the Ruby script above in a file, for example, `validate.rb`. +1. Run the script, passing the schema and report file names as arguments in order. For example: + 1. Using your local Ruby interpreter: `ruby validate.rb container-scanning-format_14-0-6.json gl-container-scanning-report.json`. + 1. Using Docker: `docker run -it --rm -v $(pwd):/ci ruby:3-slim ruby /ci/validate.rb /ci/container-scanning-format_14-0-6.json /ci/gl-container-scanning-report.json` +1. Validation errors are shown on the screen. You must resolve these errors before GitLab can ingest your report. + ### Report Fields #### Version diff --git a/doc/user/analytics/ci_cd_analytics.md b/doc/user/analytics/ci_cd_analytics.md index 920b651c094..f4075c3420b 100644 --- a/doc/user/analytics/ci_cd_analytics.md +++ b/doc/user/analytics/ci_cd_analytics.md @@ -78,7 +78,7 @@ To view the lead time for changes chart: ![Lead time](img/lead_time_chart_v13_11.png) -## View time to restore service chart **(PREMIUM)** +## View time to restore service chart **(ULTIMATE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/356959) in GitLab 15.1 @@ -93,3 +93,17 @@ To view the time to restore service chart: 1. Select the **Time to restore service** tab. ![Lead time](img/time_to_restore_service_charts_v15_1.png) + +## View change failure rate chart **(ULTIMATE)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/357072) in GitLab 15.2 + +The change failure rate chart shows information about the percentage of deployments that cause an incident in a production environment. This chart is available for groups and projects. + +Change failure rate is one of the four [DORA metrics](index.md#devops-research-and-assessment-dora-key-metrics) that DevOps teams use for measuring excellence in software delivery. + +To view the change failure rate chart: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Analytics > CI/CD Analytics**. +1. Select the **Change failure rate** tab. diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md index e6bf766e177..32e5f949c15 100644 --- a/doc/user/project/integrations/webhook_events.md +++ b/doc/user/project/integrations/webhook_events.md @@ -873,6 +873,7 @@ Payload example: }, "object_attributes": { "id": 99, + "iid": 1, "target_branch": "master", "source_branch": "ms-viewport", "source_project_id": 14, @@ -884,10 +885,12 @@ Payload example: "milestone_id": null, "state": "opened", "blocking_discussions_resolved": true, + "work_in_progress": false, + "first_contribution": true, "merge_status": "unchecked", "target_project_id": 14, - "iid": 1, "description": "", + "url": "http://example.com/diaspora/merge_requests/1", "source": { "name":"Awesome Project", "description":"Aut reprehenderit ut est.", @@ -930,8 +933,18 @@ Payload example: "email": "gitlabdev@dv6700.(none)" } }, - "work_in_progress": false, - "url": "http://example.com/diaspora/merge_requests/1", + "labels": [{ + "id": 206, + "title": "API", + "color": "#ffffff", + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "template": false, + "description": "API related issues", + "type": "ProjectLabel", + "group_id": 41 + }], "action": "open", "assignee": { "name": "User1", @@ -990,6 +1003,9 @@ Payload example: } ``` +NOTE: +The fields `assignee_id`, and `state` are deprecated. + ## Wiki page events Wiki page events are triggered when a wiki page is created, updated, or deleted. diff --git a/lib/api/entities/terraform/module_version.rb b/lib/api/entities/terraform/module_version.rb new file mode 100644 index 00000000000..411fa09465e --- /dev/null +++ b/lib/api/entities/terraform/module_version.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module API + module Entities + module Terraform + class ModuleVersion < Grape::Entity + expose :name + expose :provider + expose :providers + expose :root + expose :source + expose :submodules + expose :version + expose :versions + end + end + end +end diff --git a/lib/api/terraform/modules/v1/packages.rb b/lib/api/terraform/modules/v1/packages.rb index 816a02966a7..4d3719a04fe 100644 --- a/lib/api/terraform/modules/v1/packages.rb +++ b/lib/api/terraform/modules/v1/packages.rb @@ -151,6 +151,16 @@ module API present_carrierwave_file!(package_file.file) end end + + # This endpoint has to be the last within namespace '*module_version' block + # due to how the route matching works in grape + # format: false is required, otherwise grape splits the semver version into 2 params: + # params[:module_version] and params[:format], + # thus leading to an invalid/not found module version + get format: false do + presenter = ::Terraform::ModuleVersionPresenter.new(package, params[:module_system]) + present presenter, with: ::API::Entities::Terraform::ModuleVersion + end end end diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb index 6e81a578d4a..9e446aff605 100644 --- a/lib/api/usage_data.rb +++ b/lib/api/usage_data.rb @@ -29,7 +29,7 @@ module API params do requires :event, type: String, desc: 'The event name that should be tracked' end - post 'increment_unique_users' do + post 'increment_unique_users', urgency: :low do event_name = params[:event] increment_unique_values(event_name, current_user.id) diff --git a/lib/gitlab/data_builder/issuable.rb b/lib/gitlab/data_builder/issuable.rb index 9a0b964915c..d12537c4874 100644 --- a/lib/gitlab/data_builder/issuable.rb +++ b/lib/gitlab/data_builder/issuable.rb @@ -18,7 +18,7 @@ module Gitlab user: user.hook_attrs, project: issuable.project.hook_attrs, object_attributes: issuable_builder.new(issuable).build, - labels: issuable.labels.map(&:hook_attrs), + labels: issuable.labels_hook_attrs, changes: final_changes(changes.slice(*safe_keys)), # DEPRECATED repository: issuable.project.hook_attrs.slice(:name, :url, :description, :homepage) diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml index 6ddbb64b956..e850e876ec1 100644 --- a/lib/gitlab/database/gitlab_schemas.yml +++ b/lib/gitlab/database/gitlab_schemas.yml @@ -327,6 +327,7 @@ milestone_releases: :gitlab_main milestones: :gitlab_main namespace_admin_notes: :gitlab_main namespace_aggregation_schedules: :gitlab_main +namespace_bans: :gitlab_main namespace_limits: :gitlab_main namespace_package_settings: :gitlab_main namespace_root_storage_statistics: :gitlab_main diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb index 2b4bdbd48bd..b4f90715293 100644 --- a/lib/gitlab/hook_data/merge_request_builder.rb +++ b/lib/gitlab/hook_data/merge_request_builder.rb @@ -62,7 +62,8 @@ module Gitlab assignee_id: merge_request.assignee_ids.first, # This key is deprecated labels: merge_request.labels_hook_attrs, state: merge_request.state, # This key is deprecated - blocking_discussions_resolved: merge_request.mergeable_discussions_state? + blocking_discussions_resolved: merge_request.mergeable_discussions_state?, + first_contribution: merge_request.first_contribution? } merge_request.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes) diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index 22c98981f64..50ff6146174 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -977,7 +977,7 @@ methods: statuses: - :type merge_request_diff_files: - - :diff_export + - :utf8_diff merge_requests: - :diff_head_sha - :source_branch_sha diff --git a/lib/gitlab/metrics/memory.rb b/lib/gitlab/metrics/memory.rb new file mode 100644 index 00000000000..c165cdec7a3 --- /dev/null +++ b/lib/gitlab/metrics/memory.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Metrics + module Memory + extend self + + HEAP_SLOTS_PER_PAGE = GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT] + + def gc_heap_fragmentation(gc_stat = GC.stat) + 1 - (gc_stat[:heap_live_slots] / (HEAP_SLOTS_PER_PAGE * gc_stat[:heap_eden_pages].to_f)) + end + end + end +end diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb index 4a3ef3711a5..8e002293347 100644 --- a/lib/gitlab/metrics/samplers/ruby_sampler.rb +++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb @@ -39,7 +39,8 @@ module Gitlab process_proportional_memory_bytes: ::Gitlab::Metrics.gauge(metric_name(:process, :proportional_memory_bytes), 'Memory used (PSS)', labels), process_start_time_seconds: ::Gitlab::Metrics.gauge(metric_name(:process, :start_time_seconds), 'Process start time seconds'), sampler_duration: ::Gitlab::Metrics.counter(metric_name(:sampler, :duration_seconds_total), 'Sampler time', labels), - gc_duration_seconds: ::Gitlab::Metrics.histogram(metric_name(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS) + gc_duration_seconds: ::Gitlab::Metrics.histogram(metric_name(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS), + heap_fragmentation: ::Gitlab::Metrics.gauge(metric_name(:gc_stat_ext, :heap_fragmentation), 'Ruby heap fragmentation', labels) } GC.stat.keys.each do |key| @@ -76,8 +77,13 @@ module Gitlab end # Collect generic GC stats - GC.stat.each do |key, value| - metrics[key].set(labels, value) + GC.stat.then do |gc_stat| + gc_stat.each do |key, value| + metrics[key].set(labels, value) + end + + # Collect custom GC stats + metrics[:heap_fragmentation].set(labels, Memory.gc_heap_fragmentation(gc_stat)) end end diff --git a/lib/unnested_in_filters/rewriter.rb b/lib/unnested_in_filters/rewriter.rb index c953b673c0e..cba002a5632 100644 --- a/lib/unnested_in_filters/rewriter.rb +++ b/lib/unnested_in_filters/rewriter.rb @@ -105,6 +105,8 @@ module UnnestedInFilters # LIMIT 20 # def rewrite + log_rewrite + model.from(from) .limit(limit_value) .order(order_values) @@ -125,6 +127,10 @@ module UnnestedInFilters delegate :model, :order_values, :limit_value, :where_values_hash, to: :relation, private: true + def log_rewrite + ::Gitlab::AppLogger.info(message: 'Query is being rewritten by `UnnestedInFilters`', model: model.name) + end + def from [value_tables.map(&:to_sql) + [lateral]].join(', ') end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index eb973319562..3eef677b848 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6969,6 +6969,9 @@ msgstr "" msgid "CICDAnalytics|All time" msgstr "" +msgid "CICDAnalytics|Change failure rate" +msgstr "" + msgid "CICDAnalytics|Deployment frequency" msgstr "" @@ -23625,9 +23628,6 @@ msgstr "" msgid "Manage access" msgstr "" -msgid "Manage all notifications" -msgstr "" - msgid "Manage applications that can use GitLab as an OAuth provider, and applications that you've authorized to use your account." msgstr "" @@ -32982,12 +32982,6 @@ msgstr "" msgid "Retry migration" msgstr "" -msgid "Retry the downstream pipeline" -msgstr "" - -msgid "Retry the trigger job" -msgstr "" - msgid "Retry this job" msgstr "" @@ -39678,9 +39672,6 @@ msgstr "" msgid "This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes." msgstr "" -msgid "This job triggers a downstream pipeline" -msgstr "" - msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action." msgstr "" @@ -42467,9 +42458,6 @@ msgstr "" msgid "View documentation" msgstr "" -msgid "View downstream pipeline" -msgstr "" - msgid "View eligible approvers" msgstr "" @@ -44489,18 +44477,36 @@ msgstr "" msgid "You're receiving this email because of your account on %{host}." msgstr "" -msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}" +msgid "You're receiving this email because of your account on %{host}. %{manage_label_subscriptions_link_start}Manage label subscriptions%{manage_label_subscriptions_link_end} · %{help_link_start}Help%{help_link_end}" +msgstr "" + +msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}" +msgstr "" + +msgid "You're receiving this email because of your account on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}" msgstr "" msgid "You're receiving this email because of your activity on %{host}." msgstr "" +msgid "You're receiving this email because of your activity on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}" +msgstr "" + msgid "You're receiving this email because you have been assigned an item on %{host}." msgstr "" +msgid "You're receiving this email because you have been assigned an item on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}" +msgstr "" + msgid "You're receiving this email because you have been mentioned on %{host}." msgstr "" +msgid "You're receiving this email because you have been mentioned on %{host}. %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}" +msgstr "" + +msgid "You're receiving this email because you have been mentioned on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}" +msgstr "" + msgid "You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication." msgstr "" @@ -44930,6 +44936,9 @@ msgstr "" msgid "allowed to fail" msgstr "" +msgid "already banned from namespace" +msgstr "" + msgid "already being used for another group or project %{timebox_name}." msgstr "" diff --git a/metrics_server/dependencies.rb b/metrics_server/dependencies.rb index 3f188658ba2..233511eb505 100644 --- a/metrics_server/dependencies.rb +++ b/metrics_server/dependencies.rb @@ -20,6 +20,7 @@ require_relative '../lib/prometheus/cleanup_multiproc_dir_service' require_relative '../lib/gitlab/metrics/prometheus' require_relative '../lib/gitlab/metrics' require_relative '../lib/gitlab/metrics/system' +require_relative '../lib/gitlab/metrics/memory' require_relative '../lib/gitlab/metrics/samplers/base_sampler' require_relative '../lib/gitlab/metrics/samplers/ruby_sampler' require_relative '../lib/gitlab/metrics/exporter/base_exporter' diff --git a/qa/qa/page/group/settings/package_registries.rb b/qa/qa/page/group/settings/package_registries.rb index 433872a378a..fa24f7c2ce8 100644 --- a/qa/qa/page/group/settings/package_registries.rb +++ b/qa/qa/page/group/settings/package_registries.rb @@ -18,13 +18,13 @@ module QA end def set_allow_duplicates_disabled - expand_content :package_registry_settings_content do + within_element :package_registry_settings_content do click_on_allow_duplicates_button if duplicates_enabled? end end def set_allow_duplicates_enabled - expand_content :package_registry_settings_content do + within_element :package_registry_settings_content do click_on_allow_duplicates_button unless duplicates_enabled? end end @@ -49,7 +49,7 @@ module QA end def has_dependency_proxy_enabled? - expand_content :dependency_proxy_settings_content do + within_element :dependency_proxy_settings_content do within_element :dependency_proxy_setting_toggle do toggle = find('button.gl-toggle') toggle[:class].include?('is-checked') diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb index 3fe276ce162..5317f586390 100644 --- a/spec/features/unsubscribe_links_spec.rb +++ b/spec/features/unsubscribe_links_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'Unsubscribe links', :sidekiq_might_not_need_inline do let(:mail) { ActionMailer::Base.deliveries.last } let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) } let(:header_link) { mail.header['List-Unsubscribe'].to_s[1..-2] } # Strip angle brackets - let(:body_link) { body.find_link('unsubscribe')['href'] } + let(:body_link) { body.find_link('Unsubscribe')['href'] } before do perform_enqueued_jobs { issue } diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/single_version.json b/spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/single_version.json new file mode 100644 index 00000000000..689b9427f38 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/single_version.json @@ -0,0 +1,55 @@ +{ + "type": "object", + "required": [ + "name", + "provider", + "providers", + "root", + "source", + "submodules", + "version", + "versions" + ], + "properties": { + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "providers": { + "type": "array", + "items": { + "type": "string" + } + }, + "root": { + "type": "object", + "required": [ + "dependencies" + ], + "properties": { + "dependencies": { + "type": "array", + "maxItems": 0 + } + } + }, + "source": { + "type": "string" + }, + "submodules": { + "type": "array", + "maxItems": 0 + }, + "version": { + "type": "string" + }, + "versions": { + "type": "array", + "items": { + "type": "string" + } + } + } +}
\ No newline at end of file diff --git a/spec/frontend/jobs/bridge/app_spec.js b/spec/frontend/jobs/bridge/app_spec.js deleted file mode 100644 index 210dcfa364b..00000000000 --- a/spec/frontend/jobs/bridge/app_spec.js +++ /dev/null @@ -1,146 +0,0 @@ -import Vue, { nextTick } from 'vue'; -import { shallowMount } from '@vue/test-utils'; - -import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; -import { GlLoadingIcon } from '@gitlab/ui'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import getPipelineQuery from '~/jobs/bridge/graphql/queries/pipeline.query.graphql'; -import waitForPromises from 'helpers/wait_for_promises'; -import BridgeApp from '~/jobs/bridge/app.vue'; -import BridgeEmptyState from '~/jobs/bridge/components/empty_state.vue'; -import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue'; -import CiHeader from '~/vue_shared/components/header_ci_component.vue'; -import { - MOCK_BUILD_ID, - MOCK_PIPELINE_IID, - MOCK_PROJECT_FULL_PATH, - mockPipelineQueryResponse, -} from './mock_data'; - -describe('Bridge Show Page', () => { - let wrapper; - let mockApollo; - let mockPipelineQuery; - - const createComponent = (options) => { - wrapper = shallowMount(BridgeApp, { - provide: { - buildId: MOCK_BUILD_ID, - projectFullPath: MOCK_PROJECT_FULL_PATH, - pipelineIid: MOCK_PIPELINE_IID, - }, - mocks: { - $apollo: { - queries: { - pipeline: { - loading: true, - }, - }, - }, - }, - ...options, - }); - }; - - const createComponentWithApollo = () => { - const handlers = [[getPipelineQuery, mockPipelineQuery]]; - Vue.use(VueApollo); - mockApollo = createMockApollo(handlers); - - createComponent({ - apolloProvider: mockApollo, - mocks: {}, - }); - }; - - const findCiHeader = () => wrapper.findComponent(CiHeader); - const findEmptyState = () => wrapper.findComponent(BridgeEmptyState); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findSidebar = () => wrapper.findComponent(BridgeSidebar); - - beforeEach(() => { - mockPipelineQuery = jest.fn(); - }); - - afterEach(() => { - mockPipelineQuery.mockReset(); - wrapper.destroy(); - }); - - describe('while pipeline query is loading', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders loading icon', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); - }); - - describe('after pipeline query is loaded', () => { - beforeEach(async () => { - mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse); - createComponentWithApollo(); - await waitForPromises(); - }); - - it('query is called with correct variables', async () => { - expect(mockPipelineQuery).toHaveBeenCalledTimes(1); - expect(mockPipelineQuery).toHaveBeenCalledWith({ - fullPath: MOCK_PROJECT_FULL_PATH, - iid: MOCK_PIPELINE_IID, - }); - }); - - it('renders CI header state', () => { - expect(findCiHeader().exists()).toBe(true); - }); - - it('renders empty state', () => { - expect(findEmptyState().exists()).toBe(true); - }); - - it('renders sidebar', () => { - expect(findSidebar().exists()).toBe(true); - }); - }); - - describe('sidebar expansion', () => { - beforeEach(async () => { - mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse); - createComponentWithApollo(); - await waitForPromises(); - }); - - describe('on resize', () => { - it.each` - breakpoint | isSidebarExpanded - ${'xs'} | ${false} - ${'sm'} | ${false} - ${'md'} | ${true} - ${'lg'} | ${true} - ${'xl'} | ${true} - `( - 'sets isSidebarExpanded to `$isSidebarExpanded` when the breakpoint is "$breakpoint"', - async ({ breakpoint, isSidebarExpanded }) => { - jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue(breakpoint); - - window.dispatchEvent(new Event('resize')); - await nextTick(); - - expect(findSidebar().exists()).toBe(isSidebarExpanded); - }, - ); - }); - - it('toggles expansion on button click', async () => { - expect(findSidebar().exists()).toBe(true); - - wrapper.vm.toggleSidebar(); - await nextTick(); - - expect(findSidebar().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/jobs/bridge/components/empty_state_spec.js b/spec/frontend/jobs/bridge/components/empty_state_spec.js deleted file mode 100644 index 38c55b296f0..00000000000 --- a/spec/frontend/jobs/bridge/components/empty_state_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import BridgeEmptyState from '~/jobs/bridge/components/empty_state.vue'; -import { MOCK_EMPTY_ILLUSTRATION_PATH, MOCK_PATH_TO_DOWNSTREAM } from '../mock_data'; - -describe('Bridge Empty State', () => { - let wrapper; - - const createComponent = ({ downstreamPipelinePath }) => { - wrapper = shallowMount(BridgeEmptyState, { - provide: { - emptyStateIllustrationPath: MOCK_EMPTY_ILLUSTRATION_PATH, - }, - propsData: { - downstreamPipelinePath, - }, - }); - }; - - const findSvg = () => wrapper.find('img'); - const findTitle = () => wrapper.find('h1'); - const findLinkBtn = () => wrapper.findComponent(GlButton); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('template', () => { - beforeEach(() => { - createComponent({ downstreamPipelinePath: MOCK_PATH_TO_DOWNSTREAM }); - }); - - it('renders illustration', () => { - expect(findSvg().exists()).toBe(true); - }); - - it('renders title', () => { - expect(findTitle().exists()).toBe(true); - expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.title); - }); - - it('renders CTA button', () => { - expect(findLinkBtn().exists()).toBe(true); - expect(findLinkBtn().text()).toBe(wrapper.vm.$options.i18n.linkBtnText); - expect(findLinkBtn().attributes('href')).toBe(MOCK_PATH_TO_DOWNSTREAM); - }); - }); - - describe('without downstream pipeline', () => { - beforeEach(() => { - createComponent({ downstreamPipelinePath: undefined }); - }); - - it('does not render CTA button', () => { - expect(findLinkBtn().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/jobs/bridge/components/sidebar_spec.js b/spec/frontend/jobs/bridge/components/sidebar_spec.js deleted file mode 100644 index 5006d4f08a6..00000000000 --- a/spec/frontend/jobs/bridge/components/sidebar_spec.js +++ /dev/null @@ -1,99 +0,0 @@ -import { GlButton, GlDropdown } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue'; -import CommitBlock from '~/jobs/components/commit_block.vue'; -import { mockCommit, mockJob } from '../mock_data'; - -describe('Bridge Sidebar', () => { - let wrapper; - - const MockHeaderEl = { - getBoundingClientRect() { - return { - bottom: '40', - }; - }, - }; - - const createComponent = ({ featureFlag } = {}) => { - wrapper = shallowMount(BridgeSidebar, { - provide: { - glFeatures: { - triggerJobRetryAction: featureFlag, - }, - }, - propsData: { - bridgeJob: mockJob, - commit: mockCommit, - }, - }); - }; - - const findJobTitle = () => wrapper.find('h4'); - const findCommitBlock = () => wrapper.findComponent(CommitBlock); - const findRetryDropdown = () => wrapper.find(GlDropdown); - const findToggleBtn = () => wrapper.findComponent(GlButton); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('template', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders job name', () => { - expect(findJobTitle().text()).toBe(mockJob.name); - }); - - it('renders commit information', () => { - expect(findCommitBlock().exists()).toBe(true); - }); - }); - - describe('styles', () => { - beforeEach(async () => { - jest.spyOn(document, 'querySelector').mockReturnValue(MockHeaderEl); - createComponent(); - }); - - it('calculates root styles correctly', () => { - expect(wrapper.attributes('style')).toBe('width: 290px; top: 40px;'); - }); - }); - - describe('sidebar expansion', () => { - beforeEach(() => { - createComponent(); - }); - - it('emits toggle sidebar event on button click', async () => { - expect(wrapper.emitted('toggleSidebar')).toBe(undefined); - - findToggleBtn().vm.$emit('click'); - - expect(wrapper.emitted('toggleSidebar')).toHaveLength(1); - }); - }); - - describe('retry action', () => { - describe('when feature flag is ON', () => { - beforeEach(() => { - createComponent({ featureFlag: true }); - }); - - it('renders retry dropdown', () => { - expect(findRetryDropdown().exists()).toBe(true); - }); - }); - - describe('when feature flag is OFF', () => { - it('does not render retry dropdown', () => { - createComponent({ featureFlag: false }); - - expect(findRetryDropdown().exists()).toBe(false); - }); - }); - }); -}); diff --git a/spec/frontend/jobs/bridge/mock_data.js b/spec/frontend/jobs/bridge/mock_data.js deleted file mode 100644 index 4084bb54163..00000000000 --- a/spec/frontend/jobs/bridge/mock_data.js +++ /dev/null @@ -1,102 +0,0 @@ -export const MOCK_EMPTY_ILLUSTRATION_PATH = '/path/to/svg'; -export const MOCK_PATH_TO_DOWNSTREAM = '/path/to/downstream/pipeline'; -export const MOCK_BUILD_ID = '1331'; -export const MOCK_PIPELINE_IID = '174'; -export const MOCK_PROJECT_FULL_PATH = '/root/project/'; -export const MOCK_SHA = '38f3d89147765427a7ce58be28cd76d14efa682a'; - -export const mockCommit = { - id: `gid://gitlab/CommitPresenter/${MOCK_SHA}`, - shortId: '38f3d891', - title: 'Update .gitlab-ci.yml file', - webPath: `/root/project/-/commit/${MOCK_SHA}`, - __typename: 'Commit', -}; - -export const mockJob = { - createdAt: '2021-12-10T09:05:45Z', - id: 'gid://gitlab/Ci::Build/1331', - name: 'triggerJobName', - scheduledAt: null, - startedAt: '2021-12-10T09:13:43Z', - status: 'SUCCESS', - triggered: null, - detailedStatus: { - id: '1', - detailsPath: '/root/project/-/jobs/1331', - icon: 'status_success', - group: 'success', - text: 'passed', - tooltip: 'passed', - __typename: 'DetailedStatus', - }, - downstreamPipeline: { - id: '1', - path: '/root/project/-/pipelines/175', - }, - stage: { - id: '1', - name: 'build', - __typename: 'CiStage', - }, - __typename: 'CiJob', -}; - -export const mockUser = { - id: 'gid://gitlab/User/1', - avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - name: 'Administrator', - username: 'root', - webPath: '/root', - webUrl: 'http://gdk.test:3000/root', - status: { - message: 'making great things', - __typename: 'UserStatus', - }, - __typename: 'UserCore', -}; - -export const mockStage = { - id: '1', - name: 'build', - jobs: { - nodes: [mockJob], - __typename: 'CiJobConnection', - }, - __typename: 'CiStage', -}; - -export const mockPipelineQueryResponse = { - data: { - project: { - id: '1', - pipeline: { - commit: mockCommit, - id: 'gid://gitlab/Ci::Pipeline/174', - iid: '88', - path: '/root/project/-/pipelines/174', - sha: MOCK_SHA, - ref: 'main', - refPath: 'path/to/ref', - user: mockUser, - detailedStatus: { - id: '1', - icon: 'status_failed', - group: 'failed', - __typename: 'DetailedStatus', - }, - stages: { - edges: [ - { - node: mockStage, - __typename: 'CiStageEdge', - }, - ], - __typename: 'CiStageConnection', - }, - __typename: 'Pipeline', - }, - __typename: 'Project', - }, - }, -}; diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js index 98c7856a61a..df8455531b3 100644 --- a/spec/frontend/projects/pipelines/charts/components/app_spec.js +++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js @@ -14,6 +14,7 @@ jest.mock('~/lib/utils/url_utility'); const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} }; const LeadTimeChartsStub = { name: 'LeadTimeCharts', render: () => {} }; const TimeToRestoreServiceChartsStub = { name: 'TimeToRestoreServiceCharts', render: () => {} }; +const ChangeFailureRateChartsStub = { name: 'ChangeFailureRateCharts', render: () => {} }; const ProjectQualitySummaryStub = { name: 'ProjectQualitySummary', render: () => {} }; describe('ProjectsPipelinesChartsApp', () => { @@ -33,6 +34,7 @@ describe('ProjectsPipelinesChartsApp', () => { DeploymentFrequencyCharts: DeploymentFrequencyChartsStub, LeadTimeCharts: LeadTimeChartsStub, TimeToRestoreServiceCharts: TimeToRestoreServiceChartsStub, + ChangeFailureRateCharts: ChangeFailureRateChartsStub, ProjectQualitySummary: ProjectQualitySummaryStub, }, }, @@ -50,6 +52,7 @@ describe('ProjectsPipelinesChartsApp', () => { const findGlTabAtIndex = (index) => findAllGlTabs().at(index); const findLeadTimeCharts = () => wrapper.find(LeadTimeChartsStub); const findTimeToRestoreServiceCharts = () => wrapper.find(TimeToRestoreServiceChartsStub); + const findChangeFailureRateCharts = () => wrapper.find(ChangeFailureRateChartsStub); const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub); const findPipelineCharts = () => wrapper.find(PipelineCharts); const findProjectQualitySummary = () => wrapper.find(ProjectQualitySummaryStub); @@ -59,58 +62,49 @@ describe('ProjectsPipelinesChartsApp', () => { createComponent(); }); - it('renders tabs', () => { - expect(findGlTabs().exists()).toBe(true); + describe.each` + title | finderFn | index + ${'Pipelines'} | ${findPipelineCharts} | ${0} + ${'Deployment frequency'} | ${findDeploymentFrequencyCharts} | ${1} + ${'Lead time'} | ${findLeadTimeCharts} | ${2} + ${'Time to restore service'} | ${findTimeToRestoreServiceCharts} | ${3} + ${'Change failure rate'} | ${findChangeFailureRateCharts} | ${4} + ${'Project quality'} | ${findProjectQualitySummary} | ${5} + `('Tabs', ({ title, finderFn, index }) => { + it(`renders tab with a title ${title} at index ${index}`, () => { + expect(findGlTabAtIndex(index).attributes('title')).toBe(title); + }); - expect(findGlTabAtIndex(0).attributes('title')).toBe('Pipelines'); - expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployment frequency'); - expect(findGlTabAtIndex(2).attributes('title')).toBe('Lead time'); - expect(findGlTabAtIndex(3).attributes('title')).toBe('Time to restore service'); - }); + it(`renders the ${title} chart`, () => { + expect(finderFn().exists()).toBe(true); + }); - it('renders the pipeline charts', () => { - expect(findPipelineCharts().exists()).toBe(true); - }); + it(`updates the current tab and url when the ${title} tab is clicked`, async () => { + let chartsPath; + const tabName = title.toLowerCase().replace(/\s/g, '-'); - it('renders the deployment frequency charts', () => { - expect(findDeploymentFrequencyCharts().exists()).toBe(true); - }); + setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`); - it('renders the lead time charts', () => { - expect(findLeadTimeCharts().exists()).toBe(true); - }); + mergeUrlParams.mockImplementation(({ chart }, path) => { + expect(chart).toBe(tabName); + expect(path).toBe(window.location.pathname); + chartsPath = `${path}?chart=${chart}`; + return chartsPath; + }); - it('renders the time to restore service charts', () => { - expect(findTimeToRestoreServiceCharts().exists()).toBe(true); - }); + updateHistory.mockImplementation(({ url }) => { + expect(url).toBe(chartsPath); + }); + const tabs = findGlTabs(); - it('renders the project quality summary', () => { - expect(findProjectQualitySummary().exists()).toBe(true); - }); + expect(tabs.attributes('value')).toBe('0'); - it('sets the tab and url when a tab is clicked', async () => { - let chartsPath; - setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`); + tabs.vm.$emit('input', index); - mergeUrlParams.mockImplementation(({ chart }, path) => { - expect(chart).toBe('deployment-frequency'); - expect(path).toBe(window.location.pathname); - chartsPath = `${path}?chart=${chart}`; - return chartsPath; - }); + await nextTick(); - updateHistory.mockImplementation(({ url }) => { - expect(url).toBe(chartsPath); + expect(tabs.attributes('value')).toBe(index.toString()); }); - const tabs = findGlTabs(); - - expect(tabs.attributes('value')).toBe('0'); - - tabs.vm.$emit('input', 1); - - await nextTick(); - - expect(tabs.attributes('value')).toBe('1'); }); it('should not try to push history if the tab does not change', async () => { @@ -151,6 +145,7 @@ describe('ProjectsPipelinesChartsApp', () => { describe('when provided with a query param', () => { it.each` chart | tab + ${'change-failure-rate'} | ${'4'} ${'time-to-restore-service'} | ${'3'} ${'lead-time'} | ${'2'} ${'deployment-frequency'} | ${'1'} diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index 220e154aad8..04653d9ff03 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -77,7 +77,7 @@ RSpec.describe EmailsHelper do end describe 'notification_reason_text' do - subject { helper.notification_reason_text(reason_code) } + subject { helper.notification_reason_text(reason: reason_code) } using RSpec::Parameterized::TableSyntax diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index 771fc0218e2..25b84a67ab2 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -7,13 +7,12 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do let(:builder) { described_class.new(merge_request) } - describe '#build' do - let(:data) { builder.build } + describe '.safe_hook_attributes' do + let(:safe_attribute_keys) { described_class.safe_hook_attributes } it 'includes safe attribute' do - %w[ + expected_safe_attribute_keys = %i[ assignee_id - assignee_ids author_id blocking_discussions_resolved created_at @@ -32,17 +31,21 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do milestone_id source_branch source_project_id - state + state_id target_branch target_project_id time_estimate title updated_at updated_by_id - ].each do |key| - expect(data).to include(key) - end + ].freeze + + expect(safe_attribute_keys).to match_array(expected_safe_attribute_keys) end + end + + describe '#build' do + let(:data) { builder.build } %i[source target].each do |key| describe "#{key} key" do @@ -52,17 +55,30 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do end end + it 'includes safe attributes' do + expect(data).to include(*described_class.safe_hook_attributes) + end + it 'includes additional attrs' do - expect(data).to include(:source) - expect(data).to include(:target) - expect(data).to include(:last_commit) - expect(data).to include(:work_in_progress) - expect(data).to include(:total_time_spent) - expect(data).to include(:time_change) - expect(data).to include(:human_time_estimate) - expect(data).to include(:human_total_time_spent) - expect(data).to include(:human_time_change) - expect(data).to include(:labels) + expected_additional_attributes = %w[ + description + url + last_commit + work_in_progress + total_time_spent + time_change + human_total_time_spent + human_time_change + human_time_estimate + assignee_ids + assignee_id + labels + state + blocking_discussions_resolved + first_contribution + ].freeze + + expect(data).to include(*expected_additional_attributes) end context 'when the MR has an image in the description' do diff --git a/spec/lib/gitlab/metrics/memory_spec.rb b/spec/lib/gitlab/metrics/memory_spec.rb new file mode 100644 index 00000000000..fd8ca3b37c6 --- /dev/null +++ b/spec/lib/gitlab/metrics/memory_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Metrics::Memory do + describe '.gc_heap_fragmentation' do + subject(:call) do + described_class.gc_heap_fragmentation( + heap_live_slots: gc_stat_heap_live_slots, + heap_eden_pages: gc_stat_heap_eden_pages + ) + end + + context 'when the Ruby heap is perfectly utilized' do + # All objects are located in a single heap page. + let(:gc_stat_heap_live_slots) { described_class::HEAP_SLOTS_PER_PAGE } + let(:gc_stat_heap_eden_pages) { 1 } + + it { is_expected.to eq(0) } + end + + context 'when the Ruby heap is greatly fragmented' do + # There is one object per heap page. + let(:gc_stat_heap_live_slots) { described_class::HEAP_SLOTS_PER_PAGE } + let(:gc_stat_heap_eden_pages) { described_class::HEAP_SLOTS_PER_PAGE } + + # The heap can never be "perfectly fragmented" because that would require + # zero objects per page. + it { is_expected.to be > 0.99 } + end + + context 'when the Ruby heap is semi-fragmented' do + # All objects are spread over two pages i.e. each page is 50% utilized. + let(:gc_stat_heap_live_slots) { described_class::HEAP_SLOTS_PER_PAGE } + let(:gc_stat_heap_eden_pages) { 2 } + + it { is_expected.to eq(0.5) } + end + end +end diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index dfae5aa6784..b1566ffa7b4 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -125,5 +125,11 @@ RSpec.describe Gitlab::Metrics::Samplers::RubySampler do sampler.sample end + + it 'adds a heap fragmentation metric' do + expect(sampler.metrics[:heap_fragmentation]).to receive(:set).with({}, anything) + + sampler.sample + end end end diff --git a/spec/lib/unnested_in_filters/rewriter_spec.rb b/spec/lib/unnested_in_filters/rewriter_spec.rb index f4fff393f28..e2ccbd92504 100644 --- a/spec/lib/unnested_in_filters/rewriter_spec.rb +++ b/spec/lib/unnested_in_filters/rewriter_spec.rb @@ -138,5 +138,20 @@ RSpec.describe UnnestedInFilters::Rewriter do end end end + + describe 'logging' do + subject(:load_reload) { rewriter.rewrite } + + before do + allow(::Gitlab::AppLogger).to receive(:info) + end + + it 'logs the call' do + load_reload + + expect(::Gitlab::AppLogger) + .to have_received(:info).with(message: 'Query is being rewritten by `UnnestedInFilters`', model: 'User') + end + end end end diff --git a/spec/mailers/emails/admin_notification_spec.rb b/spec/mailers/emails/admin_notification_spec.rb index bfd07f1d438..1b770d6d4a2 100644 --- a/spec/mailers/emails/admin_notification_spec.rb +++ b/spec/mailers/emails/admin_notification_spec.rb @@ -64,7 +64,7 @@ RSpec.describe Emails::AdminNotification do end it 'includes the email reason' do - is_expected.to have_body_text "You're receiving this email because of your account on localhost" + is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>} end context 'when scoped to a group' do diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb index f4483f7e8f5..09ed27eb90f 100644 --- a/spec/mailers/emails/profile_spec.rb +++ b/spec/mailers/emails/profile_spec.rb @@ -151,7 +151,7 @@ RSpec.describe Emails::Profile do end it 'includes the email reason' do - is_expected.to have_body_text /You're receiving this email because of your account on localhost/ + is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>} end end end @@ -187,7 +187,7 @@ RSpec.describe Emails::Profile do end it 'includes the email reason' do - is_expected.to have_body_text /You're receiving this email because of your account on localhost/ + is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>} end context 'with User does not exist' do @@ -222,7 +222,7 @@ RSpec.describe Emails::Profile do end it 'includes the email reason' do - is_expected.to have_body_text /You're receiving this email because of your account on localhost/ + is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>} end end @@ -266,7 +266,7 @@ RSpec.describe Emails::Profile do end shared_examples 'includes the email reason' do - it { is_expected.to have_body_text /You're receiving this email because of your account on localhost/ } + it { is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>} } end shared_examples 'valid use case' do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index f501488bb8c..8beb54bca4d 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Notify do include EmailSpec::Helpers include EmailSpec::Matchers include EmailHelpers + include EmailsHelper include RepoHelpers include MembersHelper @@ -396,7 +397,7 @@ RSpec.describe Notify do end end - context 'when sent with a reason' do + context 'when sent with a reason', type: :helper do subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::ASSIGNED) } it_behaves_like 'appearance header and footer enabled' @@ -407,15 +408,15 @@ RSpec.describe Notify do end it 'includes the reason in the footer' do - text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::ASSIGNED) + text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(reason: NotificationReason::ASSIGNED, format: :html) is_expected.to have_body_text(text) new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::MENTIONED) - text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::MENTIONED) + text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(reason: NotificationReason::MENTIONED, format: :html) expect(new_subject).to have_body_text(text) new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, nil) - text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(nil) + text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(format: :html) expect(new_subject).to have_body_text(text) end end diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb index 64203c50759..7dc550a6c93 100644 --- a/spec/models/merge_request_diff_file_spec.rb +++ b/spec/models/merge_request_diff_file_spec.rb @@ -97,10 +97,8 @@ RSpec.describe MergeRequestDiffFile do file.utf8_diff end - end - describe '#diff_export' do - context 'when diff is externally stored' do + context 'externally stored diff caching' do let(:test_dir) { 'tmp/tests/external-diffs' } around do |example| @@ -121,7 +119,7 @@ RSpec.describe MergeRequestDiffFile do it 'caches external diffs' do expect(file.merge_request_diff).to receive(:cache_external_diff).and_call_original - expect(file.diff_export).to eq(file.utf8_diff) + expect(file.utf8_diff).to eq(file.diff) end end @@ -133,7 +131,7 @@ RSpec.describe MergeRequestDiffFile do expect(file_stub).to receive(:seek).with(file.external_diff_offset) expect(file_stub).to receive(:read).with(file.external_diff_size) - file.diff_export + file.utf8_diff end end @@ -151,28 +149,28 @@ RSpec.describe MergeRequestDiffFile do end it 'unpacks from base 64' do - expect(file.diff_export).to eq(unpacked) + expect(file.utf8_diff).to eq(unpacked) end context 'invalid base64' do let(:packed) { '---/dev/null' } it 'returns the raw diff' do - expect(file.diff_export).to eq(packed) + expect(file.utf8_diff).to eq(packed) end end end context 'when the diff is not marked as binary' do it 'returns the raw diff' do - expect(file.diff_export).to eq(packed) + expect(file.utf8_diff).to eq(packed) end end end context 'when content responds to #encoding' do it 'encodes content to utf8 encoding' do - expect(file.diff_export.encoding).to eq(Encoding::UTF_8) + expect(file.utf8_diff.encoding).to eq(Encoding::UTF_8) end end @@ -180,35 +178,46 @@ RSpec.describe MergeRequestDiffFile do it 'returns an empty string' do allow(file.merge_request_diff).to receive(:cached_external_diff).and_return(nil) - expect(file.diff_export).to eq('') + expect(file.utf8_diff).to eq('') end end context 'when exception is raised' do - it 'falls back to #utf8_diff' do - allow(file).to receive(:binary?).and_raise(StandardError) - expect(file).to receive(:utf8_diff) - - file.diff_export + it 'falls back to #diff' do + allow(file).to receive(:binary?).and_raise(StandardError, 'Error!') + expect(file).to receive(:diff) + expect(Gitlab::AppLogger) + .to receive(:warn) + .with( + a_hash_including( + :message => 'Cached external diff export failed', + :merge_request_diff_file_id => file.id, + :merge_request_diff_id => file.merge_request_diff.id, + 'exception.class' => 'StandardError', + 'exception.message' => 'Error!' + ) + ) + + file.utf8_diff end end end context 'when externally_stored_diffs_caching_export feature flag is disabled' do - it 'calls #utf8_diff' do + it 'calls #diff' do stub_feature_flags(externally_stored_diffs_caching_export: false) - expect(file).to receive(:utf8_diff) + expect(file).to receive(:diff) - file.diff_export + file.utf8_diff end end context 'when diff is not stored externally' do - it 'calls #utf8_diff' do - expect(file).to receive(:utf8_diff) + it 'calls #diff' do + expect(file).to receive(:diff) - file.diff_export + file.utf8_diff end end end diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb index cb305227b01..d802282f8dc 100644 --- a/spec/requests/api/terraform/modules/v1/packages_spec.rb +++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb @@ -171,6 +171,75 @@ RSpec.describe API::Terraform::Modules::V1::Packages do end end + describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version' do + let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}") } + let(:headers) { {} } + + subject { get(url, headers: headers) } + + context 'not found' do + let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/2.0.0") } + let(:headers) { { 'Authorization' => "Bearer #{tokens[:job_token]}" } } + + subject { get(url, headers: headers) } + + it 'returns not found when the specified version is not present in the registry' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with valid namespace' do + where(:visibility, :user_role, :member, :token_type, :shared_examples_name, :expected_status) do + :public | :developer | true | :personal_access_token | 'returns terraform module version' | :success + :public | :guest | true | :personal_access_token | 'returns terraform module version' | :success + :public | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | :personal_access_token | 'returns terraform module version' | :success + :public | :guest | false | :personal_access_token | 'returns terraform module version' | :success + :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :anonymous | false | nil | 'returns terraform module version' | :success + :private | :developer | true | :personal_access_token | 'returns terraform module version' | :success + :private | :guest | true | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :guest | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :anonymous | false | nil | 'rejects terraform module packages access' | :unauthorized + :public | :developer | true | :job_token | 'returns terraform module version' | :success + :public | :guest | true | :job_token | 'returns terraform module version' | :success + :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | :job_token | 'returns terraform module version' | :success + :public | :guest | false | :job_token | 'returns terraform module version' | :success + :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | true | :job_token | 'returns terraform module version' | :success + :private | :guest | true | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :guest | false | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + end + + with_them do + let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } } + + before do + group.update!(visibility: visibility.to_s) + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/download' do let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/download") } let(:headers) { {} } diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb index f3c616c7d9b..9d8f37a3e64 100644 --- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb +++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb @@ -269,6 +269,17 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context it 'returns items not assigned to that milestone' do expect(items).to contain_exactly(item2, item3, item4, item5) end + + context 'with multiple milestones' do + let(:milestone2) { create(:milestone, project: project2) } + let(:params) { { not: { milestone_title: [milestone.title, milestone2.title] } } } + + it 'returns items not assigned to both milestones' do + item2.update!(milestone: milestone2) + + expect(items).to contain_exactly(item3, item4, item5) + end + end end context 'filtering by group milestone' do diff --git a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb index ab994e2c8c7..544a0ed8fdd 100644 --- a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb @@ -102,6 +102,22 @@ RSpec.shared_examples 'returns terraform module packages' do |user_type, status, end end +RSpec.shared_examples 'returns terraform module version' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'returning a valid response' do + subject + + expect(json_response).to match_schema('public_api/v4/packages/terraform/modules/v1/single_version') + end + end +end + RSpec.shared_examples 'returns no terraform module packages' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do diff --git a/spec/views/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb index da93871e0e4..2ca23d4cb2d 100644 --- a/spec/views/projects/commits/_commit.html.haml_spec.rb +++ b/spec/views/projects/commits/_commit.html.haml_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'projects/commits/_commit.html.haml' do - let(:template) { 'projects/commits/commit.html.haml' } + let(:template) { 'projects/commits/commit' } let(:project) { create(:project, :repository) } let(:commit) { project.repository.commit(ref) } diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb index 8242d20a9e7..2ea3dc9f76a 100644 --- a/spec/views/projects/jobs/show.html.haml_spec.rb +++ b/spec/views/projects/jobs/show.html.haml_spec.rb @@ -27,7 +27,6 @@ RSpec.describe 'projects/jobs/show' do it 'shows job vue app' do expect(rendered).to have_css('#js-job-page') - expect(rendered).not_to have_css('#js-bridge-page') end context 'when job is running' do @@ -42,18 +41,4 @@ RSpec.describe 'projects/jobs/show' do end end end - - context 'when showing a bridge job' do - let(:bridge) { create(:ci_bridge, status: :pending) } - - before do - assign(:build, bridge) - render - end - - it 'shows bridge vue app' do - expect(rendered).to have_css('#js-bridge-page') - expect(rendered).not_to have_css('#js-job-page') - end - end end diff --git a/workhorse/go.mod b/workhorse/go.mod index 6832647e57f..4afb47fb82a 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -25,7 +25,7 @@ require ( github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a github.com/sirupsen/logrus v1.8.1 github.com/smartystreets/goconvey v1.7.2 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.8.0 gitlab.com/gitlab-org/gitaly/v15 v15.1.0 gitlab.com/gitlab-org/golang-archive-zip v0.1.1 gitlab.com/gitlab-org/labkit v1.15.0 @@ -113,5 +113,5 @@ require ( google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de // indirect gopkg.in/DataDog/dd-trace-go.v1 v1.32.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/workhorse/go.sum b/workhorse/go.sum index 1a4f0fd500e..063d287b61c 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -1044,16 +1044,19 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= @@ -1815,8 +1818,9 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= |