Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-04 15:10:25 +00:00
parent c80a1141e3
commit d4194db620
66 changed files with 1327 additions and 761 deletions

View file

@ -1 +1 @@
13.18.0
13.19.0

View file

@ -301,9 +301,7 @@ gem 'gitlab-license', '~> 1.5'
gem 'rack-attack', '~> 6.3.0'
# Sentry integration
gem 'sentry-ruby', '~> 4.4.0'
gem 'sentry-sidekiq', '~> 4.4.0'
gem 'sentry-rails', '~> 4.4.0'
gem 'sentry-raven', '~> 3.1'
# PostgreSQL query parsing
#

View file

@ -1172,18 +1172,8 @@ GEM
selenium-webdriver (3.142.7)
childprocess (>= 0.5, < 4.0)
rubyzip (>= 1.2.2)
sentry-rails (4.4.0)
railties (>= 5.0)
sentry-ruby-core (~> 4.4.0.pre.beta)
sentry-ruby (4.4.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-raven (3.1.2)
faraday (>= 1.0)
sentry-ruby-core (= 4.4.2)
sentry-ruby-core (4.4.2)
concurrent-ruby
faraday
sentry-sidekiq (4.4.0)
sentry-ruby-core (~> 4.4.0.pre.beta)
settingslogic (2.0.9)
sexp_processor (4.15.1)
shellany (0.0.1)
@ -1629,9 +1619,7 @@ DEPENDENCIES
sassc-rails (~> 2.1.0)
seed-fu (~> 2.3.7)
selenium-webdriver (~> 3.142)
sentry-rails (~> 4.4.0)
sentry-ruby (~> 4.4.0)
sentry-sidekiq (~> 4.4.0)
sentry-raven (~> 3.1)
settingslogic (~> 2.0.9)
shoulda-matchers (~> 4.0.1)
sidekiq (~> 5.2.7)

View file

@ -46,7 +46,7 @@ export function initMermaid(mermaid) {
theme,
flowchart: {
useMaxWidth: true,
htmlLabels: false,
htmlLabels: true,
},
securityLevel: 'strict',
});

View file

@ -1,5 +1,7 @@
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
import { initSidebarTracking } from '../shared/nav/sidebar_tracking';
import Project from './project';
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initSidebarTracking();

View file

@ -0,0 +1,44 @@
function onSidebarLinkClick() {
const setDataTrackAction = (element, action) => {
element.setAttribute('data-track-action', action);
};
const setDataTrackExtra = (element, value) => {
const SIDEBAR_COLLAPSED = 'Collapsed';
const SIDEBAR_EXPANDED = 'Expanded';
const sidebarCollapsed = document
.querySelector('.nav-sidebar')
.classList.contains('js-sidebar-collapsed')
? SIDEBAR_COLLAPSED
: SIDEBAR_EXPANDED;
element.setAttribute(
'data-track-extra',
JSON.stringify({ sidebar_display: sidebarCollapsed, menu_display: value }),
);
};
const EXPANDED = 'Expanded';
const FLY_OUT = 'Fly out';
const CLICK_MENU_ACTION = 'click_menu';
const CLICK_MENU_ITEM_ACTION = 'click_menu_item';
const parentElement = this.parentNode;
const subMenuList = parentElement.closest('.sidebar-sub-level-items');
if (subMenuList) {
const isFlyOut = subMenuList.classList.contains('fly-out-list') ? FLY_OUT : EXPANDED;
setDataTrackExtra(parentElement, isFlyOut);
setDataTrackAction(parentElement, CLICK_MENU_ITEM_ACTION);
} else {
const isFlyOut = parentElement.classList.contains('is-showing-fly-out') ? FLY_OUT : EXPANDED;
setDataTrackExtra(parentElement, isFlyOut);
setDataTrackAction(parentElement, CLICK_MENU_ACTION);
}
}
export const initSidebarTracking = () => {
document.querySelectorAll('.nav-sidebar li[data-track-label] > a').forEach((link) => {
link.addEventListener('click', onSidebarLinkClick);
});
};

View file

@ -60,8 +60,16 @@ export const generateLinksData = ({ links }, containerID, modifier = '') => {
paddingTop +
sourceNodeCoordinates.height / 2;
// Start point
path.moveTo(sourceNodeX, sourceNodeY);
const sourceNodeLeftX = sourceNodeCoordinates.left - containerCoordinates.x - paddingLeft;
// If the source and target X values are the same,
// it means the nodes are in the same column so we
// want to start the line on the left of the pill
// instead of the right to have a nice curve.
const firstPointCoordinateX = sourceNodeLeftX === targetNodeX ? sourceNodeLeftX : sourceNodeX;
// First point
path.moveTo(firstPointCoordinateX, sourceNodeY);
// Make cross-stages lines a straight line all the way
// until we can safely draw the bezier to look nice.

View file

@ -1,90 +0,0 @@
<script>
import { GlBanner, GlLink, GlSprintf } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
import getUserCallouts from '../../graphql/queries/get_user_callouts.query.graphql';
const featureName = 'pipeline_needs_banner';
const enumFeatureName = featureName.toUpperCase();
export default {
i18n: {
title: __('View job dependencies in the pipeline graph!'),
description: __(
'You can now group jobs in the pipeline graph based on which jobs are configured to run first, if you use the %{codeStart}needs:%{codeEnd} keyword to establish job dependencies in your CI/CD pipelines. %{linkStart}Learn how to speed up your pipeline with needs.%{linkEnd}',
),
buttonText: __('Provide feedback'),
},
components: {
GlBanner,
GlLink,
GlSprintf,
},
apollo: {
callouts: {
query: getUserCallouts,
update(data) {
return data?.currentUser?.callouts?.nodes.map((c) => c.featureName);
},
error() {
this.hasError = true;
},
},
},
inject: ['dagDocPath'],
data() {
return {
callouts: [],
dismissedAlert: false,
hasError: false,
};
},
computed: {
showBanner() {
return (
!this.$apollo.queries.callouts?.loading &&
!this.hasError &&
!this.dismissedAlert &&
!this.callouts.includes(enumFeatureName)
);
},
},
methods: {
handleClose() {
this.dismissedAlert = true;
try {
this.$apollo.mutate({
mutation: DismissPipelineGraphCallout,
variables: {
featureName,
},
});
} catch {
createFlash(__('There was a problem dismissing this notification.'));
}
},
},
};
</script>
<template>
<gl-banner
v-if="showBanner"
:title="$options.i18n.title"
:button-text="$options.i18n.buttonText"
button-link="https://gitlab.com/gitlab-org/gitlab/-/issues/327688"
variant="introduction"
@close="handleClose"
>
<p>
<gl-sprintf :message="$options.i18n.description">
<template #link="{ content }">
<gl-link :href="dagDocPath" target="_blank"> {{ content }}</gl-link>
</template>
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
</gl-banner>
</template>

View file

@ -9,7 +9,6 @@ import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header';
import { createPipelineNotificationApp } from './pipeline_details_notification';
import { apolloProvider } from './pipeline_shared_client';
import createTestReportsStore from './stores/test_reports';
import { reportToSentry } from './utils';
@ -20,7 +19,6 @@ const SELECTORS = {
PIPELINE_DETAILS: '.js-pipeline-details-vue',
PIPELINE_GRAPH: '#js-pipeline-graph-vue',
PIPELINE_HEADER: '#js-pipeline-header-vue',
PIPELINE_NOTIFICATION: '#js-pipeline-notification',
PIPELINE_TESTS: '#js-pipeline-tests-detail',
};
@ -101,14 +99,6 @@ export default async function initPipelineDetailsBundle() {
Flash(__('An error occurred while loading a section of this page.'));
}
if (gon.features.pipelineGraphLayersView) {
try {
createPipelineNotificationApp(SELECTORS.PIPELINE_NOTIFICATION, apolloProvider);
} catch {
Flash(__('An error occurred while loading a section of this page.'));
}
}
if (canShowNewPipelineDetails) {
try {
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);

View file

@ -1,29 +0,0 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import PipelineNotification from './components/notification/pipeline_notification.vue';
Vue.use(VueApollo);
export const createPipelineNotificationApp = (elSelector, apolloProvider) => {
const el = document.querySelector(elSelector);
if (!el) {
return;
}
const { dagDocPath } = el?.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
PipelineNotification,
},
provide: {
dagDocPath,
},
apolloProvider,
render(createElement) {
return createElement('pipeline-notification');
},
});
};

View file

@ -930,7 +930,7 @@ input {
}
@media (min-width: 768px) {
.page-with-contextual-sidebar {
padding-left: 50px;
padding-left: 48px;
}
}
@media (min-width: 1200px) {
@ -940,7 +940,7 @@ input {
}
@media (min-width: 768px) {
.page-with-icon-sidebar {
padding-left: 50px;
padding-left: 48px;
}
}
.nav-sidebar {
@ -960,7 +960,7 @@ input {
}
}
.nav-sidebar.sidebar-collapsed-desktop {
width: 50px;
width: 48px;
}
.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
overflow-x: hidden;
@ -1026,7 +1026,7 @@ input {
}
@media (min-width: 768px) and (max-width: 1199px) {
.nav-sidebar:not(.sidebar-expanded-mobile) {
width: 50px;
width: 48px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
overflow-x: hidden;
@ -1055,7 +1055,7 @@ input {
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
height: 60px;
width: 50px;
width: 48px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
padding: 10px 4px;
@ -1082,7 +1082,7 @@ input {
}
.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
padding: 16px;
width: 49px;
width: 47px;
}
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
@ -1172,7 +1172,7 @@ input {
}
.sidebar-collapsed-desktop .context-header {
height: 60px;
width: 50px;
width: 48px;
}
.sidebar-collapsed-desktop .context-header a {
padding: 10px 4px;
@ -1199,7 +1199,7 @@ input {
}
.sidebar-collapsed-desktop .toggle-sidebar-button {
padding: 16px;
width: 49px;
width: 47px;
}
.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text,
.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left {
@ -1273,7 +1273,7 @@ body.sidebar-refactoring .nav-sidebar li.active {
}
@media (min-width: 768px) {
body.sidebar-refactoring .page-with-contextual-sidebar {
padding-left: 50px;
padding-left: 48px;
}
}
@media (min-width: 1200px) {
@ -1283,26 +1283,21 @@ body.sidebar-refactoring .nav-sidebar li.active {
}
@media (min-width: 768px) {
body.sidebar-refactoring .page-with-icon-sidebar {
padding-left: 50px;
padding-left: 48px;
}
}
body.sidebar-refactoring .nav-sidebar {
position: fixed;
bottom: 0;
left: 0;
z-index: 600;
width: 220px;
top: 40px;
bottom: 0;
left: 0;
background-color: #303030;
transform: translate3d(0, 0, 0);
}
@media (min-width: 576px) and (max-width: 576px) {
body.sidebar-refactoring .nav-sidebar:not(.sidebar-collapsed-desktop) {
box-shadow: inset -1px 0 0 #404040, 2px 1px 3px rgba(0, 0, 0, 0.1);
}
}
body.sidebar-refactoring .nav-sidebar.sidebar-collapsed-desktop {
width: 50px;
width: 48px;
}
body.sidebar-refactoring
.nav-sidebar.sidebar-collapsed-desktop
@ -1312,7 +1307,8 @@ body.sidebar-refactoring
body.sidebar-refactoring
.nav-sidebar.sidebar-collapsed-desktop
.badge.badge-pill:not(.fly-out-badge),
body.sidebar-refactoring .nav-sidebar.sidebar-collapsed-desktop .nav-item-name {
body.sidebar-refactoring .nav-sidebar.sidebar-collapsed-desktop .nav-item-name,
body.sidebar-refactoring .nav-sidebar.sidebar-collapsed-desktop .collapse-text {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
@ -1328,7 +1324,7 @@ body.sidebar-refactoring
.sidebar-top-level-items
> li
> a {
min-height: 45px;
min-height: unset;
}
body.sidebar-refactoring
.nav-sidebar.sidebar-collapsed-desktop
@ -1340,6 +1336,9 @@ body.sidebar-refactoring
.avatar-container {
margin: 0 auto;
}
body.sidebar-refactoring .nav-sidebar.sidebar-collapsed-desktop li.active > a {
background-color: rgba(41, 41, 97, 0.08);
}
body.sidebar-refactoring .nav-sidebar a {
text-decoration: none;
line-height: 1rem;
@ -1386,7 +1385,7 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
}
@media (min-width: 768px) and (max-width: 1199px) {
body.sidebar-refactoring .nav-sidebar:not(.sidebar-expanded-mobile) {
width: 50px;
width: 48px;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
@ -1398,7 +1397,10 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
.badge.badge-pill:not(.fly-out-badge),
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.nav-item-name {
.nav-item-name,
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.collapse-text {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
@ -1414,7 +1416,7 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
.sidebar-top-level-items
> li
> a {
min-height: 45px;
min-height: unset;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
@ -1426,11 +1428,17 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
.avatar-container {
margin: 0 auto;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
li.active
> a {
background-color: rgba(41, 41, 97, 0.08);
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.context-header {
height: 60px;
width: 50px;
width: 48px;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
@ -1451,6 +1459,17 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
white-space: nowrap;
width: 1px;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.context-header {
height: auto;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.context-header
a {
padding: 0.25rem;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.sidebar-top-level-items
@ -1466,23 +1485,19 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button {
padding: 16px;
width: 49px;
width: 48px;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
.collapse-text,
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
.icon-chevron-double-lg-left {
.collapse-text {
display: none;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
.icon-chevron-double-lg-right {
.icon-chevron-double-lg-left {
transform: rotate(180deg);
display: block;
margin: 0;
}
@ -1507,6 +1522,7 @@ body.sidebar-refactoring .nav-sidebar-inner-scroll > div.context-header a {
margin: 1px 4px;
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0;
}
body.sidebar-refactoring
.nav-sidebar-inner-scroll
@ -1547,6 +1563,7 @@ body.sidebar-refactoring .sidebar-top-level-items {
body.sidebar-refactoring .sidebar-top-level-items .context-header a {
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0;
}
body.sidebar-refactoring
.sidebar-top-level-items
@ -1619,7 +1636,11 @@ body.sidebar-refactoring .close-nav-button {
display: flex;
align-items: center;
background-color: #303030;
border-top: 1px solid #404040;
color: #2f2a6b;
position: fixed;
bottom: 0;
width: 220px;
}
body.sidebar-refactoring .toggle-sidebar-button .collapse-text,
body.sidebar-refactoring .toggle-sidebar-button .icon-chevron-double-lg-left,
@ -1629,28 +1650,13 @@ body.sidebar-refactoring .close-nav-button .icon-chevron-double-lg-left,
body.sidebar-refactoring .close-nav-button .icon-chevron-double-lg-right {
color: inherit;
}
body.sidebar-refactoring .toggle-sidebar-button,
body.sidebar-refactoring .close-nav-button {
position: fixed;
bottom: 0;
width: 219px;
border-top: 1px solid #404040;
}
body.sidebar-refactoring .toggle-sidebar-button svg,
body.sidebar-refactoring .close-nav-button svg {
margin-right: 8px;
}
body.sidebar-refactoring .toggle-sidebar-button .icon-chevron-double-lg-right,
body.sidebar-refactoring .close-nav-button .icon-chevron-double-lg-right {
display: none;
}
body.sidebar-refactoring .collapse-text {
white-space: nowrap;
overflow: hidden;
}
body.sidebar-refactoring .sidebar-collapsed-desktop .context-header {
height: 60px;
width: 50px;
width: 48px;
}
body.sidebar-refactoring .sidebar-collapsed-desktop .context-header a {
padding: 10px 4px;
@ -1666,6 +1672,12 @@ body.sidebar-refactoring .sidebar-collapsed-desktop .sidebar-context-title {
white-space: nowrap;
width: 1px;
}
body.sidebar-refactoring .sidebar-collapsed-desktop .context-header {
height: auto;
}
body.sidebar-refactoring .sidebar-collapsed-desktop .context-header a {
padding: 0.25rem;
}
body.sidebar-refactoring
.sidebar-collapsed-desktop
.sidebar-top-level-items
@ -1677,23 +1689,19 @@ body.sidebar-refactoring .sidebar-collapsed-desktop .nav-icon-container {
margin-right: 0;
}
body.sidebar-refactoring .sidebar-collapsed-desktop .toggle-sidebar-button {
padding: 16px;
width: 49px;
width: 48px;
}
body.sidebar-refactoring
.sidebar-collapsed-desktop
.toggle-sidebar-button
.collapse-text,
body.sidebar-refactoring
.sidebar-collapsed-desktop
.toggle-sidebar-button
.icon-chevron-double-lg-left {
.collapse-text {
display: none;
}
body.sidebar-refactoring
.sidebar-collapsed-desktop
.toggle-sidebar-button
.icon-chevron-double-lg-right {
.icon-chevron-double-lg-left {
transform: rotate(180deg);
display: block;
margin: 0;
}
@ -1834,6 +1842,12 @@ svg.s16 {
height: 32px;
margin-right: 8px;
}
.avatar.s40,
.avatar-container.s40 {
width: 40px;
height: 40px;
margin-right: 8px;
}
.avatar {
transition-property: none;
width: 40px;
@ -1861,6 +1875,10 @@ svg.s16 {
font-size: 14px;
line-height: 32px;
}
.identicon.s40 {
font-size: 16px;
line-height: 38px;
}
.identicon.bg1 {
background-color: #ffebee;
}
@ -1900,6 +1918,10 @@ svg.s16 {
margin: 0;
align-self: center;
}
.avatar-container.s40 {
min-width: 40px;
min-height: 40px;
}
.rect-avatar {
border-radius: 2px;
}
@ -1924,6 +1946,9 @@ body.sidebar-refactoring
.avatar.s32 {
border-radius: 4px;
}
.rect-avatar.s40 {
border-radius: 4px;
}
body.gl-dark .navbar-gitlab {
background-color: #fafafa;
}

View file

@ -915,7 +915,7 @@ input {
}
@media (min-width: 768px) {
.page-with-contextual-sidebar {
padding-left: 50px;
padding-left: 48px;
}
}
@media (min-width: 1200px) {
@ -925,7 +925,7 @@ input {
}
@media (min-width: 768px) {
.page-with-icon-sidebar {
padding-left: 50px;
padding-left: 48px;
}
}
.nav-sidebar {
@ -945,7 +945,7 @@ input {
}
}
.nav-sidebar.sidebar-collapsed-desktop {
width: 50px;
width: 48px;
}
.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
overflow-x: hidden;
@ -1011,7 +1011,7 @@ input {
}
@media (min-width: 768px) and (max-width: 1199px) {
.nav-sidebar:not(.sidebar-expanded-mobile) {
width: 50px;
width: 48px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
overflow-x: hidden;
@ -1040,7 +1040,7 @@ input {
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
height: 60px;
width: 50px;
width: 48px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
padding: 10px 4px;
@ -1067,7 +1067,7 @@ input {
}
.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
padding: 16px;
width: 49px;
width: 47px;
}
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
@ -1157,7 +1157,7 @@ input {
}
.sidebar-collapsed-desktop .context-header {
height: 60px;
width: 50px;
width: 48px;
}
.sidebar-collapsed-desktop .context-header a {
padding: 10px 4px;
@ -1184,7 +1184,7 @@ input {
}
.sidebar-collapsed-desktop .toggle-sidebar-button {
padding: 16px;
width: 49px;
width: 47px;
}
.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text,
.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left {
@ -1235,7 +1235,7 @@ body.sidebar-refactoring .nav-sidebar li.active {
}
@media (min-width: 768px) {
body.sidebar-refactoring .page-with-contextual-sidebar {
padding-left: 50px;
padding-left: 48px;
}
}
@media (min-width: 1200px) {
@ -1245,26 +1245,21 @@ body.sidebar-refactoring .nav-sidebar li.active {
}
@media (min-width: 768px) {
body.sidebar-refactoring .page-with-icon-sidebar {
padding-left: 50px;
padding-left: 48px;
}
}
body.sidebar-refactoring .nav-sidebar {
position: fixed;
bottom: 0;
left: 0;
z-index: 600;
width: 220px;
top: 40px;
bottom: 0;
left: 0;
background-color: #f0f0f0;
transform: translate3d(0, 0, 0);
}
@media (min-width: 576px) and (max-width: 576px) {
body.sidebar-refactoring .nav-sidebar:not(.sidebar-collapsed-desktop) {
box-shadow: inset -1px 0 0 #dbdbdb, 2px 1px 3px rgba(0, 0, 0, 0.1);
}
}
body.sidebar-refactoring .nav-sidebar.sidebar-collapsed-desktop {
width: 50px;
width: 48px;
}
body.sidebar-refactoring
.nav-sidebar.sidebar-collapsed-desktop
@ -1274,7 +1269,8 @@ body.sidebar-refactoring
body.sidebar-refactoring
.nav-sidebar.sidebar-collapsed-desktop
.badge.badge-pill:not(.fly-out-badge),
body.sidebar-refactoring .nav-sidebar.sidebar-collapsed-desktop .nav-item-name {
body.sidebar-refactoring .nav-sidebar.sidebar-collapsed-desktop .nav-item-name,
body.sidebar-refactoring .nav-sidebar.sidebar-collapsed-desktop .collapse-text {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
@ -1290,7 +1286,7 @@ body.sidebar-refactoring
.sidebar-top-level-items
> li
> a {
min-height: 45px;
min-height: unset;
}
body.sidebar-refactoring
.nav-sidebar.sidebar-collapsed-desktop
@ -1302,6 +1298,9 @@ body.sidebar-refactoring
.avatar-container {
margin: 0 auto;
}
body.sidebar-refactoring .nav-sidebar.sidebar-collapsed-desktop li.active > a {
background-color: rgba(41, 41, 97, 0.08);
}
body.sidebar-refactoring .nav-sidebar a {
text-decoration: none;
line-height: 1rem;
@ -1348,7 +1347,7 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
}
@media (min-width: 768px) and (max-width: 1199px) {
body.sidebar-refactoring .nav-sidebar:not(.sidebar-expanded-mobile) {
width: 50px;
width: 48px;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
@ -1360,7 +1359,10 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
.badge.badge-pill:not(.fly-out-badge),
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.nav-item-name {
.nav-item-name,
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.collapse-text {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
@ -1376,7 +1378,7 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
.sidebar-top-level-items
> li
> a {
min-height: 45px;
min-height: unset;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
@ -1388,11 +1390,17 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
.avatar-container {
margin: 0 auto;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
li.active
> a {
background-color: rgba(41, 41, 97, 0.08);
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.context-header {
height: 60px;
width: 50px;
width: 48px;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
@ -1413,6 +1421,17 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
white-space: nowrap;
width: 1px;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.context-header {
height: auto;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.context-header
a {
padding: 0.25rem;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.sidebar-top-level-items
@ -1428,23 +1447,19 @@ body.sidebar-refactoring .nav-sidebar .fly-out-top-item {
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button {
padding: 16px;
width: 49px;
width: 48px;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
.collapse-text,
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
.icon-chevron-double-lg-left {
.collapse-text {
display: none;
}
body.sidebar-refactoring
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
.icon-chevron-double-lg-right {
.icon-chevron-double-lg-left {
transform: rotate(180deg);
display: block;
margin: 0;
}
@ -1469,6 +1484,7 @@ body.sidebar-refactoring .nav-sidebar-inner-scroll > div.context-header a {
margin: 1px 4px;
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0;
}
body.sidebar-refactoring
.nav-sidebar-inner-scroll
@ -1509,6 +1525,7 @@ body.sidebar-refactoring .sidebar-top-level-items {
body.sidebar-refactoring .sidebar-top-level-items .context-header a {
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0;
}
body.sidebar-refactoring
.sidebar-top-level-items
@ -1581,7 +1598,11 @@ body.sidebar-refactoring .close-nav-button {
display: flex;
align-items: center;
background-color: #f0f0f0;
border-top: 1px solid #dbdbdb;
color: #2f2a6b;
position: fixed;
bottom: 0;
width: 220px;
}
body.sidebar-refactoring .toggle-sidebar-button .collapse-text,
body.sidebar-refactoring .toggle-sidebar-button .icon-chevron-double-lg-left,
@ -1591,28 +1612,13 @@ body.sidebar-refactoring .close-nav-button .icon-chevron-double-lg-left,
body.sidebar-refactoring .close-nav-button .icon-chevron-double-lg-right {
color: inherit;
}
body.sidebar-refactoring .toggle-sidebar-button,
body.sidebar-refactoring .close-nav-button {
position: fixed;
bottom: 0;
width: 219px;
border-top: 1px solid #dbdbdb;
}
body.sidebar-refactoring .toggle-sidebar-button svg,
body.sidebar-refactoring .close-nav-button svg {
margin-right: 8px;
}
body.sidebar-refactoring .toggle-sidebar-button .icon-chevron-double-lg-right,
body.sidebar-refactoring .close-nav-button .icon-chevron-double-lg-right {
display: none;
}
body.sidebar-refactoring .collapse-text {
white-space: nowrap;
overflow: hidden;
}
body.sidebar-refactoring .sidebar-collapsed-desktop .context-header {
height: 60px;
width: 50px;
width: 48px;
}
body.sidebar-refactoring .sidebar-collapsed-desktop .context-header a {
padding: 10px 4px;
@ -1628,6 +1634,12 @@ body.sidebar-refactoring .sidebar-collapsed-desktop .sidebar-context-title {
white-space: nowrap;
width: 1px;
}
body.sidebar-refactoring .sidebar-collapsed-desktop .context-header {
height: auto;
}
body.sidebar-refactoring .sidebar-collapsed-desktop .context-header a {
padding: 0.25rem;
}
body.sidebar-refactoring
.sidebar-collapsed-desktop
.sidebar-top-level-items
@ -1639,23 +1651,19 @@ body.sidebar-refactoring .sidebar-collapsed-desktop .nav-icon-container {
margin-right: 0;
}
body.sidebar-refactoring .sidebar-collapsed-desktop .toggle-sidebar-button {
padding: 16px;
width: 49px;
width: 48px;
}
body.sidebar-refactoring
.sidebar-collapsed-desktop
.toggle-sidebar-button
.collapse-text,
body.sidebar-refactoring
.sidebar-collapsed-desktop
.toggle-sidebar-button
.icon-chevron-double-lg-left {
.collapse-text {
display: none;
}
body.sidebar-refactoring
.sidebar-collapsed-desktop
.toggle-sidebar-button
.icon-chevron-double-lg-right {
.icon-chevron-double-lg-left {
transform: rotate(180deg);
display: block;
margin: 0;
}
@ -1796,6 +1804,12 @@ svg.s16 {
height: 32px;
margin-right: 8px;
}
.avatar.s40,
.avatar-container.s40 {
width: 40px;
height: 40px;
margin-right: 8px;
}
.avatar {
transition-property: none;
width: 40px;
@ -1823,6 +1837,10 @@ svg.s16 {
font-size: 14px;
line-height: 32px;
}
.identicon.s40 {
font-size: 16px;
line-height: 38px;
}
.identicon.bg1 {
background-color: #ffebee;
}
@ -1862,6 +1880,10 @@ svg.s16 {
margin: 0;
align-self: center;
}
.avatar-container.s40 {
min-width: 40px;
min-height: 40px;
}
.rect-avatar {
border-radius: 2px;
}
@ -1886,6 +1908,9 @@ body.sidebar-refactoring
.avatar.s32 {
border-radius: 4px;
}
.rect-avatar.s40 {
border-radius: 4px;
}
.tab-width-8 {
-moz-tab-size: 8;

View file

@ -78,7 +78,7 @@ class Import::BulkImportsController < ApplicationController
def query_params
query_params = {
top_level_only: true,
min_access_level: Gitlab::Access::MAINTAINER
min_access_level: Gitlab::Access::OWNER
}
query_params[:search] = sanitized_filter_param if sanitized_filter_param

View file

@ -163,7 +163,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end
def render_merge_ref_head_diff?
Gitlab::Utils.to_boolean(params[:diff_head]) && @merge_request.diffable_merge_ref? && @start_sha.nil?
params[:diff_id].blank? &&
Gitlab::Utils.to_boolean(params[:diff_head]) &&
@merge_request.diffable_merge_ref? &&
@start_sha.nil?
end
def note_positions

View file

@ -57,6 +57,9 @@ class CommitStatus < ApplicationRecord
scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
scope :eager_load_pipeline, -> { eager_load(:pipeline, project: { namespace: :route }) }
scope :with_pipeline, -> { joins(:pipeline) }
scope :updated_before, ->(lookback:, timeout:) {
where('(ci_builds.created_at BETWEEN ? AND ?) AND (ci_builds.updated_at BETWEEN ? AND ?)', lookback, timeout, lookback, timeout)
}
scope :for_project_paths, -> (paths) do
where(project: Project.where_full_path_in(Array(paths)))

View file

@ -26,7 +26,7 @@ module Postgresql
"(pg_current_wal_insert_lsn(), restart_lsn)::bigint"
# We force the use of a transaction here so the query always goes to the
# primary, even when using the EE DB load balancer.
# primary, even when using the DB load balancer.
sizes = transaction { pluck(Arel.sql(lag_function)) }
too_great = sizes.compact.count { |size| size >= max }

View file

@ -28,7 +28,6 @@
- lint_link_start = '<a href="%{url}">'.html_safe % { url: lint_link_url }
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
#js-pipeline-notification{ data: { dag_doc_path: help_page_path('ci/yaml/README.md', anchor: 'needs') } }
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json), metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline) } }

View file

@ -1,4 +1,4 @@
= nav_link(**sidebar_menu_item.active_routes) do
= nav_link(**sidebar_menu_item.active_routes, html_options: sidebar_menu_item.nav_link_html_options) do
= link_to sidebar_menu_item.link, **sidebar_menu_item.container_html_options, data: { qa_selector: 'sidebar_menu_item_link', qa_menu_item: sidebar_menu_item.title } do
%span
= sidebar_menu_item.title

View file

@ -186,6 +186,12 @@ module WorkerAttributes
class_attributes[:deduplication_options] || {}
end
def deduplication_enabled?
return true unless get_deduplication_options[:feature_flag]
Feature.enabled?(get_deduplication_options[:feature_flag], default_enabled: :yaml)
end
def big_payload!
set_class_attribute(:big_payload, true)
end

View file

@ -15,22 +15,46 @@ class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
BUILD_PENDING_OUTDATED_TIMEOUT = 1.day
BUILD_SCHEDULED_OUTDATED_TIMEOUT = 1.hour
BUILD_PENDING_STUCK_TIMEOUT = 1.hour
BUILD_LOOKBACK = 5.days
def perform
return unless try_obtain_lease
Gitlab::AppLogger.info "#{self.class}: Cleaning stuck builds"
drop :running, BUILD_RUNNING_OUTDATED_TIMEOUT, 'ci_builds.updated_at < ?', :stuck_or_timeout_failure
drop :pending, BUILD_PENDING_OUTDATED_TIMEOUT, 'ci_builds.updated_at < ?', :stuck_or_timeout_failure
drop :scheduled, BUILD_SCHEDULED_OUTDATED_TIMEOUT, 'scheduled_at IS NOT NULL AND scheduled_at < ?', :stale_schedule
drop_stuck :pending, BUILD_PENDING_STUCK_TIMEOUT, 'ci_builds.updated_at < ?', :stuck_or_timeout_failure
drop(running_timed_out_builds, failure_reason: :stuck_or_timeout_failure)
drop(
Ci::Build.pending.updated_before(lookback: BUILD_LOOKBACK.ago, timeout: BUILD_PENDING_OUTDATED_TIMEOUT.ago),
failure_reason: :stuck_or_timeout_failure
)
drop(scheduled_timed_out_builds, failure_reason: :stale_schedule)
drop_stuck(
Ci::Build.pending.updated_before(lookback: BUILD_LOOKBACK.ago, timeout: BUILD_PENDING_STUCK_TIMEOUT.ago),
failure_reason: :stuck_or_timeout_failure
)
remove_lease
end
private
def scheduled_timed_out_builds
Ci::Build.where(status: :scheduled).where( # rubocop: disable CodeReuse/ActiveRecord
'ci_builds.scheduled_at IS NOT NULL AND ci_builds.scheduled_at < ?',
BUILD_SCHEDULED_OUTDATED_TIMEOUT.ago
)
end
def running_timed_out_builds
Ci::Build.running.where( # rubocop: disable CodeReuse/ActiveRecord
'ci_builds.updated_at < ?',
BUILD_RUNNING_OUTDATED_TIMEOUT.ago
)
end
def try_obtain_lease
@uuid = Gitlab::ExclusiveLease.new(EXCLUSIVE_LEASE_KEY, timeout: 30.minutes).try_obtain
end
@ -39,28 +63,27 @@ class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
Gitlab::ExclusiveLease.cancel(EXCLUSIVE_LEASE_KEY, @uuid)
end
def drop(status, timeout, condition, reason)
search(status, timeout, condition) do |build|
drop_build :outdated, build, status, timeout, reason
def drop(builds, failure_reason:)
fetch(builds) do |build|
drop_build :outdated, build, failure_reason
end
end
def drop_stuck(status, timeout, condition, reason)
search(status, timeout, condition) do |build|
def drop_stuck(builds, failure_reason:)
fetch(builds) do |build|
break unless build.stuck?
drop_build :stuck, build, status, timeout, reason
drop_build :stuck, build, failure_reason
end
end
# rubocop: disable CodeReuse/ActiveRecord
def search(status, timeout, condition)
def fetch(builds)
loop do
jobs = Ci::Build.where(status: status)
.where(condition, timeout.ago)
.includes(:tags, :runner, project: [:namespace, :route])
jobs = builds.includes(:tags, :runner, project: [:namespace, :route])
.limit(100)
.to_a
break if jobs.empty?
jobs.each do |job|
@ -70,8 +93,8 @@ class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
end
# rubocop: enable CodeReuse/ActiveRecord
def drop_build(type, build, status, timeout, reason)
Gitlab::AppLogger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout}, reason: #{reason})"
def drop_build(type, build, reason)
Gitlab::AppLogger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{build.status}, failure_reason: #{reason})"
Gitlab::OptimisticLocking.retry_lock(build, 3, name: 'stuck_ci_jobs_worker_drop_build') do |b|
b.drop(reason)
end

View file

@ -17,4 +17,3 @@ tier:
- free
- premium
- ultimate
skip_validation: true

View file

@ -653,6 +653,8 @@ NOTE:
Administrators are not synced unless `group_base` is also
specified alongside `admin_group`. Also, only specify the CN of the `admin_group`,
as opposed to the full DN.
Additionally, note that if an LDAP user has an `admin` role, but is not a member of the `admin_group`
group, GitLab revokes their `admin` role when syncing.
**Omnibus configuration**

View file

@ -1293,6 +1293,88 @@ curl "localhost:5001/debug/health"
curl "localhost:5001/debug/vars"
```
### Access old schema v1 Docker images
Support for the [Docker registry v1 API](https://www.docker.com/blog/registry-v1-api-deprecation/),
including [schema V1 image manifests](https://docs.docker.com/registry/spec/manifest-v2-1/),
was:
- [Deprecated in GitLab 13.7](https://about.gitlab.com/releases/2020/12/22/gitlab-13-7-released/#deprecate-pulls-that-use-v1-of-the-docker-registry-api)
- [Removed in GitLab 13.9](https://about.gitlab.com/releases/2021/02/22/gitlab-13-9-released/#deprecate-pulls-that-use-v1-of-the-docker-registry-api)
It's no longer possible to push or pull v1 images from the GitLab Container Registry.
If you had v1 images in the GitLab Container Registry, but you did not upgrade them (following the
[steps Docker recommends](https://docs.docker.com/registry/spec/deprecated-schema-v1/))
ahead of the GitLab 13.9 upgrade, these images are no longer accessible. If you try to pull them,
this error appears:
- `Error response from daemon: manifest invalid: Schema 1 manifest not supported`
For Self-Managed GitLab instances, you can regain access to these images by temporarily downgrading
the GitLab Container Registry to a version lower than `v3.0.0-gitlab`. Follow these steps to regain
access to these images:
1. Downgrade the Container Registry to [`v2.13.1-gitlab`](https://gitlab.com/gitlab-org/container-registry/-/releases/v2.13.1-gitlab).
1. Upgrade any v1 images.
1. Revert the Container Registry downgrade.
There's no need to put the registry in read-only mode during the image upgrade process. Ensure that
you are not relying on any new feature introduced since `v3.0.0-gitlab`. Such features are
unavailable during the upgrade process. See the [complete registry changelog](https://gitlab.com/gitlab-org/container-registry/-/blob/master/CHANGELOG.md)
for more information.
The following sections provide additional details about each installation method.
#### Helm chart installations
For Helm chart installations:
1. Override the [`image.tag`](https://docs.gitlab.com/charts/charts/registry/#configuration)
configuration parameter with `v2.13.1-gitlab`.
1. Restart.
1. Performing the [images upgrade](#images-upgrade)) steps.
1. Revert the `image.tag` parameter to the previous value.
No other registry configuration changes are required.
#### Omnibus installations
For Omnibus installations:
1. Temporarily replace the registry binary that ships with GitLab 13.9+ for one prior to
`v3.0.0-gitlab`. To do so, pull a previous version of the Docker image for the GitLab Container
Registry, such as `v2.13.1-gitlab`. You can then grab the `registry` binary from within this
image, located at `/bin/registry`:
```shell
id=$(docker create registry.gitlab.com/gitlab-org/build/cng/gitlab-container-registry:v2.13.1-gitlab)
docker cp $id:/bin/registry registry-2.13.1-gitlab
docker rm $id
```
1. Replace the binary embedded in the Omnibus install, located at
`/opt/gitlab/embedded/bin/registry`, with `registry-2.13.1-gitlab`. Make sure to start by backing
up the original binary embedded in Omnibus, and restore it after performing the
[image upgrade](#images-upgrade)) steps. You should [stop](https://docs.gitlab.com/omnibus/maintenance/#starting-and-stopping)
the registry service before replacing its binary and start it right after. No registry
configuration changes are required.
#### Source installations
For source installations, locate your `registry` binary and temporarily replace it with the one
obtained from `v3.0.0-gitlab`, as explained for [Omnibus installations](#omnibus-installations).
Make sure to start by backing up the original registry binary, and restore it after performing the
[images upgrade](#images-upgrade))
steps.
#### Images upgrade
Follow the [steps that Docker recommends to upgrade v1 images](https://docs.docker.com/registry/spec/deprecated-schema-v1/).
The most straightforward option is to pull those images and push them once again to the registry,
using a Docker client version above v1.12. Docker converts images automatically before pushing them
to the registry. Once done, all your v1 images should now be available as v2 images.
### Advanced Troubleshooting
We use a concrete example to illustrate how to

View file

@ -7712,7 +7712,7 @@ Group: `group::product intelligence`
Status: `data_available`
Tiers:
Tiers: `premium`, `ultimate`
### `redis_hll_counters.analytics.analytics_total_unique_counts_monthly`

View file

@ -117,7 +117,8 @@ on an existing group's page.
### Selecting which groups to import
After you have authorized access to GitLab instance, you are redirected to the GitLab Group Migration importer page and your remote GitLab groups are listed.
After you have authorized access to the GitLab instance, you are redirected to the GitLab Group
Migration importer page. Your remote GitLab groups, which you have Owner access to, are listed.
1. By default, the proposed group namespaces match the names as they exist in remote instance, but based on your permissions, you can choose to edit these names before you proceed to import any of them.

View file

@ -65,7 +65,7 @@ To filter results:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13216) in GitLab 12.4.
GitLab provides the ability to filter analytics based on a date range. To filter results:
GitLab provides the ability to filter analytics based on a date range. Data is shown for workflow items created during the selected date range. To filter results:
1. Select a group.
1. Optionally select a project.

View file

@ -512,6 +512,17 @@ On GitLab.com, the execution time for the cleanup policy is limited, and some of
the Container Registry after the policy runs. The next time the policy runs, the remaining tags are included,
so it may take multiple runs for all tags to be deleted.
WARNING:
GitLab self-managed installs support for third-party container registries that comply with the
[Docker Registry HTTP API V2](https://docs.docker.com/registry/spec/api/)
specification. However, this specification does not include a tag delete operation. Therefore, when
interacting with third-party container registries, GitLab uses a workaround to delete tags. See the
[related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/15737)
for more information. Due to possible implementation variations, this workaround is not guaranteed
to work with all third-party registries in the same predictable way. If you use the GitLab Container
Registry, this workaround is not required because we implemented a special tag delete operation. In
this case, you can expect cleanup policies to be consistent and predictable.
### Create a cleanup policy
You can create a cleanup policy in [the API](#use-the-cleanup-policy-api) or the UI.

View file

@ -110,7 +110,8 @@ To authenticate, use one of the following:
- It's not recommended, but you can use [OAuth tokens](../../../api/oauth2.md#resource-owner-password-credentials-flow).
Standard OAuth tokens cannot authenticate to the GitLab npm Registry. You must use a personal access token with OAuth headers.
- A [CI job token](#authenticate-with-a-ci-job-token).
- Your npm package name must be in the format of [@scope/package-name](#package-naming-convention). It must match exactly, including the case.
- Your npm package name must be in the format of [`@scope/package-name`](#package-naming-convention).
It must match exactly, including the case.
### Authenticate with a personal access token or deploy token
@ -282,7 +283,7 @@ Prerequisites:
- [Authenticate](#authenticate-to-the-package-registry) to the Package Registry.
- Set a [project-level npm endpoint](#use-the-gitlab-endpoint-for-npm-packages).
- Your npm package name must be in the format of [@scope/package-name](#package-naming-convention).
- Your npm package name must be in the format of [`@scope/package-name`](#package-naming-convention).
It must match exactly, including the case. This is different than the
npm naming convention, but it is required to work with the GitLab Package Registry.
@ -532,7 +533,7 @@ If you get this error, ensure that:
### `npm publish` returns `npm ERR! 400 Bad Request`
If you get this error, your package name may not meet the
[@scope/package-name package naming convention](#package-naming-convention).
[`@scope/package-name` package naming convention](#package-naming-convention).
Ensure the name meets the convention exactly, including the case.
Then try to publish again.

View file

@ -14,6 +14,8 @@ module Gitlab
UPDATE_FREQUENCY_DEFAULT = 60.seconds
UPDATE_FREQUENCY_WHEN_BEING_WATCHED = 3.seconds
LOAD_BALANCING_STICKING_NAMESPACE = 'ci/build/trace'
ArchiveError = Class.new(StandardError)
AlreadyArchivedError = Class.new(StandardError)
LockedError = Class.new(StandardError)
@ -296,25 +298,31 @@ module Gitlab
read_trace_artifact(job) { job.job_artifacts_trace }
end
##
# Overridden in EE
#
def destroy_stream(job)
def destroy_stream(build)
if consistent_archived_trace?(build)
::Gitlab::Database::LoadBalancing::Sticking
.stick(LOAD_BALANCING_STICKING_NAMESPACE, build.id)
end
yield
end
##
# Overriden in EE
#
def read_trace_artifact(job)
def read_trace_artifact(build)
if consistent_archived_trace?(build)
::Gitlab::Database::LoadBalancing::Sticking
.unstick_or_continue_sticking(LOAD_BALANCING_STICKING_NAMESPACE, build.id)
end
yield
end
def consistent_archived_trace?(build)
::Feature.enabled?(:gitlab_ci_archived_trace_consistent_reads, build.project, default_enabled: false)
end
def being_watched_cache_key
"gitlab:ci:trace:#{job.id}:watched"
end
end
end
end
::Gitlab::Ci::Trace.prepend_mod_with('Gitlab::Ci::Trace')

View file

@ -19,21 +19,22 @@ module Gitlab
PROCESSORS = [
::Gitlab::ErrorTracking::Processor::SidekiqProcessor,
::Gitlab::ErrorTracking::Processor::GrpcErrorProcessor,
::Gitlab::ErrorTracking::Processor::ContextPayloadProcessor,
# IMPORTANT: this processor must stay at the bottom, right before
# sending the event to Sentry.
::Gitlab::ErrorTracking::Processor::SanitizerProcessor
::Gitlab::ErrorTracking::Processor::ContextPayloadProcessor
].freeze
class << self
def configure
Sentry.init do |config|
Raven.configure do |config|
config.dsn = sentry_dsn
config.release = Gitlab.revision
config.environment = Gitlab.config.sentry.environment
config.current_environment = Gitlab.config.sentry.environment
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
# Sanitize authentication headers
config.sanitize_http_headers = %w[Authorization Private-Token]
config.before_send = method(:before_send)
config.background_worker_threads = 0
config.send_default_pii = true
yield config if block_given?
end
@ -107,11 +108,8 @@ module Gitlab
def process_exception(exception, sentry: false, logging: true, extra:)
context_payload = Gitlab::ErrorTracking::ContextPayloadGenerator.generate(exception, extra)
# There is a possibility that this method is called before Sentry is
# configured. Since Sentry 4.0, some methods of Sentry are forwarded to
# to `nil`, hence we have to check the client as well.
if sentry && ::Sentry.get_current_client && ::Sentry.configuration.dsn
::Sentry.capture_exception(exception, **context_payload)
if sentry && Raven.configuration.server
Raven.capture_exception(exception, **context_payload)
end
if logging

View file

@ -3,7 +3,7 @@
module Gitlab
module ErrorTracking
class LogFormatter
# Note: all the accesses to Sentry's contexts here are to keep the
# Note: all the accesses to Raven's contexts here are to keep the
# backward-compatibility to Sentry's built-in integrations. In future,
# they can be removed.
def generate_log(exception, context_payload)
@ -20,27 +20,21 @@ module Gitlab
private
def append_user_to_log!(payload, context_payload)
return if current_scope.blank?
user_context = current_scope.user.merge(context_payload[:user])
user_context = Raven.context.user.merge(context_payload[:user])
user_context.each do |key, value|
payload["user.#{key}"] = value
end
end
def append_tags_to_log!(payload, context_payload)
return if current_scope.blank?
tags_context = current_scope.tags.merge(context_payload[:tags])
tags_context = Raven.context.tags.merge(context_payload[:tags])
tags_context.each do |key, value|
payload["tags.#{key}"] = value
end
end
def append_extra_to_log!(payload, context_payload)
return if current_scope.blank?
extra = current_scope.extra.merge(context_payload[:extra])
extra = Raven.context.extra.merge(context_payload[:extra])
extra = extra.except(:server)
# The extra value for sidekiq is a hash whose keys are strings.
@ -56,10 +50,6 @@ module Gitlab
payload["extra.#{key}"] = value
end
end
def current_scope
Sentry.get_current_scope
end
end
end
end

View file

@ -17,7 +17,8 @@ module Gitlab
# Sentry can report multiple exceptions in an event. Sanitize
# only the first one since that's what is used for grouping.
def process_first_exception_value(event)
exceptions = event.exception&.instance_variable_get(:@values)
# Better in new version, will be event.exception.values
exceptions = event.instance_variable_get(:@interfaces)[:exception]&.values
return unless exceptions.is_a?(Array)
@ -32,7 +33,9 @@ module Gitlab
message, debug_str = split_debug_error_string(raw_message)
exception.instance_variable_set(:@value, message) if message
# Worse in new version, no setter! Have to poke at the
# instance variable
exception.value = message if message
event.extra[:grpc_debug_error_string] = debug_str if debug_str
end
@ -63,7 +66,7 @@ module Gitlab
def valid_exception?(exception)
case exception
when Sentry::SingleExceptionInterface
when Raven::SingleExceptionInterface
exception&.value
else
false

View file

@ -1,33 +0,0 @@
# frozen_string_literal: true
module Gitlab
module ErrorTracking
module Processor
module SanitizerProcessor
SANITIZED_HTTP_HEADERS = %w[Authorization Private-Token].freeze
SANITIZED_ATTRIBUTES = %i[user contexts extra tags].freeze
# This processor removes sensitive fields or headers from the event
# before sending. Sentry versions above 4.0 don't support
# sanitized_fields and sanitized_http_headers anymore. The official
# document recommends using before_send instead.
#
# For more information, please visit:
# https://docs.sentry.io/platforms/ruby/guides/rails/configuration/filtering/#using-beforesend
def self.call(event)
if event.request.present? && event.request.headers.is_a?(Hash)
header_filter = ActiveSupport::ParameterFilter.new(SANITIZED_HTTP_HEADERS)
event.request.headers = header_filter.filter(event.request.headers)
end
attribute_filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
SANITIZED_ATTRIBUTES.each do |attribute|
event.send("#{attribute}=", attribute_filter.filter(event.send(attribute))) # rubocop:disable GitlabSecurity/PublicSend
end
event
end
end
end
end
end

View file

@ -19,6 +19,7 @@ module Gitlab
class DuplicateJob
DUPLICATE_KEY_TTL = 6.hours
DEFAULT_STRATEGY = :until_executing
STRATEGY_NONE = :none
attr_reader :existing_jid
@ -102,6 +103,7 @@ module Gitlab
def strategy
return DEFAULT_STRATEGY unless worker_klass
return DEFAULT_STRATEGY unless worker_klass.respond_to?(:idempotent?)
return STRATEGY_NONE unless worker_klass.deduplication_enabled?
worker_klass.get_deduplicate_strategy
end

View file

@ -43,6 +43,11 @@ module Peek
count[item[:transaction]] ||= 0
count[item[:transaction]] += 1
end
if ::Gitlab::Database::LoadBalancing.enable?
count[item[:db_role]] ||= 0
count[item[:db_role]] += 1
end
end
def setup_subscribers
@ -60,11 +65,19 @@ module Peek
sql: data[:sql].strip,
backtrace: Gitlab::BacktraceCleaner.clean_backtrace(caller),
cached: data[:cached] ? 'Cached' : '',
transaction: data[:connection].transaction_open? ? 'In a transaction' : ''
transaction: data[:connection].transaction_open? ? 'In a transaction' : '',
db_role: db_role(data)
}
end
def db_role(data)
return unless ::Gitlab::Database::LoadBalancing.enable?
role = ::Gitlab::Database::LoadBalancing.db_role_for_connection(data[:connection]) ||
::Gitlab::Database::LoadBalancing::ROLE_UNKNOWN
role.to_s.capitalize
end
end
end
end
Peek::Views::ActiveRecord.prepend_mod_with('Peek::Views::ActiveRecord')

View file

@ -38,6 +38,16 @@ module Sidebars
# in the helper method that sets the active class
# on each element.
def nav_link_html_options
{
data: {
track_label: self.class.name.demodulize.underscore
}
}.deep_merge(extra_nav_link_html_options)
end
# Classes should mostly override this method
# and not `nav_link_html_options`.
def extra_nav_link_html_options
{}
end

View file

@ -22,5 +22,13 @@ module Sidebars
def render?
true
end
def nav_link_html_options
{
data: {
track_label: item_id
}
}
end
end
end

View file

@ -35,14 +35,13 @@ module Sidebars
end
end
override :extra_container_html_options
def nav_link_html_options
override :extra_nav_link_html_options
def extra_nav_link_html_options
{
class: 'home',
data: {
track_action: 'click_menu',
track_property: context.learn_gitlab_experiment_tracking_category,
track_label: 'learn_gitlab'
track_label: 'learn_gitlab',
track_property: context.learn_gitlab_experiment_tracking_category
}
}
end

View file

@ -29,8 +29,8 @@ module Sidebars
end
end
override :nav_link_html_options
def nav_link_html_options
override :extra_nav_link_html_options
def extra_nav_link_html_options
{ class: 'home' }
end

View file

@ -28,8 +28,8 @@ module Sidebars
}
end
override :nav_link_html_options
def nav_link_html_options
override :extra_nav_link_html_options
def extra_nav_link_html_options
return {} if Feature.disabled?(:sidebar_refactor, context.current_user, default_enabled: :yaml)
{ class: 'context-header' }

View file

@ -10437,9 +10437,6 @@ msgstr ""
msgid "Date range"
msgstr ""
msgid "Date range cannot exceed %{maxDateRange} days."
msgstr ""
msgid "Date range must be shorter than %{max_range} days."
msgstr ""
@ -26792,9 +26789,6 @@ msgstr ""
msgid "Protocol"
msgstr ""
msgid "Provide feedback"
msgstr ""
msgid "Provider"
msgstr ""
@ -30192,6 +30186,9 @@ msgstr ""
msgid "Showing all issues"
msgstr ""
msgid "Showing data for workflow items created in this date range. Date range cannot exceed %{maxDateRange} days."
msgstr ""
msgid "Showing graphs based on events of the last %{timerange} days."
msgstr ""
@ -33000,9 +32997,6 @@ msgstr ""
msgid "There was a problem communicating with your device."
msgstr ""
msgid "There was a problem dismissing this notification."
msgstr ""
msgid "There was a problem fetching branches."
msgstr ""
@ -36139,9 +36133,6 @@ msgstr ""
msgid "View job"
msgstr ""
msgid "View job dependencies in the pipeline graph!"
msgstr ""
msgid "View job log"
msgstr ""
@ -37401,9 +37392,6 @@ msgstr ""
msgid "You can now export your security dashboard to a CSV report."
msgstr ""
msgid "You can now group jobs in the pipeline graph based on which jobs are configured to run first, if you use the %{codeStart}needs:%{codeEnd} keyword to establish job dependencies in your CI/CD pipelines. %{linkStart}Learn how to speed up your pipeline with needs.%{linkEnd}"
msgstr ""
msgid "You can now submit a merge request to get this change into the original branch."
msgstr ""

View file

@ -73,7 +73,7 @@ RSpec.describe Import::BulkImportsController do
let(:client_params) do
{
top_level_only: true,
min_access_level: Gitlab::Access::MAINTAINER
min_access_level: Gitlab::Access::OWNER
}
end

View file

@ -69,6 +69,20 @@ RSpec.describe Projects::MergeRequests::DiffsController do
end
end
shared_examples 'show the right diff files with previous diff_id' do
context 'with previous diff_id' do
let!(:merge_request_diff_1) { merge_request.merge_request_diffs.create!(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
let!(:merge_request_diff_2) { merge_request.merge_request_diffs.create!(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e', diff_type: :merge_head) }
subject { go(diff_id: merge_request_diff_1.id, diff_head: true) }
it 'shows the right diff files' do
subject
expect(json_response["diff_files"].size).to eq(merge_request_diff_1.files_count)
end
end
end
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@ -142,6 +156,8 @@ RSpec.describe Projects::MergeRequests::DiffsController do
it_behaves_like '404 for unexistent diffable'
it_behaves_like 'show the right diff files with previous diff_id'
context 'when not authorized' do
let(:another_user) { create(:user) }
@ -480,6 +496,8 @@ RSpec.describe Projects::MergeRequests::DiffsController do
it_behaves_like '404 for unexistent diffable'
it_behaves_like 'show the right diff files with previous diff_id'
context 'when not authorized' do
let(:other_user) { create(:user) }
@ -499,7 +517,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do
it_behaves_like 'serializes diffs with expected arguments' do
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
let(:expected_options) { collection_arguments(current_page: 1, total_pages: 1) }
let(:expected_options) { collection_arguments(current_page: 1, total_pages: 1).merge(merge_ref_head_diff: false) }
end
it_behaves_like 'successful request'
@ -522,7 +540,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do
it_behaves_like 'serializes diffs with expected arguments' do
let(:collection) { Gitlab::Diff::FileCollection::Compare }
let(:expected_options) { collection_arguments }
let(:expected_options) { collection_arguments.merge(merge_ref_head_diff: false) }
end
it_behaves_like 'successful request'

View file

@ -24,7 +24,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do
pat = 'demo-pat'
stub_path = 'stub-group'
total = 37
stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=20&top_level_only=true&min_access_level=40&search=" % { url: source_url }).to_return(
stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=20&top_level_only=true&min_access_level=50&search=" % { url: source_url }).to_return(
body: [{
id: 2595438,
web_url: 'https://gitlab.com/groups/auto-breakfast',

View file

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Mermaid rendering', :js do
let_it_be(:project) { create(:project, :public) }
it 'renders Mermaid diagrams correctly' do
description = <<~MERMAID
```mermaid
@ -14,7 +16,6 @@ RSpec.describe 'Mermaid rendering', :js do
```
MERMAID
project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@ -36,7 +37,6 @@ RSpec.describe 'Mermaid rendering', :js do
```
MERMAID
project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@ -44,10 +44,33 @@ RSpec.describe 'Mermaid rendering', :js do
wait_for_requests
wait_for_mermaid
expected = '<text style=""><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>'
# From https://github.com/mermaid-js/mermaid/blob/d3f8f03a7d03a052e1fe0251d5a6d8d1f48d67ee/src/dagre-wrapper/createLabel.js#L79-L82
expected = %(<div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Line 1<br>Line 2</div>)
expect(page.html.scan(expected).count).to be(4)
end
it 'does not allow XSS in HTML labels' do
description = <<~MERMAID
```mermaid
graph LR;
A-->CLICK_HERE_AND_GET_BONUS;
click A alert "aaa"
click CLICK_HERE_AND_GET_BONUS "javascript:alert%28%64%6f%63%75%6d%65%6e%74%2e%64%6f%6d%61%69%6e%29" "Here is the XSS"
```
MERMAID
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
wait_for_requests
wait_for_mermaid
# From https://github.com/mermaid-js/mermaid/blob/d3f8f03a7d03a052e1fe0251d5a6d8d1f48d67ee/src/dagre-wrapper/createLabel.js#L79-L82
expected = %(<div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">CLICK_HERE_AND_GET_BONUS</div>)
expect(page.html).to include(expected)
end
it 'renders only 2 Mermaid blocks and', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/234081' do
description = <<~MERMAID
```mermaid
@ -64,7 +87,6 @@ RSpec.describe 'Mermaid rendering', :js do
```
MERMAID
project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@ -94,7 +116,6 @@ RSpec.describe 'Mermaid rendering', :js do
</details>
MERMAID
project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@ -108,7 +129,37 @@ RSpec.describe 'Mermaid rendering', :js do
expect(svg[:style]).to match(/max-width/)
expect(svg[:width].to_i).to eq(100)
expect(svg[:height].to_i).to be_within(5).of(220)
expect(svg[:height].to_i).to be_within(5).of(236)
end
end
it 'renders V2 state diagrams' do
description = <<~MERMAID
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Active : CONTINUE
state Active {
[*] --> Run
Run--> Stop: CONTINUE
Stop--> Run: CONTINUE
Run: Run
Run: entry/start
Run: check
}
```
MERMAID
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
wait_for_requests
wait_for_mermaid
page.within('.description') do
expect(page).to have_selector('svg')
end
end
@ -123,7 +174,6 @@ RSpec.describe 'Mermaid rendering', :js do
```
MERMAID
project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@ -144,7 +194,6 @@ RSpec.describe 'Mermaid rendering', :js do
```
MERMAID
project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@ -183,8 +232,6 @@ RSpec.describe 'Mermaid rendering', :js do
description *= 51
project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)

View file

@ -5,23 +5,25 @@ require 'spec_helper'
RSpec.describe "Private Project Snippets Access" do
include AccessMatchers
let(:project) { create(:project, :private) }
let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
let_it_be(:project) { create(:project, :private) }
let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
it('is allowed for admin when admin mode is enabled', :enable_admin_mode) { is_expected.to be_allowed_for(:admin) }
it('is denied for admin when admin mode is disabled') { is_expected.to be_denied_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
specify :aggregate_failures do
is_expected.to be_allowed_for(:owner).of(project)
is_expected.to be_allowed_for(:maintainer).of(project)
is_expected.to be_allowed_for(:developer).of(project)
is_expected.to be_allowed_for(:reporter).of(project)
is_expected.to be_allowed_for(:guest).of(project)
is_expected.to be_denied_for(:user)
is_expected.to be_denied_for(:external)
is_expected.to be_denied_for(:visitor)
end
end
describe "GET /:project_path/snippets/new" do
@ -29,14 +31,17 @@ RSpec.describe "Private Project Snippets Access" do
it('is allowed for admin when admin mode is enabled', :enable_admin_mode) { is_expected.to be_allowed_for(:admin) }
it('is denied for admin when admin mode is disabled') { is_expected.to be_denied_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
specify :aggregate_failures do
is_expected.to be_allowed_for(:maintainer).of(project)
is_expected.to be_allowed_for(:owner).of(project)
is_expected.to be_allowed_for(:developer).of(project)
is_expected.to be_allowed_for(:reporter).of(project)
is_expected.to be_denied_for(:guest).of(project)
is_expected.to be_denied_for(:user)
is_expected.to be_denied_for(:external)
is_expected.to be_denied_for(:visitor)
end
end
describe "GET /:project_path/snippets/:id for a private snippet" do
@ -44,14 +49,17 @@ RSpec.describe "Private Project Snippets Access" do
it('is allowed for admin when admin mode is enabled', :enable_admin_mode) { is_expected.to be_allowed_for(:admin) }
it('is denied for admin when admin mode is disabled') { is_expected.to be_denied_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
specify :aggregate_failures do
is_expected.to be_allowed_for(:owner).of(project)
is_expected.to be_allowed_for(:maintainer).of(project)
is_expected.to be_allowed_for(:developer).of(project)
is_expected.to be_allowed_for(:reporter).of(project)
is_expected.to be_allowed_for(:guest).of(project)
is_expected.to be_denied_for(:user)
is_expected.to be_denied_for(:external)
is_expected.to be_denied_for(:visitor)
end
end
describe "GET /:project_path/snippets/:id/raw for a private snippet" do
@ -59,13 +67,16 @@ RSpec.describe "Private Project Snippets Access" do
it('is allowed for admin when admin mode is enabled', :enable_admin_mode) { is_expected.to be_allowed_for(:admin) }
it('is denied for admin when admin mode is disabled') { is_expected.to be_denied_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
specify :aggregate_failures do
is_expected.to be_allowed_for(:owner).of(project)
is_expected.to be_allowed_for(:maintainer).of(project)
is_expected.to be_allowed_for(:developer).of(project)
is_expected.to be_allowed_for(:reporter).of(project)
is_expected.to be_allowed_for(:guest).of(project)
is_expected.to be_denied_for(:user)
is_expected.to be_denied_for(:external)
is_expected.to be_denied_for(:visitor)
end
end
end

View file

@ -0,0 +1,160 @@
import { setHTMLFixture } from 'helpers/fixtures';
import { initSidebarTracking } from '~/pages/shared/nav/sidebar_tracking';
describe('~/pages/shared/nav/sidebar_tracking.js', () => {
beforeEach(() => {
setHTMLFixture(`
<aside class="nav-sidebar">
<div class="nav-sidebar-inner-scroll">
<ul class="sidebar-top-level-items">
<li data-track-label="project_information_menu" class="home">
<a aria-label="Project information" class="shortcuts-project-information has-sub-items" href="">
<span class="nav-icon-container">
<svg class="s16" data-testid="project-icon">
<use xlink:href="/assets/icons-1b2dadc4c3d49797908ba67b8f10da5d63dd15d859bde28d66fb60bbb97a4dd5.svg#project"></use>
</svg>
</span>
<span class="nav-item-name">Project information</span>
</a>
<ul class="sidebar-sub-level-items">
<li class="fly-out-top-item">
<a aria-label="Project information" href="#">
<strong class="fly-out-top-item-name">Project information</strong>
</a>
</li>
<li class="divider fly-out-top-item"></li>
<li data-track-label="activity" class="">
<a aria-label="Activity" class="shortcuts-project-activity" href=#">
<span>Activity</span>
</a>
</li>
<li data-track-label="labels" class="">
<a aria-label="Labels" href="#">
<span>Labels</span>
</a>
</li>
<li data-track-label="members" class="">
<a aria-label="Members" href="#">
<span>Members</span>
</a>
</li>
</ul>
</li>
</ul>
</div>
</aside>
`);
initSidebarTracking();
});
describe('sidebar is not collapsed', () => {
describe('menu is not expanded', () => {
it('sets the proper data tracking attributes when clicking on menu', () => {
const menu = document.querySelector('li[data-track-label="project_information_menu"]');
const menuLink = menu.querySelector('a');
menu.classList.add('is-over', 'is-showing-fly-out');
menuLink.click();
expect(menu.dataset).toMatchObject({
trackAction: 'click_menu',
trackExtra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Fly out',
}),
});
});
it('sets the proper data tracking attributes when clicking on submenu', () => {
const menu = document.querySelector('li[data-track-label="activity"]');
const menuLink = menu.querySelector('a');
const submenuList = document.querySelector('ul.sidebar-sub-level-items');
submenuList.classList.add('fly-out-list');
menuLink.click();
expect(menu.dataset).toMatchObject({
trackAction: 'click_menu_item',
trackExtra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Fly out',
}),
});
});
});
describe('menu is expanded', () => {
it('sets the proper data tracking attributes when clicking on menu', () => {
const menu = document.querySelector('li[data-track-label="project_information_menu"]');
const menuLink = menu.querySelector('a');
menu.classList.add('active');
menuLink.click();
expect(menu.dataset).toMatchObject({
trackAction: 'click_menu',
trackExtra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Expanded',
}),
});
});
it('sets the proper data tracking attributes when clicking on submenu', () => {
const menu = document.querySelector('li[data-track-label="activity"]');
const menuLink = menu.querySelector('a');
menu.classList.add('active');
menuLink.click();
expect(menu.dataset).toMatchObject({
trackAction: 'click_menu_item',
trackExtra: JSON.stringify({
sidebar_display: 'Expanded',
menu_display: 'Expanded',
}),
});
});
});
});
describe('sidebar is collapsed', () => {
beforeEach(() => {
document.querySelector('aside.nav-sidebar').classList.add('js-sidebar-collapsed');
});
it('sets the proper data tracking attributes when clicking on menu', () => {
const menu = document.querySelector('li[data-track-label="project_information_menu"]');
const menuLink = menu.querySelector('a');
menu.classList.add('is-over', 'is-showing-fly-out');
menuLink.click();
expect(menu.dataset).toMatchObject({
trackAction: 'click_menu',
trackExtra: JSON.stringify({
sidebar_display: 'Collapsed',
menu_display: 'Fly out',
}),
});
});
it('sets the proper data tracking attributes when clicking on submenu', () => {
const menu = document.querySelector('li[data-track-label="activity"]');
const menuLink = menu.querySelector('a');
const submenuList = document.querySelector('ul.sidebar-sub-level-items');
submenuList.classList.add('fly-out-list');
menuLink.click();
expect(menu.dataset).toMatchObject({
trackAction: 'click_menu_item',
trackExtra: JSON.stringify({
sidebar_display: 'Collapsed',
menu_display: 'Fly out',
}),
});
});
});
});

View file

@ -21,3 +21,10 @@ exports[`Links Inner component with one need matches snapshot and has expected p
<path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
</svg> </div>"
`;
exports[`Links Inner component with same stage needs matches snapshot and has expected path 1`] = `
"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
<path d=\\"M192,108L22,108C52,108,52,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
<path d=\\"M202,118L32,118C62,118,62,128,92,128\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
</svg> </div>"
`;

View file

@ -10,6 +10,7 @@ import {
pipelineData,
pipelineDataWithNoNeeds,
rootRect,
sameStageNeeds,
} from '../pipeline_graph/mock_data';
describe('Links Inner component', () => {
@ -40,7 +41,7 @@ describe('Links Inner component', () => {
// We create fixture so that each job has an empty div that represent
// the JobPill in the DOM. Each `JobPill` would have different coordinates,
// so we increment their coordinates on each iteration to simulat different positions.
// so we increment their coordinates on each iteration to simulate different positions.
const setFixtures = ({ stages }) => {
const jobs = createJobsHash(stages);
const arrayOfJobs = Object.keys(jobs);
@ -81,7 +82,6 @@ describe('Links Inner component', () => {
afterEach(() => {
jest.restoreAllMocks();
wrapper.destroy();
wrapper = null;
});
describe('basic SVG creation', () => {
@ -160,6 +160,25 @@ describe('Links Inner component', () => {
});
});
describe('with same stage needs', () => {
beforeEach(() => {
setFixtures(sameStageNeeds);
createComponent({ pipelineData: sameStageNeeds.stages });
});
it('renders the correct number of links', () => {
expect(findAllLinksPath()).toHaveLength(2);
});
it('path does not contain NaN values', () => {
expect(wrapper.html()).not.toContain('NaN');
});
it('matches snapshot and has expected path', () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
describe('with a large number of needs', () => {
beforeEach(() => {
setFixtures(largePipelineData);

View file

@ -1,79 +0,0 @@
import { GlBanner } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import PipelineNotification from '~/pipelines/components/notification/pipeline_notification.vue';
import getUserCallouts from '~/pipelines/graphql/queries/get_user_callouts.query.graphql';
describe('Pipeline notification', () => {
const localVue = createLocalVue();
let wrapper;
const dagDocPath = 'my/dag/path';
const createWrapper = (apolloProvider) => {
return shallowMount(PipelineNotification, {
localVue,
provide: {
dagDocPath,
},
apolloProvider,
});
};
const createWrapperWithApollo = async ({ callouts = [], isLoading = false } = {}) => {
localVue.use(VueApollo);
const mappedCallouts = callouts.map((callout) => {
return { featureName: callout, __typename: 'UserCallout' };
});
const mockCalloutsResponse = {
data: {
currentUser: {
id: 45,
__typename: 'User',
callouts: {
id: 5,
__typename: 'UserCalloutConnection',
nodes: mappedCallouts,
},
},
},
};
const getUserCalloutsHandler = jest.fn().mockResolvedValue(mockCalloutsResponse);
const requestHandlers = [[getUserCallouts, getUserCalloutsHandler]];
const apolloWrapper = createWrapper(createMockApollo(requestHandlers));
if (!isLoading) {
await nextTick();
}
return apolloWrapper;
};
const findBanner = () => wrapper.findComponent(GlBanner);
afterEach(() => {
wrapper.destroy();
});
it('shows the banner if the user has never seen it', async () => {
wrapper = await createWrapperWithApollo({ callouts: ['random'] });
expect(findBanner().exists()).toBe(true);
});
it('does not show the banner while the user callout query is loading', async () => {
wrapper = await createWrapperWithApollo({ callouts: ['random'], isLoading: true });
expect(findBanner().exists()).toBe(false);
});
it('does not show the banner if the user has previously dismissed it', async () => {
wrapper = await createWrapperWithApollo({ callouts: ['pipeline_needs_banner'.toUpperCase()] });
expect(findBanner().exists()).toBe(false);
});
});

View file

@ -162,6 +162,38 @@ export const parallelNeedData = {
],
};
export const sameStageNeeds = {
stages: [
{
name: 'build',
groups: [
{
name: 'build_1',
jobs: [{ script: 'echo hello', stage: 'build', name: 'build_1' }],
},
],
},
{
name: 'build',
groups: [
{
name: 'build_2',
jobs: [{ script: 'yarn test', stage: 'build', needs: ['build_1'] }],
},
],
},
{
name: 'build',
groups: [
{
name: 'build_3',
jobs: [{ script: 'yarn test', stage: 'build', needs: ['build_2'] }],
},
],
},
],
};
export const largePipelineData = {
stages: [
{

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'spec_helper'
require 'fast_spec_helper'
require 'rspec-parameterized'
RSpec.describe Gitlab::ErrorTracking::ContextPayloadGenerator do

View file

@ -27,9 +27,9 @@ RSpec.describe Gitlab::ErrorTracking::LogFormatter do
end
before do
Sentry.set_user(user_flag: 'flag')
Sentry.set_tags(shard: 'catchall')
Sentry.set_extras(some_info: 'info')
Raven.context.user[:user_flag] = 'flag'
Raven.context.tags[:shard] = 'catchall'
Raven.context.extra[:some_info] = 'info'
allow(exception).to receive(:backtrace).and_return(
[
@ -40,7 +40,7 @@ RSpec.describe Gitlab::ErrorTracking::LogFormatter do
end
after do
::Sentry.get_current_scope.clear
::Raven::Context.clear!
end
it 'appends error-related log fields and filters sensitive Sidekiq arguments' do

View file

@ -4,14 +4,18 @@ require 'spec_helper'
RSpec.describe Gitlab::ErrorTracking::Processor::ContextPayloadProcessor do
describe '.call' do
let(:exception) { StandardError.new('Test exception') }
let(:event) { Sentry.get_current_client.event_from_exception(exception) }
let(:required_options) do
{
configuration: Raven.configuration,
context: Raven.context,
breadcrumbs: Raven.breadcrumbs
}
end
let(:event) { Raven::Event.new(required_options.merge(payload)) }
let(:result_hash) { described_class.call(event).to_hash }
before do
Sentry.get_current_scope.update_from_options(**payload)
Sentry.get_current_scope.apply_to_event(event)
allow_next_instance_of(Gitlab::ErrorTracking::ContextPayloadGenerator) do |generator|
allow(generator).to receive(:generate).and_return(
user: { username: 'root' },
@ -21,10 +25,6 @@ RSpec.describe Gitlab::ErrorTracking::Processor::ContextPayloadProcessor do
end
end
after do
Sentry.get_current_scope.clear
end
let(:payload) do
{
user: { ip_address: '127.0.0.1' },

View file

@ -4,18 +4,17 @@ require 'spec_helper'
RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor do
describe '.call' do
let(:event) { Sentry.get_current_client.event_from_exception(exception) }
let(:required_options) do
{
configuration: Raven.configuration,
context: Raven.context,
breadcrumbs: Raven.breadcrumbs
}
end
let(:event) { Raven::Event.from_exception(exception, required_options.merge(data)) }
let(:result_hash) { described_class.call(event).to_hash }
before do
Sentry.get_current_scope.update_from_options(**data)
Sentry.get_current_scope.apply_to_event(event)
end
after do
Sentry.get_current_scope.clear
end
context 'when there is no GRPC exception' do
let(:exception) { RuntimeError.new }
let(:data) { { fingerprint: ['ArgumentError', 'Missing arguments'] } }
@ -57,7 +56,7 @@ RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor do
end
it 'removes the debug error string and stores it as an extra field' do
expect(result_hash[:fingerprint]).to be_empty
expect(result_hash).not_to include(:fingerprint)
expect(result_hash[:exception][:values].first)
.to include(type: 'GRPC::DeadlineExceeded', value: '4:Deadline Exceeded.')

View file

@ -1,96 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::ErrorTracking::Processor::SanitizerProcessor do
describe '.call' do
let(:event) { Sentry.get_current_client.event_from_exception(exception) }
let(:result_hash) { described_class.call(event).to_hash }
before do
data.each do |key, value|
event.send("#{key}=", value)
end
end
after do
Sentry.get_current_scope.clear
end
context 'when event attributes contains sensitive information' do
let(:exception) { RuntimeError.new }
let(:data) do
{
contexts: {
jwt: 'abcdef',
controller: 'GraphController#execute'
},
tags: {
variables: %w[some sensitive information'],
deep_hash: {
sharedSecret: 'secret123'
}
},
user: {
email: 'a@a.com',
password: 'nobodyknows'
},
extra: {
issue_url: 'http://gitlab.com/gitlab-org/gitlab-foss/-/issues/1',
my_token: '[FILTERED]',
another_token: '[FILTERED]'
}
}
end
it 'filters sensitive attributes' do
expect_next_instance_of(ActiveSupport::ParameterFilter) do |instance|
expect(instance).to receive(:filter).exactly(4).times.and_call_original
end
expect(result_hash).to include(
contexts: {
jwt: '[FILTERED]',
controller: 'GraphController#execute'
},
tags: {
variables: '[FILTERED]',
deep_hash: {
sharedSecret: '[FILTERED]'
}
},
user: {
email: 'a@a.com',
password: '[FILTERED]'
},
extra: {
issue_url: 'http://gitlab.com/gitlab-org/gitlab-foss/-/issues/1',
my_token: '[FILTERED]',
another_token: '[FILTERED]'
}
)
end
end
context 'when request headers contains sensitive information' do
let(:exception) { RuntimeError.new }
let(:data) { {} }
before do
event.rack_env = {
'HTTP_AUTHORIZATION' => 'Bearer 123456',
'HTTP_PRIVATE_TOKEN' => 'abcdef',
'HTTP_GITLAB_WORKHORSE_PROXY_START' => 123456
}
end
it 'filters sensitive headers' do
expect(result_hash[:request][:headers]).to include(
'Authorization' => '[FILTERED]',
'Private-Token' => '[FILTERED]',
'Gitlab-Workhorse-Proxy-Start' => '123456'
)
end
end
end
end

View file

@ -95,19 +95,17 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do
end
describe '.call' do
let(:exception) { StandardError.new('Test exception') }
let(:event) { Sentry.get_current_client.event_from_exception(exception) }
let(:required_options) do
{
configuration: Raven.configuration,
context: Raven.context,
breadcrumbs: Raven.breadcrumbs
}
end
let(:event) { Raven::Event.new(required_options.merge(wrapped_value)) }
let(:result_hash) { described_class.call(event).to_hash }
before do
Sentry.get_current_scope.update_from_options(**wrapped_value)
Sentry.get_current_scope.apply_to_event(event)
end
after do
Sentry.get_current_scope.clear
end
context 'when there is Sidekiq data' do
let(:wrapped_value) { { extra: { sidekiq: value } } }

View file

@ -2,7 +2,7 @@
require 'spec_helper'
require 'sentry/transport/dummy_transport'
require 'raven/transports/dummy'
RSpec.describe Gitlab::ErrorTracking do
let(:exception) { RuntimeError.new('boom') }
@ -43,7 +43,7 @@ RSpec.describe Gitlab::ErrorTracking do
}
end
let(:sentry_event) { Sentry.get_current_client.transport.events.last }
let(:sentry_event) { Gitlab::Json.parse(Raven.client.transport.events.last[1]) }
before do
stub_sentry_settings
@ -53,7 +53,7 @@ RSpec.describe Gitlab::ErrorTracking do
allow(I18n).to receive(:locale).and_return('en')
described_class.configure do |config|
config.transport.transport_class = Sentry::DummyTransport
config.encoding = 'json'
end
end
@ -63,10 +63,6 @@ RSpec.describe Gitlab::ErrorTracking do
end
end
after do
::Sentry.get_current_scope.clear
end
describe '.track_and_raise_for_dev_exception' do
context 'when exceptions for dev should be raised' do
before do
@ -74,7 +70,7 @@ RSpec.describe Gitlab::ErrorTracking do
end
it 'raises the exception' do
expect(Sentry).to receive(:capture_exception).with(exception, sentry_payload)
expect(Raven).to receive(:capture_exception).with(exception, sentry_payload)
expect do
described_class.track_and_raise_for_dev_exception(
@ -92,7 +88,7 @@ RSpec.describe Gitlab::ErrorTracking do
end
it 'logs the exception with all attributes passed' do
expect(Sentry).to receive(:capture_exception).with(exception, sentry_payload)
expect(Raven).to receive(:capture_exception).with(exception, sentry_payload)
described_class.track_and_raise_for_dev_exception(
exception,
@ -115,7 +111,7 @@ RSpec.describe Gitlab::ErrorTracking do
describe '.track_and_raise_exception' do
it 'always raises the exception' do
expect(Sentry).to receive(:capture_exception).with(exception, sentry_payload)
expect(Raven).to receive(:capture_exception).with(exception, sentry_payload)
expect do
described_class.track_and_raise_for_dev_exception(
@ -143,14 +139,14 @@ RSpec.describe Gitlab::ErrorTracking do
subject(:track_exception) { described_class.track_exception(exception, extra) }
before do
allow(Sentry).to receive(:capture_exception).and_call_original
allow(Raven).to receive(:capture_exception).and_call_original
allow(Gitlab::ErrorTracking::Logger).to receive(:error)
end
it 'calls Sentry.capture_exception' do
it 'calls Raven.capture_exception' do
track_exception
expect(Sentry).to have_received(:capture_exception).with(
expect(Raven).to have_received(:capture_exception).with(
exception,
sentry_payload
)
@ -176,31 +172,25 @@ RSpec.describe Gitlab::ErrorTracking do
context 'the exception implements :sentry_extra_data' do
let(:extra_info) { { event: 'explosion', size: :massive } }
before do
allow(exception).to receive(:sentry_extra_data).and_return(extra_info)
end
let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info, backtrace: caller, cause: nil) }
it 'includes the extra data from the exception in the tracking information' do
track_exception
expect(Sentry).to have_received(:capture_exception).with(
expect(Raven).to have_received(:capture_exception).with(
exception, a_hash_including(extra: a_hash_including(extra_info))
)
end
end
context 'the exception implements :sentry_extra_data, which returns nil' do
let(:exception) { double(message: 'bang!', sentry_extra_data: nil, backtrace: caller, cause: nil) }
let(:extra) { { issue_url: issue_url } }
before do
allow(exception).to receive(:sentry_extra_data).and_return(nil)
end
it 'just includes the other extra info' do
track_exception
expect(Sentry).to have_received(:capture_exception).with(
expect(Raven).to have_received(:capture_exception).with(
exception, a_hash_including(extra: a_hash_including(extra))
)
end
@ -212,7 +202,7 @@ RSpec.describe Gitlab::ErrorTracking do
it 'injects the normalized sql query into extra' do
track_exception
expect(sentry_event.extra[:sql]).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
expect(sentry_event.dig('extra', 'sql')).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
end
end
@ -222,7 +212,7 @@ RSpec.describe Gitlab::ErrorTracking do
track_exception
expect(sentry_event.extra[:sql]).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
expect(sentry_event.dig('extra', 'sql')).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
end
end
end
@ -231,27 +221,27 @@ RSpec.describe Gitlab::ErrorTracking do
subject(:track_exception) { described_class.track_exception(exception, extra) }
before do
allow(Sentry).to receive(:capture_exception).and_call_original
allow(Raven).to receive(:capture_exception).and_call_original
allow(Gitlab::ErrorTracking::Logger).to receive(:error)
end
context 'custom GitLab context when using Sentry.capture_exception directly' do
subject(:track_exception) { Sentry.capture_exception(exception) }
context 'custom GitLab context when using Raven.capture_exception directly' do
subject(:raven_capture_exception) { Raven.capture_exception(exception) }
it 'merges a default set of tags into the existing tags' do
Sentry.set_tags(foo: 'bar')
allow(Raven.context).to receive(:tags).and_return(foo: 'bar')
track_exception
raven_capture_exception
expect(sentry_event.tags).to include(:correlation_id, :feature_category, :foo, :locale, :program)
expect(sentry_event['tags']).to include('correlation_id', 'feature_category', 'foo', 'locale', 'program')
end
it 'merges the current user information into the existing user information' do
Sentry.set_user(id: -1)
Raven.user_context(id: -1)
track_exception
raven_capture_exception
expect(sentry_event.user).to eq(id: -1, username: user.username)
expect(sentry_event['user']).to eq('id' => -1, 'username' => user.username)
end
end
@ -275,7 +265,7 @@ RSpec.describe Gitlab::ErrorTracking do
it 'does not filter parameters when sending to Sentry' do
track_exception
expect(sentry_event.extra[:sidekiq]['args']).to eq([1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value'])
expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq([1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value'])
end
end
@ -285,7 +275,7 @@ RSpec.describe Gitlab::ErrorTracking do
it 'filters sensitive arguments before sending and logging' do
track_exception
expect(sentry_event.extra[:sidekiq]['args']).to eq(['[FILTERED]', 1, 2])
expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq(['[FILTERED]', 1, 2])
expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with(
hash_including(
'extra.sidekiq' => {
@ -305,8 +295,8 @@ RSpec.describe Gitlab::ErrorTracking do
it 'sets the GRPC debug error string in the Sentry event and adds a custom fingerprint' do
track_exception
expect(sentry_event.extra[:grpc_debug_error_string]).to eq('{"hello":1}')
expect(sentry_event.fingerprint).to eq(['GRPC::DeadlineExceeded', '4:unknown cause.'])
expect(sentry_event.dig('extra', 'grpc_debug_error_string')).to eq('{"hello":1}')
expect(sentry_event['fingerprint']).to eq(['GRPC::DeadlineExceeded', '4:unknown cause.'])
end
end
@ -316,8 +306,8 @@ RSpec.describe Gitlab::ErrorTracking do
it 'does not do any processing on the event' do
track_exception
expect(sentry_event.extra).not_to include(:grpc_debug_error_string)
expect(sentry_event.fingerprint).to eq(['GRPC::DeadlineExceeded', '4:unknown cause'])
expect(sentry_event['extra']).not_to include('grpc_debug_error_string')
expect(sentry_event['fingerprint']).to eq(['GRPC::DeadlineExceeded', '4:unknown cause'])
end
end
end

View file

@ -18,14 +18,43 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
describe '#schedule' do
it 'calls schedule on the strategy' do
expect do |block|
expect_next_instance_of(Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies::UntilExecuting) do |strategy|
expect(strategy).to receive(:schedule).with(job, &block)
shared_examples 'scheduling with deduplication class' do |strategy_class|
it 'calls schedule on the strategy' do
expect do |block|
expect_next_instance_of("Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies::#{strategy_class}".constantize) do |strategy|
expect(strategy).to receive(:schedule).with(job, &block)
end
duplicate_job.schedule(&block)
end.to yield_control
end
end
it_behaves_like 'scheduling with deduplication class', 'UntilExecuting'
context 'when the deduplication depends on a FF' do
before do
skip_feature_flags_yaml_validation
skip_default_enabled_yaml_check
allow(AuthorizedProjectsWorker).to receive(:get_deduplication_options).and_return(feature_flag: :my_feature_flag)
end
context 'when the feature flag is enabled' do
before do
stub_feature_flags(my_feature_flag: true)
end
duplicate_job.schedule(&block)
end.to yield_control
it_behaves_like 'scheduling with deduplication class', 'UntilExecuting'
end
context 'when the feature flag is disabled' do
before do
stub_feature_flags(my_feature_flag: false)
end
it_behaves_like 'scheduling with deduplication class', 'None'
end
end
end

View file

@ -5,16 +5,17 @@ require 'spec_helper'
RSpec.describe Peek::Views::ActiveRecord, :request_store do
subject { Peek.views.find { |v| v.instance_of?(Peek::Views::ActiveRecord) } }
let(:connection_1) { double(:connection) }
let(:connection_2) { double(:connection) }
let(:connection_3) { double(:connection) }
let(:connection_replica) { double(:connection_replica) }
let(:connection_primary_1) { double(:connection_primary) }
let(:connection_primary_2) { double(:connection_primary) }
let(:connection_unknown) { double(:connection_unknown) }
let(:event_1) do
{
name: 'SQL',
sql: 'SELECT * FROM users WHERE id = 10',
cached: false,
connection: connection_1
connection: connection_primary_1
}
end
@ -23,7 +24,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
name: 'SQL',
sql: 'SELECT * FROM users WHERE id = 10',
cached: true,
connection: connection_2
connection: connection_replica
}
end
@ -32,55 +33,141 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
name: 'SQL',
sql: 'UPDATE users SET admin = true WHERE id = 10',
cached: false,
connection: connection_3
connection: connection_primary_2
}
end
let(:event_4) do
{
name: 'SCHEMA',
sql: 'SELECT VERSION()',
cached: false,
connection: connection_unknown
}
end
before do
allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
allow(connection_1).to receive(:transaction_open?).and_return(false)
allow(connection_2).to receive(:transaction_open?).and_return(false)
allow(connection_3).to receive(:transaction_open?).and_return(true)
allow(connection_replica).to receive(:transaction_open?).and_return(false)
allow(connection_primary_1).to receive(:transaction_open?).and_return(false)
allow(connection_primary_2).to receive(:transaction_open?).and_return(true)
allow(connection_unknown).to receive(:transaction_open?).and_return(false)
end
it 'subscribes and store data into peek views' do
Timecop.freeze(2021, 2, 23, 10, 0) do
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1)
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 2.seconds, '2', event_2)
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 3.seconds, '3', event_3)
end
context 'when database load balancing is not enabled' do
it 'subscribes and store data into peek views' do
Timecop.freeze(2021, 2, 23, 10, 0) do
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1)
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 2.seconds, '2', event_2)
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 3.seconds, '3', event_3)
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 4.seconds, '4', event_4)
end
expect(subject.results).to match(
calls: 3,
summary: {
"Cached" => 1,
"In a transaction" => 1
},
duration: '6000.00ms',
warnings: ["active-record duration: 6000.0 over 3000"],
details: contain_exactly(
a_hash_including(
start: be_a(Time),
cached: '',
transaction: '',
duration: 1000.0,
sql: 'SELECT * FROM users WHERE id = 10'
),
a_hash_including(
start: be_a(Time),
cached: 'Cached',
transaction: '',
duration: 2000.0,
sql: 'SELECT * FROM users WHERE id = 10'
),
a_hash_including(
start: be_a(Time),
cached: '',
transaction: 'In a transaction',
duration: 3000.0,
sql: 'UPDATE users SET admin = true WHERE id = 10'
expect(subject.results).to match(
calls: 4,
summary: {
"Cached" => 1,
"In a transaction" => 1
},
duration: '10000.00ms',
warnings: ["active-record duration: 10000.0 over 3000"],
details: contain_exactly(
a_hash_including(
start: be_a(Time),
cached: '',
transaction: '',
duration: 1000.0,
sql: 'SELECT * FROM users WHERE id = 10'
),
a_hash_including(
start: be_a(Time),
cached: 'Cached',
transaction: '',
duration: 2000.0,
sql: 'SELECT * FROM users WHERE id = 10'
),
a_hash_including(
start: be_a(Time),
cached: '',
transaction: 'In a transaction',
duration: 3000.0,
sql: 'UPDATE users SET admin = true WHERE id = 10'
),
a_hash_including(
start: be_a(Time),
cached: '',
transaction: '',
duration: 4000.0,
sql: 'SELECT VERSION()'
)
)
)
)
end
end
context 'when database load balancing is enabled' do
before do
allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_replica).and_return(:replica)
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_primary_1).and_return(:primary)
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_primary_2).and_return(:primary)
allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_unknown).and_return(nil)
end
it 'includes db role data' do
Timecop.freeze(2021, 2, 23, 10, 0) do
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1)
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 2.seconds, '2', event_2)
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 3.seconds, '3', event_3)
ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 4.seconds, '4', event_4)
end
expect(subject.results).to match(
calls: 4,
summary: {
"Cached" => 1,
"In a transaction" => 1,
"Primary" => 2,
"Replica" => 1,
"Unknown" => 1
},
duration: '10000.00ms',
warnings: ["active-record duration: 10000.0 over 3000"],
details: contain_exactly(
a_hash_including(
start: be_a(Time),
cached: '',
transaction: '',
duration: 1000.0,
sql: 'SELECT * FROM users WHERE id = 10',
db_role: 'Primary'
),
a_hash_including(
start: be_a(Time),
cached: 'Cached',
transaction: '',
duration: 2000.0,
sql: 'SELECT * FROM users WHERE id = 10',
db_role: 'Replica'
),
a_hash_including(
start: be_a(Time),
cached: '',
transaction: 'In a transaction',
duration: 3000.0,
sql: 'UPDATE users SET admin = true WHERE id = 10',
db_role: 'Primary'
),
a_hash_including(
start: be_a(Time),
cached: '',
transaction: '',
duration: 4000.0,
sql: 'SELECT VERSION()',
db_role: 'Unknown'
)
)
)
end
end
end

View file

@ -27,7 +27,6 @@ RSpec.describe Sidebars::Projects::Menus::LearnGitlabMenu do
{
class: 'home',
data: {
track_action: 'click_menu',
track_property: tracking_category,
track_label: 'learn_gitlab'
}

View file

@ -79,6 +79,32 @@ RSpec.describe CommitStatus do
end
end
describe '.updated_before' do
let!(:lookback) { 5.days.ago }
let!(:timeout) { 1.day.ago }
let!(:before_lookback) { lookback - 1.hour }
let!(:after_lookback) { lookback + 1.hour }
let!(:before_timeout) { timeout - 1.hour }
let!(:after_timeout) { timeout + 1.hour }
subject { described_class.updated_before(lookback: lookback, timeout: timeout) }
def create_build_with_set_timestamps(created_at:, updated_at:)
travel_to(created_at) { create(:ci_build, created_at: Time.current) }.tap do |build|
travel_to(updated_at) { build.update!(status: :failed) }
end
end
it 'finds builds updated and created in the window between lookback and timeout' do
build_in_lookback_timeout_window = create_build_with_set_timestamps(created_at: after_lookback, updated_at: before_timeout)
build_outside_lookback_window = create_build_with_set_timestamps(created_at: before_lookback, updated_at: before_timeout)
build_outside_timeout_window = create_build_with_set_timestamps(created_at: after_lookback, updated_at: after_timeout)
expect(subject).to contain_exactly(build_in_lookback_timeout_window)
expect(subject).not_to include(build_outside_lookback_window, build_outside_timeout_window)
end
end
describe '#processed' do
subject { commit_status.processed }

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
require 'sentry/transport/dummy_transport'
require 'raven/transports/dummy'
require_relative '../../../config/initializers/sentry'
RSpec.describe API::Helpers do

View file

@ -29,6 +29,34 @@ RSpec.shared_examples 'common trace features' do
end
end
describe '#read' do
context 'gitlab_ci_archived_trace_consistent_reads feature flag enabled' do
before do
stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: trace.job.project)
end
it 'calls ::Gitlab::Database::LoadBalancing::Sticking.unstick_or_continue_sticking' do
expect(::Gitlab::Database::LoadBalancing::Sticking).to receive(:unstick_or_continue_sticking)
.with(described_class::LOAD_BALANCING_STICKING_NAMESPACE, trace.job.id)
.and_call_original
trace.read { |stream| stream }
end
end
context 'gitlab_ci_archived_trace_consistent_reads feature flag disabled' do
before do
stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: false)
end
it 'does not call ::Gitlab::Database::LoadBalancing::Sticking.unstick_or_continue_sticking' do
expect(::Gitlab::Database::LoadBalancing::Sticking).not_to receive(:unstick_or_continue_sticking)
trace.read { |stream| stream }
end
end
end
describe '#extract_coverage' do
let(:regex) { '\(\d+.\d+\%\) covered' }
@ -253,6 +281,52 @@ RSpec.shared_examples 'common trace features' do
describe '#archive!' do
subject { trace.archive! }
context 'when live trace chunks exists' do
before do
# Build a trace_chunk manually
# It is possible to do so with trace.set but only if ci_enable_live_trace FF is enabled
#
# We need the job to have a trace_chunk because we only use #stick in
# the case where trace_chunks exist.
stream = Gitlab::Ci::Trace::Stream.new do
Gitlab::Ci::Trace::ChunkedIO.new(trace.job)
end
stream.set(+"12\n34")
end
# We check the before setup actually sets up job trace_chunks
it 'has job trace_chunks' do
expect(trace.job.trace_chunks).to be_present
end
context 'gitlab_ci_archived_trace_consistent_reads feature flag enabled' do
before do
stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: trace.job.project)
end
it 'calls ::Gitlab::Database::LoadBalancing::Sticking.stick' do
expect(::Gitlab::Database::LoadBalancing::Sticking).to receive(:stick)
.with(described_class::LOAD_BALANCING_STICKING_NAMESPACE, trace.job.id)
.and_call_original
subject
end
end
context 'gitlab_ci_archived_trace_consistent_reads feature flag disabled' do
before do
stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: false)
end
it 'does not call ::Gitlab::Database::LoadBalancing::Sticking.stick' do
expect(::Gitlab::Database::LoadBalancing::Sticking).not_to receive(:stick)
subject
end
end
end
context 'when build status is success' do
let!(:build) { create(:ci_build, :success, :trace_live) }

View file

@ -62,6 +62,12 @@ RSpec.describe WorkerAttributes do
end
describe '.idempotent!' do
it 'sets `idempotent` attribute of the worker class to true' do
worker.idempotent!
expect(worker.send(:class_attributes)[:idempotent]).to eq(true)
end
context 'when data consistency is not :always' do
it 'raise exception' do
worker.data_consistency(:sticky)
@ -71,4 +77,66 @@ RSpec.describe WorkerAttributes do
end
end
end
describe '.idempotent?' do
subject(:idempotent?) { worker.idempotent? }
context 'when the worker is idempotent' do
before do
worker.idempotent!
end
it { is_expected.to be_truthy }
end
context 'when the worker is not idempotent' do
it { is_expected.to be_falsey }
end
end
describe '.deduplicate' do
it 'sets deduplication_strategy and deduplication_options' do
worker.deduplicate(:until_executing, including_scheduled: true)
expect(worker.send(:class_attributes)[:deduplication_strategy]).to eq(:until_executing)
expect(worker.send(:class_attributes)[:deduplication_options]).to eq(including_scheduled: true)
end
end
describe '#deduplication_enabled?' do
subject(:deduplication_enabled?) { worker.deduplication_enabled? }
context 'when no feature flag is set' do
before do
worker.deduplicate(:until_executing)
end
it { is_expected.to eq(true) }
end
context 'when feature flag is set' do
before do
skip_feature_flags_yaml_validation
skip_default_enabled_yaml_check
worker.deduplicate(:until_executing, feature_flag: :my_feature_flag)
end
context 'when the FF is enabled' do
before do
stub_feature_flags(my_feature_flag: true)
end
it { is_expected.to eq(true) }
end
context 'when the FF is disabled' do
before do
stub_feature_flags(my_feature_flag: false)
end
it { is_expected.to eq(false) }
end
end
end
end

View file

@ -9,12 +9,17 @@ RSpec.describe StuckCiJobsWorker do
let!(:job) { create :ci_build, runner: runner }
let(:worker_lease_key) { StuckCiJobsWorker::EXCLUSIVE_LEASE_KEY }
let(:worker_lease_uuid) { SecureRandom.uuid }
let(:created_at) { }
let(:updated_at) { }
subject(:worker) { described_class.new }
before do
stub_exclusive_lease(worker_lease_key, worker_lease_uuid)
job.update!(status: status, updated_at: updated_at)
job_attributes = { status: status }
job_attributes[:created_at] = created_at if created_at
job_attributes[:updated_at] = updated_at if updated_at
job.update!(job_attributes)
end
shared_examples 'job is dropped' do
@ -63,22 +68,70 @@ RSpec.describe StuckCiJobsWorker do
allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(false)
end
context 'when job was not updated for more than 1 day ago' do
let(:updated_at) { 2.days.ago }
context 'when job was updated_at more than 1 day ago' do
let(:updated_at) { 1.5.days.ago }
it_behaves_like 'job is dropped'
context 'when created_at is the same as updated_at' do
let(:created_at) { 1.5.days.ago }
it_behaves_like 'job is dropped'
end
context 'when created_at is before updated_at' do
let(:created_at) { 3.days.ago }
it_behaves_like 'job is dropped'
end
context 'when created_at is outside lookback window' do
let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
it_behaves_like 'job is unchanged'
end
end
context 'when job was updated in less than 1 day ago' do
context 'when job was updated less than 1 day ago' do
let(:updated_at) { 6.hours.ago }
it_behaves_like 'job is unchanged'
context 'when created_at is the same as updated_at' do
let(:created_at) { 1.5.days.ago }
it_behaves_like 'job is unchanged'
end
context 'when created_at is before updated_at' do
let(:created_at) { 3.days.ago }
it_behaves_like 'job is unchanged'
end
context 'when created_at is outside lookback window' do
let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
it_behaves_like 'job is unchanged'
end
end
context 'when job was not updated for more than 1 hour ago' do
context 'when job was updated more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
it_behaves_like 'job is unchanged'
context 'when created_at is the same as updated_at' do
let(:created_at) { 2.hours.ago }
it_behaves_like 'job is unchanged'
end
context 'when created_at is before updated_at' do
let(:created_at) { 3.days.ago }
it_behaves_like 'job is unchanged'
end
context 'when created_at is outside lookback window' do
let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
it_behaves_like 'job is unchanged'
end
end
end
@ -87,17 +140,48 @@ RSpec.describe StuckCiJobsWorker do
allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(true)
end
context 'when job was not updated for more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
context 'when job was updated_at more than 1 hour ago' do
let(:updated_at) { 1.5.hours.ago }
it_behaves_like 'job is dropped'
context 'when created_at is the same as updated_at' do
let(:created_at) { 1.5.hours.ago }
it_behaves_like 'job is dropped'
end
context 'when created_at is before updated_at' do
let(:created_at) { 3.days.ago }
it_behaves_like 'job is dropped'
end
context 'when created_at is outside lookback window' do
let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
it_behaves_like 'job is unchanged'
end
end
context 'when job was updated in less than 1
hour ago' do
context 'when job was updated in less than 1 hour ago' do
let(:updated_at) { 30.minutes.ago }
it_behaves_like 'job is unchanged'
context 'when created_at is the same as updated_at' do
let(:created_at) { 30.minutes.ago }
it_behaves_like 'job is unchanged'
end
context 'when created_at is before updated_at' do
let(:created_at) { 2.days.ago }
it_behaves_like 'job is unchanged'
end
context 'when created_at is outside lookback window' do
let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
it_behaves_like 'job is unchanged'
end
end
end
end
@ -105,7 +189,7 @@ RSpec.describe StuckCiJobsWorker do
context 'when job is running' do
let(:status) { 'running' }
context 'when job was not updated for more than 1 hour ago' do
context 'when job was updated_at more than an hour ago' do
let(:updated_at) { 2.hours.ago }
it_behaves_like 'job is dropped'
@ -123,7 +207,23 @@ RSpec.describe StuckCiJobsWorker do
let(:status) { status }
let(:updated_at) { 2.days.ago }
it_behaves_like 'job is unchanged'
context 'when created_at is the same as updated_at' do
let(:created_at) { 2.days.ago }
it_behaves_like 'job is unchanged'
end
context 'when created_at is before updated_at' do
let(:created_at) { 3.days.ago }
it_behaves_like 'job is unchanged'
end
context 'when created_at is outside lookback window' do
let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
it_behaves_like 'job is unchanged'
end
end
end