Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
be6b9a238f
commit
44d49505c7
|
|
@ -1,10 +1,4 @@
|
|||
---
|
||||
# Cop supports --autocorrect.
|
||||
Lint/RedundantCopDisableDirective:
|
||||
# Offense count: 3
|
||||
# Temporarily disabled due to too many offenses
|
||||
Enabled: false
|
||||
Exclude:
|
||||
- 'ee/app/controllers/admin/gitlab_duo/configuration_controller.rb'
|
||||
- 'ee/app/presenters/ee/onboarding/status_presenter.rb'
|
||||
- 'ee/spec/helpers/admin/application_settings_helper_spec.rb'
|
||||
Details: grace period
|
||||
|
|
|
|||
|
|
@ -40,3 +40,10 @@ export const TRACKING_CATEGORIES = {
|
|||
tests: 'pipeline_tests_tab',
|
||||
listbox: 'pipeline_id_iid_listbox',
|
||||
};
|
||||
|
||||
// For pipeline polling
|
||||
export const PIPELINE_POLL_INTERVAL_DEFAULT = 1000 * 8;
|
||||
export const PIPELINE_POLL_INTERVAL_BACKOFF = 1.2;
|
||||
export const FOUR_MINUTES_IN_MS = 1000 * 60 * 4;
|
||||
|
||||
export const NETWORK_STATUS_READY = 7;
|
||||
|
|
|
|||
|
|
@ -66,7 +66,3 @@ export const DEFAULT_FIELDS = [
|
|||
columnClass: 'gl-w-1/5',
|
||||
},
|
||||
];
|
||||
|
||||
// Pipeline Mini Graph
|
||||
|
||||
export const PIPELINE_MINI_GRAPH_POLL_INTERVAL = 5000;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
|||
import { truncateSha } from '~/lib/utils/text_utility';
|
||||
import { s__ } from '~/locale';
|
||||
import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
|
||||
import getPipelineStatusQuery from '~/ci/pipeline_editor/graphql/queries/get_pipeline_status.query.graphql';
|
||||
import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
|
||||
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
|
@ -59,31 +60,17 @@ export default {
|
|||
context() {
|
||||
return getQueryHeaders(this.pipelineEtag);
|
||||
},
|
||||
query: getPipelineQuery,
|
||||
query() {
|
||||
return this.isUsingPipelineMiniGraphQueries ? getPipelineStatusQuery : getPipelineQuery;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.projectFullPath,
|
||||
sha: this.commitSha,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
const {
|
||||
id,
|
||||
iid,
|
||||
commit = {},
|
||||
detailedStatus = {},
|
||||
stages,
|
||||
status,
|
||||
} = data.project?.pipeline || {};
|
||||
|
||||
return {
|
||||
id,
|
||||
iid,
|
||||
commit,
|
||||
detailedStatus,
|
||||
stages,
|
||||
status,
|
||||
};
|
||||
update({ project }) {
|
||||
return project?.pipeline || {};
|
||||
},
|
||||
result(res) {
|
||||
if (res.data?.project?.pipeline) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
query getPipelineStatus($fullPath: ID!, $sha: String!) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
pipeline(sha: $sha) {
|
||||
id
|
||||
iid
|
||||
commit {
|
||||
id
|
||||
title
|
||||
webPath
|
||||
}
|
||||
detailedStatus {
|
||||
id
|
||||
detailsPath
|
||||
icon
|
||||
group
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['jobActionExecuted'],
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
|
|
@ -107,6 +108,7 @@ export default {
|
|||
reportToSentry(this.$options.name, error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.$emit('jobActionExecuted');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['jobActionExecuted'],
|
||||
computed: {
|
||||
hasJobAction() {
|
||||
return Boolean(this.status?.action?.id);
|
||||
|
|
@ -64,6 +65,7 @@ export default {
|
|||
:job-id="job.id"
|
||||
:job-action="status.action"
|
||||
:job-name="job.name"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import { normalizeDownstreamPipelines, normalizeStages } from '../utils';
|
||||
import { normalizeDownstreamPipelines, normalizeStages } from '../utils/data_utils';
|
||||
import DownstreamPipelines from '../downstream_pipelines.vue';
|
||||
import PipelineStages from '../pipeline_stages.vue';
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<script>
|
||||
import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import Visibility from 'visibilityjs';
|
||||
import { createAlert } from '~/alert';
|
||||
import { __ } from '~/locale';
|
||||
import { reportToSentry } from '~/ci/utils';
|
||||
import { getIncreasedPollInterval } from '~/ci/utils/polling_utils';
|
||||
import { NETWORK_STATUS_READY, PIPELINE_POLL_INTERVAL_DEFAULT } from '~/ci/constants';
|
||||
import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
|
||||
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
|
||||
import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/ci/pipeline_details/constants';
|
||||
import { getQueryHeaders } from '~/ci/pipeline_details/graph/utils';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import getPipelineMiniGraphQuery from './graphql/queries/get_pipeline_mini_graph.query.graphql';
|
||||
import DownstreamPipelines from './downstream_pipelines.vue';
|
||||
|
|
@ -47,15 +49,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
pollInterval: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: PIPELINE_MINI_GRAPH_POLL_INTERVAL,
|
||||
},
|
||||
},
|
||||
emits: ['miniGraphStageClick'],
|
||||
data() {
|
||||
return {
|
||||
pollInterval: PIPELINE_POLL_INTERVAL_DEFAULT,
|
||||
pipeline: {},
|
||||
};
|
||||
},
|
||||
|
|
@ -65,21 +63,29 @@ export default {
|
|||
return getQueryHeaders(this.pipelineEtag);
|
||||
},
|
||||
query: getPipelineMiniGraphQuery,
|
||||
pollInterval() {
|
||||
return this.pollInterval;
|
||||
},
|
||||
notifyOnNetworkStatusChange: true,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
iid: this.iid,
|
||||
};
|
||||
},
|
||||
pollInterval() {
|
||||
return this.pollInterval;
|
||||
},
|
||||
result({ networkStatus }) {
|
||||
// We need this for handling the reactive poll interval while also using frontend cache
|
||||
// a status of 7 = 'ready'
|
||||
if (networkStatus !== NETWORK_STATUS_READY) return;
|
||||
this.pollInterval = this.increasePollInterval();
|
||||
},
|
||||
update({ project }) {
|
||||
return project?.pipeline || {};
|
||||
},
|
||||
error(error) {
|
||||
createAlert({ message: this.$options.i18n.pipelineMiniGraphFetchError });
|
||||
reportToSentry(this.$options.name, error);
|
||||
this.pollInterval = 0;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -104,7 +110,28 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
toggleQueryPollingByVisibility(this.$apollo.queries.pipeline);
|
||||
Visibility.change(() => {
|
||||
this.handlePolling();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
/** Note: cannot use `toggleQueryPollingByVisibility` because interval is dynamic */
|
||||
handlePolling() {
|
||||
if (!Visibility.hidden()) {
|
||||
this.resetPollInterval();
|
||||
} else {
|
||||
this.pollInterval = 0;
|
||||
}
|
||||
},
|
||||
increasePollInterval() {
|
||||
return getIncreasedPollInterval(this.pollInterval);
|
||||
},
|
||||
onJobActionExecuted() {
|
||||
this.resetPollInterval();
|
||||
},
|
||||
resetPollInterval() {
|
||||
this.pollInterval = PIPELINE_POLL_INTERVAL_DEFAULT;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -133,6 +160,7 @@ export default {
|
|||
<pipeline-stages
|
||||
:is-merge-train="isMergeTrain"
|
||||
:stages="pipelineStages"
|
||||
@jobActionExecuted="onJobActionExecuted"
|
||||
@miniGraphStageClick="$emit('miniGraphStageClick')"
|
||||
/>
|
||||
<gl-icon
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { GlButton, GlDisclosureDropdown, GlLoadingIcon, GlTooltipDirective } fro
|
|||
import { createAlert } from '~/alert';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import { reportToSentry } from '~/ci/utils';
|
||||
import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/ci/pipeline_details/constants';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
|
||||
import { graphqlEtagStagePath } from '~/ci/pipeline_details/utils';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { PIPELINE_POLL_INTERVAL_DEFAULT } from '~/ci/constants';
|
||||
import getPipelineStageJobsQuery from './graphql/queries/get_pipeline_stage_jobs.query.graphql';
|
||||
import JobItem from './job_item.vue';
|
||||
|
||||
|
|
@ -36,22 +36,16 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
pollInterval: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: PIPELINE_MINI_GRAPH_POLL_INTERVAL,
|
||||
},
|
||||
stage: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['miniGraphStageClick'],
|
||||
emits: ['jobActionExecuted', 'miniGraphStageClick'],
|
||||
data() {
|
||||
return {
|
||||
isDropdownOpen: false,
|
||||
stageJobs: [],
|
||||
isPolling: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
|
@ -66,10 +60,10 @@ export default {
|
|||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.isPolling;
|
||||
return !this.isDropdownOpen;
|
||||
},
|
||||
result() {
|
||||
this.$apollo.queries.stageJobs.startPolling(this.pollInterval);
|
||||
pollInterval() {
|
||||
return this.pollInterval;
|
||||
},
|
||||
update(data) {
|
||||
return data?.ciPipelineStage?.jobs?.nodes || [];
|
||||
|
|
@ -93,6 +87,9 @@ export default {
|
|||
isLoading() {
|
||||
return this.$apollo.queries.stageJobs.loading;
|
||||
},
|
||||
pollInterval() {
|
||||
return this.isDropdownOpen ? PIPELINE_POLL_INTERVAL_DEFAULT : 0;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
toggleQueryPollingByVisibility(this.$apollo.queries.stageJobs);
|
||||
|
|
@ -100,11 +97,9 @@ export default {
|
|||
methods: {
|
||||
onHideDropdown() {
|
||||
this.isDropdownOpen = false;
|
||||
this.isPolling = false;
|
||||
},
|
||||
onShowDropdown() {
|
||||
this.isDropdownOpen = true;
|
||||
this.isPolling = true;
|
||||
|
||||
// used for tracking in the pipeline table
|
||||
this.$emit('miniGraphStageClick');
|
||||
|
|
@ -159,6 +154,7 @@ export default {
|
|||
:key="job.id"
|
||||
:dropdown-length="stageJobs.length"
|
||||
:job="job"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['miniGraphStageClick'],
|
||||
emits: ['jobActionExecuted', 'miniGraphStageClick'],
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
|
@ -32,6 +32,7 @@ export default {
|
|||
<pipeline-stage
|
||||
:stage="stage"
|
||||
:is-merge-train="isMergeTrain"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
@miniGraphStageClick="$emit('miniGraphStageClick')"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import { FOUR_MINUTES_IN_MS, PIPELINE_POLL_INTERVAL_BACKOFF } from '~/ci/constants';
|
||||
|
||||
export const getIncreasedPollInterval = (currentInterval) => {
|
||||
const intervalIncreased = PIPELINE_POLL_INTERVAL_BACKOFF * currentInterval;
|
||||
|
||||
return intervalIncreased >= FOUR_MINUTES_IN_MS ? FOUR_MINUTES_IN_MS : intervalIncreased;
|
||||
};
|
||||
|
|
@ -26,6 +26,8 @@ import HighlightWorker from '~/vue_shared/components/source_viewer/workers/highl
|
|||
import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
import { initFindFileShortcut } from '~/projects/behaviors';
|
||||
import initHeaderApp from '~/repository/init_header_app';
|
||||
import createRouter from '~/repository/router';
|
||||
|
||||
Vue.use(Vuex);
|
||||
Vue.use(VueApollo);
|
||||
|
|
@ -35,8 +37,6 @@ const apolloProvider = new VueApollo({
|
|||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
const router = new VueRouter({ mode: 'history' });
|
||||
|
||||
const viewBlobEl = document.querySelector('#js-view-blob-app');
|
||||
|
||||
const initRefSwitcher = () => {
|
||||
|
|
@ -84,6 +84,9 @@ if (viewBlobEl) {
|
|||
canDownloadCode,
|
||||
...dataset
|
||||
} = viewBlobEl.dataset;
|
||||
const router = createRouter(projectPath, originalBranch);
|
||||
|
||||
initHeaderApp({ router, isBlobView: true });
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
|
|
@ -215,7 +218,7 @@ if (treeHistoryLinkEl) {
|
|||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: treeHistoryLinkEl,
|
||||
router,
|
||||
router: new VueRouter({ mode: 'history' }),
|
||||
render(h) {
|
||||
const url = generateHistoryUrl(
|
||||
historyLink,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ if (document.querySelector('.blob-viewer')) {
|
|||
import(/* webpackChunkName: 'blobViewer' */ '~/blob/viewer')
|
||||
.then(({ BlobViewer }) => {
|
||||
new BlobViewer(); // eslint-disable-line no-new
|
||||
initHeaderApp(true);
|
||||
initHeaderApp({ isReadmeView: true });
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showBlobControls" class="gl-flex gl-items-baseline gl-gap-3">
|
||||
<div v-if="showBlobControls" class="gl-flex gl-flex-wrap gl-items-baseline gl-gap-3">
|
||||
<gl-button
|
||||
v-gl-tooltip.html="findFileTooltip"
|
||||
:aria-keyshortcuts="findFileShortcutKey"
|
||||
|
|
@ -181,7 +181,7 @@ export default {
|
|||
data-testid="history"
|
||||
:href="blobInfo.historyPath"
|
||||
:class="$options.buttonClassList"
|
||||
class="gl-block sm:gl-hidden"
|
||||
class="sm:gl-hidden"
|
||||
>
|
||||
{{ $options.i18n.history }}
|
||||
</gl-button>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlDisclosureDropdown, GlModalDirective } from '@gitlab/ui';
|
||||
import { GlDisclosureDropdown, GlModalDirective, GlLink } from '@gitlab/ui';
|
||||
import permissionsQuery from 'shared_queries/repository/permissions.query.graphql';
|
||||
import { joinPaths, escapeFileUrl, buildURLwithRefType } from '~/lib/utils/url_utility';
|
||||
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
|
||||
|
|
@ -19,6 +19,7 @@ export default {
|
|||
GlDisclosureDropdown,
|
||||
UploadBlobModal,
|
||||
NewDirectoryModal,
|
||||
GlLink,
|
||||
},
|
||||
apollo: {
|
||||
projectShortPath: {
|
||||
|
|
@ -48,6 +49,9 @@ export default {
|
|||
projectRootPath: {
|
||||
default: '',
|
||||
},
|
||||
isBlobView: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
currentPath: {
|
||||
|
|
@ -161,15 +165,27 @@ export default {
|
|||
return acc.concat({
|
||||
name,
|
||||
path,
|
||||
to: buildURLwithRefType({ path: to, refType: this.refType }),
|
||||
to: !this.isBlobView
|
||||
? buildURLwithRefType({ path: to, refType: this.refType })
|
||||
: null,
|
||||
url: buildURLwithRefType({
|
||||
path: joinPaths(this.projectPath, to),
|
||||
refType: this.refType,
|
||||
}),
|
||||
});
|
||||
},
|
||||
[
|
||||
{
|
||||
name: this.projectShortPath,
|
||||
path: '/',
|
||||
to: buildURLwithRefType({
|
||||
path: joinPaths('/-/tree', this.escapedRef),
|
||||
to: !this.isBlobView
|
||||
? buildURLwithRefType({
|
||||
path: joinPaths('/-/tree', this.escapedRef),
|
||||
refType: this.refType,
|
||||
})
|
||||
: null,
|
||||
url: buildURLwithRefType({
|
||||
path: joinPaths(this.projectPath, '/-/tree', this.escapedRef),
|
||||
refType: this.refType,
|
||||
}),
|
||||
},
|
||||
|
|
@ -293,9 +309,10 @@ export default {
|
|||
>
|
||||
<ol class="breadcrumb repo-breadcrumb">
|
||||
<li v-for="(link, i) in pathLinks" :key="i" class="breadcrumb-item">
|
||||
<router-link :to="link.to" :aria-current="isLast(i) ? 'page' : null">
|
||||
{{ link.name }}
|
||||
</router-link>
|
||||
<gl-link :to="link.to" :href="link.url" :aria-current="isLast(i) ? 'page' : null">
|
||||
<strong v-if="isLast(i)">{{ link.name }}</strong>
|
||||
<span v-else>{{ link.name }}</span>
|
||||
</gl-link>
|
||||
</li>
|
||||
<li v-if="renderAddToTreeDropdown" class="breadcrumb-item">
|
||||
<gl-disclosure-dropdown
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ export default function setupVueRepositoryList() {
|
|||
});
|
||||
};
|
||||
|
||||
initHeaderApp();
|
||||
initHeaderApp({ router });
|
||||
initCodeDropdown();
|
||||
initLastCommitApp();
|
||||
initBlobControlsApp();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,35 @@
|
|||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import apolloProvider from './graphql';
|
||||
import projectShortPathQuery from './queries/project_short_path.query.graphql';
|
||||
import projectPathQuery from './queries/project_path.query.graphql';
|
||||
import HeaderArea from './components/header_area.vue';
|
||||
import createRouter from './router';
|
||||
import refsQuery from './queries/ref.query.graphql';
|
||||
|
||||
export default function initHeaderApp(isReadmeView = false) {
|
||||
const initClientQueries = ({ projectPath, projectShortPath, ref, escapedRef }) => {
|
||||
// These queries are used in the breadcrumbs component as GraphQL client queries.
|
||||
|
||||
if (projectPath)
|
||||
apolloProvider.clients.defaultClient.cache.writeQuery({
|
||||
query: projectPathQuery,
|
||||
data: { projectPath },
|
||||
});
|
||||
|
||||
if (projectShortPath)
|
||||
apolloProvider.clients.defaultClient.cache.writeQuery({
|
||||
query: projectShortPathQuery,
|
||||
data: { projectShortPath },
|
||||
});
|
||||
|
||||
if (ref || escapedRef)
|
||||
apolloProvider.clients.defaultClient.cache.writeQuery({
|
||||
query: refsQuery,
|
||||
data: { ref, escapedRef },
|
||||
});
|
||||
};
|
||||
|
||||
export default function initHeaderApp({ router, isReadmeView = false, isBlobView = false }) {
|
||||
const headerEl = document.getElementById('js-repository-blob-header-app');
|
||||
if (headerEl) {
|
||||
const {
|
||||
|
|
@ -27,8 +52,11 @@ export default function initHeaderApp(isReadmeView = false) {
|
|||
projectRootPath,
|
||||
comparePath,
|
||||
projectPath,
|
||||
projectShortPath,
|
||||
} = headerEl.dataset;
|
||||
|
||||
initClientQueries({ projectPath, projectShortPath, ref, escapedRef });
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: headerEl,
|
||||
|
|
@ -47,11 +75,13 @@ export default function initHeaderApp(isReadmeView = false) {
|
|||
uploadPath: breadcrumbsUploadPath,
|
||||
newDirPath: breadcrumbsNewDirPath,
|
||||
projectRootPath,
|
||||
projectShortPath,
|
||||
comparePath,
|
||||
isReadmeView,
|
||||
isBlobView,
|
||||
},
|
||||
apolloProvider,
|
||||
router: createRouter(projectPath, escapedRef),
|
||||
router: router || createRouter(projectPath, escapedRef),
|
||||
render(h) {
|
||||
return h(HeaderArea, {
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import { n__, __ } from '~/locale';
|
|||
import { createAlert } from '~/alert';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
|
||||
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
|
||||
import { getQueryHeaders } from '~/ci/pipeline_details/graph/utils';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/ci/pipeline_details/constants';
|
||||
import MergeRequestStore from '../stores/mr_widget_store';
|
||||
import getMergePipeline from '../queries/get_merge_pipeline.query.graphql';
|
||||
import ArtifactsApp from './artifacts_list_app.vue';
|
||||
|
|
@ -64,7 +63,6 @@ export default {
|
|||
id: convertToGraphQLId(TYPENAME_CI_PIPELINE, this.mr.mergePipeline.id),
|
||||
};
|
||||
},
|
||||
pollInterval: PIPELINE_MINI_GRAPH_POLL_INTERVAL,
|
||||
update({ project }) {
|
||||
return project?.pipeline || {};
|
||||
},
|
||||
|
|
@ -123,11 +121,6 @@ export default {
|
|||
return this.isPostMerge ? this.mr?.mergePipeline?.details?.status?.text : this.mr.ciStatus;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.useMergePipelineQuery) {
|
||||
toggleQueryPollingByVisibility(this.$apollo.queries.mergePipeline);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -12,23 +12,25 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="gl-my-5 gl-flex gl-flex-wrap gl-items-center gl-justify-between gl-gap-x-5 gl-gap-y-2"
|
||||
>
|
||||
<h1 class="gl-heading-1 !gl-m-0" data-testid="page-heading">
|
||||
<slot name="heading"></slot>
|
||||
<template v-if="!$scopedSlots.heading">{{ heading }}</template>
|
||||
</h1>
|
||||
<div class="gl-my-5 gl-flex gl-flex-wrap gl-items-center gl-justify-between gl-gap-y-3">
|
||||
<div
|
||||
v-if="$scopedSlots.actions"
|
||||
class="gl-flex gl-items-center gl-gap-3"
|
||||
data-testid="page-heading-actions"
|
||||
class="gl-flex gl-w-full gl-flex-wrap gl-justify-between gl-gap-x-5 gl-gap-y-3 md:gl-flex-nowrap"
|
||||
>
|
||||
<slot name="actions"></slot>
|
||||
<h1 class="gl-heading-1 !gl-m-0" data-testid="page-heading">
|
||||
<slot name="heading"></slot>
|
||||
<template v-if="!$scopedSlots.heading">{{ heading }}</template>
|
||||
</h1>
|
||||
<div
|
||||
v-if="$scopedSlots.actions"
|
||||
class="page-heading-actions gl-flex gl-w-full gl-shrink-0 gl-flex-wrap gl-items-start gl-gap-3 sm:gl-w-auto md:gl-mt-1 lg:gl-mt-2"
|
||||
data-testid="page-heading-actions"
|
||||
>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="$scopedSlots.description"
|
||||
class="gl-mt-2 gl-w-full gl-text-subtle"
|
||||
class="gl-w-full gl-text-subtle"
|
||||
data-testid="page-heading-description"
|
||||
>
|
||||
<slot name="description"></slot>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ $body-bg: $gray-10;
|
|||
$input-bg: $white;
|
||||
$input-focus-bg: $white;
|
||||
$input-color: $gray-900;
|
||||
$input-group-addon-bg: $gray-900;
|
||||
|
||||
$card-cap-bg: $gray-50;
|
||||
|
||||
|
|
|
|||
|
|
@ -144,11 +144,6 @@ label {
|
|||
}
|
||||
|
||||
.input-group {
|
||||
.input-group-prepend,
|
||||
.input-group-append {
|
||||
background-color: $input-group-addon-bg;
|
||||
}
|
||||
|
||||
.input-group-prepend:not(:first-child):not(:last-child),
|
||||
.input-group-append:not(:first-child):not(:last-child) {
|
||||
border-left: 0;
|
||||
|
|
|
|||
|
|
@ -40,3 +40,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-heading-actions {
|
||||
> .gl-button,
|
||||
> .gl-disclosure-dropdown {
|
||||
@apply gl-w-full sm:gl-w-auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -333,7 +333,6 @@ $logs-p-color: #333;
|
|||
*/
|
||||
$input-height: 32px;
|
||||
$input-danger-bg: #f2dede;
|
||||
$input-group-addon-bg: $gray-10;
|
||||
$gl-field-focus-shadow: rgba(0, 0, 0, 0.075);
|
||||
$gl-field-focus-shadow-error: rgba($red-500, 0.6);
|
||||
$input-short-width: 200px;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
.gl-flex.gl-flex-wrap.gl-items-center.gl-justify-between.gl-gap-y-2.gl-gap-x-5.gl-my-5{ @options }
|
||||
%h1.gl-heading-1{ class: '!gl-m-0', data: { testid: 'page-heading' } }
|
||||
= heading || @heading
|
||||
.gl-flex.gl-flex-wrap.gl-items-center.gl-justify-between.gl-gap-y-3.gl-my-5{ @options }
|
||||
.gl-flex.gl-flex-wrap.md:gl-flex-nowrap.gl-justify-between.gl-gap-x-5.gl-gap-y-3.gl-w-full
|
||||
%h1.gl-heading-1{ class: '!gl-m-0', data: { testid: 'page-heading' } }
|
||||
= heading || @heading
|
||||
|
||||
- if actions?
|
||||
.gl-flex.gl-items-center.gl-gap-3{ data: { testid: 'page-heading-actions' } }
|
||||
= actions
|
||||
- if actions?
|
||||
.page-heading-actions.gl-self-start.md:gl-mt-1.lg:gl-mt-2.gl-flex.gl-flex-wrap.gl-items-start.gl-gap-3.gl-w-full.sm:gl-w-auto.gl-shrink-0{ data: { testid: 'page-heading-actions' } }
|
||||
= actions
|
||||
|
||||
- if description? || @description
|
||||
.gl-w-full.gl-mt-2.gl-text-subtle{ data: { testid: 'page-heading-description' } }
|
||||
.gl-w-full.gl-text-subtle{ data: { testid: 'page-heading-description' } }
|
||||
= description || @description
|
||||
|
|
|
|||
|
|
@ -302,6 +302,21 @@ module BlobHelper
|
|||
can_download_code: can?(current_user, :download_code, project).to_s
|
||||
}
|
||||
end
|
||||
|
||||
def vue_blob_header_app_data(project, blob, ref)
|
||||
{
|
||||
blob_path: blob.path,
|
||||
breadcrumbs: breadcrumb_data_attributes,
|
||||
escaped_ref: ActionDispatch::Journey::Router::Utils.escape_path(ref),
|
||||
history_link: project_commits_path(project, ref),
|
||||
project_id: project.id,
|
||||
project_root_path: project_path(project),
|
||||
project_path: project.full_path,
|
||||
project_short_path: project.path,
|
||||
ref_type: @ref_type.to_s,
|
||||
ref: ref
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
BlobHelper.prepend_mod_with('BlobHelper')
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ module PageLayoutHelper
|
|||
end
|
||||
|
||||
def full_content_class
|
||||
"#{container_class} #{@content_class}" # rubocop:disable Rails/HelperInstanceVariable
|
||||
"#{container_class} #{@content_class}"
|
||||
end
|
||||
|
||||
def page_itemtype(itemtype = nil)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class ApplicationSetting < ApplicationRecord
|
|||
# We won't add a prefix here as this token is deprecated and being
|
||||
# disabled in 17.0
|
||||
# https://docs.gitlab.com/ee/ci/runners/new_creation_workflow.html
|
||||
add_authentication_token_field :runners_registration_token, encrypted: :required # rubocop:disable Gitlab/TokenWithoutPrefix -- wontfix
|
||||
add_authentication_token_field :runners_registration_token, encrypted: :required
|
||||
add_authentication_token_field :health_check_access_token # rubocop:todo -- https://gitlab.com/gitlab-org/gitlab/-/issues/376751
|
||||
add_authentication_token_field :static_objects_external_storage_auth_token, encrypted: :required # rubocop:todo -- https://gitlab.com/gitlab-org/gitlab/-/issues/439292
|
||||
add_authentication_token_field :error_tracking_access_token, encrypted: :required # rubocop:todo -- https://gitlab.com/gitlab-org/gitlab/-/issues/439292
|
||||
|
|
@ -1007,7 +1007,7 @@ class ApplicationSetting < ApplicationRecord
|
|||
kroki_url, schemes: %w[http https],
|
||||
enforce_sanitization: true,
|
||||
deny_all_requests_except_allowed: Gitlab::CurrentSettings.deny_all_requests_except_allowed?,
|
||||
outbound_local_requests_allowlist: Gitlab::CurrentSettings.outbound_local_requests_whitelist)[0] # rubocop:disable Naming/InclusiveLanguage -- existing setting
|
||||
outbound_local_requests_allowlist: Gitlab::CurrentSettings.outbound_local_requests_whitelist)[0]
|
||||
rescue Gitlab::HTTP_V2::UrlBlocker::BlockedUrlError => e
|
||||
self.errors.add(
|
||||
:kroki_url,
|
||||
|
|
|
|||
|
|
@ -707,7 +707,7 @@ class MergeRequest < ApplicationRecord
|
|||
end
|
||||
|
||||
def self.use_locked_set?
|
||||
Feature.enabled?(:unstick_locked_merge_requests_redis) # rubocop: disable Gitlab/FeatureFlagWithoutActor -- no actor needed
|
||||
Feature.enabled?(:unstick_locked_merge_requests_redis)
|
||||
end
|
||||
|
||||
def committers(with_merge_commits: false, lazy: false, include_author_when_signed: false)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,9 @@ module Import
|
|||
# Default API max page size
|
||||
BATCH_SIZE = GitlabSchema.default_max_page_size
|
||||
|
||||
def initialize(bulk_import:, namespace:, options: {})
|
||||
def initialize(bulk_import:, namespace:)
|
||||
@bulk_import = bulk_import
|
||||
@namespace = namespace
|
||||
@minimum_batch_size = options[:minimum_batch_size] || BATCH_SIZE
|
||||
end
|
||||
|
||||
def execute
|
||||
|
|
@ -22,7 +21,7 @@ module Import
|
|||
|
||||
private
|
||||
|
||||
attr_reader :bulk_import, :namespace, :force, :minimum_batch_size
|
||||
attr_reader :bulk_import, :namespace, :force
|
||||
|
||||
def graphql_client
|
||||
@graphql_client ||= ::BulkImports::Clients::Graphql.new(
|
||||
|
|
@ -61,18 +60,20 @@ module Import
|
|||
has_next_page = true
|
||||
next_page = nil
|
||||
|
||||
next if ids.size < minimum_batch_size
|
||||
|
||||
# Make subsequent API calls if the API is configured with a max
|
||||
# page size smaller than the default value
|
||||
while has_next_page
|
||||
response = graphql_client.execute(parsed_query, ids: ids, after: next_page).original_hash
|
||||
|
||||
next_page = response.dig('page_info', 'next_page')
|
||||
has_next_page = response.dig('page_info', 'has_next_page')
|
||||
users = response.dig('data', 'users', 'nodes')
|
||||
data = response.dig('data', 'users')
|
||||
next_page = data.dig('pageInfo', 'next_page')
|
||||
has_next_page = data.dig('pageInfo', 'has_next_page')
|
||||
users = data['nodes']
|
||||
|
||||
next unless users.present?
|
||||
unless users.present?
|
||||
logger.error(message: 'No users present in response', response: response, user_ids: ids)
|
||||
next
|
||||
end
|
||||
|
||||
users.each do |user|
|
||||
yielder << user
|
||||
|
|
@ -84,13 +85,23 @@ module Import
|
|||
|
||||
def update_source_user(user_data)
|
||||
source_user_identifier = GlobalID.parse(user_data['id'])&.model_id
|
||||
return unless source_user_identifier
|
||||
|
||||
unless source_user_identifier
|
||||
logger.error(message: 'Missing source user identifier', user_data: user_data)
|
||||
return
|
||||
end
|
||||
|
||||
source_user = find_source_user(source_user_identifier)
|
||||
return unless source_user
|
||||
|
||||
params = { source_name: user_data['name'], source_username: user_data['username'] }.compact
|
||||
return if params.blank?
|
||||
|
||||
if params.blank?
|
||||
logger.error(message: 'Missing source user information', user_data: user_data, source_user_id: source_user.id,
|
||||
bulk_import_id: bulk_import.id,
|
||||
importer: Import::SOURCE_DIRECT_TRANSFER)
|
||||
return
|
||||
end
|
||||
|
||||
result = SourceUsers::UpdateService.new(source_user, params).execute
|
||||
|
||||
|
|
|
|||
|
|
@ -3,18 +3,23 @@
|
|||
module Packages
|
||||
module Npm
|
||||
class DeprecatePackageService < BaseService
|
||||
DeprecatedMetadatum = Struct.new(:package_id, :message, :attributes)
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
BATCH_SIZE = 50
|
||||
|
||||
def initialize(project, params)
|
||||
super(project, nil, params)
|
||||
@enqueue_metadata_cache_worker = false
|
||||
end
|
||||
|
||||
def execute
|
||||
enqueue_metadata_cache_worker = false
|
||||
|
||||
packages.select(:id, :version, :package_type).each_batch(of: BATCH_SIZE) do |relation|
|
||||
deprecated_metadata = handle_batch(relation)
|
||||
update_or_create_metadata(deprecated_metadata)
|
||||
attributes = relation.preload_npm_metadatum.filter_map { |package| metadatum_attributes(package) }
|
||||
next if attributes.empty?
|
||||
|
||||
::Packages::Npm::Metadatum.upsert_all(attributes)
|
||||
enqueue_metadata_cache_worker = true
|
||||
end
|
||||
|
||||
if enqueue_metadata_cache_worker
|
||||
|
|
@ -26,8 +31,6 @@ module Packages
|
|||
|
||||
private
|
||||
|
||||
attr_accessor :enqueue_metadata_cache_worker
|
||||
|
||||
def packages
|
||||
::Packages::Npm::PackageFinder.new(
|
||||
project: project,
|
||||
|
|
@ -38,65 +41,50 @@ module Packages
|
|||
).execute
|
||||
end
|
||||
|
||||
def handle_batch(relation)
|
||||
relation
|
||||
.preload_npm_metadatum
|
||||
.filter_map { |package| deprecate(package) }
|
||||
end
|
||||
|
||||
def deprecate(package)
|
||||
def metadatum_attributes(package)
|
||||
package_json = params.dig('versions', package.version)
|
||||
deprecation_message = package_json&.dig('deprecated')
|
||||
return if deprecation_message.nil?
|
||||
|
||||
npm_metadatum = package.npm_metadatum || package.build_npm_metadatum(package_json: package_json)
|
||||
return if npm_metadatum.persisted? && identical?(npm_metadatum.package_json['deprecated'], deprecation_message)
|
||||
return if npm_metadatum.persisted? && identical?(npm_metadatum.package_json['deprecated'])
|
||||
|
||||
DeprecatedMetadatum.new.tap do |deprecated|
|
||||
if npm_metadatum.persisted?
|
||||
deprecated.package_id = package.id
|
||||
deprecated.message = deprecation_message
|
||||
elsif npm_metadatum.valid?
|
||||
deprecated.attributes = npm_metadatum.attributes
|
||||
else
|
||||
Gitlab::ErrorTracking.track_exception(
|
||||
ActiveRecord::RecordInvalid.new(npm_metadatum),
|
||||
class: self.class.name,
|
||||
package_id: package.id
|
||||
)
|
||||
break # to return nil instead of an empty struct
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def identical?(package_json_deprecated, deprecation_message)
|
||||
package_json_deprecated == deprecation_message ||
|
||||
(package_json_deprecated.nil? && deprecation_message.empty?)
|
||||
end
|
||||
|
||||
def update_or_create_metadata(deprecated_metadata)
|
||||
return if deprecated_metadata.empty?
|
||||
|
||||
to_update, to_create = deprecated_metadata.partition(&:message)
|
||||
|
||||
if to_update.any?
|
||||
::Packages::Npm::Metadatum
|
||||
.package_id_in(to_update.map(&:package_id))
|
||||
.update_all(update_clause(to_update.first.message))
|
||||
end
|
||||
|
||||
::Packages::Npm::Metadatum.insert_all(to_create.map(&:attributes), returning: false) if to_create.any?
|
||||
|
||||
self.enqueue_metadata_cache_worker = true
|
||||
end
|
||||
|
||||
def update_clause(deprecation_message)
|
||||
if deprecation_message.empty?
|
||||
"package_json = package_json - 'deprecated'"
|
||||
if npm_metadatum.valid?
|
||||
{ package_id: package.id, package_json: update_package_json(npm_metadatum.package_json) }
|
||||
else
|
||||
["package_json = jsonb_set(package_json, '{deprecated}', ?)", deprecation_message.to_json]
|
||||
Gitlab::ErrorTracking.track_exception(
|
||||
ActiveRecord::RecordInvalid.new(npm_metadatum),
|
||||
class: self.class.name,
|
||||
package_id: package.id
|
||||
)
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def identical?(package_json_deprecated)
|
||||
package_json_deprecated == deprecation_message ||
|
||||
(package_json_deprecated.nil? && deprecation_message_empty?)
|
||||
end
|
||||
|
||||
def update_package_json(package_json)
|
||||
if deprecation_message_empty?
|
||||
package_json.delete('deprecated')
|
||||
else
|
||||
package_json['deprecated'] = deprecation_message
|
||||
end
|
||||
|
||||
package_json
|
||||
end
|
||||
|
||||
def deprecation_message_empty?
|
||||
deprecation_message.empty?
|
||||
end
|
||||
strong_memoize_attr :deprecation_message_empty?
|
||||
|
||||
def deprecation_message
|
||||
_, metadatum = params['versions'].first
|
||||
metadatum['deprecated']
|
||||
end
|
||||
strong_memoize_attr :deprecation_message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
= render "projects/blob/breadcrumb", blob: blob
|
||||
- project = @project.present(current_user: current_user)
|
||||
- ref = local_assigns[:ref] || @ref
|
||||
- expanded = params[:expanded].present?
|
||||
|
|
@ -7,6 +6,11 @@
|
|||
- if blob.rich_viewer && blob.extension != 'geojson'
|
||||
- add_page_startup_api_call local_assigns.fetch(:viewer_url) { url_for(safe_params.merge(viewer: blob.rich_viewer.type, format: :json)) }
|
||||
|
||||
- if params[:common_repository_blob_header_app] == 'true'
|
||||
#js-repository-blob-header-app{ data: vue_blob_header_app_data(project, blob, ref) }
|
||||
- else
|
||||
= render "projects/blob/breadcrumb", blob: blob
|
||||
|
||||
- if project.forked?
|
||||
#js-fork-info{ data: vue_fork_divergence_data(project, ref) }
|
||||
|
||||
|
|
|
|||
|
|
@ -12,21 +12,16 @@ module Import
|
|||
sidekiq_options max_retries_after_interruption: 20
|
||||
worker_has_external_dependencies!
|
||||
|
||||
PERFORM_DELAY = 1.minute
|
||||
PERFORM_DELAY = 5.minutes
|
||||
|
||||
def perform(bulk_import_id)
|
||||
bulk_import = BulkImport.find_by_id(bulk_import_id)
|
||||
return unless bulk_import
|
||||
|
||||
options = {}
|
||||
# When bulk_import is in a completed status, uses a minimum batch size
|
||||
# of 1 to ensure all source user are updated
|
||||
options[:minimum_batch_size] = 1 if bulk_import.completed?
|
||||
|
||||
portables = bulk_import.entities.filter_map { |entity| entity.group || entity.project }
|
||||
root_ancestors = portables.map(&:root_ancestor).uniq
|
||||
root_ancestors.each do |root_ancestor|
|
||||
UpdateSourceUsersService.new(namespace: root_ancestor, bulk_import: bulk_import, options: options).execute
|
||||
UpdateSourceUsersService.new(namespace: root_ancestor, bulk_import: bulk_import).execute
|
||||
end
|
||||
|
||||
# Stop re-enqueueing when the import is completed
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
internal_users.add_comment_for_internal_users_changes
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../tooling/danger/internal_users'
|
||||
|
||||
module Danger
|
||||
class InternalUsers < Plugin
|
||||
include Tooling::Danger::InternalUsers
|
||||
end
|
||||
end
|
||||
|
|
@ -24528,7 +24528,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="groupapprovalpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="groupapprovalpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="groupapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Group.autocompleteUsers`
|
||||
|
|
@ -25414,7 +25414,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="grouppipelineexecutionpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="grouppipelineexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="grouppipelineexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Group.projectComplianceStandardsAdherence`
|
||||
|
|
@ -25588,7 +25588,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="groupscanexecutionpoliciesactionscantypes"></a>`actionScanTypes` | [`[SecurityReportTypeEnum!]`](#securityreporttypeenum) | Filters policies by the action scan type. Only these scan types are supported: `dast`, `secret_detection`, `cluster_image_scanning`, `container_scanning`, `sast`, `sast_iac`, `dependency_scanning`. |
|
||||
| <a id="groupscanexecutionpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="groupscanexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="groupscanexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Group.scanResultPolicies`
|
||||
|
|
@ -25609,7 +25609,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="groupscanresultpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="groupscanresultpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="groupscanresultpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Group.securityPolicyProjectSuggestions`
|
||||
|
|
@ -29524,7 +29524,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="namespaceapprovalpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="namespaceapprovalpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="namespaceapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Namespace.complianceFrameworks`
|
||||
|
|
@ -29599,7 +29599,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="namespacepipelineexecutionpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="namespacepipelineexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="namespacepipelineexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Namespace.projects`
|
||||
|
|
@ -29661,7 +29661,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="namespacescanexecutionpoliciesactionscantypes"></a>`actionScanTypes` | [`[SecurityReportTypeEnum!]`](#securityreporttypeenum) | Filters policies by the action scan type. Only these scan types are supported: `dast`, `secret_detection`, `cluster_image_scanning`, `container_scanning`, `sast`, `sast_iac`, `dependency_scanning`. |
|
||||
| <a id="namespacescanexecutionpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="namespacescanexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="namespacescanexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Namespace.scanResultPolicies`
|
||||
|
|
@ -29682,7 +29682,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="namespacescanresultpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="namespacescanresultpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="namespacescanresultpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Namespace.vulnerabilityManagementPolicies`
|
||||
|
|
@ -31403,7 +31403,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="projectapprovalpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="projectapprovalpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="projectapprovalpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Project.autocompleteUsers`
|
||||
|
|
@ -32546,7 +32546,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="projectpipelineexecutionpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="projectpipelineexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="projectpipelineexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Project.pipelineSchedules`
|
||||
|
|
@ -32770,7 +32770,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="projectscanexecutionpoliciesactionscantypes"></a>`actionScanTypes` | [`[SecurityReportTypeEnum!]`](#securityreporttypeenum) | Filters policies by the action scan type. Only these scan types are supported: `dast`, `secret_detection`, `cluster_image_scanning`, `container_scanning`, `sast`, `sast_iac`, `dependency_scanning`. |
|
||||
| <a id="projectscanexecutionpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="projectscanexecutionpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="projectscanexecutionpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Project.scanResultPolicies`
|
||||
|
|
@ -32791,7 +32791,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="projectscanresultpoliciesincludeunscoped"></a>`includeUnscoped` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 17.3. **Status**: Experiment. Filter policies that are scoped to the project. |
|
||||
| <a id="projectscanresultpoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
|
||||
| <a id="projectscanresultpoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. |
|
||||
|
||||
##### `Project.securityExclusion`
|
||||
|
|
|
|||
|
|
@ -341,6 +341,11 @@ In the following example workflow, a developer creates an MR that touches Observ
|
|||
|
||||
In addition, the developer can manually add `pipeline:run-observability-e2e-tests-main-branch` to force the MR to run the `e2e:observability-backend-main-branch` job. This could be useful in case of changes to files that are not being tracked as related to observability.
|
||||
|
||||
There might be situations where the developer would need to skip those tests. To skip tests:
|
||||
|
||||
- For an MR, apply the label `pipeline:skip-observability-e2e-tests label`.
|
||||
- For a whole project, set the CI variable `SKIP_GITLAB_OBSERVABILITY_BACKEND_TRIGGER`.
|
||||
|
||||
### Review app jobs
|
||||
|
||||
The [`start-review-app-pipeline`](../testing_guide/review_apps.md) child pipeline deploys a Review App and runs
|
||||
|
|
|
|||
|
|
@ -9,21 +9,29 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
DETAILS:
|
||||
**Tier:** Ultimate
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
**Status:** Experiment
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/14878) as an [experiment](../../../policy/development_stages_support.md) in GitLab 17.5 [with a flag](../../feature_flags.md) named `secret_detection_project_level_exclusions`. Enabled by default.
|
||||
> - `secret_detection_project_level_exclusions` feature flag [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/499059) in GitLab 17.7.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/499059) in GitLab 17.7. Feature flag `secret_detection_project_level_exclusions` removed.
|
||||
|
||||
Secret detection may detect something that's not actually a secret. For example, if you use
|
||||
a fake value as a placeholder in your code, it might be detected and possibly blocked.
|
||||
|
||||
To avoid false positives, define a secret detection exclusion. A secret detection exclusion defines a path, a raw value or a rule from the [default ruleset](https://gitlab.com/gitlab-org/gitlab/-/blob/master/gems/gitlab-secret_detection/lib/gitleaks.toml) to exclude from secret detection. You can define multiples of each type of
|
||||
exclusion for a project.
|
||||
To avoid false positives you can exclude from secret detection:
|
||||
|
||||
In the [first iteration](https://gitlab.com/groups/gitlab-org/-/epics/14878) of this feature:
|
||||
- A path.
|
||||
- A raw value.
|
||||
- A rule from the [default ruleset](https://gitlab.com/gitlab-org/gitlab/-/blob/master/gems/gitlab-secret_detection/lib/gitleaks.toml)
|
||||
|
||||
You can define multiple exclusions for a project.
|
||||
|
||||
## Restrictions
|
||||
|
||||
The following restrictions apply:
|
||||
|
||||
- Exclusions can only be defined for each project.
|
||||
- Exclusions apply only to [secret push protection](secret_push_protection/index.md).
|
||||
- The maximum number of path-based exclusions per project is 10.
|
||||
- The maximum depth for path-based exclusions is 20.
|
||||
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
For an overview, see [Secret Detection Exclusions - Demonstration](https://www.youtube.com/watch?v=vh_Uh4_4aoc).
|
||||
|
|
@ -33,13 +41,10 @@ For an overview, see [Secret Detection Exclusions - Demonstration](https://www.y
|
|||
|
||||
Define an exclusion to avoid false positives from secret detection.
|
||||
|
||||
Note the following before defining an exclusion:
|
||||
|
||||
- The maximum number of path-based exclusions per project is 10.
|
||||
- The maximum depth for path-based exclusions is 20.
|
||||
- Glob patterns are interpreted with Ruby's [`File.fnmatch`](https://docs.ruby-lang.org/en/master/File.html#method-c-fnmatch)
|
||||
with the [flags](https://docs.ruby-lang.org/en/master/File/Constants.html#module-File::Constants-label-Filename+Globbing+Constants+-28File-3A-3AFNM_-2A-29)
|
||||
`File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB`.
|
||||
Path exclusions support glob patterns which are supported and interpreted with the Ruby method
|
||||
[`File.fnmatch`](https://docs.ruby-lang.org/en/master/File.html#method-c-fnmatch)
|
||||
with the [flags](https://docs.ruby-lang.org/en/master/File/Constants.html#module-File::Constants-label-Filename+Globbing+Constants+-28File-3A-3AFNM_-2A-29)
|
||||
`File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB`.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
|
|
@ -47,10 +52,10 @@ Prerequisites:
|
|||
|
||||
To define an exclusion:
|
||||
|
||||
1. In the left sidebar, select **Search or go to** and navigate to your project or group.
|
||||
1. In the left sidebar, select **Search or go to** and go to your project or group.
|
||||
1. Select **Secure > Security configuration**.
|
||||
1. Scroll down to **Secret push protection**.
|
||||
1. Turn on the **Secret push protection** toggle.
|
||||
1. Select **Configure Secret Detection** (**{settings}**).
|
||||
1. Select **Add exclusion** to open the exclusion form.
|
||||
1. Enter the details of the exclusion, then select **Add Exclusion**.
|
||||
1. Enter the details of the exclusion, then select **Add exclusion**.
|
||||
|
|
|
|||
|
|
@ -168,12 +168,10 @@ spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
|
|||
spec/frontend/ci/pipeline_mini_graph/pipeline_mini_graph_spec.js
|
||||
spec/frontend/ci/pipelines_page/components/pipeline_multi_actions_spec.js
|
||||
spec/frontend/ci/pipelines_page/components/pipelines_artifacts_spec.js
|
||||
spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
|
||||
spec/frontend/ci/runner/components/runner_details_spec.js
|
||||
spec/frontend/ci/runner/components/runner_form_fields_spec.js
|
||||
spec/frontend/ci/runner/components/runner_managers_table_spec.js
|
||||
spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
|
||||
spec/frontend/ci/runner/components/runner_tags_spec.js
|
||||
spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
|
||||
spec/frontend/ci_settings_pipeline_triggers/components/edit_trigger_modal_spec.js
|
||||
spec/frontend/clusters/agents/components/activity_history_item_spec.js
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ RSpec.describe Layouts::PageHeadingComponent, type: :component, feature_category
|
|||
c.with_description { description }
|
||||
end
|
||||
|
||||
expect(page).to have_css('.gl-w-full.gl-mt-2.gl-text-subtle', text: description)
|
||||
expect(page).to have_css('.gl-w-full.gl-text-subtle', text: description)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -574,7 +574,7 @@ FactoryBot.define do
|
|||
trait :pages_published do
|
||||
after(:create) do |project|
|
||||
project.mark_pages_onboarding_complete
|
||||
create(:pages_deployment, project: project) # rubocop: disable RSpec/FactoryBot/StrategyInCallback
|
||||
create(:pages_deployment, project: project)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import PipelineStatus, { i18n } from '~/ci/pipeline_editor/components/header/pipeline_status.vue';
|
||||
import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
|
||||
import getPipelineStatusQuery from '~/ci/pipeline_editor/graphql/queries/get_pipeline_status.query.graphql';
|
||||
import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
|
||||
import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
|
||||
import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
|
||||
|
|
@ -19,7 +20,10 @@ describe('Pipeline Status', () => {
|
|||
let mockPipelineQuery;
|
||||
|
||||
const createComponentWithApollo = ({ ciGraphqlPipelineMiniGraph = false } = {}) => {
|
||||
const handlers = [[getPipelineQuery, mockPipelineQuery]];
|
||||
const handlers = [
|
||||
[getPipelineQuery, mockPipelineQuery],
|
||||
[getPipelineStatusQuery, mockPipelineQuery],
|
||||
];
|
||||
mockApollo = createMockApollo(handlers);
|
||||
|
||||
mockApollo.clients.defaultClient.cache.writeQuery({
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ describe('JobActionButton', () => {
|
|||
await clickActionButton();
|
||||
|
||||
expect(handler).toHaveBeenCalledWith({ id: defaultProps.jobId });
|
||||
expect(wrapper.emitted('jobActionExecuted')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('displays the appropriate error message if mutation is not successful', async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import JobActionButton from '~/ci/pipeline_mini_graph/job_action_button.vue';
|
||||
import JobItem from '~/ci/pipeline_mini_graph/job_item.vue';
|
||||
import JobNameComponent from '~/ci/common/private/job_name_component.vue';
|
||||
|
||||
|
|
@ -22,27 +23,51 @@ describe('JobItem', () => {
|
|||
};
|
||||
|
||||
const findJobNameComponent = () => wrapper.findComponent(JobNameComponent);
|
||||
const findJobActionButton = () => wrapper.findComponent(JobActionButton);
|
||||
|
||||
describe('when mounted', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders the job name component', () => {
|
||||
expect(findJobNameComponent().exists()).toBe(true);
|
||||
});
|
||||
describe('job name', () => {
|
||||
it('renders the job name component', () => {
|
||||
expect(findJobNameComponent().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sends the necessary props to the job name component', () => {
|
||||
expect(findJobNameComponent().props()).toMatchObject({
|
||||
name: mockPipelineJob.name,
|
||||
status: mockPipelineJob.detailedStatus,
|
||||
it('sends the necessary props to the job name component', () => {
|
||||
expect(findJobNameComponent().props()).toMatchObject({
|
||||
name: mockPipelineJob.name,
|
||||
status: mockPipelineJob.detailedStatus,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the correct tooltip for the job item', () => {
|
||||
const tooltip = capitalizeFirstCharacter(mockPipelineJob.detailedStatus.tooltip);
|
||||
|
||||
expect(findJobNameComponent().attributes('title')).toBe(tooltip);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the correct tooltip for the job item', () => {
|
||||
const tooltip = capitalizeFirstCharacter(mockPipelineJob.detailedStatus.tooltip);
|
||||
describe('job action button', () => {
|
||||
describe('with a job action', () => {
|
||||
it('renders the job action button component', () => {
|
||||
expect(findJobActionButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
expect(findJobNameComponent().attributes('title')).toBe(tooltip);
|
||||
it('sends the necessary props to the job action button', () => {
|
||||
expect(findJobActionButton().props()).toMatchObject({
|
||||
jobId: mockPipelineJob.id,
|
||||
jobAction: mockPipelineJob.detailedStatus.action,
|
||||
jobName: mockPipelineJob.name,
|
||||
});
|
||||
});
|
||||
|
||||
it('emits jobActionExecuted', () => {
|
||||
findJobActionButton().vm.$emit('jobActionExecuted');
|
||||
expect(wrapper.emitted('jobActionExecuted')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Visibility from 'visibilityjs';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
|
@ -7,12 +8,12 @@ import { createAlert } from '~/alert';
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
|
||||
import { PIPELINE_POLL_INTERVAL_DEFAULT } from '~/ci/constants';
|
||||
import getPipelineMiniGraphQuery from '~/ci/pipeline_mini_graph/graphql/queries/get_pipeline_mini_graph.query.graphql';
|
||||
import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
|
||||
import DownstreamPipelines from '~/ci/pipeline_mini_graph/downstream_pipelines.vue';
|
||||
import PipelineStages from '~/ci/pipeline_mini_graph/pipeline_stages.vue';
|
||||
import * as sharedGraphQlUtils from '~/graphql_shared/utils';
|
||||
|
||||
import {
|
||||
pipelineMiniGraphFetchError,
|
||||
|
|
@ -23,6 +24,7 @@ import {
|
|||
|
||||
Vue.use(VueApollo);
|
||||
jest.mock('~/alert');
|
||||
jest.mock('visibilityjs');
|
||||
|
||||
describe('PipelineMiniGraph', () => {
|
||||
let wrapper;
|
||||
|
|
@ -54,9 +56,10 @@ describe('PipelineMiniGraph', () => {
|
|||
const findDownstream = () => wrapper.findComponent(DownstreamPipelines);
|
||||
const findStages = () => wrapper.findComponent(PipelineStages);
|
||||
|
||||
const getPollInterval = () => wrapper.vm.$apollo.queries.pipeline.pollInterval;
|
||||
|
||||
beforeEach(() => {
|
||||
pipelineMiniGraphResponse = jest.fn();
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPipelineMiniGraphQueryResponse);
|
||||
});
|
||||
|
||||
describe('when initial query is loading', () => {
|
||||
|
|
@ -75,6 +78,7 @@ describe('PipelineMiniGraph', () => {
|
|||
|
||||
describe('when query has loaded', () => {
|
||||
beforeEach(async () => {
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPipelineMiniGraphQueryResponse);
|
||||
await createComponent();
|
||||
});
|
||||
|
||||
|
|
@ -91,65 +95,71 @@ describe('PipelineMiniGraph', () => {
|
|||
|
||||
expect(pipelineMiniGraphResponse).toHaveBeenCalledWith({ iid, fullPath });
|
||||
});
|
||||
});
|
||||
|
||||
describe('stages', () => {
|
||||
it('renders stages', () => {
|
||||
expect(findStages().exists()).toBe(true);
|
||||
});
|
||||
describe('stages', () => {
|
||||
beforeEach(async () => {
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPipelineMiniGraphQueryResponse);
|
||||
await createComponent();
|
||||
});
|
||||
|
||||
it('sends the necessary props', () => {
|
||||
expect(findStages().props()).toMatchObject({
|
||||
isMergeTrain: expect.any(Boolean),
|
||||
stages: expect.any(Array),
|
||||
});
|
||||
it('renders stages', () => {
|
||||
expect(findStages().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sends the necessary props', () => {
|
||||
expect(findStages().props()).toMatchObject({
|
||||
isMergeTrain: expect.any(Boolean),
|
||||
stages: expect.any(Array),
|
||||
});
|
||||
});
|
||||
|
||||
describe('upstream', () => {
|
||||
it('renders upstream if available', () => {
|
||||
expect(findUpstream().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render upstream if not available', () => {
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPMGQueryNoUpstreamResponse);
|
||||
createComponent();
|
||||
expect(findUpstream().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downstream', () => {
|
||||
it('renders downstream if available', () => {
|
||||
expect(findDownstream().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sends the necessary props', () => {
|
||||
expect(findDownstream().props()).toMatchObject({
|
||||
pipelines: expect.any(Array),
|
||||
pipelinePath: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the latest downstream pipelines', () => {
|
||||
expect(findDownstream().props('pipelines')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('does not render downstream if not available', () => {
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPMGQueryNoDownstreamResponse);
|
||||
createComponent();
|
||||
expect(findDownstream().exists()).toBe(false);
|
||||
});
|
||||
it('emits miniGraphStageClick', () => {
|
||||
findStages().vm.$emit('miniGraphStageClick');
|
||||
expect(wrapper.emitted('miniGraphStageClick')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('polling', () => {
|
||||
it('toggles query polling with visibility check', async () => {
|
||||
jest.spyOn(sharedGraphQlUtils, 'toggleQueryPollingByVisibility');
|
||||
describe('upstream', () => {
|
||||
it('renders upstream if available', async () => {
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPipelineMiniGraphQueryResponse);
|
||||
await createComponent();
|
||||
expect(findUpstream().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render upstream if not available', () => {
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPMGQueryNoUpstreamResponse);
|
||||
createComponent();
|
||||
expect(findUpstream().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
describe('downstream', () => {
|
||||
it('renders downstream if available', async () => {
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPipelineMiniGraphQueryResponse);
|
||||
await createComponent();
|
||||
expect(findDownstream().exists()).toBe(true);
|
||||
});
|
||||
|
||||
expect(sharedGraphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledTimes(1);
|
||||
it('sends the necessary props', async () => {
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPipelineMiniGraphQueryResponse);
|
||||
await createComponent();
|
||||
expect(findDownstream().props()).toMatchObject({
|
||||
pipelines: expect.any(Array),
|
||||
pipelinePath: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the latest downstream pipelines', async () => {
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPipelineMiniGraphQueryResponse);
|
||||
await createComponent();
|
||||
expect(findDownstream().props('pipelines')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('does not render downstream if not available', () => {
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPMGQueryNoDownstreamResponse);
|
||||
createComponent();
|
||||
expect(findDownstream().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -164,4 +174,36 @@ describe('PipelineMiniGraph', () => {
|
|||
expect(createAlert).toHaveBeenCalledWith({ message: pipelineMiniGraphFetchError });
|
||||
});
|
||||
});
|
||||
|
||||
describe('polling', () => {
|
||||
beforeEach(async () => {
|
||||
Visibility.hidden.mockReturnValue(true);
|
||||
pipelineMiniGraphResponse.mockResolvedValue(mockPipelineMiniGraphQueryResponse);
|
||||
await createComponent();
|
||||
});
|
||||
|
||||
it('increases the poll interval after each query call', () => {
|
||||
expect(pipelineMiniGraphResponse).toHaveBeenCalled();
|
||||
expect(getPollInterval()).toBeGreaterThan(PIPELINE_POLL_INTERVAL_DEFAULT);
|
||||
});
|
||||
|
||||
it('handles visibility change for polling correctly', async () => {
|
||||
expect(getPollInterval()).toBeGreaterThan(PIPELINE_POLL_INTERVAL_DEFAULT);
|
||||
|
||||
Visibility.hidden.mockReturnValue(false);
|
||||
wrapper.vm.handlePolling();
|
||||
await nextTick();
|
||||
|
||||
expect(getPollInterval()).toBe(PIPELINE_POLL_INTERVAL_DEFAULT);
|
||||
});
|
||||
|
||||
it('resets poll interval on job action executed', async () => {
|
||||
expect(getPollInterval()).toBeGreaterThan(PIPELINE_POLL_INTERVAL_DEFAULT);
|
||||
|
||||
findStages().vm.$emit('jobActionExecuted');
|
||||
await nextTick();
|
||||
|
||||
expect(getPollInterval()).toBe(PIPELINE_POLL_INTERVAL_DEFAULT);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -103,6 +103,12 @@ describe('PipelineStage', () => {
|
|||
expect(findDropdownHeader().text()).toBe('Stage: build');
|
||||
});
|
||||
|
||||
it('emits miniGraphStageClick', async () => {
|
||||
await openStageDropdown();
|
||||
|
||||
expect(wrapper.emitted('miniGraphStageClick')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('has fired the stage query', async () => {
|
||||
await openStageDropdown();
|
||||
const { stage } = defaultProps;
|
||||
|
|
@ -135,7 +141,13 @@ describe('PipelineStage', () => {
|
|||
expect(findJobItems().exists()).toBe(true);
|
||||
expect(findJobItems()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('emits jobActionExecuted', () => {
|
||||
findJobItems().at(0).vm.$emit('jobActionExecuted');
|
||||
expect(wrapper.emitted('jobActionExecuted')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and query is not successful', () => {
|
||||
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import PipelineStages from '~/ci/pipeline_mini_graph/pipeline_stages.vue';
|
||||
import PipelineStage from '~/ci/pipeline_mini_graph/pipeline_stage.vue';
|
||||
|
||||
import { pipelineStage } from './mock_data';
|
||||
|
||||
describe('PipelineStages', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultProps = {
|
||||
stages: [pipelineStage],
|
||||
isMergeTrain: false,
|
||||
};
|
||||
|
||||
const createComponent = ({ props = {} } = {}) => {
|
||||
wrapper = shallowMount(PipelineStages, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findStages = () => wrapper.findAllComponents(PipelineStage);
|
||||
|
||||
describe('when mounted', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('sends the necessary props to the stage', () => {
|
||||
expect(findStages().at(0).props()).toMatchObject({
|
||||
stage: defaultProps.stages[0],
|
||||
isMergeTrain: defaultProps.isMergeTrain,
|
||||
});
|
||||
});
|
||||
|
||||
it('emits jobActionExecuted', () => {
|
||||
findStages().at(0).vm.$emit('jobActionExecuted');
|
||||
expect(wrapper.emitted('jobActionExecuted')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('emits miniGraphStageClick', () => {
|
||||
findStages().at(0).vm.$emit('miniGraphStageClick');
|
||||
expect(wrapper.emitted('miniGraphStageClick')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
import { normalizeDownstreamPipelines, normalizeStages } from '~/ci/pipeline_mini_graph/utils';
|
||||
import {
|
||||
normalizeDownstreamPipelines,
|
||||
normalizeStages,
|
||||
} from '~/ci/pipeline_mini_graph/utils/data_utils';
|
||||
|
||||
const graphqlDownstream = [
|
||||
{
|
||||
|
|
@ -60,7 +63,7 @@ const restStage = [
|
|||
},
|
||||
];
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('Data utils', () => {
|
||||
describe('stages', () => {
|
||||
it('Does not normalize GraphQL stages', () => {
|
||||
expect(normalizeStages(graphqlStage)).toEqual(graphqlStage);
|
||||
|
|
@ -32,6 +32,6 @@ describe('RunnerTags', () => {
|
|||
props: { tagList: null },
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toEqual('');
|
||||
expect(wrapper.text()).toEqual('');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import { FOUR_MINUTES_IN_MS } from '~/ci/constants';
|
||||
import { getIncreasedPollInterval } from '~/ci/utils/polling_utils';
|
||||
|
||||
describe('Polling utils', () => {
|
||||
describe('interval under max limit', () => {
|
||||
it('increases the interval', () => {
|
||||
expect(getIncreasedPollInterval(1000)).toBe(1000 * 1.2);
|
||||
expect(getIncreasedPollInterval(2000)).toBe(2000 * 1.2);
|
||||
expect(getIncreasedPollInterval(10000)).toBe(10000 * 1.2);
|
||||
expect(getIncreasedPollInterval(200000)).toBe(200000 * 1.2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interval over max limit', () => {
|
||||
it('returns max interval value', () => {
|
||||
expect(getIncreasedPollInterval(300000)).toBe(FOUR_MINUTES_IN_MS);
|
||||
expect(getIncreasedPollInterval(400000)).toBe(FOUR_MINUTES_IN_MS);
|
||||
expect(getIncreasedPollInterval(500000)).toBe(FOUR_MINUTES_IN_MS);
|
||||
expect(getIncreasedPollInterval(600000)).toBe(FOUR_MINUTES_IN_MS);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
|
||||
import { GlDisclosureDropdown, GlDisclosureDropdownGroup, GlLink } from '@gitlab/ui';
|
||||
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
|
||||
import Breadcrumbs from '~/repository/components/header_area/breadcrumbs.vue';
|
||||
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
|
||||
|
|
@ -57,6 +57,7 @@ describe('Repository breadcrumbs component', () => {
|
|||
apolloProvider,
|
||||
provide: {
|
||||
projectRootPath: TEST_PROJECT_PATH,
|
||||
isBlobView: extraProps.isBlobView,
|
||||
},
|
||||
propsData: {
|
||||
currentPath,
|
||||
|
|
@ -79,7 +80,7 @@ describe('Repository breadcrumbs component', () => {
|
|||
const findDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
|
||||
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
|
||||
const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal);
|
||||
const findRouterLink = () => wrapper.findAllComponents(RouterLinkStub);
|
||||
const findRouterLinks = () => wrapper.findAllComponents(GlLink);
|
||||
|
||||
beforeEach(() => {
|
||||
permissionsQuerySpy = jest.fn().mockResolvedValue(createPermissionsQueryResponse());
|
||||
|
|
@ -105,7 +106,7 @@ describe('Repository breadcrumbs component', () => {
|
|||
`('renders $linkCount links for path $path', ({ path, linkCount }) => {
|
||||
factory(path);
|
||||
|
||||
expect(findRouterLink().length).toEqual(linkCount);
|
||||
expect(findRouterLinks().length).toEqual(linkCount);
|
||||
});
|
||||
|
||||
it.each`
|
||||
|
|
@ -118,20 +119,20 @@ describe('Repository breadcrumbs component', () => {
|
|||
'links to the correct router path when routeName is $routeName',
|
||||
({ routeName, path, linkTo }) => {
|
||||
factory(path, {}, { name: routeName });
|
||||
expect(findRouterLink().at(3).props('to')).toEqual(linkTo);
|
||||
expect(findRouterLinks().at(3).attributes('to')).toEqual(linkTo);
|
||||
},
|
||||
);
|
||||
|
||||
it('escapes hash in directory path', () => {
|
||||
factory('app/assets/javascripts#');
|
||||
|
||||
expect(findRouterLink().at(3).props('to')).toEqual('/-/tree/app/assets/javascripts%23');
|
||||
expect(findRouterLinks().at(3).attributes('to')).toEqual('/-/tree/app/assets/javascripts%23');
|
||||
});
|
||||
|
||||
it('renders last link as active', () => {
|
||||
factory('app/assets');
|
||||
|
||||
expect(findRouterLink().at(2).attributes('aria-current')).toEqual('page');
|
||||
expect(findRouterLinks().at(2).attributes('aria-current')).toEqual('page');
|
||||
});
|
||||
|
||||
it('does not render add to tree dropdown when permissions are false', async () => {
|
||||
|
|
@ -244,4 +245,18 @@ describe('Repository breadcrumbs component', () => {
|
|||
expect(findDropdownGroup().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('link rendering', () => {
|
||||
it('passes `href` to GlLink when isBlobView is true', () => {
|
||||
factory('/', { isBlobView: true });
|
||||
|
||||
expect(findRouterLinks().at(0).attributes('href')).toBe('/test-project/path/-/tree');
|
||||
});
|
||||
|
||||
it('passes `to` to GlLink when isBlobView is false', () => {
|
||||
factory('/', { isBlobView: false });
|
||||
|
||||
expect(findRouterLinks().at(0).attributes('to')).toBe('/-/tree');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pi
|
|||
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
|
||||
|
||||
import getMergePipeline from '~/vue_merge_request_widget/queries/get_merge_pipeline.query.graphql';
|
||||
import * as sharedGraphQlUtils from '~/graphql_shared/utils';
|
||||
import { mockStore, mockMergePipelineQueryResponse } from '../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
|
@ -149,16 +148,6 @@ describe('MrWidgetPipelineContainer', () => {
|
|||
expect(mergePipelineResponse).toHaveBeenCalledWith(queryVariables);
|
||||
});
|
||||
|
||||
describe('polling', () => {
|
||||
it('toggles query polling with visibility check', async () => {
|
||||
jest.spyOn(sharedGraphQlUtils, 'toggleQueryPollingByVisibility');
|
||||
|
||||
await createComponent();
|
||||
|
||||
expect(sharedGraphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the merge pipeline query is unsuccessful', () => {
|
||||
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ describe('Pagination links component', () => {
|
|||
it('renders its description slot content', () => {
|
||||
expect(description().text()).toBe('Description go here');
|
||||
expect(description().classes()).toEqual(
|
||||
expect.arrayContaining(['gl-w-full', 'gl-mt-2', 'gl-text-subtle']),
|
||||
expect.arrayContaining(['gl-w-full', 'gl-text-subtle']),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe BlobHelper do
|
||||
include TreeHelper
|
||||
include FakeBlobHelpers
|
||||
include Devise::Test::ControllerHelpers
|
||||
|
||||
describe "#sanitize_svg_data" do
|
||||
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
|
||||
|
|
@ -477,4 +478,34 @@ RSpec.describe BlobHelper do
|
|||
expect(rendered_button).to have_selector('button[data-action="edit"]')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#vue_blob_header_app_data' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:blob) { fake_blob(path: 'README.md') }
|
||||
let(:ref) { 'main' }
|
||||
let(:ref_type) { :branch }
|
||||
let(:breadcrumb_data) { { title: 'README.md', 'is-last': true } }
|
||||
|
||||
before do
|
||||
assign(:project, project)
|
||||
assign(:ref, ref)
|
||||
assign(:ref_type, ref_type)
|
||||
allow(helper).to receive(:breadcrumb_data_attributes).and_return(breadcrumb_data)
|
||||
end
|
||||
|
||||
it 'returns data related to blob header' do
|
||||
expect(helper.vue_blob_header_app_data(project, blob, ref)).to include({
|
||||
blob_path: blob.path,
|
||||
breadcrumbs: breadcrumb_data,
|
||||
escaped_ref: ref,
|
||||
history_link: project_commits_path(project, ref),
|
||||
project_id: project.id,
|
||||
project_root_path: project_path(project),
|
||||
project_path: project.full_path,
|
||||
project_short_path: project.path,
|
||||
ref_type: ref_type.to_s,
|
||||
ref: ref
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -60,9 +60,8 @@ RSpec.describe Import::BulkImports::UpdateSourceUsersService, :clean_gitlab_redi
|
|||
let_it_be(:node_1) { graphql_response_node(gid: source_user_identifiers[1]) }
|
||||
let_it_be(:node_2) { graphql_response_node(gid: source_user_identifiers[2]) }
|
||||
|
||||
let(:minimum_batch_size) { 1 }
|
||||
let(:service_args) do
|
||||
{ bulk_import: bulk_import, namespace: portable, options: { minimum_batch_size: minimum_batch_size } }
|
||||
{ bulk_import: bulk_import, namespace: portable }
|
||||
end
|
||||
|
||||
def graphql_response_node(gid:, name: 'Name', username: 'username')
|
||||
|
|
@ -77,12 +76,12 @@ RSpec.describe Import::BulkImports::UpdateSourceUsersService, :clean_gitlab_redi
|
|||
instance_double(
|
||||
GraphQL::Client::Response,
|
||||
original_hash: {
|
||||
'page_info' => {
|
||||
'next_page' => next_page,
|
||||
'has_next_page' => has_next_page
|
||||
},
|
||||
'data' => {
|
||||
'users' => {
|
||||
'pageInfo' => {
|
||||
'next_page' => next_page,
|
||||
'has_next_page' => has_next_page
|
||||
},
|
||||
'nodes' => nodes
|
||||
}
|
||||
}
|
||||
|
|
@ -154,6 +153,13 @@ RSpec.describe Import::BulkImports::UpdateSourceUsersService, :clean_gitlab_redi
|
|||
expect(client).to receive(:execute).and_return(graphql_response([]))
|
||||
end
|
||||
|
||||
expect_next_instance_of(BulkImports::Logger) do |logger|
|
||||
expect(logger).to receive(:error).with(
|
||||
message: 'No users present in response',
|
||||
response: anything,
|
||||
user_ids: source_user_identifiers)
|
||||
end
|
||||
|
||||
expect(fetch_users_data.each.to_a).to eq([])
|
||||
end
|
||||
end
|
||||
|
|
@ -179,7 +185,7 @@ RSpec.describe Import::BulkImports::UpdateSourceUsersService, :clean_gitlab_redi
|
|||
end
|
||||
|
||||
context 'when the number of source users is higher than the batch size' do
|
||||
it 'makes a request for each batch in decending order' do
|
||||
it 'makes a request for each batch in descending order' do
|
||||
stub_const("#{described_class.name}::BATCH_SIZE", 2)
|
||||
|
||||
expect_next_instance_of(BulkImports::Clients::Graphql) do |client|
|
||||
|
|
@ -199,24 +205,6 @@ RSpec.describe Import::BulkImports::UpdateSourceUsersService, :clean_gitlab_redi
|
|||
expect(fetch_users_data.each.to_a).to match_array([node_0, node_1, node_2])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when current batch size is smaller than minimum batch size' do
|
||||
let(:minimum_batch_size) { 2 }
|
||||
|
||||
it 'does not request fewer user details than the minimum batch size' do
|
||||
stub_const("#{described_class.name}::BATCH_SIZE", 2)
|
||||
|
||||
expect_next_instance_of(BulkImports::Clients::Graphql) do |client|
|
||||
expect(client).to receive(:parse).and_return('query').ordered
|
||||
|
||||
expect(client).to receive(:execute)
|
||||
.with('query', ids: [source_user_identifiers[1], source_user_identifiers[2]], after: nil)
|
||||
.and_return(graphql_response([node_1, node_2]))
|
||||
end
|
||||
|
||||
expect(fetch_users_data.each.to_a).to match_array([node_1, node_2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_source_user' do
|
||||
|
|
@ -259,6 +247,12 @@ RSpec.describe Import::BulkImports::UpdateSourceUsersService, :clean_gitlab_redi
|
|||
it 'does not update the source user attributes' do
|
||||
expect(Import::SourceUsers::UpdateService).not_to receive(:new)
|
||||
|
||||
expect_next_instance_of(BulkImports::Logger) do |logger|
|
||||
expect(logger).to receive(:error).with(
|
||||
hash_including(message: 'Missing source user identifier')
|
||||
)
|
||||
end
|
||||
|
||||
update_source_user
|
||||
end
|
||||
end
|
||||
|
|
@ -274,6 +268,12 @@ RSpec.describe Import::BulkImports::UpdateSourceUsersService, :clean_gitlab_redi
|
|||
it 'does not update the source user attributes' do
|
||||
expect(Import::SourceUsers::UpdateService).not_to receive(:new)
|
||||
|
||||
expect_next_instance_of(BulkImports::Logger) do |logger|
|
||||
expect(logger).to receive(:error).with(
|
||||
hash_including(message: 'Missing source user information')
|
||||
)
|
||||
end
|
||||
|
||||
update_source_user
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,256 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
require 'gitlab/dangerfiles/spec_helper'
|
||||
|
||||
require_relative '../../../tooling/danger/internal_users'
|
||||
require_relative '../../../tooling/danger/project_helper'
|
||||
|
||||
RSpec.describe Tooling::Danger::InternalUsers, feature_category: :tooling do
|
||||
include_context "with dangerfile"
|
||||
|
||||
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
|
||||
let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
|
||||
let(:file_path) { 'app/models/user.rb' }
|
||||
let(:changed_lines) { [] }
|
||||
let(:file_lines) { [] }
|
||||
|
||||
subject(:helper) { fake_danger.new(helper: fake_helper) }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:project_helper).and_return(fake_project_helper)
|
||||
allow(fake_helper).to receive(:all_changed_files).and_return([file_path])
|
||||
allow(fake_helper).to receive(:changed_lines).with(file_path).and_return(changed_lines)
|
||||
allow(fake_project_helper).to receive(:file_lines).with(file_path).and_return(file_lines)
|
||||
end
|
||||
|
||||
describe '#add_comment_for_internal_users_changes' do
|
||||
shared_examples 'no violations' do
|
||||
it 'adds no comments' do
|
||||
expect(helper).not_to receive(:markdown)
|
||||
expect(helper).not_to receive(:warn)
|
||||
|
||||
helper.add_comment_for_internal_users_changes
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'has violations' do
|
||||
it 'adds file and MR level comments' do
|
||||
expect(helper).to receive(:markdown).once
|
||||
expect(helper).to receive(:warn).once
|
||||
|
||||
helper.add_comment_for_internal_users_changes
|
||||
end
|
||||
end
|
||||
|
||||
context 'when documentation is changed' do
|
||||
before do
|
||||
allow(fake_helper).to receive(:all_changed_files).and_return([described_class::DOCS_PATH])
|
||||
end
|
||||
|
||||
include_examples 'no violations'
|
||||
end
|
||||
|
||||
context 'when method name is changed' do
|
||||
let(:changed_lines) do
|
||||
[
|
||||
'- def security_bot',
|
||||
'+ def security_bot_v2'
|
||||
]
|
||||
end
|
||||
|
||||
let(:file_lines) { [' def security_bot_v2'] }
|
||||
|
||||
include_examples 'has violations'
|
||||
end
|
||||
|
||||
context 'when method body is changed' do
|
||||
let(:changed_lines) do
|
||||
[
|
||||
' def security_bot',
|
||||
'+ super',
|
||||
' end'
|
||||
]
|
||||
end
|
||||
|
||||
let(:file_lines) do
|
||||
[
|
||||
'def security_bot',
|
||||
' super',
|
||||
'end'
|
||||
]
|
||||
end
|
||||
|
||||
include_examples 'has violations'
|
||||
end
|
||||
|
||||
context 'when bot module is referenced' do
|
||||
let(:changed_lines) do
|
||||
[
|
||||
'+ Users::Internal.security_bot'
|
||||
]
|
||||
end
|
||||
|
||||
let(:file_lines) { [' Users::Internal.security_bot'] }
|
||||
|
||||
include_examples 'has violations'
|
||||
end
|
||||
|
||||
context 'when bot symbol is used' do
|
||||
let(:changed_lines) do
|
||||
[
|
||||
'+ user_type: :security_bot'
|
||||
]
|
||||
end
|
||||
|
||||
let(:file_lines) { [' user_type: :security_bot'] }
|
||||
|
||||
include_examples 'has violations'
|
||||
end
|
||||
|
||||
context 'when changes are unrelated' do
|
||||
let(:changed_lines) do
|
||||
[
|
||||
'+ user.name = "Regular User"'
|
||||
]
|
||||
end
|
||||
|
||||
let(:file_lines) { [' user.name = "Regular User"'] }
|
||||
|
||||
include_examples 'no violations'
|
||||
end
|
||||
|
||||
context 'when changing lines inside a bot method definition' do
|
||||
let(:changed_lines) do
|
||||
[
|
||||
' user.name = "Bot Name"'
|
||||
]
|
||||
end
|
||||
|
||||
let(:file_lines) do
|
||||
[
|
||||
'def security_bot',
|
||||
' user.name = "Bot Name"',
|
||||
'end'
|
||||
]
|
||||
end
|
||||
|
||||
include_examples 'has violations'
|
||||
end
|
||||
|
||||
context 'when changing lines outside any bot method definition' do
|
||||
let(:changed_lines) do
|
||||
[
|
||||
' user.name = "Regular Name"'
|
||||
]
|
||||
end
|
||||
|
||||
let(:file_lines) do
|
||||
[
|
||||
'def regular_method',
|
||||
' user.name = "Regular Name"',
|
||||
'end'
|
||||
]
|
||||
end
|
||||
|
||||
include_examples 'no violations'
|
||||
end
|
||||
|
||||
context 'when bot method spans multiple ends' do
|
||||
let(:changed_lines) do
|
||||
[
|
||||
' user.tap do |u|',
|
||||
' u.name = "Bot Name"',
|
||||
' end'
|
||||
]
|
||||
end
|
||||
|
||||
let(:file_lines) do
|
||||
[
|
||||
'def security_bot',
|
||||
' user.tap do |u|',
|
||||
' u.name = "Bot Name"',
|
||||
' end',
|
||||
'end'
|
||||
]
|
||||
end
|
||||
|
||||
include_examples 'has violations'
|
||||
end
|
||||
|
||||
context 'when modifying code between bot methods' do
|
||||
let(:changed_lines) do
|
||||
[
|
||||
' user.name = "Regular Name"'
|
||||
]
|
||||
end
|
||||
|
||||
let(:file_lines) do
|
||||
[
|
||||
'def security_bot',
|
||||
' # bot code',
|
||||
'end',
|
||||
' user.name = "Regular Name"',
|
||||
'def alert_bot',
|
||||
' # bot code',
|
||||
'end'
|
||||
]
|
||||
end
|
||||
|
||||
include_examples 'no violations'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#file_has_violations?' do
|
||||
context 'when tracking bot method boundaries' do
|
||||
let(:changed_lines) { [' some_code'] }
|
||||
|
||||
it 'correctly tracks entering and exiting bot methods' do
|
||||
allow(fake_project_helper).to receive(:file_lines).and_return([
|
||||
'def security_bot',
|
||||
' some_code',
|
||||
'end'
|
||||
])
|
||||
|
||||
expect(helper.send(:file_has_violations?, file_path)).to be true
|
||||
|
||||
allow(fake_project_helper).to receive(:file_lines).and_return([
|
||||
'def security_bot',
|
||||
' other_code',
|
||||
'end',
|
||||
' some_code'
|
||||
])
|
||||
|
||||
expect(helper.send(:file_has_violations?, file_path)).to be false
|
||||
end
|
||||
|
||||
it 'handles nested end keywords' do
|
||||
allow(fake_project_helper).to receive(:file_lines).and_return([
|
||||
'def security_bot',
|
||||
' some_code',
|
||||
' if true',
|
||||
' some_code',
|
||||
' end',
|
||||
'end'
|
||||
])
|
||||
|
||||
expect(helper.send(:file_has_violations?, file_path)).to be true
|
||||
end
|
||||
|
||||
it 'processes all lines in method body' do
|
||||
lines = [
|
||||
'def security_bot',
|
||||
' line1',
|
||||
' line2',
|
||||
' line3',
|
||||
'end'
|
||||
]
|
||||
|
||||
allow(fake_project_helper).to receive(:file_lines).and_return(lines)
|
||||
allow(fake_helper).to receive(:changed_lines).and_return([' line2'])
|
||||
|
||||
expect(helper.send(:file_has_violations?, file_path)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -37,14 +37,12 @@ RSpec.describe Import::BulkImports::SourceUsersAttributesWorker, feature_categor
|
|||
|
||||
expect(Import::BulkImports::UpdateSourceUsersService).to receive(:new).with(
|
||||
namespace: project.root_ancestor,
|
||||
bulk_import: bulk_import,
|
||||
options: {}
|
||||
bulk_import: bulk_import
|
||||
).and_return(service_double).ordered
|
||||
|
||||
expect(Import::BulkImports::UpdateSourceUsersService).to receive(:new).with(
|
||||
namespace: group,
|
||||
bulk_import: bulk_import,
|
||||
options: {}
|
||||
bulk_import: bulk_import
|
||||
).and_return(service_double).ordered
|
||||
|
||||
perform
|
||||
|
|
@ -66,14 +64,13 @@ RSpec.describe Import::BulkImports::SourceUsersAttributesWorker, feature_categor
|
|||
allow(bulk_import).to receive(:completed?).and_return(true)
|
||||
end
|
||||
|
||||
it 'calls the service with a minimum_batch_size set' do
|
||||
it 'executes UpdateSourceUsersService' do
|
||||
service_double = instance_double(Import::BulkImports::UpdateSourceUsersService)
|
||||
allow(service_double).to receive(:execute)
|
||||
|
||||
expect(Import::BulkImports::UpdateSourceUsersService).to receive(:new).with(
|
||||
namespace: anything,
|
||||
bulk_import: bulk_import,
|
||||
options: { minimum_batch_size: 1 }
|
||||
bulk_import: bulk_import
|
||||
).twice.and_return(service_double)
|
||||
|
||||
perform
|
||||
|
|
|
|||
|
|
@ -1,110 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'suggestor'
|
||||
|
||||
module Tooling
|
||||
module Danger
|
||||
module InternalUsers
|
||||
include ::Tooling::Danger::Suggestor
|
||||
|
||||
DOCS_PATH = 'doc/administration/internal_users.md'
|
||||
|
||||
INLINE_COMMENT =
|
||||
<<~MESSAGE.freeze
|
||||
This line modifies internal user behavior or references an internal bot user.
|
||||
If this introduces new capabilities or changes existing behavior, please update the [internal users documentation](#{DOCS_PATH}).
|
||||
|
||||
This comment will only appear once per file.
|
||||
MESSAGE
|
||||
|
||||
MR_LEVEL_COMMENT =
|
||||
<<~SUGGEST_COMMENT.freeze
|
||||
This MR changes internal user behavior or usage.
|
||||
Please ensure [internal users documentation](#{DOCS_PATH}) is up to date.
|
||||
|
||||
If this is not applicable, please ignore this message.
|
||||
|
||||
Consider documenting:
|
||||
- Any changes to internal user behavior or capabilities
|
||||
- New use cases or integrations
|
||||
- Changes in how bots may interact with GitLab and its features
|
||||
SUGGEST_COMMENT
|
||||
|
||||
BOT_METHODS = %w[
|
||||
support_bot
|
||||
alert_bot
|
||||
visual_review_bot
|
||||
ghost
|
||||
migration_bot
|
||||
security_bot
|
||||
automation_bot
|
||||
security_policy_bot
|
||||
admin_bot
|
||||
suggested_reviewers_bot
|
||||
llm_bot
|
||||
placeholder
|
||||
duo_code_review_bot
|
||||
import_user
|
||||
].freeze
|
||||
|
||||
BOT_NAMES = BOT_METHODS.join('|')
|
||||
|
||||
BOT_METHOD_DEF_REGEX = /^\s*def\s+(?:self\.)?\s*(#{BOT_NAMES})(?!\w)/
|
||||
BOT_MODULE_CALL_REGEX = /^\s*Users::Internal\.(?:#{BOT_NAMES})(?!\w)/
|
||||
BOT_SYMBOL_TYPE_REGEX = /^\s*(?!#|")[^#"]*:(?:#{BOT_NAMES})(?!\w)/
|
||||
|
||||
def add_comment_for_internal_users_changes
|
||||
return if helper.all_changed_files.include?(DOCS_PATH)
|
||||
|
||||
violations_found = false
|
||||
|
||||
helper.all_changed_files.each do |filename|
|
||||
next unless file_has_violations?(filename)
|
||||
|
||||
violations_found = true
|
||||
markdown(INLINE_COMMENT, file: filename)
|
||||
end
|
||||
|
||||
warn(MR_LEVEL_COMMENT) if violations_found
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def file_has_violations?(filename)
|
||||
changed_lines = helper.changed_lines(filename)
|
||||
file_lines = project_helper.file_lines(filename)
|
||||
|
||||
in_bot_method = false
|
||||
has_violation = false
|
||||
|
||||
changed_lines.each do |line|
|
||||
clean_line = line.delete_prefix('+').delete_prefix('-').strip
|
||||
next unless clean_line.match?(BOT_METHOD_DEF_REGEX) ||
|
||||
clean_line.match?(BOT_MODULE_CALL_REGEX) ||
|
||||
clean_line.match?(BOT_SYMBOL_TYPE_REGEX)
|
||||
|
||||
has_violation = true
|
||||
break
|
||||
end
|
||||
|
||||
return has_violation if has_violation
|
||||
|
||||
file_lines.each_with_index do |line, _|
|
||||
if line.match?(BOT_METHOD_DEF_REGEX)
|
||||
in_bot_method = true
|
||||
elsif line.strip == 'end'
|
||||
in_bot_method = false
|
||||
end
|
||||
|
||||
next unless in_bot_method
|
||||
next unless changed_lines.any? { |cl| cl.delete_prefix('+').delete_prefix('-').strip == line.strip }
|
||||
|
||||
has_violation = true
|
||||
break
|
||||
end
|
||||
|
||||
has_violation
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue