Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-01 15:13:55 +00:00
parent c9439a09c5
commit 66629d156e
105 changed files with 783 additions and 702 deletions

View File

@ -13,7 +13,7 @@ import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { sprintf, __, n__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import { ListType } from '../constants';
import eventHub from '../eventhub';

View File

@ -3,7 +3,7 @@ import { GlTable, GlButton, GlBadge, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
export default {

View File

@ -5,7 +5,7 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __, s__, sprintf } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import eventHub from '../event_hub';

View File

@ -17,7 +17,7 @@ import createFlash from '~/flash';
import { __, sprintf, n__ } from '~/locale';
import Tracking from '~/tracking';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import query from '../queries/details.query.graphql';
import {

View File

@ -302,6 +302,7 @@ export default {
unique: true,
defaultAuthors: [],
fetchAuthors: this.fetchUsers,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-author`,
preloadedAuthors,
},
{
@ -313,6 +314,7 @@ export default {
unique: !this.hasMultipleIssueAssigneesFeature,
defaultAuthors: DEFAULT_NONE_ANY,
fetchAuthors: this.fetchUsers,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
preloadedAuthors,
},
{
@ -321,6 +323,7 @@ export default {
icon: 'clock',
token: MilestoneToken,
fetchMilestones: this.fetchMilestones,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-milestone`,
},
{
type: TOKEN_TYPE_LABEL,
@ -329,6 +332,7 @@ export default {
token: LabelToken,
defaultLabels: DEFAULT_NONE_ANY,
fetchLabels: this.fetchLabels,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
},
{
type: TOKEN_TYPE_TYPE,
@ -350,6 +354,7 @@ export default {
icon: 'rocket',
token: ReleaseToken,
fetchReleases: this.fetchReleases,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-release`,
});
}
@ -361,6 +366,7 @@ export default {
token: EmojiToken,
unique: true,
fetchEmojis: this.fetchEmojis,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-my_reaction`,
});
tokens.push({
@ -446,7 +452,12 @@ export default {
query: searchLabelsQuery,
variables: { fullPath: this.fullPath, search, isProject: this.isProject },
})
.then(({ data }) => data[this.namespace]?.labels.nodes);
.then(({ data }) => data[this.namespace]?.labels.nodes)
.then((labels) =>
// TODO remove once we can search by title-only on the backend
// https://gitlab.com/gitlab-org/gitlab/-/issues/346353
labels.filter((label) => label.title.toLowerCase().includes(search.toLowerCase())),
);
},
fetchMilestones(search) {
return this.$apollo

View File

@ -2,7 +2,7 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { JOB_SIDEBAR } from '../constants';
import ArtifactsBlock from './artifacts_block.vue';
import CommitBlock from './commit_block.vue';

View File

@ -2,7 +2,7 @@
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.graphql';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import {
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_LOADING,

View File

@ -1,5 +1,5 @@
<script>
import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
export default {
components: {

View File

@ -1,6 +1,6 @@
<script>
import { capitalize, escape } from 'lodash';
import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
export default {
components: {

View File

@ -1,5 +1,6 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import API from '~/api';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import PipelineCharts from './pipeline_charts.vue';
@ -13,6 +14,9 @@ export default {
LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'),
ProjectQualitySummary: () => import('ee_component/project_quality_summary/app.vue'),
},
piplelinesTabEvent: 'p_analytics_ci_cd_pipelines',
deploymentFrequencyTabEvent: 'p_analytics_ci_cd_deployment_frequency',
leadTimeTabEvent: 'p_analytics_ci_cd_lead_time',
inject: {
shouldRenderDoraCharts: {
type: Boolean,
@ -60,20 +64,35 @@ export default {
updateHistory({ url: path, title: window.title });
}
},
trackTabClick(tab) {
API.trackRedisHllUserEvent(tab);
},
},
};
</script>
<template>
<div>
<gl-tabs v-if="charts.length > 1" :value="selectedTab" @input="onTabChange">
<gl-tab :title="__('Pipelines')">
<gl-tab
:title="__('Pipelines')"
data-testid="pipelines-tab"
@click="trackTabClick($options.piplelinesTabEvent)"
>
<pipeline-charts />
</gl-tab>
<template v-if="shouldRenderDoraCharts">
<gl-tab :title="__('Deployment frequency')">
<gl-tab
:title="__('Deployment frequency')"
data-testid="deployment-frequency-tab"
@click="trackTabClick($options.deploymentFrequencyTabEvent)"
>
<deployment-frequency-charts />
</gl-tab>
<gl-tab :title="__('Lead time')">
<gl-tab
:title="__('Lead time')"
data-testid="lead-time-tab"
@click="trackTabClick($options.leadTimeTabEvent)"
>
<lead-time-charts />
</gl-tab>
</template>

View File

@ -12,6 +12,8 @@ export const loadViewer = (type) => {
return () => import(/* webpackChunkName: 'blob_image_viewer' */ './image_viewer.vue');
case 'video':
return () => import(/* webpackChunkName: 'blob_video_viewer' */ './video_viewer.vue');
case 'pdf':
return () => import(/* webpackChunkName: 'blob_pdf_viewer' */ './pdf_viewer.vue');
default:
return null;
}
@ -36,5 +38,8 @@ export const viewerProps = (type, blob) => {
video: {
url: blob.rawPath,
},
pdf: {
url: blob.rawPath,
},
}[type];
};

View File

@ -0,0 +1,16 @@
<script>
import PdfViewer from '~/blob/pdf/pdf_viewer.vue';
export default {
components: { PdfViewer },
props: {
url: {
type: String,
required: true,
},
},
};
</script>
<template>
<pdf-viewer :pdf="url" />
</template>

View File

@ -116,7 +116,7 @@ export default {
statusTokenConfig,
{
...tagTokenConfig,
recentTokenValuesStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
recentSuggestionsStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
},
];
},

View File

@ -1,7 +1,7 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import RunnerName from '../runner_name.vue';
import RunnerTypeBadge from '../runner_type_badge.vue';

View File

@ -1,6 +1,6 @@
<script>
import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';

View File

@ -68,7 +68,6 @@ export default {
:config="config"
:suggestions-loading="loading"
:suggestions="tags"
:recent-suggestions-storage-key="config.recentTokenValuesStorageKey"
@fetch-suggestions="fetchTags"
v-on="$listeners"
>

View File

@ -266,7 +266,7 @@ export default {
</gl-popover>
</template>
<template #collapsed>
<div v-gl-tooltip :title="dateLabel" class="sidebar-collapsed-icon">
<div v-gl-tooltip.viewport.left :title="dateLabel" class="sidebar-collapsed-icon">
<gl-icon :size="16" name="calendar" />
<span class="collapse-truncated-title">{{ formattedDate }}</span>
</div>

View File

@ -1,7 +1,7 @@
<script>
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import {
MANUAL_DEPLOY,

View File

@ -13,7 +13,7 @@ import {
import { constructWebIDEPath } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import MrWidgetHowToMergeModal from './mr_widget_how_to_merge_modal.vue';
import MrWidgetIcon from './mr_widget_icon.vue';

View File

@ -15,7 +15,7 @@ import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mi
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { MT_MERGE_STRATEGY } from '../constants';
export default {

View File

@ -2,7 +2,7 @@
import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { isString, isEmpty } from 'lodash';
import { __, sprintf } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
export default {

View File

@ -3,7 +3,7 @@ import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitl
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
import { __, sprintf } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import DateTimePickerInput from './date_time_picker_input.vue';
import {
defaultTimeRanges,

View File

@ -87,7 +87,6 @@ export default {
:get-active-token-value="getActiveAuthor"
:default-suggestions="defaultAuthors"
:preloaded-suggestions="preloadedAuthors"
:recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
@fetch-suggestions="fetchAuthors"
v-on="$listeners"
>

View File

@ -4,6 +4,7 @@ import {
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlDropdownSectionHeader,
GlDropdownText,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
@ -17,6 +18,7 @@ export default {
GlFilteredSearchSuggestion,
GlDropdownDivider,
GlDropdownSectionHeader,
GlDropdownText,
GlLoadingIcon,
},
props: {
@ -57,11 +59,6 @@ export default {
required: false,
default: () => [],
},
recentSuggestionsStorageKey: {
type: String,
required: false,
default: '',
},
valueIdentifier: {
type: String,
required: false,
@ -76,14 +73,14 @@ export default {
data() {
return {
searchKey: '',
recentSuggestions: this.recentSuggestionsStorageKey
? getRecentlyUsedSuggestions(this.recentSuggestionsStorageKey)
recentSuggestions: this.config.recentSuggestionsStorageKey
? getRecentlyUsedSuggestions(this.config.recentSuggestionsStorageKey)
: [],
};
},
computed: {
isRecentSuggestionsEnabled() {
return Boolean(this.recentSuggestionsStorageKey);
return Boolean(this.config.recentSuggestionsStorageKey);
},
recentTokenIds() {
return this.recentSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
@ -119,6 +116,9 @@ export default {
showDefaultSuggestions() {
return this.availableDefaultSuggestions.length > 0;
},
showNoMatchesText() {
return this.searchKey && !this.availableSuggestions.length;
},
showRecentSuggestions() {
return (
this.isRecentSuggestionsEnabled && this.recentSuggestions.length > 0 && !this.searchKey
@ -167,7 +167,9 @@ export default {
this.$emit('fetch-suggestions', search);
}
}, DEBOUNCE_DELAY),
handleTokenValueSelected(activeTokenValue) {
handleTokenValueSelected(selectedValue) {
const activeTokenValue = this.getActiveTokenValue(this.suggestions, selectedValue);
// Make sure that;
// 1. Recently used values feature is enabled
// 2. User has actually selected a value
@ -177,7 +179,7 @@ export default {
activeTokenValue &&
!this.preloadedTokenIds.includes(activeTokenValue[this.valueIdentifier])
) {
setTokenValueToRecentlyUsed(this.recentSuggestionsStorageKey, activeTokenValue);
setTokenValueToRecentlyUsed(this.config.recentSuggestionsStorageKey, activeTokenValue);
}
},
},
@ -192,7 +194,7 @@ export default {
v-bind="$attrs"
v-on="$listeners"
@input="handleInput"
@select="handleTokenValueSelected(activeTokenValue)"
@select="handleTokenValueSelected"
>
<template #view-token="viewTokenProps">
<slot name="view-token" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
@ -222,6 +224,9 @@ export default {
:suggestions="preloadedSuggestions"
></slot>
<gl-loading-icon v-if="suggestionsLoading" size="sm" />
<gl-dropdown-text v-else-if="showNoMatchesText">
{{ __('No matches found') }}
</gl-dropdown-text>
<template v-else>
<slot name="suggestions-list" :suggestions="availableSuggestions"></slot>
</template>

View File

@ -104,7 +104,6 @@ export default {
:suggestions="labels"
:get-active-token-value="getActiveLabel"
:default-suggestions="defaultLabels"
:recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
@fetch-suggestions="fetchLabels"
v-on="$listeners"
>

View File

@ -1,6 +1,6 @@
<script>
import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
export default {
name: 'MetadataItem',

View File

@ -1,101 +0,0 @@
<script>
import { dateInWords, timeFor } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
export default {
name: 'SidebarCollapsedGroupedDatePicker',
components: {
collapsedCalendarIcon,
},
mixins: [timeagoMixin],
props: {
collapsed: {
type: Boolean,
required: false,
default: true,
},
minDate: {
type: Date,
required: false,
default: null,
},
maxDate: {
type: Date,
required: false,
default: null,
},
disableClickableIcons: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
hasMinAndMaxDates() {
return this.minDate && this.maxDate;
},
hasNoMinAndMaxDates() {
return !this.minDate && !this.maxDate;
},
showMinDateBlock() {
return this.minDate || this.hasNoMinAndMaxDates;
},
showFromText() {
return !this.maxDate && this.minDate;
},
iconClass() {
const disabledClass = this.disableClickableIcons ? 'disabled' : '';
return `sidebar-collapsed-icon calendar-icon ${disabledClass}`;
},
},
methods: {
toggleSidebar() {
this.$emit('toggleCollapse');
},
dateText(dateType = 'min') {
const date = this[`${dateType}Date`];
const dateWords = dateInWords(date, true);
const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
return date ? parsedDateWords : __('None');
},
tooltipText(dateType = 'min') {
const defaultText = dateType === 'min' ? __('Start date') : __('Due date');
const date = this[`${dateType}Date`];
const timeAgo = dateType === 'min' ? this.timeFormatted(date) : timeFor(date);
const dateText = date ? [this.dateText(dateType), `(${timeAgo})`].join(' ') : '';
if (date) {
return [defaultText, dateText].join('<br />');
}
return __('Start and due date');
},
},
};
</script>
<template>
<div class="block sidebar-grouped-item gl-cursor-pointer" role="button" @click="toggleSidebar">
<collapsed-calendar-icon
v-if="showMinDateBlock"
:container-class="iconClass"
:tooltip-text="tooltipText('min')"
>
<span class="sidebar-collapsed-value">
<span v-if="showFromText">{{ __('From') }}</span> <span>{{ dateText('min') }}</span>
</span>
</collapsed-calendar-icon>
<div v-if="hasMinAndMaxDates" class="text-center sidebar-collapsed-divider">-</div>
<collapsed-calendar-icon
v-if="maxDate"
:container-class="iconClass"
:tooltip-text="tooltipText('max')"
>
<span class="sidebar-collapsed-value">
<span v-if="!minDate">{{ __('Until') }}</span> <span>{{ dateText('max') }}</span>
</span>
</collapsed-calendar-icon>
</div>
</template>

View File

@ -0,0 +1,88 @@
/* eslint-disable @gitlab/require-i18n-strings */
import TooltipOnTruncate from './tooltip_on_truncate.vue';
const defaultWidth = '250px';
export default {
component: TooltipOnTruncate,
title: 'vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue',
};
const createStory = ({ ...options }) => {
return (_, { argTypes }) => {
const comp = {
components: { TooltipOnTruncate },
props: Object.keys(argTypes),
template: `
<div class="gl-bg-blue-50" :style="{ width }">
<tooltip-on-truncate :title="title" :placement="placement" class="gl-display-block gl-text-truncate">
{{title}}
</tooltip-on-truncate>
</div>
`,
...options,
};
return comp;
};
};
export const Default = createStory();
Default.args = {
width: defaultWidth,
title: 'Hover on this text to see the content in a tooltip.',
};
export const NoOverflow = createStory();
NoOverflow.args = {
width: defaultWidth,
title: "Short text doesn't need a tooltip.",
};
export const Placement = createStory();
Placement.args = {
width: defaultWidth,
title: 'Use `placement="right"` to display this tooltip at the right.',
placement: 'right',
};
const TIMEOUT_S = 3;
export const LiveUpdates = createStory({
props: ['width', 'placement'],
data() {
return {
title: `(loading in ${TIMEOUT_S}s)`,
};
},
mounted() {
setTimeout(() => {
this.title = 'Content updated! The content is now overflowing so we use a tooltip!';
}, TIMEOUT_S * 1000);
},
});
LiveUpdates.args = {
width: defaultWidth,
};
LiveUpdates.argTypes = {
title: {
control: false,
},
};
export const TruncateTarget = createStory({
template: `
<div class="gl-bg-black" :style="{ width }">
<tooltip-on-truncate class="gl-display-flex" :truncate-target="truncateTarget" :title="title">
<div class="gl-m-5 gl-bg-blue-50 gl-text-truncate">
{{ title }}
</div>
</tooltip-on-truncate>
</div>
`,
});
TruncateTarget.args = {
width: defaultWidth,
truncateTarget: 'child',
title: 'Wrap in container and use `truncate-target="child"` prop.',
};

View File

@ -31,6 +31,7 @@ class Admin::PlanLimitsController < Admin::ApplicationController
params.require(:plan_limits).permit(%i[
plan_id
conan_max_file_size
helm_max_file_size
maven_max_file_size
npm_max_file_size
nuget_max_file_size

View File

@ -19,7 +19,7 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
feature_category :dependency_proxy
def manifest
result = DependencyProxy::FindOrCreateManifestService.new(group, image, tag, token).execute
result = DependencyProxy::FindCachedManifestService.new(group, image, tag, token).execute
if result[:status] == :success
if result[:manifest]

View File

@ -19,8 +19,13 @@ class Projects::PipelinesController < Projects::ApplicationController
around_action :allow_gitaly_ref_name_caching, only: [:index, :show]
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/345074
track_redis_hll_event :charts, name: 'p_analytics_pipelines'
track_redis_hll_event :charts, name: 'p_analytics_ci_cd_pipelines', if: -> { should_track_ci_cd_pipelines? }
track_redis_hll_event :charts, name: 'p_analytics_ci_cd_deployment_frequency', if: -> { should_track_ci_cd_deployment_frequency? }
track_redis_hll_event :charts, name: 'p_analytics_ci_cd_lead_time', if: -> { should_track_ci_cd_lead_time? }
wrap_parameters Ci::Pipeline
POLLING_INTERVAL = 10_000
@ -323,6 +328,18 @@ class Projects::PipelinesController < Projects::ApplicationController
e.record!
end
end
def should_track_ci_cd_pipelines?
params[:chart].blank? || params[:chart] == 'pipelines'
end
def should_track_ci_cd_deployment_frequency?
params[:chart] == 'deployment-frequency'
end
def should_track_ci_cd_lead_time?
params[:chart] == 'lead-time'
end
end
Projects::PipelinesController.prepend_mod_with('Projects::PipelinesController')

View File

@ -26,6 +26,12 @@ module Analytics
:project_id
end
def self.distinct_stages_within_hierarchy(group)
with_preloaded_labels
.where(project_id: group.all_projects.select(:id))
.select("DISTINCT ON(stage_event_hash_id) #{quoted_table_name}.*")
end
private
# Project should belong to a group when the stage has Label based events since only GroupLabels are allowed.

View File

@ -957,7 +957,7 @@ module Ci
.limit(100)
.pluck(:expanded_environment_name)
Environment.where(project: project, name: expanded_environment_names)
Environment.where(project: project, name: expanded_environment_names).with_deployments
else
environment_ids = self_and_descendants.joins(:deployments).select(:'deployments.environment_id')

View File

@ -59,9 +59,6 @@ class CommitStatus < Ci::ApplicationRecord
scope :with_pipeline, -> { joins(:pipeline) }
scope :updated_at_before, ->(date) { where('ci_builds.updated_at < ?', date) }
scope :created_at_before, ->(date) { where('ci_builds.created_at < ?', date) }
scope :updated_before, ->(lookback:, timeout:) {
where('(ci_builds.created_at BETWEEN ? AND ?) AND (ci_builds.updated_at BETWEEN ? AND ?)', lookback, timeout, lookback, timeout)
}
scope :scheduled_at_before, ->(date) {
where('ci_builds.scheduled_at IS NOT NULL AND ci_builds.scheduled_at < ?', date)
}

View File

@ -89,6 +89,7 @@ class Environment < ApplicationRecord
scope :for_project, -> (project) { where(project_id: project) }
scope :for_tier, -> (tier) { where(tier: tier).where.not(tier: nil) }
scope :with_deployments, -> { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id')) }
scope :with_deployment, -> (sha) { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)) }
scope :unfoldered, -> { where(environment_type: nil) }
scope :with_rank, -> do

View File

@ -62,6 +62,7 @@ class InstanceConfiguration
def plan_file_size_limits(plan)
{
conan: plan.actual_limits[:conan_max_file_size],
helm: plan.actual_limits[:helm_max_file_size],
maven: plan.actual_limits[:maven_max_file_size],
npm: plan.actual_limits[:npm_max_file_size],
nuget: plan.actual_limits[:nuget_max_file_size],

View File

@ -7,7 +7,6 @@ module Ci
BUILD_PENDING_OUTDATED_TIMEOUT = 1.day
BUILD_PENDING_STUCK_TIMEOUT = 1.hour
BUILD_LOOKBACK = 5.days
def execute
Gitlab::AppLogger.info "#{self.class}: Cleaning pending timed-out builds"
@ -30,11 +29,11 @@ module Ci
# because we want to force the query planner to use the
# `ci_builds_gitlab_monitor_metrics` index all the time.
def pending_builds(timeout)
if Feature.enabled?(:ci_new_query_for_pending_stuck_jobs)
Ci::Build.pending.created_at_before(timeout).updated_at_before(timeout).order(created_at: :asc, project_id: :asc)
else
Ci::Build.pending.updated_before(lookback: BUILD_LOOKBACK.ago, timeout: timeout)
end
Ci::Build
.pending
.created_at_before(timeout)
.updated_at_before(timeout)
.order(created_at: :asc, project_id: :asc)
end
# rubocop: enable CodeReuse/ActiveRecord
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module DependencyProxy
class FindOrCreateManifestService < DependencyProxy::BaseService
class FindCachedManifestService < DependencyProxy::BaseService
def initialize(group, image, tag, token)
@group = group
@image = image
@ -20,36 +20,13 @@ module DependencyProxy
return respond if cached_manifest_matches?(head_result)
if Feature.enabled?(:dependency_proxy_manifest_workhorse, @group, default_enabled: :yaml)
success(manifest: nil, from_cache: false)
else
pull_new_manifest
respond(from_cache: false)
end
success(manifest: nil, from_cache: false)
rescue Timeout::Error, *Gitlab::HTTP::HTTP_ERRORS
respond
end
private
def pull_new_manifest
DependencyProxy::PullManifestService.new(@image, @tag, @token).execute_with_manifest do |new_manifest|
params = {
file_name: @file_name,
content_type: new_manifest[:content_type],
digest: new_manifest[:digest],
file: new_manifest[:file],
size: new_manifest[:file].size
}
if @manifest
@manifest.update!(params)
else
@manifest = @group.dependency_proxy_manifests.create!(params)
end
end
end
def cached_manifest_matches?(head_result)
return false if head_result[:status] == :error

View File

@ -1,47 +0,0 @@
# frozen_string_literal: true
module DependencyProxy
class PullManifestService < DependencyProxy::BaseService
def initialize(image, tag, token)
@image = image
@tag = tag
@token = token
end
def execute_with_manifest
raise ArgumentError, 'Block must be provided' unless block_given?
response = Gitlab::HTTP.get(manifest_url, headers: auth_headers.merge(Accept: ::ContainerRegistry::Client::ACCEPTED_TYPES.join(',')))
if response.success?
file = Tempfile.new
begin
file.write(response.body)
file.flush
yield(
success(
file: file,
digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER],
content_type: response.headers['content-type']
)
)
ensure
file.close
file.unlink
end
else
yield(error(response.body, response.code))
end
rescue Timeout::Error => exception
error(exception.message, 599)
end
private
def manifest_url
registry.manifest_url(@image, @tag)
end
end
end

View File

@ -16,7 +16,7 @@ module LooseForeignKeys
def execute
result = connection.execute(build_query)
{ affected_rows: result.cmd_tuples, table: loose_foreign_key_definition.to_table }
{ affected_rows: result.cmd_tuples, table: loose_foreign_key_definition.from_table }
end
def async_delete?
@ -48,15 +48,15 @@ module LooseForeignKeys
end
def arel_table
@arel_table ||= Arel::Table.new(loose_foreign_key_definition.to_table)
@arel_table ||= Arel::Table.new(loose_foreign_key_definition.from_table)
end
def primary_keys
@primary_keys ||= connection.primary_keys(loose_foreign_key_definition.to_table).map { |key| arel_table[key] }
@primary_keys ||= connection.primary_keys(loose_foreign_key_definition.from_table).map { |key| arel_table[key] }
end
def quoted_table_name
@quoted_table_name ||= Arel.sql(connection.quote_table_name(loose_foreign_key_definition.to_table))
@quoted_table_name ||= Arel.sql(connection.quote_table_name(loose_foreign_key_definition.from_table))
end
def delete_query

View File

@ -32,6 +32,9 @@
.form-group
= f.label :conan_max_file_size, _('Maximum Conan package file size in bytes'), class: 'label-bold'
= f.number_field :conan_max_file_size, class: 'form-control gl-form-input'
.form-group
= f.label :helm_max_file_size, _('Maximum Helm chart file size in bytes'), class: 'label-bold'
= f.number_field :helm_max_file_size, class: 'form-control gl-form-input'
.form-group
= f.label :maven_max_file_size, _('Maximum Maven package file size in bytes'), class: 'label-bold'
= f.number_field :maven_max_file_size, class: 'form-control gl-form-input'

View File

@ -6,7 +6,7 @@
= hidden_field_tag 'new_parent_group_id'
%ul
- side_effects_link_start = '<a href="https://docs.gitlab.com/ee/user/project/index.html#redirects-when-changing-repository-paths" target="_blank">'.html_safe
- side_effects_link_start = '<a href="https://docs.gitlab.com/ee/user/project/repository/index.html#what-happens-when-a-repository-path-changes" target="_blank">'.html_safe
- warning_text = s_("GroupSettings|Be careful. Changing a group's parent can have unintended %{side_effects_link_start}side effects%{side_effects_link_end}.") % { side_effects_link_start: side_effects_link_start, side_effects_link_end: '</a>'.html_safe }
%li= warning_text.html_safe
%li= s_('GroupSettings|You can only transfer the group to a group you manage.')

View File

@ -22,6 +22,10 @@
%td= 'Conan'
- package_file_size_limits.each_value do |limits|
%td= instance_configuration_human_size_cell(limits[:conan])
%tr
%td= 'Helm'
- package_file_size_limits.each_value do |limits|
%td= instance_configuration_human_size_cell(limits[:helm])
%tr
%td= 'Maven'
- package_file_size_limits.each_value do |limits|

View File

@ -1,8 +0,0 @@
---
name: ci_new_query_for_pending_stuck_jobs
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68880
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339322
milestone: '14.3'
type: development
group: group::pipeline execution
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: dependency_proxy_manifest_workhorse
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73033
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344216
milestone: '14.4'
type: development
group: group::package
default_enabled: true

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334162
milestone: '14.1'
type: development
group: group::source code
default_enabled: false
default_enabled: true

View File

@ -13,29 +13,32 @@ data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- users_viewing_analytics_group_devops_adoption
- i_analytics_dev_ops_adoption
- i_analytics_dev_ops_score
- p_analytics_merge_request
- i_analytics_instance_statistics
- g_analytics_contribution
- g_analytics_insights
- g_analytics_issues
- g_analytics_productivity
- g_analytics_valuestream
- p_analytics_pipelines
- p_analytics_code_reviews
- p_analytics_valuestream
- p_analytics_insights
- p_analytics_issues
- p_analytics_repo
- i_analytics_cohorts
- users_viewing_analytics_group_devops_adoption
- i_analytics_dev_ops_adoption
- i_analytics_dev_ops_score
- p_analytics_merge_request
- i_analytics_instance_statistics
- g_analytics_contribution
- g_analytics_insights
- g_analytics_issues
- g_analytics_productivity
- g_analytics_valuestream
- p_analytics_pipelines
- p_analytics_ci_cd_pipelines
- p_analytics_ci_cd_deployment_frequency
- p_analytics_ci_cd_lead_time
- p_analytics_code_reviews
- p_analytics_valuestream
- p_analytics_insights
- p_analytics_issues
- p_analytics_repo
- i_analytics_cohorts
distribution:
- ce
- ee
- ce
- ee
tier:
- free
- premium
- ultimate
- free
- premium
- ultimate
performance_indicator_type: []
milestone: "<13.9"
milestone: '<13.9'

View File

@ -0,0 +1,26 @@
---
key_path: redis_hll_counters.analytics.p_analytics_ci_cd_pipelines_monthly
description: Count of unique visits to the project level CI CD Analytics pipelines tab
product_section: dev
product_stage: manage
product_group: group::optimize
product_category:
value_type: number
status: active
milestone: '14.6'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_analytics_ci_cd_pipelines

View File

@ -0,0 +1,26 @@
---
key_path: redis_hll_counters.analytics.p_analytics_ci_cd_deployment_frequency_monthly
description: Count of unique visits to the project level CI CD Analytics deployment frequency tab
product_section: dev
product_stage: manage
product_group: group::optimize
product_category:
value_type: number
status: active
milestone: '14.6'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_analytics_ci_cd_deployment_frequency

View File

@ -0,0 +1,26 @@
---
key_path: redis_hll_counters.analytics.p_analytics_ci_cd_lead_time_monthly
description: Count of unique visits to the project level CI CD Analytics lead time tab
product_section: dev
product_stage: manage
product_group: group::optimize
product_category:
value_type: number
status: active
milestone: '14.6'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_analytics_ci_cd_lead_time

View File

@ -13,29 +13,32 @@ data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- users_viewing_analytics_group_devops_adoption
- i_analytics_dev_ops_adoption
- i_analytics_dev_ops_score
- p_analytics_merge_request
- i_analytics_instance_statistics
- g_analytics_contribution
- g_analytics_insights
- g_analytics_issues
- g_analytics_productivity
- g_analytics_valuestream
- p_analytics_pipelines
- p_analytics_code_reviews
- p_analytics_valuestream
- p_analytics_insights
- p_analytics_issues
- p_analytics_repo
- i_analytics_cohorts
- users_viewing_analytics_group_devops_adoption
- i_analytics_dev_ops_adoption
- i_analytics_dev_ops_score
- p_analytics_merge_request
- i_analytics_instance_statistics
- g_analytics_contribution
- g_analytics_insights
- g_analytics_issues
- g_analytics_productivity
- g_analytics_valuestream
- p_analytics_pipelines
- p_analytics_ci_cd_pipelines
- p_analytics_ci_cd_deployment_frequency
- p_analytics_ci_cd_lead_time
- p_analytics_code_reviews
- p_analytics_valuestream
- p_analytics_insights
- p_analytics_issues
- p_analytics_repo
- i_analytics_cohorts
distribution:
- ce
- ee
- ce
- ee
tier:
- free
- premium
- ultimate
- free
- premium
- ultimate
performance_indicator_type: []
milestone: "<13.9"
milestone: '<13.9'

View File

@ -0,0 +1,26 @@
---
key_path: redis_hll_counters.analytics.p_analytics_ci_cd_pipelines_weekly
description: Count of unique visits to the project level CI CD Analytics pipelines tab
product_section: dev
product_stage: manage
product_group: group::optimize
product_category:
value_type: number
status: active
milestone: '14.6'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_analytics_ci_cd_pipelines

View File

@ -0,0 +1,26 @@
---
key_path: redis_hll_counters.analytics.p_analytics_ci_cd_deployment_frequency_weekly
description: Count of unique visits to the project level CI CD Analytics deployment frequency tab
product_section: dev
product_stage: manage
product_group: group::optimize
product_category:
value_type: number
status: active
milestone: '14.6'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_analytics_ci_cd_deployment_frequency

View File

@ -0,0 +1,26 @@
---
key_path: redis_hll_counters.analytics.p_analytics_ci_cd_lead_time_weekly
description: Count of unique visits to the project level CI CD Analytics lead time tab
product_section: dev
product_stage: manage
product_group: group::optimize
product_category:
value_type: number
status: active
milestone: '14.6'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_analytics_ci_cd_lead_time

View File

@ -0,0 +1,21 @@
---
data_category: optional
key_path: analytics_unique_visits.p_analytics_ci_cd_pipelines
description: Count of unique visits to the project level CI CD Analytics pipelines tab
product_section: dev
product_stage: manage
product_group: group::optimize
product_category:
value_type: number
status: active
milestone: '14.6'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187
time_frame: all
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
data_category: optional
key_path: analytics_unique_visits.p_analytics_ci_cd_deployment_frequency
description: Count of unique visits to the project level CI CD Analytics deployment frequency tab
product_section: dev
product_stage: manage
product_group: group::optimize
product_category:
value_type: number
status: active
milestone: '14.6'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187
time_frame: all
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
data_category: optional
key_path: analytics_unique_visits.p_analytics_ci_cd_lead_time
description: Count of unique visits to the project level CI CD Analytics lead time tab
product_section: dev
product_stage: manage
product_group: group::optimize
product_category:
value_type: number
status: active
milestone: '14.6'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75187
time_frame: all
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -848,16 +848,20 @@ More information can be found in the [Push event activities limit and bulk push
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218017) in GitLab 13.4.
On GitLab.com, the maximum file size for a package that's uploaded to the [GitLab Package Registry](../user/packages/package_registry/index.md) varies by format:
The default maximum file size for a package that's uploaded to the [GitLab Package Registry](../user/packages/package_registry/index.md) varies by format:
- Conan: 5 GB
- Conan: 3 GB
- Generic: 5 GB
- Maven: 5 GB
- npm: 5 GB
- NuGet: 5 GB
- PyPI: 5 GB
- Helm: 5 MB
- Maven: 3 GB
- npm: 500 MB
- NuGet: 500 MB
- PyPI: 3 GB
- Terraform: 1 GB
To set this limit for a self-managed installation, run the following in the
The [maximum file sizes on GitLab.com](../user/gitlab_com/index.md) might be different.
To set these limits for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby
@ -881,6 +885,9 @@ Plan.default.actual_limits.update!(pypi_max_file_size: 100.megabytes)
# For Debian Packages
Plan.default.actual_limits.update!(debian_max_file_size: 100.megabytes)
# For Helm Charts
Plan.default.actual_limits.update!(helm_max_file_size: 100.megabytes)
# For Generic Packages
Plan.default.actual_limits.update!(generic_packages_max_file_size: 100.megabytes)
```

View File

@ -62,28 +62,28 @@ following information:
- The data cleanup method (`async_delete` or `async_nullify`)
The YAML file is located at `lib/gitlab/database/gitlab_loose_foreign_keys.yml`. The file groups
foreign key definitions by the name of the parent table. The parent table can have multiple loose
foreign key definitions by the name of the child table. The child table can have multiple loose
foreign key definitions, therefore we store them as an array.
Example definition:
```yaml
projects:
- to_table: ci_pipelines
ci_pipelines:
- table: projects
column: project_id
on_delete: async_delete
```
If the `projects` key is already present in the YAML file, then a new entry can be added
If the `ci_pipelines` key is already present in the YAML file, then a new entry can be added
to the array:
```yaml
projects:
- to_table: ci_pipelines
ci_pipelines:
- table: projects
column: project_id
on_delete: async_delete
- to_table: another_table
column: project_id
- table: another_table
column: another_id
on_delete: :async_nullify
```

View File

@ -103,10 +103,14 @@ You can override this behavior by defining specific variables:
| Image Path | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` for branch pipelines. `$CI_REGISTRY_IMAGE` for tag pipelines. | `$CI_APPLICATION_REPOSITORY` |
| Image Tag | `$CI_COMMIT_SHA` for branch pipelines. `$CI_COMMIT_TAG` for tag pipelines. | `$CI_APPLICATION_TAG` |
These variables also affect Auto Build. If you don't want to build and push an image to
These variables also affect Auto Build and Auto Container Scanning. If you don't want to build and push an image to
`$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG`, consider
including only `Jobs/Deploy.gitlab-ci.yml`, or [disabling the `build` jobs](#disable-jobs).
If you use Auto Container Scanning and set a value for `$CI_APPLICATION_REPOSITORY`, then you should
also update `$CS_DEFAULT_BRANCH_IMAGE`. See [Setting the default branch image](../../user/application_security/container_scanning/index.md#setting-the-default-branch-image)
for more details.
Here is an example setup in your `.gitlab-ci.yml`:
```yaml

View File

@ -37,6 +37,7 @@ module API
optional :conan_max_file_size, type: Integer, desc: 'Maximum Conan package file size in bytes'
optional :generic_packages_max_file_size, type: Integer, desc: 'Maximum generic package file size in bytes'
optional :helm_max_file_size, type: Integer, desc: 'Maximum Helm chart file size in bytes'
optional :maven_max_file_size, type: Integer, desc: 'Maximum Maven package file size in bytes'
optional :npm_max_file_size, type: Integer, desc: 'Maximum NPM package file size in bytes'
optional :nuget_max_file_size, type: Integer, desc: 'Maximum NuGet package file size in bytes'

View File

@ -52,7 +52,7 @@ module API
# HTTP status codes to terminate the job on GitLab Runner:
# - 403
def authenticate_job!(require_running: true)
def authenticate_job!(require_running: true, heartbeat_runner: false)
job = current_job
# 404 is not returned here because we want to terminate the job if it's
@ -70,7 +70,17 @@ module API
job_forbidden!(job, 'Job is not running') unless job.running?
end
job.runner&.heartbeat(get_runner_ip)
# Only some requests (like updating the job or patching the trace) should trigger
# runner heartbeat. Operations like artifacts uploading are executed in context of
# the running job and in the job environment, which in many cases will cause the IP
# to be updated to not the expected value. And operations like artifacts downloads can
# be done even after the job is finished and from totally different runners - while
# they would then update the connection status of not the runner that they should.
# Runner requests done in context of job authentication should explicitly define when
# the heartbeat should be triggered.
if heartbeat_runner
job.runner&.heartbeat(get_runner_ip)
end
job
end

View File

@ -176,7 +176,7 @@ module API
optional :exit_code, type: Integer, desc: %q(Job's exit code)
end
put '/:id', feature_category: :continuous_integration do
job = authenticate_job!
job = authenticate_job!(heartbeat_runner: true)
Gitlab::Metrics.add_event(:update_build)
@ -203,7 +203,7 @@ module API
optional :token, type: String, desc: %q(Job's authentication token)
end
patch '/:id/trace', feature_category: :continuous_integration do
job = authenticate_job!
job = authenticate_job!(heartbeat_runner: true)
error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
content_range = request.headers['Content-Range']

View File

@ -5,6 +5,7 @@ module API
class PlanLimit < Grape::Entity
expose :conan_max_file_size
expose :generic_packages_max_file_size
expose :helm_max_file_size
expose :maven_max_file_size
expose :npm_max_file_size
expose :nuget_max_file_size

View File

@ -53,6 +53,10 @@ variables:
# KUBE_INGRESS_BASE_DOMAIN is the application deployment domain and should be set as a variable at the group or project level.
# KUBE_INGRESS_BASE_DOMAIN: domain.example.com
# Allows Container-Scanning to correctly correlate image names when using Jobs/Build.gitlab-ci.yml
CI_APPLICATION_TAG: $CI_COMMIT_SHA
CS_DEFAULT_BRANCH_IMAGE: $CI_REGISTRY_IMAGE/$CI_DEFAULT_BRANCH:$CI_APPLICATION_TAG
POSTGRES_USER: user
POSTGRES_PASSWORD: testing-password
POSTGRES_ENABLED: "true"

View File

@ -1,19 +1,20 @@
chat_names:
- to_table: ci_pipeline_chat_data
ci_pipeline_chat_data:
- table: chat_names
column: chat_name_id
on_delete: async_delete
ci_builds:
- to_table: dast_site_profiles_builds
dast_scanner_profiles_builds:
- table: ci_builds
column: ci_build_id
on_delete: async_delete
- to_table: dast_scanner_profiles_builds
dast_scanner_profiles_builds:
- table: ci_builds
column: ci_build_id
on_delete: async_delete
ci_pipelines:
- to_table: dast_profiles_pipelines
dast_profiles_pipelines:
- table: ci_pipelines
column: ci_pipeline_id
on_delete: async_delete
ci_runners:
- to_table: clusters_applications_runners
clusters_applications_runners:
- table: ci_runners
column: runner_id
on_delete: async_nullify

View File

@ -4,25 +4,25 @@ module Gitlab
module Database
module LooseForeignKeys
def self.definitions_by_table
@definitions_by_table ||= definitions.group_by(&:from_table).with_indifferent_access.freeze
@definitions_by_table ||= definitions.group_by(&:to_table).with_indifferent_access.freeze
end
def self.definitions
@definitions ||= loose_foreign_keys_yaml.flat_map do |parent_table_name, configs|
configs.map { |config| build_definition(parent_table_name, config) }
@definitions ||= loose_foreign_keys_yaml.flat_map do |child_table_name, configs|
configs.map { |config| build_definition(child_table_name, config) }
end.freeze
end
def self.build_definition(parent_table_name, config)
to_table = config.fetch('to_table')
def self.build_definition(child_table_name, config)
parent_table_name = config.fetch('table')
ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
child_table_name,
parent_table_name,
to_table,
{
column: config.fetch('column'),
on_delete: config.fetch('on_delete').to_sym,
gitlab_schema: GitlabSchema.table_schema(to_table)
gitlab_schema: GitlabSchema.table_schema(child_table_name)
}
)
end

View File

@ -2,11 +2,9 @@
module Gitlab
module Tracking
SNOWPLOW_NAMESPACE = 'gl'
class << self
def enabled?
snowplow_micro_enabled? || Gitlab::CurrentSettings.snowplow_enabled?
snowplow.enabled?
end
def event(category, action, label: nil, property: nil, value: nil, context: [], project: nil, user: nil, namespace: nil, **extra) # rubocop:disable Metrics/ParameterLists

View File

@ -8,6 +8,8 @@ module Gitlab
class Snowplow < Base
extend ::Gitlab::Utils::Override
SNOWPLOW_NAMESPACE = 'gl'
override :event
def event(category, action, label: nil, property: nil, value: nil, context: nil)
return unless enabled?
@ -19,7 +21,7 @@ module Gitlab
def options(group)
additional_features = Feature.enabled?(:additional_snowplow_tracking, group, type: :ops)
{
namespace: Gitlab::Tracking::SNOWPLOW_NAMESPACE,
namespace: SNOWPLOW_NAMESPACE,
hostname: hostname,
cookie_domain: cookie_domain,
app_id: app_id,
@ -28,16 +30,16 @@ module Gitlab
}.transform_keys! { |key| key.to_s.camelize(:lower).to_sym }
end
def enabled?
Gitlab::CurrentSettings.snowplow_enabled?
end
def hostname
Gitlab::CurrentSettings.snowplow_collector_hostname
end
private
def enabled?
Gitlab::Tracking.enabled?
end
def app_id
Gitlab::CurrentSettings.snowplow_app_id
end
@ -54,7 +56,7 @@ module Gitlab
@tracker ||= SnowplowTracker::Tracker.new(
emitter,
SnowplowTracker::Subject.new,
Gitlab::Tracking::SNOWPLOW_NAMESPACE,
SNOWPLOW_NAMESPACE,
app_id
)
end

View File

@ -18,6 +18,11 @@ module Gitlab
).transform_keys! { |key| key.to_s.camelize(:lower).to_sym }
end
override :enabled?
def enabled?
true
end
override :hostname
def hostname
"#{uri.host}:#{uri.port}"

View File

@ -66,3 +66,15 @@
category: analytics
redis_slot: analytics
aggregation: weekly
- name: p_analytics_ci_cd_pipelines
category: analytics
redis_slot: analytics
aggregation: weekly
- name: p_analytics_ci_cd_deployment_frequency
category: analytics
redis_slot: analytics
aggregation: weekly
- name: p_analytics_ci_cd_lead_time
category: analytics
redis_slot: analytics
aggregation: weekly

View File

@ -5469,6 +5469,9 @@ msgstr ""
msgid "Billing|Users occupying seats in"
msgstr ""
msgid "Billing|View pending approvals"
msgstr ""
msgid "Billing|You are about to remove user %{username} from your subscription. If you continue, the user will be removed from the %{namespace} group and all its subgroups and projects. This action can't be undone."
msgstr ""
@ -13483,9 +13486,6 @@ msgstr ""
msgid "Epics|Enter a title for your epic"
msgstr ""
msgid "Epics|How can I solve this?"
msgstr ""
msgid "Epics|Leave empty to inherit from milestone dates"
msgstr ""
@ -13534,9 +13534,6 @@ msgstr ""
msgid "Epics|Something went wrong while removing issue from epic."
msgstr ""
msgid "Epics|These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely."
msgstr ""
msgid "Epics|This epic and any containing child epics are confidential and should only be visible to team members with at least Reporter access."
msgstr ""
@ -14997,15 +14994,6 @@ msgstr ""
msgid "Fixed burndown chart"
msgstr ""
msgid "Fixed date"
msgstr ""
msgid "Fixed due date"
msgstr ""
msgid "Fixed start date"
msgstr ""
msgid "Fixed:"
msgstr ""
@ -21412,6 +21400,9 @@ msgstr ""
msgid "Maximum Conan package file size in bytes"
msgstr ""
msgid "Maximum Helm chart file size in bytes"
msgstr ""
msgid "Maximum Maven package file size in bytes"
msgstr ""
@ -33059,9 +33050,6 @@ msgstr ""
msgid "Start a review"
msgstr ""
msgid "Start and due date"
msgstr ""
msgid "Start by choosing a group to start exploring the merge requests in that group. You can then proceed to filter by projects, labels, milestones and authors."
msgstr ""
@ -35453,12 +35441,6 @@ msgstr ""
msgid "This credential has expired"
msgstr ""
msgid "This date is after the due date, so this epic won't appear in the roadmap."
msgstr ""
msgid "This date is before the start date, so this epic won't appear in the roadmap."
msgstr ""
msgid "This device has already been registered with us."
msgstr ""
@ -37250,9 +37232,6 @@ msgstr ""
msgid "Unsupported todo type passed. Supported todo types are: %{todo_types}"
msgstr ""
msgid "Until"
msgstr ""
msgid "Until revoked, expired personal access tokens pose a security risk."
msgstr ""
@ -39877,6 +39856,11 @@ msgstr ""
msgid "You dont have access to Value Stream Analytics for this group"
msgstr ""
msgid "You have %{pendingMembersCount} pending member that needs approval."
msgid_plural "You have %{pendingMembersCount} pending members that need approval."
msgstr[0] ""
msgstr[1] ""
msgid "You have been granted %{access_level} access to the %{source_link} %{source_type}."
msgstr ""

View File

@ -368,6 +368,10 @@ module QA
parse_body(response)
end
def create_wiki_page(title:, content:)
api_post_to(api_wikis_path, title: title, content: content)
end
# Object comparison
#
# @param [QA::Resource::Project] other

View File

@ -170,7 +170,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
let(:pull_response) { { status: :success, manifest: manifest, from_cache: false } }
before do
allow_next_instance_of(DependencyProxy::FindOrCreateManifestService) do |instance|
allow_next_instance_of(DependencyProxy::FindCachedManifestService) do |instance|
allow(instance).to receive(:execute).and_return(pull_response)
end
end

View File

@ -745,9 +745,28 @@ RSpec.describe Projects::PipelinesController do
describe 'GET #charts' do
let(:pipeline) { create(:ci_pipeline, project: project) }
it_behaves_like 'tracking unique visits', :charts do
let(:request_params) { { namespace_id: project.namespace, project_id: project, id: pipeline.id } }
let(:target_id) { 'p_analytics_pipelines' }
[
{
chart_param: '',
event: 'p_analytics_ci_cd_pipelines'
},
{
chart_param: 'pipelines',
event: 'p_analytics_ci_cd_pipelines'
},
{
chart_param: 'deployment-frequency',
event: 'p_analytics_ci_cd_deployment_frequency'
},
{
chart_param: 'lead-time',
event: 'p_analytics_ci_cd_lead_time'
}
].each do |tab|
it_behaves_like 'tracking unique visits', :charts do
let(:request_params) { { namespace_id: project.namespace, project_id: project, id: pipeline.id, chart: tab[:chart_param] } }
let(:target_id) { ['p_analytics_pipelines', tab[:event]] }
end
end
end

View File

@ -29,7 +29,6 @@ RSpec.describe 'Database schema' do
ci_builds: %w[erased_by_id runner_id trigger_request_id user_id],
ci_namespace_monthly_usages: %w[namespace_id],
ci_pipelines: %w[user_id],
ci_pipeline_chat_data: %w[chat_name_id], # it uses the loose foreign key featue
ci_runner_projects: %w[runner_id],
ci_trigger_requests: %w[commit_id],
cluster_providers_aws: %w[security_group_id vpc_id access_key_id],
@ -102,6 +101,8 @@ RSpec.describe 'Database schema' do
let(:indexes) { connection.indexes(table) }
let(:columns) { connection.columns(table) }
let(:foreign_keys) { connection.foreign_keys(table) }
let(:loose_foreign_keys) { Gitlab::Database::LooseForeignKeys.definitions.group_by(&:from_table).fetch(table, []) }
let(:all_foreign_keys) { foreign_keys + loose_foreign_keys }
# take the first column in case we're using a composite primary key
let(:primary_key_column) { Array(connection.primary_key(table)).first }
@ -114,7 +115,7 @@ RSpec.describe 'Database schema' do
columns = columns.split(',') if columns.is_a?(String)
columns.first.chomp
end
foreign_keys_columns = foreign_keys.map(&:column)
foreign_keys_columns = all_foreign_keys.map(&:column)
# Add the primary key column to the list of indexed columns because
# postgres and mysql both automatically create an index on the primary
@ -129,7 +130,7 @@ RSpec.describe 'Database schema' do
context 'columns ending with _id' do
let(:column_names) { columns.map(&:name) }
let(:column_names_with_id) { column_names.select { |column_name| column_name.ends_with?('_id') } }
let(:foreign_keys_columns) { foreign_keys.map(&:column) }
let(:foreign_keys_columns) { all_foreign_keys.map(&:column).uniq } # we can have FK and loose FK present at the same time
let(:ignored_columns) { ignored_fk_columns(table) }
it 'do have the foreign keys' do

View File

@ -12,6 +12,7 @@ FactoryBot.define do
trait :with_package_file_sizes do
conan_max_file_size { 100 }
helm_max_file_size { 100 }
maven_max_file_size { 100 }
npm_max_file_size { 100 }
nuget_max_file_size { 100 }

View File

@ -1,11 +1,12 @@
import { GlTabs, GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import Component from '~/projects/pipelines/charts/components/app.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import API from '~/api';
jest.mock('~/lib/utils/url_utility');
@ -17,7 +18,7 @@ describe('ProjectsPipelinesChartsApp', () => {
let wrapper;
function createComponent(mountOptions = {}) {
wrapper = shallowMount(
wrapper = shallowMountExtended(
Component,
merge(
{},
@ -118,6 +119,23 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(updateHistory).not.toHaveBeenCalled();
});
describe('event tracking', () => {
it.each`
testId | event
${'pipelines-tab'} | ${'p_analytics_ci_cd_pipelines'}
${'deployment-frequency-tab'} | ${'p_analytics_ci_cd_deployment_frequency'}
${'lead-time-tab'} | ${'p_analytics_ci_cd_lead_time'}
`('tracks the $event event when clicked', ({ testId, event }) => {
jest.spyOn(API, 'trackRedisHllUserEvent');
expect(API.trackRedisHllUserEvent).not.toHaveBeenCalled();
wrapper.findByTestId(testId).vm.$emit('click');
expect(API.trackRedisHllUserEvent).toHaveBeenCalledWith(event);
});
});
});
describe('when provided with a query param', () => {

View File

@ -0,0 +1,22 @@
import { shallowMount } from '@vue/test-utils';
import Component from '~/repository/components/blob_viewers/pdf_viewer.vue';
import PdfViewer from '~/blob/pdf/pdf_viewer.vue';
describe('PDF Viewer', () => {
let wrapper;
const propsData = { url: 'some/pdf_blob.pdf' };
const createComponent = () => {
wrapper = shallowMount(Component, { propsData });
};
const findPDFViewer = () => wrapper.findComponent(PdfViewer);
it('renders a PDF Viewer component', () => {
createComponent();
expect(findPDFViewer().exists()).toBe(true);
expect(findPDFViewer().props('pdf')).toBe(propsData.url);
});
});

View File

@ -147,7 +147,7 @@ describe('AdminRunnersApp', () => {
}),
expect.objectContaining({
type: PARAM_KEY_TAG,
recentTokenValuesStorageKey: `${ADMIN_FILTERED_SEARCH_NAMESPACE}-recent-tags`,
recentSuggestionsStorageKey: `${ADMIN_FILTERED_SEARCH_NAMESPACE}-recent-tags`,
}),
]);
});

View File

@ -41,7 +41,7 @@ const mockTagTokenConfig = {
title: 'Tags',
type: 'tag',
token: TagToken,
recentTokenValuesStorageKey: mockStorageKey,
recentSuggestionsStorageKey: mockStorageKey,
operators: OPERATOR_IS_ONLY,
};

View File

@ -46,13 +46,13 @@ const defaultSlots = {
};
const mockProps = {
config: mockLabelToken,
config: { ...mockLabelToken, recentSuggestionsStorageKey: mockStorageKey },
value: { data: '' },
active: false,
suggestions: [],
suggestionsLoading: false,
defaultSuggestions: DEFAULT_NONE_ANY,
recentSuggestionsStorageKey: mockStorageKey,
getActiveTokenValue: (labels, data) => labels.find((label) => label.title === data),
};
function createComponent({
@ -152,30 +152,22 @@ describe('BaseToken', () => {
describe('methods', () => {
describe('handleTokenValueSelected', () => {
it('calls `setTokenValueToRecentlyUsed` when `recentSuggestionsStorageKey` is defined', () => {
const mockTokenValue = {
id: 1,
title: 'Foo',
};
const mockTokenValue = mockLabels[0];
wrapper.vm.handleTokenValueSelected(mockTokenValue);
it('calls `setTokenValueToRecentlyUsed` when `recentSuggestionsStorageKey` is defined', () => {
wrapper.vm.handleTokenValueSelected(mockTokenValue.title);
expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue);
});
it('does not add token from preloadedSuggestions', async () => {
const mockTokenValue = {
id: 1,
title: 'Foo',
};
wrapper.setProps({
preloadedSuggestions: [mockTokenValue],
});
await wrapper.vm.$nextTick();
wrapper.vm.handleTokenValueSelected(mockTokenValue);
wrapper.vm.handleTokenValueSelected(mockTokenValue.title);
expect(setTokenValueToRecentlyUsed).not.toHaveBeenCalled();
});
@ -190,7 +182,7 @@ describe('BaseToken', () => {
const glFilteredSearchToken = wrapperWithNoStubs.find(GlFilteredSearchToken);
expect(glFilteredSearchToken.exists()).toBe(true);
expect(glFilteredSearchToken.props('config')).toBe(mockLabelToken);
expect(glFilteredSearchToken.props('config')).toEqual(mockProps.config);
wrapperWithNoStubs.destroy();
});

View File

@ -2,7 +2,7 @@ import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/vue_shared/components/registry/metadata_item.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
describe('Metadata Item', () => {
let wrapper;

View File

@ -1,103 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import CollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import CollapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
describe('CollapsedGroupedDatePicker', () => {
let wrapper;
const defaultProps = {
showToggleSidebar: true,
};
const minDate = new Date('07/17/2016');
const maxDate = new Date('07/17/2017');
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(CollapsedGroupedDatePicker, {
propsData: { ...defaultProps, ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
const findCollapsedCalendarIcon = () => wrapper.findComponent(CollapsedCalendarIcon);
const findAllCollapsedCalendarIcons = () => wrapper.findAllComponents(CollapsedCalendarIcon);
describe('toggleCollapse events', () => {
it('should emit when collapsed-calendar-icon is clicked', () => {
createComponent();
findCollapsedCalendarIcon().trigger('click');
expect(wrapper.emitted('toggleCollapse')[0]).toBeDefined();
});
});
describe('minDate and maxDate', () => {
it('should render both collapsed-calendar-icon', () => {
createComponent({
props: {
minDate,
maxDate,
},
});
const icons = findAllCollapsedCalendarIcons();
expect(icons.length).toBe(2);
expect(icons.at(0).text()).toBe('Jul 17 2016');
expect(icons.at(1).text()).toBe('Jul 17 2017');
});
});
describe('minDate', () => {
it('should render minDate in collapsed-calendar-icon', () => {
createComponent({
props: {
minDate,
},
});
const icons = findAllCollapsedCalendarIcons();
expect(icons.length).toBe(1);
expect(icons.at(0).text()).toBe('From Jul 17 2016');
});
});
describe('maxDate', () => {
it('should render maxDate in collapsed-calendar-icon', () => {
createComponent({
props: {
maxDate,
},
});
const icons = findAllCollapsedCalendarIcons();
expect(icons.length).toBe(1);
expect(icons.at(0).text()).toBe('Until Jul 17 2017');
});
});
describe('no dates', () => {
beforeEach(() => {
createComponent();
});
it('should render None', () => {
const icons = findAllCollapsedCalendarIcons();
expect(icons.length).toBe(1);
expect(icons.at(0).text()).toBe('None');
});
it('should have tooltip as `Start and due date`', () => {
const icons = findAllCollapsedCalendarIcons();
expect(icons.at(0).props('tooltipText')).toBe('Start and due date');
});
});
});

View File

@ -1,7 +1,7 @@
import { mount, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
const MOCK_TITLE = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do';

View File

@ -11,6 +11,7 @@ RSpec.describe API::Entities::PlanLimit do
expect(subject).to include(
:conan_max_file_size,
:generic_packages_max_file_size,
:helm_max_file_size,
:maven_max_file_size,
:npm_max_file_size,
:nuget_max_file_size,

View File

@ -24,19 +24,19 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do
Gitlab::Database.schemas_to_base_models.fetch(parent_table_schema)
end
it 'all `from_table` tables are present' do
definitions.each do |definition|
base_models_for(definition.from_table).each do |model|
expect(model.connection).to be_table_exist(definition.from_table)
end
end
end
it 'all `to_table` tables are present' do
definitions.each do |definition|
base_models_for(definition.to_table).each do |model|
expect(model.connection).to be_table_exist(definition.to_table)
expect(model.connection).to be_column_exist(definition.to_table, definition.column)
end
end
end
it 'all `from_table` tables are present' do
definitions.each do |definition|
base_models_for(definition.from_table).each do |model|
expect(model.connection).to be_table_exist(definition.from_table)
expect(model.connection).to be_column_exist(definition.from_table, definition.column)
end
end
end

View File

@ -29,7 +29,7 @@ RSpec.describe Gitlab::Tracking::Destinations::Snowplow do
expect(SnowplowTracker::Tracker)
.to receive(:new)
.with(emitter, an_instance_of(SnowplowTracker::Subject), Gitlab::Tracking::SNOWPLOW_NAMESPACE, '_abc123_')
.with(emitter, an_instance_of(SnowplowTracker::Subject), described_class::SNOWPLOW_NAMESPACE, '_abc123_')
.and_return(tracker)
end

View File

@ -1210,6 +1210,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
'i_analytics_cohorts' => 123,
'i_analytics_dev_ops_score' => 123,
'i_analytics_instance_statistics' => 123,
'p_analytics_ci_cd_deployment_frequency' => 123,
'p_analytics_ci_cd_lead_time' => 123,
'p_analytics_ci_cd_pipelines' => 123,
'p_analytics_merge_request' => 123,
'i_analytics_dev_ops_adoption' => 123,
'users_viewing_analytics_group_devops_adoption' => 123,

View File

@ -29,4 +29,29 @@ RSpec.describe Analytics::CycleAnalytics::ProjectStage do
let(:default_params) { { project: project } }
end
end
describe '.distinct_stages_within_hierarchy' do
let_it_be(:top_level_group) { create(:group) }
let_it_be(:sub_group_1) { create(:group, parent: top_level_group) }
let_it_be(:sub_group_2) { create(:group, parent: sub_group_1) }
let_it_be(:project_1) { create(:project, group: sub_group_1) }
let_it_be(:project_2) { create(:project, group: sub_group_2) }
let_it_be(:project_3) { create(:project, group: top_level_group) }
let_it_be(:stage1) { create(:cycle_analytics_project_stage, project: project_1, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production) }
let_it_be(:stage2) { create(:cycle_analytics_project_stage, project: project_3, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production) }
let_it_be(:stage3) { create(:cycle_analytics_project_stage, project: project_1, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
let_it_be(:stage4) { create(:cycle_analytics_project_stage, project: project_3, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
subject(:distinct_start_and_end_event_identifiers) { described_class.distinct_stages_within_hierarchy(top_level_group).to_a.pluck(:start_event_identifier, :end_event_identifier) }
it 'returns distinct stages by start and end events (using stage_event_hash_id)' do
expect(distinct_start_and_end_event_identifiers).to match_array([
%w[issue_created issue_deployed_to_production],
%w[merge_request_created merge_request_merged]
])
end
end
end

View File

@ -3180,6 +3180,20 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
context 'when an associated environment does not have deployments' do
let_it_be(:pipeline) { create(:ci_pipeline, :created) }
let_it_be(:build) { create(:ci_build, :stop_review_app, pipeline: pipeline) }
let_it_be(:environment) { create(:environment, project: pipeline.project) }
before_all do
build.metadata.update!(expanded_environment_name: environment.name)
end
it 'does not return environments' do
expect(subject).to be_empty
end
end
context 'when pipeline is in extended family' do
let_it_be(:parent) { create(:ci_pipeline) }
let_it_be(:parent_build) { create(:ci_build, :with_deployment, environment: 'staging', pipeline: parent) }

View File

@ -97,32 +97,6 @@ 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 '.scheduled_at_before' do
let!(:never_scheduled) { create(:commit_status) }
let!(:stale_scheduled) { create(:commit_status, scheduled_at: 1.day.ago) }

View File

@ -144,6 +144,7 @@ RSpec.describe InstanceConfiguration do
create(:plan_limits,
plan: plan1,
conan_max_file_size: 1001,
helm_max_file_size: 1008,
maven_max_file_size: 1002,
npm_max_file_size: 1003,
nuget_max_file_size: 1004,
@ -154,6 +155,7 @@ RSpec.describe InstanceConfiguration do
create(:plan_limits,
plan: plan2,
conan_max_file_size: 1101,
helm_max_file_size: 1108,
maven_max_file_size: 1102,
npm_max_file_size: 1103,
nuget_max_file_size: 1104,
@ -166,8 +168,8 @@ RSpec.describe InstanceConfiguration do
it 'returns package file size limits' do
file_size_limits = subject.settings[:package_file_size_limits]
expect(file_size_limits[:Plan1]).to eq({ conan: 1001, maven: 1002, npm: 1003, nuget: 1004, pypi: 1005, terraform_module: 1006, generic: 1007 })
expect(file_size_limits[:Plan2]).to eq({ conan: 1101, maven: 1102, npm: 1103, nuget: 1104, pypi: 1105, terraform_module: 1106, generic: 1107 })
expect(file_size_limits[:Plan1]).to eq({ conan: 1001, helm: 1008, maven: 1002, npm: 1003, nuget: 1004, pypi: 1005, terraform_module: 1006, generic: 1007 })
expect(file_size_limits[:Plan2]).to eq({ conan: 1101, helm: 1108, maven: 1102, npm: 1103, nuget: 1104, pypi: 1105, terraform_module: 1106, generic: 1107 })
end
end

View File

@ -25,6 +25,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
expect(json_response).to be_an Hash
expect(json_response['conan_max_file_size']).to eq(Plan.default.actual_limits.conan_max_file_size)
expect(json_response['generic_packages_max_file_size']).to eq(Plan.default.actual_limits.generic_packages_max_file_size)
expect(json_response['helm_max_file_size']).to eq(Plan.default.actual_limits.helm_max_file_size)
expect(json_response['maven_max_file_size']).to eq(Plan.default.actual_limits.maven_max_file_size)
expect(json_response['npm_max_file_size']).to eq(Plan.default.actual_limits.npm_max_file_size)
expect(json_response['nuget_max_file_size']).to eq(Plan.default.actual_limits.nuget_max_file_size)
@ -45,6 +46,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
expect(json_response).to be_an Hash
expect(json_response['conan_max_file_size']).to eq(Plan.default.actual_limits.conan_max_file_size)
expect(json_response['generic_packages_max_file_size']).to eq(Plan.default.actual_limits.generic_packages_max_file_size)
expect(json_response['helm_max_file_size']).to eq(Plan.default.actual_limits.helm_max_file_size)
expect(json_response['maven_max_file_size']).to eq(Plan.default.actual_limits.maven_max_file_size)
expect(json_response['npm_max_file_size']).to eq(Plan.default.actual_limits.npm_max_file_size)
expect(json_response['nuget_max_file_size']).to eq(Plan.default.actual_limits.nuget_max_file_size)
@ -84,6 +86,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
'plan_name': 'default',
'conan_max_file_size': 10,
'generic_packages_max_file_size': 20,
'helm_max_file_size': 25,
'maven_max_file_size': 30,
'npm_max_file_size': 40,
'nuget_max_file_size': 50,
@ -95,6 +98,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
expect(json_response).to be_an Hash
expect(json_response['conan_max_file_size']).to eq(10)
expect(json_response['generic_packages_max_file_size']).to eq(20)
expect(json_response['helm_max_file_size']).to eq(25)
expect(json_response['maven_max_file_size']).to eq(30)
expect(json_response['npm_max_file_size']).to eq(40)
expect(json_response['nuget_max_file_size']).to eq(50)
@ -129,6 +133,7 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
'plan_name': 'default',
'conan_max_file_size': 'a',
'generic_packages_max_file_size': 'b',
'helm_max_file_size': 'h',
'maven_max_file_size': 'c',
'npm_max_file_size': 'd',
'nuget_max_file_size': 'e',
@ -140,8 +145,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits' do
expect(json_response['error']).to include(
'conan_max_file_size is invalid',
'generic_packages_max_file_size is invalid',
'helm_max_file_size is invalid',
'maven_max_file_size is invalid',
'generic_packages_max_file_size is invalid',
'npm_max_file_size is invalid',
'nuget_max_file_size is invalid',
'pypi_max_file_size is invalid',

View File

@ -131,8 +131,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let(:send_request) { subject }
end
it 'updates runner info' do
expect { subject }.to change { runner.reload.contacted_at }
it "doesn't update runner info" do
expect { subject }.not_to change { runner.reload.contacted_at }
end
shared_examples 'authorizes local file' do
@ -280,8 +280,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
end
it 'updates runner info' do
expect { upload_artifacts(file_upload, headers_with_token) }.to change { runner.reload.contacted_at }
it "doesn't update runner info" do
expect { upload_artifacts(file_upload, headers_with_token) }.not_to change { runner.reload.contacted_at }
end
context 'when the artifact is too large' do
@ -812,8 +812,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let(:send_request) { download_artifact }
end
it 'updates runner info' do
expect { download_artifact }.to change { runner.reload.contacted_at }
it "doesn't update runner info" do
expect { download_artifact }.not_to change { runner.reload.contacted_at }
end
context 'when job has artifacts' do

View File

@ -520,7 +520,7 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
let(:head_response) { { status: :success } }
before do
allow_next_instance_of(DependencyProxy::FindOrCreateManifestService) do |instance|
allow_next_instance_of(DependencyProxy::FindCachedManifestService) do |instance|
allow(instance).to receive(:execute).and_return(pull_response)
end
allow_next_instance_of(DependencyProxy::HeadManifestService) do |instance|

View File

@ -3,8 +3,12 @@
require 'spec_helper'
RSpec.describe Ci::StuckBuilds::DropPendingService do
let!(:runner) { create :ci_runner }
let!(:job) { create :ci_build, runner: runner }
let_it_be(:runner) { create(:ci_runner) }
let_it_be(:pipeline) { create(:ci_empty_pipeline) }
let_it_be_with_reload(:job) do
create(:ci_build, pipeline: pipeline, runner: runner)
end
let(:created_at) { }
let(:updated_at) { }
@ -14,6 +18,8 @@ RSpec.describe Ci::StuckBuilds::DropPendingService do
job_attributes = { status: status }
job_attributes[:created_at] = created_at if created_at
job_attributes[:updated_at] = updated_at if updated_at
job_attributes.compact!
job.update!(job_attributes)
end
@ -41,12 +47,6 @@ RSpec.describe Ci::StuckBuilds::DropPendingService do
it_behaves_like 'job is dropped with failure reason', 'stuck_or_timeout_failure'
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 less than 1 day ago' do
@ -63,12 +63,6 @@ RSpec.describe Ci::StuckBuilds::DropPendingService do
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 updated more than 1 hour ago' do
@ -85,12 +79,6 @@ RSpec.describe Ci::StuckBuilds::DropPendingService do
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
@ -115,12 +103,6 @@ RSpec.describe Ci::StuckBuilds::DropPendingService do
it_behaves_like 'job is dropped with failure reason', 'stuck_or_timeout_failure'
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
@ -137,12 +119,6 @@ RSpec.describe Ci::StuckBuilds::DropPendingService do
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
@ -179,12 +155,6 @@ RSpec.describe Ci::StuckBuilds::DropPendingService do
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

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DependencyProxy::FindOrCreateManifestService do
RSpec.describe DependencyProxy::FindCachedManifestService do
include DependencyProxyHelpers
let_it_be(:image) { 'alpine' }
@ -49,14 +49,6 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
end
it_behaves_like 'returning no manifest'
context 'with dependency_proxy_manifest_workhorse feature disabled' do
before do
stub_feature_flags(dependency_proxy_manifest_workhorse: false)
end
it_behaves_like 'downloading the manifest'
end
end
context 'failed head request' do
@ -66,14 +58,6 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
end
it_behaves_like 'returning no manifest'
context 'with dependency_proxy_manifest_workhorse feature disabled' do
before do
stub_feature_flags(dependency_proxy_manifest_workhorse: false)
end
it_behaves_like 'downloading the manifest'
end
end
end
@ -105,20 +89,6 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
end
it_behaves_like 'returning no manifest'
context 'with dependency_proxy_manifest_workhorse feature disabled' do
before do
stub_feature_flags(dependency_proxy_manifest_workhorse: false)
end
it 'downloads the new manifest and updates the existing record', :aggregate_failures do
expect(subject[:status]).to eq(:success)
expect(subject[:manifest]).to eq(dependency_proxy_manifest)
expect(subject[:manifest].content_type).to eq(content_type)
expect(subject[:manifest].digest).to eq(digest)
expect(subject[:from_cache]).to eq false
end
end
end
context 'when the cached manifest is expired' do
@ -129,14 +99,6 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
end
it_behaves_like 'returning no manifest'
context 'with dependency_proxy_manifest_workhorse feature disabled' do
before do
stub_feature_flags(dependency_proxy_manifest_workhorse: false)
end
it_behaves_like 'downloading the manifest'
end
end
context 'failed connection' do

View File

@ -1,77 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DependencyProxy::PullManifestService do
include DependencyProxyHelpers
let(:image) { 'alpine' }
let(:tag) { '3.9' }
let(:token) { Digest::SHA256.hexdigest('123') }
let(:manifest) { { foo: 'bar' }.to_json }
let(:digest) { '12345' }
let(:content_type) { 'foo' }
let(:headers) do
{ DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type }
end
subject { described_class.new(image, tag, token).execute_with_manifest(&method(:check_response)) }
context 'remote request is successful' do
before do
stub_manifest_download(image, tag, headers: headers)
end
it 'successfully returns the manifest' do
def check_response(response)
response[:file].rewind
expect(response[:status]).to eq(:success)
expect(response[:file].read).to eq(manifest)
expect(response[:digest]).to eq(digest)
expect(response[:content_type]).to eq(content_type)
end
subject
end
end
context 'remote request is not found' do
before do
stub_manifest_download(image, tag, status: 404, body: 'Not found')
end
it 'returns a 404 not found error' do
def check_response(response)
expect(response[:status]).to eq(:error)
expect(response[:http_status]).to eq(404)
expect(response[:message]).to eq('Not found')
end
subject
end
end
context 'net timeout exception' do
before do
manifest_link = DependencyProxy::Registry.manifest_url(image, tag)
stub_full_request(manifest_link).to_timeout
end
it 'returns a 599 error' do
def check_response(response)
expect(response[:status]).to eq(:error)
expect(response[:http_status]).to eq(599)
expect(response[:message]).to eq('execution expired')
end
subject
end
end
context 'no block is given' do
subject { described_class.new(image, tag, token).execute_with_manifest }
it { expect { subject }.to raise_error(ArgumentError, 'Block must be provided') }
end
end

Some files were not shown because too many files have changed in this diff Show More