Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-09-18 18:12:22 +00:00
parent cfb1a5b025
commit af2edb33e5
67 changed files with 626 additions and 2603 deletions

View File

@ -3433,12 +3433,15 @@ Gitlab/BoundedContexts:
- 'ee/app/services/package_metadata/compressed_package_data_object.rb'
- 'ee/app/services/package_metadata/data_object.rb'
- 'ee/app/services/package_metadata/data_object_fabricator.rb'
- 'ee/app/services/package_metadata/data_objects/cve_enrichment.rb'
- 'ee/app/services/package_metadata/ingestion/advisory/advisory_ingestion_task.rb'
- 'ee/app/services/package_metadata/ingestion/advisory/affected_package_ingestion_task.rb'
- 'ee/app/services/package_metadata/ingestion/advisory/ingestion_service.rb'
- 'ee/app/services/package_metadata/ingestion/compressed_package/ingestion_service.rb'
- 'ee/app/services/package_metadata/ingestion/compressed_package/license_ingestion_task.rb'
- 'ee/app/services/package_metadata/ingestion/compressed_package/package_ingestion_task.rb'
- 'ee/app/services/package_metadata/ingestion/cve_enrichment/ingestion_service.rb'
- 'ee/app/services/package_metadata/ingestion/cve_enrichment/cve_enrichment_ingestion_task.rb'
- 'ee/app/services/package_metadata/ingestion/data_map.rb'
- 'ee/app/services/package_metadata/ingestion/ingestion_service.rb'
- 'ee/app/services/package_metadata/ingestion/tasks/base.rb'

View File

@ -18,14 +18,11 @@ export const DEFAULT = 'default';
export const DELETE_FAILURE = 'delete_pipeline_failure';
export const DRAW_FAILURE = 'draw_failure';
export const LOAD_FAILURE = 'load_failure';
export const PARSE_FAILURE = 'parse_failure';
export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
// Pipeline tabs
export const pipelineTabName = 'graph';
export const needsTabName = 'dag';
export const jobsTabName = 'builds';
export const failedJobsTabName = 'failures';
export const testReportTabName = 'test_report';
@ -35,7 +32,6 @@ export const licensesTabName = 'licenses';
export const codeQualityTabName = 'codequality_report';
export const validPipelineTabNames = [
needsTabName,
jobsTabName,
failedJobsTabName,
testReportTabName,

View File

@ -1,69 +0,0 @@
<script>
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
name: 'DagAnnotations',
components: {
GlButton,
},
props: {
annotations: {
type: Object,
required: true,
},
},
data() {
return {
showList: true,
};
},
computed: {
linkText() {
return this.showList ? __('Hide list') : __('Show list');
},
shouldShowLink() {
return Object.keys(this.annotations).length > 1;
},
wrapperClasses() {
return [
'gl-flex',
'gl-flex-col',
'gl-fixed',
'gl-right-1',
'gl-top-2/3',
'gl-w-max',
'gl-px-5',
'gl-py-4',
'gl-rounded-base',
'gl-bg-white',
].join(' ');
},
},
methods: {
toggleList() {
this.showList = !this.showList;
},
},
};
</script>
<template>
<div :class="wrapperClasses">
<div v-if="showList">
<div v-for="note in annotations" :key="note.uid" class="gl-flex gl-items-center">
<div
data-testid="dag-color-block"
class="gl-h-5 gl-w-6"
:style="{
background: `linear-gradient(0.25turn, ${note.source.color} 40%, ${note.target.color} 60%)`,
}"
></div>
<div data-testid="dag-note-text" class="gl-items-center gl-px-2 gl-text-base">
{{ note.source.name }} {{ note.target.name }}
</div>
</div>
</div>
<gl-button v-if="shouldShowLink" variant="link" @click="toggleList">{{ linkText }}</gl-button>
</div>
</template>

View File

@ -1,317 +0,0 @@
<script>
import * as d3 from 'd3';
import { uniqueId } from 'lodash';
import { getMaxNodes, removeOrphanNodes } from '~/ci/pipeline_details/utils/parsing_utils';
import { PARSE_FAILURE } from '../../constants';
import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '../constants';
import { calculateClip, createLinkPath, createSankey, labelPosition } from '../utils/drawing_utils';
import {
currentIsLive,
getLiveLinksAsDict,
highlightLinks,
restoreLinks,
toggleLinkHighlight,
togglePathHighlights,
} from '../utils/interactions';
export default {
viewOptions: {
baseHeight: 300,
baseWidth: 1000,
minNodeHeight: 60,
nodeWidth: 16,
nodePadding: 25,
paddingForLabels: 100,
labelMargin: 8,
baseOpacity: 0.8,
containerClasses: ['dag-graph-container', 'gl-flex', 'gl-flex-col'].join(' '),
hoverFadeClasses: ['gl-cursor-pointer', 'gl-duration-slow', 'gl-ease-ease'].join(' '),
},
gitLabColorRotation: [
'#e17223',
'#83ab4a',
'#5772ff',
'#b24800',
'#25d2d2',
'#006887',
'#487900',
'#d84280',
'#3547de',
'#6f3500',
'#006887',
'#275600',
'#b31756',
],
props: {
graphData: {
type: Object,
required: true,
},
},
data() {
return {
color: () => {},
height: 0,
width: 0,
};
},
mounted() {
let countedAndTransformed;
try {
countedAndTransformed = this.transformData(this.graphData);
} catch {
this.$emit('on-failure', PARSE_FAILURE);
return;
}
this.drawGraph(countedAndTransformed);
},
methods: {
addSvg() {
return d3
.select('.dag-graph-container')
.append('svg')
.attr('viewBox', [0, 0, this.width, this.height])
.attr('width', this.width)
.attr('height', this.height);
},
appendLinks(link) {
return (
link
.append('path')
.attr('d', (d, i) => createLinkPath(d, i, this.$options.viewOptions.nodeWidth))
.attr('stroke', ({ gradId }) => `url(#${gradId})`)
.style('stroke-linejoin', 'round')
// minus two to account for the rounded nodes
.attr('stroke-width', ({ width }) => Math.max(1, width - 2))
.attr('clip-path', ({ clipId }) => `url(#${clipId})`)
);
},
appendLinkInteractions(link) {
const { baseOpacity } = this.$options.viewOptions;
return link
.on('mouseover', (d, idx, collection) => {
if (currentIsLive(idx, collection)) {
return;
}
this.$emit('update-annotation', { type: ADD_NOTE, data: d });
highlightLinks(d, idx, collection);
})
.on('mouseout', (d, idx, collection) => {
if (currentIsLive(idx, collection)) {
return;
}
this.$emit('update-annotation', { type: REMOVE_NOTE, data: d });
restoreLinks(baseOpacity);
})
.on('click', (d, idx, collection) => {
toggleLinkHighlight(baseOpacity, d, idx, collection);
this.$emit('update-annotation', { type: REPLACE_NOTES, data: getLiveLinksAsDict() });
});
},
appendNodeInteractions(node) {
return node.on('click', (d, idx, collection) => {
togglePathHighlights(this.$options.viewOptions.baseOpacity, d, idx, collection);
this.$emit('update-annotation', { type: REPLACE_NOTES, data: getLiveLinksAsDict() });
});
},
appendLabelAsForeignObject(d, i, n) {
const currentNode = n[i];
const { height, wrapperWidth, width, x, y, textAlign } = labelPosition(d, {
...this.$options.viewOptions,
width: this.width,
});
const labelClasses = [
'gl-flex',
'gl-pointer-events-none',
'gl-flex-col',
'gl-justify-center',
'gl-break-words',
].join(' ');
return (
d3
.select(currentNode)
.attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
.attr('height', height)
/*
items with a 'max-content' width will have a wrapperWidth for the foreignObject
*/
.attr('width', wrapperWidth || width)
.attr('x', x)
.attr('y', y)
.classed('gl-overflow-visible', true)
.append('xhtml:div')
.classed(labelClasses, true)
.style('height', height)
.style('width', width)
.style('text-align', textAlign)
.text(({ name }) => name)
);
},
createAndAssignId(datum, field, modifier = '') {
const id = uniqueId(modifier);
/* eslint-disable-next-line no-param-reassign */
datum[field] = id;
return id;
},
createClip(link) {
return link
.append('clipPath')
.attr('id', (d) => {
return this.createAndAssignId(d, 'clipId', 'dag-clip');
})
.append('path')
.attr('d', calculateClip);
},
createGradient(link) {
const gradient = link
.append('linearGradient')
.attr('id', (d) => {
return this.createAndAssignId(d, 'gradId', 'dag-grad');
})
.attr('gradientUnits', 'userSpaceOnUse')
.attr('x1', ({ source }) => source.x1)
.attr('x2', ({ target }) => target.x0);
gradient
.append('stop')
.attr('offset', '0%')
.attr('stop-color', ({ source }) => this.color(source));
gradient
.append('stop')
.attr('offset', '100%')
.attr('stop-color', ({ target }) => this.color(target));
},
createLinks(svg, linksData) {
const links = this.generateLinks(svg, linksData);
this.createGradient(links);
this.createClip(links);
this.appendLinks(links);
this.appendLinkInteractions(links);
},
createNodes(svg, nodeData) {
const nodes = this.generateNodes(svg, nodeData);
this.labelNodes(svg, nodeData);
this.appendNodeInteractions(nodes);
},
drawGraph({ maxNodesPerLayer, linksAndNodes }) {
const { baseWidth, baseHeight, minNodeHeight, nodeWidth, nodePadding, paddingForLabels } =
this.$options.viewOptions;
this.width = baseWidth;
this.height = baseHeight + maxNodesPerLayer * minNodeHeight;
this.color = this.initColors();
const { links, nodes } = createSankey({
width: this.width,
height: this.height,
nodeWidth,
nodePadding,
paddingForLabels,
})(linksAndNodes);
const svg = this.addSvg();
this.createLinks(svg, links);
this.createNodes(svg, nodes);
},
generateLinks(svg, linksData) {
return svg
.append('g')
.attr('fill', 'none')
.attr('stroke-opacity', this.$options.viewOptions.baseOpacity)
.selectAll(`.${LINK_SELECTOR}`)
.data(linksData)
.enter()
.append('g')
.attr('id', (d) => {
return this.createAndAssignId(d, 'uid', LINK_SELECTOR);
})
.classed(
`${LINK_SELECTOR} gl-transition-stroke-opacity ${this.$options.viewOptions.hoverFadeClasses}`,
true,
);
},
generateNodes(svg, nodeData) {
const { nodeWidth } = this.$options.viewOptions;
return svg
.append('g')
.selectAll(`.${NODE_SELECTOR}`)
.data(nodeData)
.enter()
.append('line')
.classed(
`${NODE_SELECTOR} gl-transition-stroke ${this.$options.viewOptions.hoverFadeClasses}`,
true,
)
.attr('id', (d) => {
return this.createAndAssignId(d, 'uid', NODE_SELECTOR);
})
.attr('stroke', (d) => {
const color = this.color(d);
/* eslint-disable-next-line no-param-reassign */
d.color = color;
return color;
})
.attr('stroke-width', nodeWidth)
.attr('stroke-linecap', 'round')
.attr('x1', (d) => Math.floor((d.x1 + d.x0) / 2))
.attr('x2', (d) => Math.floor((d.x1 + d.x0) / 2))
.attr('y1', (d) => d.y0 + 4)
.attr('y2', (d) => d.y1 - 4);
},
initColors() {
const colorFn = d3.scaleOrdinal(this.$options.gitLabColorRotation);
return ({ name }) => colorFn(name);
},
labelNodes(svg, nodeData) {
return svg
.append('g')
.classed('gl-text-sm', true)
.selectAll('text')
.data(nodeData)
.enter()
.append('foreignObject')
.each(this.appendLabelAsForeignObject);
},
transformData(parsed) {
const baseLayout = createSankey()(parsed);
const cleanedNodes = removeOrphanNodes(baseLayout.nodes);
const maxNodesPerLayer = getMaxNodes(cleanedNodes);
return {
maxNodesPerLayer,
linksAndNodes: {
links: parsed.links,
nodes: cleanedNodes,
},
};
},
},
};
</script>
<template>
<div :class="$options.viewOptions.containerClasses" data-testid="dag-graph-container">
<!-- graph goes here -->
</div>
</template>

View File

@ -1,9 +0,0 @@
/* Interaction handles */
export const IS_HIGHLIGHTED = 'dag-highlighted';
export const LINK_SELECTOR = 'dag-link';
export const NODE_SELECTOR = 'dag-node';
/* Annotation types */
export const ADD_NOTE = 'add';
export const REMOVE_NOTE = 'remove';
export const REPLACE_NOTES = 'replace';

View File

@ -1,306 +0,0 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlAlert, GlButton, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { fetchPolicies } from '~/lib/graphql';
import { s__, __ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import {
DEFAULT,
PARSE_FAILURE,
LOAD_FAILURE,
UNSUPPORTED_DATA,
} from '~/ci/pipeline_details/constants';
import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
import getDagVisData from './graphql/queries/get_dag_vis_data.query.graphql';
import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
import DagAnnotations from './components/dag_annotations.vue';
import DagGraph from './components/dag_graph.vue';
export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Dag',
i18n: {
deprecationAlert: {
title: s__('Pipelines|Upcoming visualization change'),
message: s__(
'Pipelines|The visualization in this tab will be %{linkStart}removed%{linkEnd}. Instead, view %{codeStart}needs%{codeEnd} relationships with the %{strongStart}Job dependency%{strongEnd} option in the %{strongStart}Pipeline%{strongEnd} tab.',
),
},
},
deprecationInfoLink: helpPagePath('update/deprecations', {
anchor: 'removed-needs-tab-from-the-pipeline-view',
}),
components: {
DagAnnotations,
DagGraph,
GlAlert,
GlButton,
GlEmptyState,
GlLink,
GlSprintf,
},
inject: {
aboutDagDocPath: {
default: null,
},
dagDocPath: {
default: null,
},
emptyDagSvgPath: {
default: '',
},
pipelineIid: {
default: '',
},
pipelineProjectPath: {
default: '',
},
},
apollo: {
graphData: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query: getDagVisData,
variables() {
return {
projectPath: this.pipelineProjectPath,
iid: this.pipelineIid,
};
},
update(data) {
if (!data?.project?.pipeline) {
return this.graphData;
}
const {
stages: { nodes: stages },
} = data.project.pipeline;
const unwrappedGroups = stages
.map(({ name, groups: { nodes: groups } }) => {
return groups.map((group) => {
return { category: name, ...group };
});
})
.flat(2);
const nodes = unwrappedGroups.map((group) => {
const jobs = group.jobs.nodes.map(({ name, needs }) => {
return { name, needs: needs.nodes.map((need) => need.name) };
});
return { ...group, jobs };
});
return nodes;
},
error() {
this.reportFailure(LOAD_FAILURE);
},
},
},
data() {
return {
annotationsMap: {},
failureType: null,
graphData: null,
showFailureAlert: false,
showDeprecationAlert: true,
hasNoDependentJobs: false,
};
},
errorTexts: {
[LOAD_FAILURE]: __('We are currently unable to fetch data for this graph.'),
[PARSE_FAILURE]: __('There was an error parsing the data for this graph.'),
[UNSUPPORTED_DATA]: __('Needs visualization requires at least 3 dependent jobs.'),
[DEFAULT]: __('An unknown error occurred while loading this graph.'),
},
emptyStateTexts: {
title: __('Speed up your pipelines with Needs relationships'),
firstDescription: __(
'Using the %{codeStart}needs%{codeEnd} keyword makes jobs run before their stage is reached. Jobs run as soon as their %{codeStart}needs%{codeEnd} relationships are met, which speeds up your pipelines.',
),
secondDescription: __(
"If you add %{codeStart}needs%{codeEnd} to jobs in your pipeline you'll be able to view the %{codeStart}needs%{codeEnd} dependencies between jobs in this tab.",
),
button: __('Learn more about needs dependencies'),
},
computed: {
failure() {
switch (this.failureType) {
case LOAD_FAILURE:
return {
text: this.$options.errorTexts[LOAD_FAILURE],
variant: 'danger',
};
case PARSE_FAILURE:
return {
text: this.$options.errorTexts[PARSE_FAILURE],
variant: 'danger',
};
case UNSUPPORTED_DATA:
return {
text: this.$options.errorTexts[UNSUPPORTED_DATA],
variant: 'info',
};
default:
return {
text: this.$options.errorTexts[DEFAULT],
variant: 'danger',
};
}
},
processedData() {
return this.processGraphData(this.graphData);
},
shouldDisplayAnnotations() {
return !isEmpty(this.annotationsMap);
},
shouldDisplayGraph() {
return Boolean(!this.showFailureAlert && !this.hasNoDependentJobs && this.graphData);
},
},
methods: {
addAnnotationToMap({ uid, source, target }) {
this.annotationsMap = {
...this.annotationsMap,
[uid]: { source, target },
};
},
processGraphData(data) {
let parsed;
try {
parsed = parseData(data);
} catch {
this.reportFailure(PARSE_FAILURE);
return {};
}
if (parsed.links.length === 1) {
this.reportFailure(UNSUPPORTED_DATA);
return {};
}
// If there are no links, we don't report failure
// as it simply means the user does not use job dependencies
if (parsed.links.length === 0) {
this.hasNoDependentJobs = true;
return {};
}
return parsed;
},
hideDeprecationAlert() {
this.showDeprecationAlert = false;
},
hideFailureAlert() {
this.showFailureAlert = false;
},
removeAnnotationFromMap({ uid }) {
const copy = { ...this.annotationsMap };
delete copy[uid];
this.annotationsMap = copy;
},
reportFailure(type) {
this.showFailureAlert = true;
this.failureType = type;
},
updateAnnotation({ type, data }) {
switch (type) {
case ADD_NOTE:
this.addAnnotationToMap(data);
break;
case REMOVE_NOTE:
this.removeAnnotationFromMap(data);
break;
case REPLACE_NOTES:
this.annotationsMap = data;
break;
default:
break;
}
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="showDeprecationAlert"
:title="$options.i18n.deprecationAlert.title"
variant="warning"
data-testid="deprecation-alert"
@dismiss="hideDeprecationAlert"
>
<gl-sprintf :message="$options.i18n.deprecationAlert.message">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
<template #link="{ content }">
<gl-link
:href="$options.deprecationInfoLink"
class="gl-inline-flex !gl-no-underline"
target="_blank"
>
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<gl-alert
v-if="showFailureAlert"
:variant="failure.variant"
data-testid="failure-alert"
@dismiss="hideFailureAlert"
>
{{ failure.text }}
</gl-alert>
<div class="gl-relative">
<dag-annotations v-if="shouldDisplayAnnotations" :annotations="annotationsMap" />
<dag-graph
v-if="shouldDisplayGraph"
:graph-data="processedData"
@onFailure="reportFailure"
@update-annotation="updateAnnotation"
/>
<gl-empty-state
v-else-if="hasNoDependentJobs"
:svg-path="emptyDagSvgPath"
:svg-height="null"
:title="$options.emptyStateTexts.title"
>
<template #description>
<div class="gl-text-left">
<p>
<gl-sprintf :message="$options.emptyStateTexts.firstDescription">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
<p>
<gl-sprintf :message="$options.emptyStateTexts.secondDescription">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
<template #link="{ content }">
<gl-link :href="aboutDagDocPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</div>
</template>
<template v-if="dagDocPath" #actions>
<gl-button :href="dagDocPath" target="_blank" variant="confirm">
{{ $options.emptyStateTexts.button }}
</gl-button>
</template>
</gl-empty-state>
</div>
</div>
</template>

View File

@ -1,33 +0,0 @@
query getDagVisData($projectPath: ID!, $iid: ID!) {
project(fullPath: $projectPath) {
id
pipeline(iid: $iid) {
id
stages {
nodes {
id
name
groups {
nodes {
id
name
size
jobs {
nodes {
id
name
needs {
nodes {
id
name
}
}
}
}
}
}
}
}
}
}
}

View File

@ -1,134 +0,0 @@
import * as d3 from 'd3';
import { sankey, sankeyLeft } from 'd3-sankey';
export const calculateClip = ({ y0, y1, source, target, width }) => {
/*
Because large link values can overrun their box, we create a clip path
to trim off the excess in charts that have few nodes per column and are
therefore tall.
The box is created by
M: moving to outside midpoint of the source node
V: drawing a vertical line to maximum of the bottom link edge or
the lowest edge of the node (can be d.y0 or d.y1 depending on the link's path)
H: drawing a horizontal line to the outside edge of the destination node
V: drawing a vertical line back up to the minimum of the top link edge or
the highest edge of the node (can be d.y0 or d.y1 depending on the link's path)
H: drawing a horizontal line back to the outside edge of the source node
Z: closing the path, back to the start point
*/
const bottomLinkEdge = Math.max(y1, y0) + width / 2;
const topLinkEdge = Math.min(y0, y1) - width / 2;
/* eslint-disable @gitlab/require-i18n-strings */
return `
M${source.x0}, ${y1}
V${Math.max(bottomLinkEdge, y0, y1)}
H${target.x1}
V${Math.min(topLinkEdge, y0, y1)}
H${source.x0}
Z
`;
/* eslint-enable @gitlab/require-i18n-strings */
};
export const createLinkPath = ({ y0, y1, source, target, width }, idx, nodeWidth) => {
/*
Creates a series of staggered midpoints for the link paths, so they
don't run along one channel and can be distinguished.
First, get a point staggered by index and link width, modulated by the link box
to find a point roughly between the nodes.
Then offset it by nodeWidth, so it doesn't run under any nodes at the left.
Determine where it would overlap at the right.
Finally, select the leftmost of these options:
- offset from the source node based on index + fudge;
- a fuzzy offset from the right node, using Math.random adds a little blur
- a hard offset from the end node, if random pushes it over
Then draw a line from the start node to the bottom-most point of the midline
up to the topmost point in that line and then to the middle of the end node
*/
const xValRaw = source.x1 + (((idx + 1) * width) % (target.x1 - source.x0));
const xValMin = xValRaw + nodeWidth;
const overlapPoint = source.x1 + (target.x0 - source.x1);
const xValMax = overlapPoint - nodeWidth * 1.4;
const midPointX = Math.min(xValMin, target.x0 - nodeWidth * 4 * Math.random(), xValMax);
return d3.line()([
[(source.x0 + source.x1) / 2, y0],
[midPointX, y0],
[midPointX, y1],
[(target.x0 + target.x1) / 2, y1],
]);
};
/*
createSankey calls the d3 layout to generate the relationships and positioning
values for the nodes and links in the graph.
*/
export const createSankey = ({
width = 10,
height = 10,
nodeWidth = 10,
nodePadding = 10,
paddingForLabels = 1,
} = {}) => {
const sankeyGenerator = sankey()
.nodeId(({ name }) => name)
.nodeAlign(sankeyLeft)
.nodeWidth(nodeWidth)
.nodePadding(nodePadding)
.extent([
[paddingForLabels, paddingForLabels],
[width - paddingForLabels, height - paddingForLabels],
]);
return ({ nodes, links }) =>
sankeyGenerator({
nodes: nodes.map((d) => ({ ...d })),
links: links.map((d) => ({ ...d })),
});
};
export const labelPosition = ({ x0, x1, y0, y1 }, viewOptions) => {
const { paddingForLabels, labelMargin, nodePadding, width } = viewOptions;
const firstCol = x0 <= paddingForLabels;
const lastCol = x1 >= width - paddingForLabels;
if (firstCol) {
return {
x: 0 + labelMargin,
y: y0,
height: `${y1 - y0}px`,
width: paddingForLabels - 2 * labelMargin,
textAlign: 'right',
};
}
if (lastCol) {
return {
x: width - paddingForLabels + labelMargin,
y: y0,
height: `${y1 - y0}px`,
width: paddingForLabels - 2 * labelMargin,
textAlign: 'left',
};
}
return {
x: (x1 + x0) / 2,
y: y0 - nodePadding,
height: `${nodePadding}px`,
width: 'max-content',
wrapperWidth: paddingForLabels - 2 * labelMargin,
textAlign: x0 < width / 2 ? 'left' : 'right',
};
};

View File

@ -1,156 +0,0 @@
import * as d3 from 'd3';
import { LINK_SELECTOR, NODE_SELECTOR, IS_HIGHLIGHTED } from '../constants';
export const highlightIn = 1;
export const highlightOut = 0.2;
const getCurrent = (idx, collection) => d3.select(collection[idx]);
const getLiveLinks = () => d3.selectAll(`.${LINK_SELECTOR}.${IS_HIGHLIGHTED}`);
const getOtherLinks = () => d3.selectAll(`.${LINK_SELECTOR}:not(.${IS_HIGHLIGHTED})`);
const getNodesNotLive = () => d3.selectAll(`.${NODE_SELECTOR}:not(.${IS_HIGHLIGHTED})`);
export const getLiveLinksAsDict = () => {
return Object.fromEntries(
getLiveLinks()
.data()
.map((d) => [d.uid, d]),
);
};
export const currentIsLive = (idx, collection) =>
getCurrent(idx, collection).classed(IS_HIGHLIGHTED);
const backgroundLinks = (selection) => selection.style('stroke-opacity', highlightOut);
const backgroundNodes = (selection) => selection.attr('stroke', '#f2f2f2');
const foregroundLinks = (selection) => selection.style('stroke-opacity', highlightIn);
const foregroundNodes = (selection) => selection.attr('stroke', (d) => d.color);
const renewLinks = (selection, baseOpacity) => selection.style('stroke-opacity', baseOpacity);
const renewNodes = (selection) => selection.attr('stroke', (d) => d.color);
export const getAllLinkAncestors = (node) => {
if (node.targetLinks) {
return node.targetLinks.flatMap((n) => {
return [n, ...getAllLinkAncestors(n.source)];
});
}
return [];
};
const getAllNodeAncestors = (node) => {
let allNodes = [];
if (node.targetLinks) {
allNodes = node.targetLinks.flatMap((n) => {
return getAllNodeAncestors(n.source);
});
}
return [...allNodes, node.uid];
};
export const highlightLinks = (d, idx, collection) => {
const currentLink = getCurrent(idx, collection);
const currentSourceNode = d3.select(`#${d.source.uid}`);
const currentTargetNode = d3.select(`#${d.target.uid}`);
/* Higlight selected link, de-emphasize others */
backgroundLinks(getOtherLinks());
foregroundLinks(currentLink);
/* Do the same to related nodes */
backgroundNodes(getNodesNotLive());
foregroundNodes(currentSourceNode);
foregroundNodes(currentTargetNode);
};
const highlightPath = (parentLinks, parentNodes) => {
/* de-emphasize everything else */
backgroundLinks(getOtherLinks());
backgroundNodes(getNodesNotLive());
/* highlight correct links */
parentLinks.forEach(({ uid }) => {
foregroundLinks(d3.select(`#${uid}`)).classed(IS_HIGHLIGHTED, true);
});
/* highlight correct nodes */
parentNodes.forEach((id) => {
foregroundNodes(d3.select(`#${id}`)).classed(IS_HIGHLIGHTED, true);
});
};
const restoreNodes = () => {
/*
When paths are unclicked, they can take down nodes that
are still in use for other paths. This checks the live paths and
rehighlights their nodes.
*/
getLiveLinks().each((d) => {
foregroundNodes(d3.select(`#${d.source.uid}`)).classed(IS_HIGHLIGHTED, true);
foregroundNodes(d3.select(`#${d.target.uid}`)).classed(IS_HIGHLIGHTED, true);
});
};
const restorePath = (parentLinks, parentNodes, baseOpacity) => {
parentLinks.forEach(({ uid }) => {
renewLinks(d3.select(`#${uid}`), baseOpacity).classed(IS_HIGHLIGHTED, false);
});
parentNodes.forEach((id) => {
d3.select(`#${id}`).classed(IS_HIGHLIGHTED, false);
});
if (d3.selectAll(`.${IS_HIGHLIGHTED}`).empty()) {
renewLinks(getOtherLinks(), baseOpacity);
renewNodes(getNodesNotLive());
return;
}
backgroundLinks(getOtherLinks());
backgroundNodes(getNodesNotLive());
restoreNodes();
};
export const restoreLinks = (baseOpacity) => {
/*
if there exist live links, reset to highlight out / pale
otherwise, reset to base
*/
if (d3.selectAll(`.${IS_HIGHLIGHTED}`).empty()) {
renewLinks(d3.selectAll(`.${LINK_SELECTOR}`), baseOpacity);
renewNodes(d3.selectAll(`.${NODE_SELECTOR}`));
return;
}
backgroundLinks(getOtherLinks());
backgroundNodes(getNodesNotLive());
};
// eslint-disable-next-line max-params
export const toggleLinkHighlight = (baseOpacity, d, idx, collection) => {
if (currentIsLive(idx, collection)) {
restorePath([d], [d.source.uid, d.target.uid], baseOpacity);
restoreNodes();
return;
}
highlightPath([d], [d.source.uid, d.target.uid]);
};
// eslint-disable-next-line max-params
export const togglePathHighlights = (baseOpacity, d, idx, collection) => {
const parentLinks = getAllLinkAncestors(d);
const parentNodes = getAllNodeAncestors(d);
const currentNode = getCurrent(idx, collection);
/* if this node is already live, make it unlive and reset its path */
if (currentIsLive(idx, collection)) {
currentNode.classed(IS_HIGHLIGHTED, false);
restorePath(parentLinks, parentNodes, baseOpacity);
return;
}
highlightPath(parentLinks, parentNodes);
};

View File

@ -43,7 +43,6 @@ export const createAppOptions = (selector, apolloProvider, router) => {
suiteEndpoint,
blobPath,
hasTestReport,
emptyDagSvgPath,
emptyStateImagePath,
artifactsExpiredImagePath,
isFullCodequalityReportAvailable,
@ -95,7 +94,6 @@ export const createAppOptions = (selector, apolloProvider, router) => {
suiteEndpoint,
blobPath,
hasTestReport,
emptyDagSvgPath,
emptyStateImagePath,
artifactsExpiredImagePath,
securityPoliciesPath,

View File

@ -1,12 +1,10 @@
import PipelineGraphWrapper from './graph/graph_component_wrapper.vue';
import Dag from './dag/dag.vue';
import FailedJobsApp from './jobs/failed_jobs_app.vue';
import JobsApp from './jobs/jobs_app.vue';
import TestReports from './test_reports/test_reports.vue';
import ManualVariables from './manual_variables/manual_variables.vue';
import {
pipelineTabName,
needsTabName,
jobsTabName,
failedJobsTabName,
testReportTabName,
@ -15,7 +13,6 @@ import {
export const routes = [
{ name: pipelineTabName, path: '/', component: PipelineGraphWrapper },
{ name: needsTabName, path: '/dag', component: Dag },
{ name: jobsTabName, path: '/builds', component: JobsApp },
{ name: failedJobsTabName, path: '/failures', component: FailedJobsApp },
{ name: testReportTabName, path: '/test_report', component: TestReports },

View File

@ -1,4 +1,33 @@
import * as d3 from 'd3';
import { sankey, sankeyLeft } from 'd3-sankey';
/*
createSankey calls the d3 layout to generate the relationships and positioning
values for the nodes and links in the graph.
*/
export const createSankey = ({
width = 10,
height = 10,
nodeWidth = 10,
nodePadding = 10,
paddingForLabels = 1,
} = {}) => {
const sankeyGenerator = sankey()
.nodeId(({ name }) => name)
.nodeAlign(sankeyLeft)
.nodeWidth(nodeWidth)
.nodePadding(nodePadding)
.extent([
[paddingForLabels, paddingForLabels],
[width - paddingForLabels, height - paddingForLabels],
]);
return ({ nodes, links }) =>
sankeyGenerator({
nodes: nodes.map((d) => ({ ...d })),
links: links.map((d) => ({ ...d })),
});
};
export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`;

View File

@ -1,6 +1,6 @@
import { memoize } from 'lodash';
import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
import { createSankey } from '../dag/utils/drawing_utils';
import { createSankey } from './drawing_utils';
import { createNodeDict } from './index';
/*
@ -106,16 +106,6 @@ export const getMaxNodes = (nodes) => {
return Math.max(...counts);
};
/*
Because we cannot know if a node is part of a relationship until after we
generate the links with createSankey, this function is used after the first call
to find nodes that have no relations.
*/
export const removeOrphanNodes = (sankeyfiedNodes) => {
return sankeyfiedNodes.filter((node) => node.sourceLinks.length || node.targetLinks.length);
};
/*
This utility accepts unwrapped pipeline data in the format returned from
our standard pipeline GraphQL query and returns a list of names by layer

View File

@ -50,7 +50,7 @@ html {
}
.alert-wrapper {
.gl-alert:first-child {
&:not(:empty) {
@apply gl-mt-3;
}

View File

@ -18,6 +18,9 @@ class JwtController < ApplicationController
::Auth::DependencyProxyAuthenticationService::AUDIENCE => ::Auth::DependencyProxyAuthenticationService
}.freeze
# Currently POST requests for this route return a 404 by default and are allowed through in our readonly middleware -
# ee/lib/ee/gitlab/middleware/read_only/controller.rb
# If the action here changes to allow POST requests then a check for maintenance mode should be added
def auth
service = SERVICES[params[:service]]
return head :not_found unless service

View File

@ -7,7 +7,7 @@
= render Pajamas::AlertComponent.new(title: _('OpenSSL version 3'),
variant: :warning,
alert_options: { class: 'gl-my-5 js-openssl-callout',
alert_options: { class: 'js-openssl-callout',
data: { feature_id: Users::CalloutsHelper::OPENSSL_CALLOUT,
dismiss_endpoint: callouts_path }},
close_button_options: { data: { testid: 'close-openssl-callout' }}) do |c|

View File

@ -21,6 +21,9 @@ module Packages
def after_destroy
pkg = artifact.package
# Ml::ModelVersion need the package to be able to upload files later
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/461322
return if pkg.ml_model?
pkg.transaction do
pkg.destroy if model.for_package_ids(pkg.id).empty?

View File

@ -5,4 +5,4 @@ feature_category: source_code_management
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/160838
milestone: '17.3'
queued_migration_version: 20240726081618
finalized_by: # version of the migration that finalized this BBM
finalized_by: '20240906133341'

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class AddTemporaryIndexForSnippets < Gitlab::Database::Migration[2.2]
milestone '17.4'
disable_ddl_transaction!
def up
return unless Gitlab.com?
add_concurrent_index :snippets, :id,
where: "type = 'ProjectSnippet' and organization_id IS NOT NULL",
name: 'tmp_index_snippets_for_organization_id'
end
def down
return unless Gitlab.com?
remove_concurrent_index_by_name :snippets, 'tmp_index_snippets_for_organization_id'
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
class FixNonNullableSnippets < Gitlab::Database::Migration[2.2]
milestone '17.4'
restrict_gitlab_migration gitlab_schema: :gitlab_main
disable_ddl_transaction!
def up
return unless Gitlab.com?
retry_count = 0
begin
model = define_batchable_model(:snippets)
model.transaction do
relation = model.where(type: 'ProjectSnippet').where.not(organization_id: nil)
relation.select(:id).find_each do |snippet|
snippet.update_column(:organization_id, nil)
end
end
rescue ActiveRecord::QueryCanceled # rubocop:disable Database/RescueQueryCanceled -- to reuse a buffer cache to process stuck records
retry_count += 1
retry if retry_count < 5
end
end
def down
# no-op
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class RemoveTemporaryIndexForSnippets < Gitlab::Database::Migration[2.2]
milestone '17.4'
disable_ddl_transaction!
def up
return unless Gitlab.com?
remove_concurrent_index_by_name :snippets, 'tmp_index_snippets_for_organization_id'
end
def down
return unless Gitlab.com?
add_concurrent_index :snippets, :id,
where: "type = 'ProjectSnippet' and organization_id IS NOT NULL",
name: 'tmp_index_snippets_for_organization_id'
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class FinalizeNullifyOrganizationIdForSnippets < Gitlab::Database::Migration[2.2]
milestone '17.4'
restrict_gitlab_migration gitlab_schema: :gitlab_main
disable_ddl_transaction!
def up
ensure_batched_background_migration_is_finished(
job_class_name: 'NullifyOrganizationIdForSnippets',
table_name: 'snippets',
column_name: 'id',
job_arguments: [],
finalize: true
)
end
end

View File

@ -0,0 +1 @@
ed8265250fec53324c839f5ea5e9405ec8a74ab1777010b3a93b5f63d37c646f

View File

@ -0,0 +1 @@
151fef83e897583bb108b74a181063f8e73e18d2bafda6db6c8eff158263a89e

View File

@ -0,0 +1 @@
e40f25686e1352ddff17405937fb1b973d879bdab9d10c8cb080b5d5d9868139

View File

@ -0,0 +1 @@
b33ed25f04eb775aa69aa1e27b6da5ba2a46d5c8754de16092091c65e8a93c89

View File

@ -28,11 +28,10 @@ Please do not rely on this information for purchasing or planning purposes.
The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the
sole discretion of GitLab Inc.
To configure your GitLab instance to access the available self-hosted large language
models (LLMs) in your infrastructure:
To configure your GitLab instance to access the available self-hosted models in your infrastructure:
1. Configure the self-hosted model.
1. Configure the GitLab Duo AI-powered features to use your self-hosted models.
1. Configure the GitLab Duo features to use your self-hosted model.
## Configure the self-hosted model
@ -50,22 +49,23 @@ To configure a self-hosted model:
1. In **Subscription details**, to the right of **Last sync**, select
synchronize subscription (**{retry}**).
1. Select **Models**.
1. Set your model details by selecting **New self-hosted model**.
1. Select **New self-hosted model**.
1. Complete the fields:
- Enter the model name, for example, Mistral.
- From the **Model** dropdown list, select the model. Only GitLab-approved models are listed here.
- For **Endpoint**, select the self-hosted model endpoint, for example, the server hosting the model.
- Enter the model name, for example, `Mistral`.
- From the **Model** dropdown list, select the model. Only GitLab-approved models
are in this list.
- For **Endpoint**, select the self-hosted model endpoint. For example, the
server hosting the model.
- Optional. For **API token**, add an API key if you need one to access the model.
1. Select **Create model**.
## Configure the features to your models
## Configure GitLab Duo features to use self-hosted models
Prerequisites:
- You must be an administrator.
### View the configured features
### View configured features
1. On the left sidebar, at the bottom, select **Admin**.
1. Select **AI-powered features**.
@ -76,21 +76,22 @@ Prerequisites:
synchronize subscription (**{retry}**).
1. Select **Features**.
### Configure the features to use self-hosted models
### Configure the feature to use a self-hosted model
Use a self-hosted AI Gateway to execute queries to the configured self-hosted model:
Configure the GitLab Duo feature to send queries to the configured self-hosted model:
1. For the feature you want to set, select **Edit**.
1. In **Features**, for the feature you want to set, select **Edit**.
For example, **Code Generation**.
1. Select the model provider for the feature:
- From the list, select **Self-Hosted Model**.
- Choose the self-hosted model you would like to use, for example, Mistral.
- Choose the self-hosted model you want to use, for example, `Mistral`.
1. Select **Save Changes**.
### Configure the features to use GitLab AI Vendor models
You can set a GitLab Duo feature's model provider to the GitLab AI Vendor. That feature then uses GitLab hosted models through the Cloud Connector:
You can choose a GitLab AI vendor to be the GitLab Duo feature's model provider. The
feature then uses the GitLab-hosted model through the GitLab Cloud Connector:
1. For the feature you want to set, select **Edit**.
1. In **Features**, for the feature you want to set, select **Edit**.
1. In the list of model providers for the feature, select **AI Vendor**.
1. Select **Save Changes**.

View File

@ -25,32 +25,35 @@ DISCLAIMER:
This page contains information related to upcoming products, features, and functionality.
It is important to note that the information presented is for informational purposes only.
Please do not rely on this information for purchasing or planning purposes.
The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the
sole discretion of GitLab Inc.
The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the sole discretion of GitLab Inc.
When you deploy a self-hosted large language model (LLM), you can:
When you deploy a self-hosted model, you can:
- Manage the end-to-end transmission of requests to enterprise-hosted LLM backends for GitLab Duo features.
- Keep all of these requests within their enterprise network, ensuring no calls to external architecture.
- Isolate the GitLab instance, AI Gateway, and self-hosted AI model within their own
- Manage the end-to-end transmission of requests to enterprise-hosted large
language model (LLM) backends for GitLab Duo features.
- Keep all of these requests within that enterprise network, ensuring no calls
to external architecture.
- Isolate the GitLab instance, AI Gateway, and self-hosted model within their own
environment, ensuring complete privacy and high security for using AI features, with
no reliance on public services.
When you use self-hosted models, you:
- Can choose any GitLab-approved LLM model.
- Can choose any GitLab-approved LLM.
- Can keep all data and request/response logs in your own domain.
- Can select specific GitLab Duo features for your users.
- Do not have to rely on the GitLab shared AI Gateway.
You can connect supported models to LLM features. Model-specific prompts
and GitLab Duo feature support is provided by the self-hosted models feature. For
more information about this offering, see [subscriptions](../../subscriptions/self_managed/index.md) and the [Blueprint](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/custom_models/).
and GitLab Duo feature support is provided by the GitLab Duo Self-Hosted Models
feature. For more information about this offering, see
[subscriptions](../../subscriptions/self_managed/index.md) and the
[Blueprint](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/custom_models/).
## Prerequisites
- You must be able to manage your own LLM infrastructure.
- You must have the [GitLab Enterprise Edition](../../administration/license.md).
- You must have [GitLab Enterprise Edition](../../administration/license.md).
## Deploy a self-hosted model

View File

@ -25,10 +25,10 @@ DISCLAIMER:
This page contains information related to upcoming products, features, and functionality.
It is important to note that the information presented is for informational purposes only.
Please do not rely on this information for purchasing or planning purposes.
The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the
sole discretion of GitLab Inc.
The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the sole discretion of GitLab Inc.
By self-hosting the model, AI Gateway, and GitLab instance, there are no calls to external architecture, ensuring maximum levels of security.
By self-hosting the model, AI Gateway, and GitLab instance, there are no calls to
external architecture, ensuring maximum levels of security.
To set up your self-hosted model deployment infrastructure:
@ -36,10 +36,15 @@ To set up your self-hosted model deployment infrastructure:
1. Configure your GitLab instance.
1. Install the GitLab AI Gateway.
- [Installation video guide](https://youtu.be/UNmD9-sgUvw)
- [Installation video guide (French version)](https://www.youtube.com/watch?v=aU5vnzO-MSM)
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an installation video guide, see [Self-Hosted Models Deployment](https://youtu.be/UNmD9-sgUvw).
<!-- Video published on 2024-05-30 -->
## Step 1: Install LLM serving infrastructure
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an installation video guide in French, see [Self-Hosted Models Deployment (French Language version)](https://youtu.be/UNmD9-sgUvw).
<!-- Video published on 2024-05-30 -->
## Install large language model serving infrastructure
Install one of the following GitLab-approved LLM models:
@ -55,9 +60,10 @@ Install one of the following GitLab-approved LLM models:
| [Mixtral 8x22B](https://huggingface.co/mistral-community/Mixtral-8x22B-v0.1) | **{dotted-circle}** No | **{check-circle}** Yes | **{check-circle}** Yes |
| [Mixtral 8x7B](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1) | **{dotted-circle}** No | **{check-circle}** Yes | **{check-circle}** Yes |
### Recommended serving architectures
### Use a serving architecture
You should use one of the following architectures:
You should use one of the following serving architectures with your
installed LLM:
- [vLLM](https://docs.vllm.ai/en/stable/)
- [TensorRT-LLM](https://docs.mistral.ai/deployment/self-deployment/overview/)
@ -109,9 +115,9 @@ model_list:
api_base: YOUR_HOSTING_SERVER
```
## Step 2: Configure your GitLab instance
## Configure your GitLab instance
1. For the GitLab instance to know where AI Gateway is located so it can access
1. For the GitLab instance to know where the AI Gateway is located so it can access
the gateway, set the environment variable `AI_GATEWAY_URL` inside your GitLab
instance environment variables:
@ -119,7 +125,8 @@ model_list:
AI_GATEWAY_URL=https://<your_ai_gitlab_domain>
```
1. Where your GitLab instance is installed, [run the following Rake task](../../raketasks/index.md) to activate GitLab Duo features:
1. Where your GitLab instance is installed, [run the following Rake task](../../raketasks/index.md)
to activate GitLab Duo features:
```shell
sudo gitlab-rake gitlab:duo:enable_feature_flags
@ -139,7 +146,7 @@ model_list:
Exit the Rails console.
## Step 3: Install the GitLab AI Gateway
## Install the GitLab AI Gateway
### Install by using Docker
@ -148,7 +155,8 @@ Prerequisites:
- Install a Docker container engine, such as [Docker](https://docs.docker.com/engine/install/#server).
- Use a valid hostname accessible within your network. Do not use `localhost`.
The GitLab AI Gateway Docker image contains all necessary code and dependencies in a single container.
The GitLab AI Gateway Docker image contains all necessary code and dependencies
in a single container.
#### Find the AI Gateway release
@ -158,7 +166,9 @@ Find the GitLab official Docker image at:
- [AI Gateway Docker image on DockerHub](https://hub.docker.com/repository/docker/gitlab/model-gateway/tags).
- [Release process for self-hosted AI Gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/main/docs/release.md).
Use the image tag that corresponds to your GitLab version. For example, if the GitLab version is `v17.4.0`, use `self-hosted-v17.4.0-ee` tag.
Use the image tag that corresponds to your GitLab version. For example, if the
GitLab version is `v17.4.0`, use `self-hosted-v17.4.0-ee` tag.
For version `v17.3.0-ee`, use image tag `gitlab-v17.3.0`.
WARNING:
@ -169,8 +179,9 @@ to community resources (such as IRC or forums) to seek help from other users.
#### Set up the volumes location
Create a directory where the logs will reside on the Docker host. It can be under your user's home directory (for example
`~/gitlab-agw`), or in a directory like `/srv/gitlab-agw`. To create that directory, run:
Create a directory where the logs will reside on the Docker host. It can be under
your user's home directory (for example `~/gitlab-agw`), or in a directory like
`/srv/gitlab-agw`. To create that directory, run:
```shell
sudo mkdir -p /srv/gitlab-agw
@ -187,7 +198,7 @@ This only applies to AI Gateway image tag `gitlab-17.3.0-ee` and earlier. For im
To improve results when asking GitLab Duo Chat questions about GitLab, you can
index GitLab documentation and provide it as a file to the AI Gateway.
To index the documentation in your local installation,run:
To index the documentation in your local installation, run:
```shell
pip install requests langchain langchain_text_splitters
@ -210,7 +221,7 @@ For Docker images with version `self-hosted-17.4.0-ee` and later, run the follow
docker run -e AIGW_GITLAB_URL=<your_gitlab_instance> <image>
```
For Docker images with version `gitlab-17.3.0-ee` and `gitlab-17.2.0`:
For Docker images with version `gitlab-17.3.0-ee` and `gitlab-17.2.0`, run:
```shell
docker run -e AIGW_CUSTOM_MODELS__ENABLED=true \
@ -237,7 +248,7 @@ should open the AI Gateway API documentation.
AIGW_GITLAB_API_URL=https://<your_gitlab_domain>/api/v4/
```
1. [Configure the GitLab instance](#step-2-configure-your-gitlab-instance).
1. [Configure the GitLab instance](#configure-your-gitlab-instance).
1. After you've set up the environment variables, [run the image](#start-a-container-from-the-image).
@ -250,36 +261,15 @@ should open the AI Gateway API documentation.
After starting the container, visit `gitlab-aigw.example.com`. It might take
a while before the Docker container starts to respond to queries.
### Upgrade the AI Gateway Docker image
To upgrade the AI Gateway, download the newest Docker image tag.
1. Stop the running container:
```shell
sudo docker stop gitlab-aigw
```
1. Remove the existing container:
```shell
sudo docker rm gitlab-aigw
```
1. Pull and [run the new image](#start-a-container-from-the-image).
1. Ensure that the environment variables are all set correctly
### Install by using the AI Gateway Helm chart
#### Prerequisites
Prerequisites:
To complete this guide, you must have the following:
- A domain you own, that you can add a DNS record to.
- A Kubernetes cluster.
- A working installation of `kubectl`.
- A working installation of Helm, version v3.11.0 or later.
- You must have a:
- Domain you own, that you can add a DNS record to.
- Kubernetes cluster.
- Working installation of `kubectl`.
- Working installation of Helm, version v3.11.0 or later.
For more information, see [Test the GitLab chart on GKE or EKS](https://docs.gitlab.com/charts/quickstart/index.html).
@ -308,8 +298,8 @@ https://gitlab.com/api/v4/projects/gitlab-org%2fcharts%2fai-gateway-helm-chart/p
```
1. For the AI Gateway to access the API, it must know where the GitLab instance
is located. To do this, set the `gitlab.url` and
`gitlab.apiUrl` together with the `ingress.hosts` and `ingress.tls` values as follows:
is located. To do this, set the `gitlab.url` and `gitlab.apiUrl` together with
the `ingress.hosts` and `ingress.tls` values as follows:
```shell
helm repo add ai-gateway \
@ -332,7 +322,8 @@ is located. To do this, set the `gitlab.url` and
--timeout=300s --wait --wait-for-jobs
```
This step can take will take a few seconds in order for all resources to be allocated and the AI Gateway to start.
This step can take will take a few seconds in order for all resources to be allocated
and the AI Gateway to start.
Wait for your pods to get up and running:
@ -344,30 +335,43 @@ kubectl wait pod \
--timeout=300s
```
When it's done, you can proceed with setting up your IP ingresses and DNS records.
When your pods are up and running, you can set up your IP ingresses and DNS records.
#### Installation steps in the GitLab instance
#### Configure the GitLab instance
[Configure the GitLab instance](#step-2-configure-your-gitlab-instance).
[Configure the GitLab instance](#configure-your-gitlab-instance).
With those steps completed, your Helm chart installation is complete.
## Upgrade the AI Gateway Docker image
To upgrade the AI Gateway, download the newest Docker image tag.
1. Stop the running container:
```shell
sudo docker stop gitlab-aigw
```
1. Remove the existing container:
```shell
sudo docker rm gitlab-aigw
```
1. Pull and [run the new image](#start-a-container-from-the-image).
1. Ensure that the environment variables are all set correctly.
## Alternative installation methods
For information on alternative ways to install the AI Gateway, see [issue 463773](https://gitlab.com/gitlab-org/gitlab/-/issues/463773).
For information on alternative ways to install the AI Gateway, see
[issue 463773](https://gitlab.com/gitlab-org/gitlab/-/issues/463773).
## Troubleshooting
First, run the [debugging scripts](troubleshooting.md#use-debugging-scripts) to verify your self-hosted model setup.
First, run the [debugging scripts](troubleshooting.md#use-debugging-scripts) to
verify your self-hosted model setup.
For more information on other actions to take, see the [troubleshooting documentation](troubleshooting.md).
### The image's platform does not match the host
When [finding the AI Gateway release](#find-the-ai-gateway-release), you might get an error that states `The requested images platform (linux/amd64) does not match the detected host`.
To work around this error, add `--platform linux/amd64` to the `docker run` command:
```shell
docker run --platform linux/amd64 -e AIGW_GITLAB_URL=<your-gitlab-endpoint> <image>
```
For more information on other actions to take, see the
[troubleshooting documentation](troubleshooting.md).

View File

@ -5,33 +5,32 @@ description: Troubleshooting tips for deploying self-hosted model deployment
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Troubleshooting your self-managed GitLab Duo setup
# Troubleshooting GitLab Duo Self-Hosted Models
This content tells administrators how to debug their self-managed GitLab Duo setup.
When working with GitLab Duo Self-Hosted Models, you might encounter issues.
## Before you begin
Before you begin debugging, you should:
Before you begin troubleshooting, you should:
- Be able to access open the [`gitlab-rails` console](../../administration/operations/rails_console.md).
- Open a shell in the AI Gateway Docker image.
- Know the endpoint where your:
- AI Gateway is hosted.
- Model is hosted.
- Enable the feature flag `expanded_ai_logging` on the `gitlab-rails` console:
You should also enable the feature flag `expanded_ai_logging` on the `gitlab-rails` console:
```ruby
Feature.enable(:expanded_ai_logging)
```
```ruby
Feature.enable(:expanded_ai_logging)
```
Now, requests and responses from GitLab to the AI Gateway are logged to [`llm.log`](../logs/index.md#llmlog)
Now, requests and responses from GitLab to the AI Gateway are logged to [`llm.log`](../logs/index.md#llmlog)
## Use debugging scripts
### Use debugging scripts
We provide two debugging scripts to help administrators verify their self-hosted
model configuration.
We provide two debugging scripts to help administrators verify their self-hosted setup.
1. Debug the GitLab to AI Gateway connection. From your GitLab instance, run the [Rake task](../../raketasks/index.md):
1. Debug the GitLab to AI Gateway connection. From your GitLab instance, run the
[Rake task](../../raketasks/index.md):
```shell
gitlab-rake gitlab:duo:verify_self_hosted_setup
@ -47,11 +46,13 @@ We provide two debugging scripts to help administrators verify their self-hosted
Verify the output of the commands, and fix accordingly.
If both commands are successful, but GitLab Duo Code Suggestions is still not working, raise an issue on the issue tracker.
If both commands are successful, but GitLab Duo Code Suggestions is still not working,
raise an issue on the issue tracker.
### Check if GitLab make a request to the model
## Check if GitLab can make a request to the model
From the GitLab Rails console, verify that the model is reachable by running:
From the GitLab Rails console, verify that GitLab can make a request to the model
by running:
```ruby
model_name = "<your_model_name>"
@ -72,24 +73,26 @@ This should return a response from the model in the format:
"timestamp"=>1723448920}}
```
If that is not the case, this means that the:
If that is not the case, this might means one of the following:
- User might not have access to Code Suggestions. To resolve, [check if a user can request Code Suggestions](#check-if-a-user-can-request-code-suggestions).
- GitLab environment variables are not configured correctly. To resolve, [check that the GitLab environmental variables are set up correctly](#check-that-gitlab-environmental-variables-are-set-up-correctly).
- GitLab instance is not configured to use self-hosted models. To resolve, [check if the GitLab instance is configured to use self-hosted models](#check-if-gitlab-instance-is-configured-to-use-self-hosted-models).
- AI Gateway is not reachable. To resolve, [check if GitLab can make an HTTP request to the AI Gateway](#check-if-gitlab-can-make-an-http-request-to-ai-gateway).
- The user might not have access to Code Suggestions. To resolve,
[check if a user can request Code Suggestions](#check-if-a-user-can-request-code-suggestions).
- The GitLab environment variables are not configured correctly. To resolve, [check that the GitLab environmental variables are set up correctly](#check-that-gitlab-environmental-variables-are-set-up-correctly).
- The GitLab instance is not configured to use self-hosted models. To resolve, [check if the GitLab instance is configured to use self-hosted models](#check-if-gitlab-instance-is-configured-to-use-self-hosted-models).
- The AI Gateway is not reachable. To resolve, [check if GitLab can make an HTTP request to the AI Gateway](#check-if-gitlab-can-make-an-http-request-to-ai-gateway).
## Check if a user can request Code Suggestions
In the GitLab Rails console, verify that a user can request Code Suggestions.
In the GitLab Rails console, check if a user can request Code Suggestions by runnning:
```ruby
User.find_by_id("<user_id>").can?(:access_code_suggestions)
```
If this returns `false`, it means some configuration is missing that does not allow the user to access Code Suggestions.
If this returns `false`, it means some configuration is missing, and the user
cannot access Code Suggestions.
This missing configuration might be either of the following:
This missing configuration might be because of either of the following:
- The license is not valid. To resolve, [check or update your license](../license_file.md#see-current-license-information).
- GitLab Duo was not configured to use a self-hosted model. To resolve, [check if the GitLab instance is configured to use self-hosted models](#check-if-gitlab-instance-is-configured-to-use-self-hosted-models).
@ -105,32 +108,36 @@ To check if GitLab Duo was configured correctly:
## Check that GitLab environmental variables are set up correctly
To check if the GitLab environmental variables are set up correctly, run the following on the GitLab Rails console:
To check if the GitLab environmental variables are set up correctly, run the
following on the GitLab Rails console:
```ruby
ENV["AI_GATEWAY_URL"] == "<your-ai-gateway-endpoint>"
```
If the environmental variables are not set up correctly, set them by following the [Linux package custom environment variables setting documentation](https://docs.gitlab.com/omnibus/settings/environment-variables.html).
If the environmental variables are not set up correctly, set them by following the
[Linux package custom environment variables setting documentation](https://docs.gitlab.com/omnibus/settings/environment-variables.html).
## Check if GitLab can make an HTTP request to AI Gateway
In the GitLab Rails console, verify that the AI Gateway is reachable by running:
In the GitLab Rails console, verify that GitLab can make an HTTP request to AI
Gateway by running:
```ruby
HTTParty.get('<your-aigateway-endpoint>/monitoring/healthz', headers: { 'accept' => 'application/json' }).code
```
If the response is not `200`, this means either that:
If the response is not `200`, this means either of the following:
- The network is not properly configured to allow GitLab to reach the AI Gateway container. Contact your network administrator to verify the setup.
- AI Gateway is not able to process requests. To resolve this issue, [check if the AI Gateway can make a request to the model](#check-if-ai-gateway-can-make-a-request-to-the-model).
## Check if AI Gateway can make a request to the model
From the AI Gateway container, make an HTTP request to the AI Gateway API for a Code Suggestion. Replace:
From the AI Gateway container, make an HTTP request to the AI Gateway API for a
Code Suggestion. Replace:
- `<your_model_name>` with the name of the model you are using, for example `mistral` or `codegemma`.
- `<your_model_name>` with the name of the model you are using. For example `mistral` or `codegemma`.
- `<your_model_endpoint>` with the endpoint where the model is hosted.
```shell
@ -141,11 +148,14 @@ curl --request POST "http://localhost:5052/v1/chat/agent" \
--data '{ "prompt_components": [ { "type": "string", "metadata": { "source": "string", "version": "string" }, "payload": { "content": "Hello", "provider": "litellm", "model": "<your_model_name>", "model_endpoint": "<your_model_endpoint>" } } ], "stream": false }'
```
If the request fails:
If the request fails, the:
- AI-Gateway might not configured properly to use self-hosted models. To resolve this, [check that the AI Gateway environmental variables are set up correctly](#check-that-ai-gateway-environmental-variables-are-set-up-correctly).
- AI-Gateway might not be able access the model. To resolve, [check if the model is reachable from the AI Gateway](#check-if-the-model-is-reachable-from-ai-gateway).
- The model name or endpoint might be incorrect. Check the values, and correct them if necessary.
- AI Gateway might not be configured properly to use self-hosted models. To resolve this,
[check that the AI Gateway environmental variables are set up correctly](#check-that-ai-gateway-environmental-variables-are-set-up-correctly).
- AI Gateway might not be able to access the model. To resolve,
[check if the model is reachable from the AI Gateway](#check-if-the-model-is-reachable-from-ai-gateway).
- Model name or endpoint might be incorrect. Check the values, and correct them
if necessary.
## Check if AI Gateway can process requests
@ -158,7 +168,8 @@ If the response is not `200`, this means that AI Gateway is not installed correc
## Check that AI Gateway environmental variables are set up correctly
To check if the AI Gateway environmental variables are set up correctly, run the following in a console on the AI Gateway container:
To check that the AI Gateway environmental variables are set up correctly, run the
following in a console on the AI Gateway container:
```shell
docker exec -it <ai-gateway-container> sh
@ -166,13 +177,27 @@ echo $AIGW_AUTH__BYPASS_EXTERNAL # must be true
echo $AIGW_CUSTOM_MODELS__ENABLED # must be true
```
If the environmental variables are not set up correctly, set them by [creating a container](install_infrastructure.md#find-the-ai-gateway-release).
If the environmental variables are not set up correctly, set them by
[creating a container](install_infrastructure.md#find-the-ai-gateway-release).
## Check if the model is reachable from AI Gateway
Create a shell on the AI Gateway container and make a curl request to the model. If you find that the AI Gateway cannot make that request, this might be caused by the:
Create a shell on the AI Gateway container and make a curl request to the model.
If you find that the AI Gateway cannot make that request, this might be caused by the:
1. Model server not functioning correctly.
1. Network settings around the container not being properly configured to allow requests to where the model is hosted.
1. Network settings around the container not being properly configured to allow
requests to where the model is hosted.
To resolve this, contact your network administrator.
## The image's platform does not match the host
When [finding the AI Gateway release](install_infrastructure.md#find-the-ai-gateway-release),
you might get an error that states `The requested images platform (linux/amd64) does not match the detected host`.
To work around this error, add `--platform linux/amd64` to the `docker run` command:
```shell
docker run --platform linux/amd64 -e AIGW_GITLAB_URL=<your-gitlab-endpoint> <image>
```

View File

@ -77,7 +77,7 @@ PUT /application/plan_limits
| `ci_active_jobs` | integer | no | Total number of jobs in currently active pipelines. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
| `ci_project_subscriptions` | integer | no | Maximum number of pipeline subscriptions to and from a project. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
| `ci_pipeline_schedules` | integer | no | Maximum number of pipeline schedules. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
| `ci_needs_size_limit` | integer | no | Maximum number of [`needs`](../ci/directed_acyclic_graph/index.md) dependencies that a job can have. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
| `ci_needs_size_limit` | integer | no | Maximum number of [`needs`](../ci/yaml/needs.md) dependencies that a job can have. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
| `ci_registered_group_runners` | integer | no | Maximum number of runners registered per group. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
| `ci_registered_project_runners` | integer | no | Maximum number of runners registered per project. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
| `dotenv_size` | integer | no | Maximum size of a dotenv artifact in bytes. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/432529) in GitLab 17.1. |

View File

@ -3019,6 +3019,60 @@ Example response:
}
```
## Real-time security scan
DETAILS:
**Tier:** Ultimate
**Offering:** GitLab.com
**Status:** Experiment
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/479210) in GitLab 17.6. This feature is an [experiment](../policy/experiment-beta-support.md).
Returns SAST scan results for a single file in real-time.
```plaintext
POST /projects/:id/security_scans/sast/scan
```
Supported attributes:
| Attribute | Type | Required | Description |
|-----------|-------------------|----------|-------------|
| `id` | integer or string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
Example request:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{
"file_path":"src/main.c",
"content":"#include<string.h>\nint main(int argc, char **argv) {\n char buff[128];\n strcpy(buff, argv[1]);\n return 0;\n}\n"
}' \
--url "https://gitlab.example.com/api/v4/projects/:id/security_scans/sast/scan"
```
Example response:
```json
{
"vulnerabilities": [
{
"name": "Insecure string processing function (strcpy)",
"description": "The `strcpy` family of functions do not provide the ability to limit or check buffer\nsizes before copying to a destination buffer. This can lead to buffer overflows. Consider\nusing more secure alternatives such as `strncpy` and provide the correct limit to the\ndestination buffer and ensure the string is null terminated.\n\nFor more information please see: https://linux.die.net/man/3/strncpy\n\nIf developing for C Runtime Library (CRT), more secure versions of these functions should be\nused, see:\nhttps://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/strncpy-s-strncpy-s-l-wcsncpy-s-wcsncpy-s-l-mbsncpy-s-mbsncpy-s-l?view=msvc-170\n",
"severity": "High",
"location": {
"file": "src/main.c",
"start_line": 5,
"end_line": 5,
"start_column": 3,
"end_column": 23
}
}
]
}
```
## Get a project's pull mirror details
DETAILS:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@ -1,92 +1,11 @@
---
stage: Verify
group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
redirect_to: '../yaml/needs.md'
remove_date: '2024-12-18'
---
# Make jobs start earlier with `needs`
This document was moved to [another location](../yaml/needs.md).
DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
You can use the [`needs`](../yaml/index.md#needs) keyword to create dependencies between jobs
in a pipeline. Jobs run as soon as their dependencies are met, regardless of the pipeline's `stages`
configuration. You can even configure a pipeline with no stages defined (effectively one large stage)
and jobs still run in the proper order. This pipeline structure is a kind of
[directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
For example, you may have a specific tool or separate website that is built
as part of your main project. Using `needs`, you can specify dependencies between
these jobs and GitLab executes the jobs as soon as possible instead of waiting
for each stage to complete.
Unlike other solutions for CI/CD, GitLab does not require you to choose between staged
or stageless execution flow. You can implement a hybrid combination of staged and stageless
in a single pipeline, using only the `needs` keyword to enable the feature for any job.
Consider a monorepo as follows:
```plaintext
./service_a
./service_b
./service_c
./service_d
```
This project could have a pipeline organized into three stages:
| build | test | deploy |
|-----------|----------|--------|
| `build_a` | `test_a` | `deploy_a` |
| `build_b` | `test_b` | `deploy_b` |
| `build_c` | `test_c` | `deploy_c` |
| `build_d` | `test_d` | `deploy_d` |
You can improve job execution by using `needs` to relate the `a` jobs to each other
separately from the `b`, `c`, and `d` jobs. `build_a` could take a very long time to build,
but `test_b` doesn't need to wait, it can be configured to start as soon as `build_b` is finished,
which could be much faster.
If desired, `c` and `d` jobs can be left to run in stage sequence.
The `needs` keyword also works with the [parallel](../yaml/index.md#parallel) keyword,
giving you powerful options for parallelization in your pipeline.
## Use cases
You can use the [`needs`](../yaml/index.md#needs) keyword to define several different kinds of
dependencies between jobs in a CI/CD pipeline. You can set dependencies to fan in or out,
and even merge back together (diamond dependencies). These dependencies could be used for
pipelines that:
- Handle multi-platform builds.
- Have a complex web of dependencies like an operating system build.
- Have a deployment graph of independently deployable but related microservices.
Additionally, `needs` can help improve the overall speed of pipelines and provide fast feedback.
By creating dependencies that don't unnecessarily
block each other, your pipelines run as quickly as possible regardless of
pipeline stages, ensuring output (including errors) is available to developers
as quickly as possible.
<!--- start_remove The following content will be removed on remove_date: '2024-12-19' -->
## Needs dependency visualization (deprecated)
WARNING:
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/336560) in GitLab 17.1
and is planned for removal in 17.4. View `needs` relationships in the
[full pipeline graph](../pipelines/index.md#group-jobs-by-stage-or-needs-configuration) instead.
The needs dependency visualization makes it easier to visualize the dependencies
between jobs in a pipeline. This graph displays all the jobs in a pipeline
that need or are needed by other jobs. Jobs with no dependencies are not displayed in this view.
To see the needs visualization, select **Needs** when viewing a pipeline that uses the `needs` keyword.
![Needs visualization example.](img/dag_graph_example_v13_1.png)
Selecting a node highlights all the job paths it depends on.
![Needs visualization with path highlight.](img/dag_graph_example_clicked_v13_1.png)
<!--- end_remove -->
<!-- This redirect file can be deleted after <2024-12-18>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -45,7 +45,7 @@ Pipelines can be configured in many different ways:
- [Basic pipelines](pipeline_architectures.md#basic-pipelines) run everything in each stage concurrently,
followed by the next stage.
- [Pipelines that use the `needs` keyword](../directed_acyclic_graph/index.md) run based on dependencies
- [Pipelines that use the `needs` keyword](../yaml/needs.md) run based on dependencies
between jobs and can run more quickly than basic pipelines.
- [Merge request pipelines](../pipelines/merge_request_pipelines.md) run for merge
requests only (rather than for every commit).

View File

@ -119,7 +119,7 @@ deploy_b:
## Pipelines with the `needs` keyword
If efficiency is important and you want everything to run as quickly as possible,
you can use the [`needs` keyword](../directed_acyclic_graph/index.md) to define dependencies
you can use the [`needs` keyword](../yaml/needs.md) to define dependencies
between your jobs. When GitLab knows the dependencies between your jobs,
jobs can run as fast as possible, even starting earlier than other jobs in the same stage.

View File

@ -188,7 +188,7 @@ shouldn't run, saving pipeline resources.
In a basic configuration, jobs always wait for all other jobs in earlier stages to complete
before running. This is the simplest configuration, but it's also the slowest in most
cases. [Pipelines with the `needs` keyword](../directed_acyclic_graph/index.md) and
cases. [Pipelines with the `needs` keyword](../yaml/needs.md) and
[parent/child pipelines](downstream_pipelines.md#parent-child-pipelines) are more flexible and can
be more efficient, but can also make pipelines harder to understand and analyze.

View File

@ -147,7 +147,7 @@ For the complete `.gitlab-ci.yml` syntax, see the full [CI/CD YAML syntax refere
- Each job contains a script section and belongs to a stage:
- [`stage`](../yaml/index.md#stage) describes the sequential execution of jobs.
If there are runners available, jobs in a single stage run in parallel.
- Use the [`needs` keyword](../yaml/index.md#needs) to [run jobs out of stage order](../directed_acyclic_graph/index.md),
- Use the [`needs` keyword](../yaml/index.md#needs) to [run jobs out of stage order](../yaml/needs.md),
to increase pipeline speed and efficiency.
- You can set additional configuration to customize how your jobs and stages perform:
- Use the [`rules`](../yaml/index.md#rules) keyword to specify when to run or skip jobs.

View File

@ -10,7 +10,7 @@ DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
If you have installed your own runners, you can configure and secure them in GitLab.
This document describes how to configure runners in the GitLab UI.
If you need to configure runners on the machine where you installed GitLab Runner, see
[the GitLab Runner documentation](https://docs.gitlab.com/runner/configuration/).

View File

@ -2981,7 +2981,7 @@ In this example, a new pipeline causes a running pipeline to cancel `step-1` and
### `needs`
Use `needs` to execute jobs out-of-order. Relationships between jobs
that use `needs` can be visualized as a [directed acyclic graph](../directed_acyclic_graph/index.md).
that use `needs` can be visualized as a [directed acyclic graph](../yaml/needs.md).
You can ignore stage ordering and run some jobs without waiting for others to complete.
Jobs in multiple stages can run concurrently.
@ -3173,8 +3173,8 @@ build_job:
or the group/project must have public visibility.
- You can't use `needs:project` in the same job as [`trigger`](#trigger).
- When using `needs:project` to download artifacts from another pipeline, the job does not wait for
the needed job to complete. [Directed acyclic graph](../directed_acyclic_graph/index.md)
behavior is limited to jobs in the same pipeline. Make sure that the needed job in the other
the needed job to complete. [Using `needs` to wait for jobs to complete](../yaml/needs.md)
is limited to jobs in the same pipeline. Make sure that the needed job in the other
pipeline completes before the job that needs it tries to download the artifacts.
- You can't download artifacts from jobs that run in [`parallel`](#parallel).
- Support [CI/CD variables](../variables/index.md) in `project`, `job`, and `ref`.

88
doc/ci/yaml/needs.md Normal file
View File

@ -0,0 +1,88 @@
---
stage: Verify
group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Make jobs start earlier with `needs`
DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
You can use the [`needs`](../yaml/index.md#needs) keyword to create dependencies between jobs
in a pipeline. Jobs run as soon as their dependencies are met, regardless of the pipeline's `stages`
configuration. You can even configure a pipeline with no stages defined (effectively one large stage)
and jobs still run in the proper order. This pipeline structure is a kind of
[directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
For example, you may have a specific tool or separate website that is built
as part of your main project. Using `needs`, you can specify dependencies between
these jobs and GitLab executes the jobs as soon as possible instead of waiting
for each stage to complete.
Unlike other solutions for CI/CD, GitLab does not require you to choose between staged
or stageless execution flow. You can implement a hybrid combination of staged and stageless
in a single pipeline, using only the `needs` keyword to enable the feature for any job.
Consider a monorepo as follows:
```plaintext
./service_a
./service_b
./service_c
./service_d
```
This project could have a pipeline organized into three stages:
| build | test | deploy |
|-----------|----------|--------|
| `build_a` | `test_a` | `deploy_a` |
| `build_b` | `test_b` | `deploy_b` |
| `build_c` | `test_c` | `deploy_c` |
| `build_d` | `test_d` | `deploy_d` |
You can improve job execution by using `needs` to relate the `a` jobs to each other
separately from the `b`, `c`, and `d` jobs. `build_a` could take a very long time to build,
but `test_b` doesn't need to wait, it can be configured to start as soon as `build_b` is finished,
which could be much faster.
If desired, `c` and `d` jobs can be left to run in stage sequence.
The `needs` keyword also works with the [parallel](../yaml/index.md#parallel) keyword,
giving you powerful options for parallelization in your pipeline.
## Use cases
You can use the [`needs`](../yaml/index.md#needs) keyword to define several different kinds of
dependencies between jobs in a CI/CD pipeline. You can set dependencies to fan in or out,
and even merge back together (diamond dependencies). These dependencies could be used for
pipelines that:
- Handle multi-platform builds.
- Have a complex web of dependencies like an operating system build.
- Have a deployment graph of independently deployable but related microservices.
Additionally, `needs` can help improve the overall speed of pipelines and provide fast feedback.
By creating dependencies that don't unnecessarily
block each other, your pipelines run as quickly as possible regardless of
pipeline stages, ensuring output (including errors) is available to developers
as quickly as possible.
<!--- start_remove The following content will be removed on remove_date: '2024-12-19' -->
## Needs dependency visualization (deprecated)
WARNING:
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/336560) in GitLab 17.1
and was [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156207) in 17.4.
View `needs` relationships in the [full pipeline graph](../pipelines/index.md#group-jobs-by-stage-or-needs-configuration)
instead.
The needs dependency visualization makes it easier to visualize the dependencies
between jobs in a pipeline. This graph displays all the jobs in a pipeline
that need or are needed by other jobs. Jobs with no dependencies are not displayed in this view.
To see the needs visualization, select **Needs** when viewing a pipeline that uses the `needs` keyword.
Selecting a node highlights all the job paths it depends on.
<!--- end_remove -->

View File

@ -25,7 +25,7 @@ info: Any user with at least the Maintainer role can merge updates to this conte
## Configure self-hosted models
1. Follow the [instructions](../../administration/self_hosted_models/configure_duo_features.md#configure-the-self-hosted-model) to configure self-hosted models
1. Follow the [instructions](../../administration/self_hosted_models/configure_duo_features.md#configure-the-features-to-your-models) to configure features to use the models
1. Follow the [instructions](../../administration/self_hosted_models/configure_duo_features.md#configure-gitlab-duo-features-to-use-self-hosted-models) to configure features to use the models
AI-powered features are now powered by self-hosted models.

View File

@ -276,7 +276,7 @@ artifacts to the Google Artifact Registry from the GitLab project `gitlab-org/my
The Google attribute `my_project_maintainer` is mapped to the GitLab claims
`maintainer_access==true` and the `project_path=="gitlab-org/my-project"`.
1. In the Google Cloud Console, select [**IAM**](https://console.google.com/iam-admin/iam?supportedpurview=project).
1. In the Google Cloud Console, go to the [**IAM** page](https://console.cloud.google.com/iam-admin/iam?supportedpurview=project).
1. Select **Grant access**.
1. In the **New principals** text box, enter the principal set including the

View File

@ -49,6 +49,8 @@ To use GitLab Duo features in any project or group, you must assign the user to
1. Select **Settings > GitLab Duo**.
1. To the right of the user, turn on the toggle to assign a GitLab Duo seat.
The user is sent a confirmation email.
### For self-managed
Prerequisites:
@ -64,6 +66,10 @@ Prerequisites:
synchronize subscription (**{retry}**).
1. To the right of the user, turn on the toggle to assign a GitLab Duo seat.
The user is sent a confirmation email.
To turn off these emails, an administrator can [disable the `duo_seat_assignment_email_for_sm` feature flag](../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags).
#### Configure network and proxy settings
For self-managed instances, to enable GitLab Duo features,

View File

@ -17,5 +17,6 @@ GitLab can check your application for security vulnerabilities and that it meets
| [Set up a merge request approval policy](scan_result_policy/index.md) | Learn how to configure a merge request approval policy that takes action based on scan results. | **{star}** |
| [Set up a scan execution policy](scan_execution_policy/index.md) | Learn how to create a scan execution policy to enforce security scanning of your project. | **{star}** |
| [Scan a Docker container for vulnerabilities](container_scanning/index.md) | Learn how to use container scanning templates to add container scanning to your projects. | **{star}** |
| [Remove a secret from your commits](../user/application_security/secret_detection/remove_secrets_tutorial.md) | Learn how to remove a secret from your commit history. | **{star}** |
| [Get started with GitLab application security](../user/application_security/get-started-security.md) | Follow recommended steps to set up security tools. | |
| [GitLab Security Essentials](https://university.gitlab.com/courses/security-essentials) | Learn about the essential security capabilities of GitLab in this self-paced course. | |

View File

@ -66,7 +66,7 @@ These requirements are documented in the `Required permission` column in the fol
| Name | Required permission | Description | Introduced in | Feature flag | Enabled in |
|:-----|:------------|:------------------|:---------|:--------------|:---------|
| [`admin_runners`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151825) | | Create, view, edit, and delete group or project Runners. Includes configuring Runner settings. | GitLab [17.1](https://gitlab.com/gitlab-org/gitlab/-/issues/442851) | `custom_ability_admin_runners` | |
| [`admin_runners`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151825) | | Create, view, edit, and delete group or project Runners. Includes configuring Runner settings. | GitLab [17.1](https://gitlab.com/gitlab-org/gitlab/-/issues/442851) | | |
| [`read_runners`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156798) | | Allows read-only access to group or project runners, including the runner fleet dashboard. | GitLab [17.2](https://gitlab.com/gitlab-org/gitlab/-/issues/468202) | | |
## Secrets management

View File

@ -61,7 +61,7 @@ in your `.gitlab-ci.yml` file. The component supports these languages:
the LSIF artifact for `golang`:
```yaml
- component: ${CI_SERVER_FQDN}/components/code-intelligence/golang-code-intel@v0.0.2
- component: ${CI_SERVER_FQDN}/components/code-intelligence/golang-code-intel@v0.0.3
inputs:
golang_version: ${GO_VERSION}
```
@ -78,11 +78,11 @@ To enable code intelligence for a project, add GitLab CI/CD jobs to your project
:::TabTitle With a SCIP indexer
1. Add two jobs to your `.gitlab-ci.yml` configuration. The first job (`code_navigation_generate`)
generates the SCIP index. The second job (`code_navigation_convert`) converts the SCIP index to LSIF for use in GitLab:
1. Add a job to your `.gitlab-ci.yml` configuration. This job generates the
SCIP index and converts it to LSIF for use in GitLab:
```yaml
code_navigation_generate:
"code_navigation":
rules:
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH # the job only needs to run against the default branch
image: node:latest
@ -92,23 +92,15 @@ To enable code intelligence for a project, add GitLab CI/CD jobs to your project
- npm install -g @sourcegraph/scip-typescript
- npm install
- scip-typescript index
artifacts:
paths:
- index.scip
code_navigation_convert:
rules:
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH # the job only needs to run against the default branch
image: golang
stage: test
allow_failure: true # recommended
needs: ["code_navigation_generate"]
script:
- git clone --branch v0.4.0 --depth 1 https://github.com/sourcegraph/scip.git
- cd scip
- go build ./cmd/scip
- |
env \
TAG="v0.4.0" \
OS="$(uname -s | tr '[:upper:]' '[:lower:]')" \
ARCH="$(uname -m | sed -e 's/x86_64/amd64/')" \
bash -c 'curl --location "https://github.com/sourcegraph/scip/releases/download/$TAG/scip-$OS-$ARCH.tar.gz"' \
| tar xzf - scip
- chmod +x scip
- ./scip convert --from ../index.scip --to ../dump.lsif
- ./scip convert --from index.scip --to dump.lsif
artifacts:
reports:
lsif: dump.lsif

View File

@ -181,6 +181,13 @@ This API connection securely transmits a context window from your IDE/editor to
- Algorithms or large code blocks might take more than 10 seconds to generate.
- Streaming of code generation responses is supported in VS Code, leading to faster average response times. Other supported IDEs offer slower response times and will return the generated code in a single block.
### Use a self-hosted model
Instead of using the default model to manage Code Suggestions requests, you can
[deploy a self-hosted model](../../../../administration/self_hosted_models/index.md).
This maximizes security and privacy by making sure nothing is sent to an
external model.
### Disable direct connections to the AI Gateway
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/462791) in GitLab 17.2 [with a flag](../../../../administration/feature_flags.md) named `code_suggestions_direct_access`. Disabled by default.

View File

@ -768,7 +768,7 @@ module API
requires :group_access, type: Integer, values: Gitlab::Access.all_values, as: :link_group_access, desc: 'The group access level'
optional :expires_at, type: Date, desc: 'Share expiration date'
end
post ":id/share", feature_category: :groups_and_projects do
post ":id/share", feature_category: :groups_and_projects, urgency: :low do
authorize! :admin_project, user_project
shared_with_group = Group.find_by_id(params[:group_id])

View File

@ -44,6 +44,8 @@ module Gitlab
return false unless project_id
pipeline.update_column(:project_id, project_id)
rescue StandardError # rubocop:disable BackgroundMigration/AvoidSilentRescueExceptions -- only rescuing in one method
false
end
end
end

View File

@ -27192,9 +27192,6 @@ msgstr ""
msgid "Hide host keys manual input"
msgstr ""
msgid "Hide list"
msgstr ""
msgid "Hide marketing-related entries from the Help page"
msgstr ""
@ -27818,9 +27815,6 @@ msgstr ""
msgid "If using GitHub, youll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
msgstr ""
msgid "If you add %{codeStart}needs%{codeEnd} to jobs in your pipeline you'll be able to view the %{codeStart}needs%{codeEnd} dependencies between jobs in this tab."
msgstr ""
msgid "If you are unable to sign in or recover your password, contact a GitLab administrator."
msgstr ""
@ -31800,9 +31794,6 @@ msgstr ""
msgid "Learn more about max seats used"
msgstr ""
msgid "Learn more about needs dependencies"
msgstr ""
msgid "Learn more about seats owed"
msgstr ""
@ -35566,9 +35557,6 @@ msgstr ""
msgid "Needs attention"
msgstr ""
msgid "Needs visualization requires at least 3 dependent jobs."
msgstr ""
msgid "Network"
msgstr ""
@ -40332,9 +40320,6 @@ msgstr ""
msgid "Pipelines|The GitLab CI configuration could not be updated."
msgstr ""
msgid "Pipelines|The visualization in this tab will be %{linkStart}removed%{linkEnd}. Instead, view %{codeStart}needs%{codeEnd} relationships with the %{strongStart}Job dependency%{strongEnd} option in the %{strongStart}Pipeline%{strongEnd} tab."
msgstr ""
msgid "Pipelines|There are currently no finished pipelines."
msgstr ""
@ -40407,9 +40392,6 @@ msgstr ""
msgid "Pipelines|Unable to validate CI/CD configuration. See the %{linkStart}GitLab CI/CD troubleshooting guide%{linkEnd} for more details."
msgstr ""
msgid "Pipelines|Upcoming visualization change"
msgstr ""
msgid "Pipelines|Update Trigger"
msgstr ""
@ -51360,9 +51342,6 @@ msgstr ""
msgid "Show less"
msgstr ""
msgid "Show list"
msgstr ""
msgid "Show more"
msgstr ""
@ -52490,9 +52469,6 @@ msgstr ""
msgid "Specify an email address regex pattern to identify default internal users."
msgstr ""
msgid "Speed up your pipelines with Needs relationships"
msgstr ""
msgid "Spent at can't be a future date and time."
msgstr ""
@ -55287,9 +55263,6 @@ msgstr ""
msgid "There was an error loading users activity calendar."
msgstr ""
msgid "There was an error parsing the data for this graph."
msgstr ""
msgid "There was an error removing the e-mail."
msgstr ""
@ -59680,9 +59653,6 @@ msgstr ""
msgid "Using required encryption strategy when encrypted field is missing!"
msgstr ""
msgid "Using the %{codeStart}needs%{codeEnd} keyword makes jobs run before their stage is reached. Jobs run as soon as their %{codeStart}needs%{codeEnd} relationships are met, which speeds up your pipelines."
msgstr ""
msgid "Validate"
msgstr ""
@ -60750,9 +60720,6 @@ msgstr ""
msgid "We are currently unable to fetch data for the pipeline header."
msgstr ""
msgid "We are currently unable to fetch data for this graph."
msgstr ""
msgid "We could not determine the path to remove the epic"
msgstr ""

View File

@ -303,7 +303,7 @@
"swagger-cli": "^4.0.4",
"tailwindcss": "^3.4.1",
"timezone-mock": "^1.0.8",
"vite": "^5.4.5",
"vite": "^5.4.6",
"vite-plugin-ruby": "^5.1.0",
"vue-loader-vue3": "npm:vue-loader@17.4.2",
"vue-test-utils-compat": "0.0.14",

View File

@ -303,8 +303,8 @@ module QA
# Print difference in the description
#
expected_body = expected_item[:body]
actual_body = actual_item[:body]
expected_body = remove_backticks(expected_item[:body])
actual_body = remove_backticks(actual_item[:body])
body_msg = "#{msg} same description. diff:\n#{differ.diff(expected_body, actual_body)}"
expect(actual_body).to eq(expected_body), body_msg
@ -317,8 +317,8 @@ module QA
# Print amount difference first
#
expected_comments = expected_item[:comments]
actual_comments = actual_item[:comments]
expected_comments = expected_item[:comments].map { |comment| remove_backticks(comment) }
actual_comments = actual_item[:comments].map { |comment| remove_backticks(comment) }
comment_count_msg = <<~MSG
#{msg} same amount of comments. Source: #{expected_comments.length}, Target: #{actual_comments.length}
MSG
@ -429,6 +429,16 @@ module QA
@created_by_pattern ||= /\n\n \*By .+ on \S+\*/
end
# Remove backticks from string
#
# @param [String] text
# @return [String] modified text
def remove_backticks(text)
return unless text.present?
text.delete('`')
end
# Source project url
#
# @return [String]

View File

@ -1,743 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`The DAG graph in the basic case renders the graph svg 1`] = `
<svg
height="540"
viewBox="0,0,1000,540"
width="1000"
>
<g
fill="none"
stroke-opacity="0.8"
>
<g
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
id="reference-0"
>
<lineargradient
gradientUnits="userSpaceOnUse"
id="reference-1"
x1="116"
x2="361.3333333333333"
>
<stop
offset="0%"
stop-color="#e17223"
/>
<stop
offset="100%"
stop-color="#83ab4a"
/>
</lineargradient>
<clippath
id="reference-2"
>
<path
d="
M100, 129
V158
H377.3333333333333
V100
H100
Z
"
/>
</clippath>
<path
clip-path="url(#dag-clip63)"
d="M108,129L190,129L190,129L369.3333333333333,129"
stroke="url(#dag-grad53)"
stroke-width="56"
style="stroke-linejoin: round;"
/>
</g>
<g
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
id="reference-3"
>
<lineargradient
gradientUnits="userSpaceOnUse"
id="reference-4"
x1="377.3333333333333"
x2="622.6666666666666"
>
<stop
offset="0%"
stop-color="#83ab4a"
/>
<stop
offset="100%"
stop-color="#6f3500"
/>
</lineargradient>
<clippath
id="reference-5"
>
<path
d="
M361.3333333333333, 129.0000000000002
V158.0000000000002
H638.6666666666666
V100
H361.3333333333333
Z
"
/>
</clippath>
<path
clip-path="url(#dag-clip64)"
d="M369.3333333333333,129L509.3333333333333,129L509.3333333333333,129.0000000000002L630.6666666666666,129.0000000000002"
stroke="url(#dag-grad54)"
stroke-width="56"
style="stroke-linejoin: round;"
/>
</g>
<g
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
id="reference-6"
>
<lineargradient
gradientUnits="userSpaceOnUse"
id="reference-7"
x1="116"
x2="622.6666666666666"
>
<stop
offset="0%"
stop-color="#5772ff"
/>
<stop
offset="100%"
stop-color="#6f3500"
/>
</lineargradient>
<clippath
id="reference-8"
>
<path
d="
M100, 187.0000000000002
V241.00000000000003
H638.6666666666666
V158.0000000000002
H100
Z
"
/>
</clippath>
<path
clip-path="url(#dag-clip65)"
d="M108,212.00000000000003L306,212.00000000000003L306,187.0000000000002L630.6666666666666,187.0000000000002"
stroke="url(#dag-grad55)"
stroke-width="56"
style="stroke-linejoin: round;"
/>
</g>
<g
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
id="reference-9"
>
<lineargradient
gradientUnits="userSpaceOnUse"
id="reference-10"
x1="116"
x2="361.3333333333333"
>
<stop
offset="0%"
stop-color="#b24800"
/>
<stop
offset="100%"
stop-color="#006887"
/>
</lineargradient>
<clippath
id="reference-11"
>
<path
d="
M100, 269.9999999999998
V324
H377.3333333333333
V240.99999999999977
H100
Z
"
/>
</clippath>
<path
clip-path="url(#dag-clip66)"
d="M108,295L338.93333333333334,295L338.93333333333334,269.9999999999998L369.3333333333333,269.9999999999998"
stroke="url(#dag-grad56)"
stroke-width="56"
style="stroke-linejoin: round;"
/>
</g>
<g
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
id="reference-12"
>
<lineargradient
gradientUnits="userSpaceOnUse"
id="reference-13"
x1="116"
x2="361.3333333333333"
>
<stop
offset="0%"
stop-color="#25d2d2"
/>
<stop
offset="100%"
stop-color="#487900"
/>
</lineargradient>
<clippath
id="reference-14"
>
<path
d="
M100, 352.99999999999994
V407.00000000000006
H377.3333333333333
V323.99999999999994
H100
Z
"
/>
</clippath>
<path
clip-path="url(#dag-clip67)"
d="M108,378.00000000000006L144.66666666666669,378.00000000000006L144.66666666666669,352.99999999999994L369.3333333333333,352.99999999999994"
stroke="url(#dag-grad57)"
stroke-width="56"
style="stroke-linejoin: round;"
/>
</g>
<g
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
id="reference-15"
>
<lineargradient
gradientUnits="userSpaceOnUse"
id="reference-16"
x1="377.3333333333333"
x2="622.6666666666666"
>
<stop
offset="0%"
stop-color="#006887"
/>
<stop
offset="100%"
stop-color="#d84280"
/>
</lineargradient>
<clippath
id="reference-17"
>
<path
d="
M361.3333333333333, 270.0000000000001
V299.0000000000001
H638.6666666666666
V240.99999999999977
H361.3333333333333
Z
"
/>
</clippath>
<path
clip-path="url(#dag-clip68)"
d="M369.3333333333333,269.9999999999998L464,269.9999999999998L464,270.0000000000001L630.6666666666666,270.0000000000001"
stroke="url(#dag-grad58)"
stroke-width="56"
style="stroke-linejoin: round;"
/>
</g>
<g
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
id="reference-18"
>
<lineargradient
gradientUnits="userSpaceOnUse"
id="reference-19"
x1="377.3333333333333"
x2="622.6666666666666"
>
<stop
offset="0%"
stop-color="#487900"
/>
<stop
offset="100%"
stop-color="#d84280"
/>
</lineargradient>
<clippath
id="reference-20"
>
<path
d="
M361.3333333333333, 328.0000000000001
V381.99999999999994
H638.6666666666666
V299.0000000000001
H361.3333333333333
Z
"
/>
</clippath>
<path
clip-path="url(#dag-clip69)"
d="M369.3333333333333,352.99999999999994L522,352.99999999999994L522,328.0000000000001L630.6666666666666,328.0000000000001"
stroke="url(#dag-grad59)"
stroke-width="56"
style="stroke-linejoin: round;"
/>
</g>
<g
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
id="reference-21"
>
<lineargradient
gradientUnits="userSpaceOnUse"
id="reference-22"
x1="377.3333333333333"
x2="622.6666666666666"
>
<stop
offset="0%"
stop-color="#487900"
/>
<stop
offset="100%"
stop-color="#3547de"
/>
</lineargradient>
<clippath
id="reference-23"
>
<path
d="
M361.3333333333333, 411
V440
H638.6666666666666
V381.99999999999994
H361.3333333333333
Z
"
/>
</clippath>
<path
clip-path="url(#dag-clip70)"
d="M369.3333333333333,410.99999999999994L580,410.99999999999994L580,411L630.6666666666666,411"
stroke="url(#dag-grad60)"
stroke-width="56"
style="stroke-linejoin: round;"
/>
</g>
<g
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
id="reference-24"
>
<lineargradient
gradientUnits="userSpaceOnUse"
id="reference-25"
x1="638.6666666666666"
x2="884"
>
<stop
offset="0%"
stop-color="#d84280"
/>
<stop
offset="100%"
stop-color="#006887"
/>
</lineargradient>
<clippath
id="reference-26"
>
<path
d="
M622.6666666666666, 270.1890725105691
V299.1890725105691
H900
V241.0000000000001
H622.6666666666666
Z
"
/>
</clippath>
<path
clip-path="url(#dag-clip71)"
d="M630.6666666666666,270.0000000000001L861.6,270.0000000000001L861.6,270.1890725105691L892,270.1890725105691"
stroke="url(#dag-grad61)"
stroke-width="56"
style="stroke-linejoin: round;"
/>
</g>
<g
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
id="reference-27"
>
<lineargradient
gradientUnits="userSpaceOnUse"
id="reference-28"
x1="638.6666666666666"
x2="884"
>
<stop
offset="0%"
stop-color="#3547de"
/>
<stop
offset="100%"
stop-color="#275600"
/>
</lineargradient>
<clippath
id="reference-29"
>
<path
d="
M622.6666666666666, 411
V440
H900
V382
H622.6666666666666
Z
"
/>
</clippath>
<path
clip-path="url(#dag-clip72)"
d="M630.6666666666666,411L679.9999999999999,411L679.9999999999999,411L892,411"
stroke="url(#dag-grad62)"
stroke-width="56"
style="stroke-linejoin: round;"
/>
</g>
</g>
<g>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-30"
stroke="#e17223"
stroke-linecap="round"
stroke-width="16"
x1="108"
x2="108"
y1="104"
y2="154.00000000000003"
/>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-31"
stroke="#83ab4a"
stroke-linecap="round"
stroke-width="16"
x1="369"
x2="369"
y1="104"
y2="154"
/>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-32"
stroke="#5772ff"
stroke-linecap="round"
stroke-width="16"
x1="108"
x2="108"
y1="187.00000000000003"
y2="237.00000000000003"
/>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-33"
stroke="#b24800"
stroke-linecap="round"
stroke-width="16"
x1="108"
x2="108"
y1="270"
y2="320.00000000000006"
/>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-34"
stroke="#25d2d2"
stroke-linecap="round"
stroke-width="16"
x1="108"
x2="108"
y1="353.00000000000006"
y2="403.0000000000001"
/>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-35"
stroke="#6f3500"
stroke-linecap="round"
stroke-width="16"
x1="630"
x2="630"
y1="104.0000000000002"
y2="212.00000000000009"
/>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-36"
stroke="#006887"
stroke-linecap="round"
stroke-width="16"
x1="369"
x2="369"
y1="244.99999999999977"
y2="294.99999999999994"
/>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-37"
stroke="#487900"
stroke-linecap="round"
stroke-width="16"
x1="369"
x2="369"
y1="327.99999999999994"
y2="436"
/>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-38"
stroke="#d84280"
stroke-linecap="round"
stroke-width="16"
x1="630"
x2="630"
y1="245.00000000000009"
y2="353"
/>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-39"
stroke="#3547de"
stroke-linecap="round"
stroke-width="16"
x1="630"
x2="630"
y1="386"
y2="436"
/>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-40"
stroke="#006887"
stroke-linecap="round"
stroke-width="16"
x1="892"
x2="892"
y1="245.18907251056908"
y2="295.1890725105691"
/>
<line
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
id="reference-41"
stroke="#275600"
stroke-linecap="round"
stroke-width="16"
x1="892"
x2="892"
y1="386"
y2="436"
/>
</g>
<g
class="gl-text-sm"
>
<foreignobject
class="gl-overflow-visible"
height="58.00000000000003px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="8"
y="100"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 58.00000000000003px; text-align: right;"
>
build_a
</div>
</foreignobject>
<foreignobject
class="gl-overflow-visible"
height="25px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="369.3333333333333"
y="75"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 25px; text-align: left;"
>
test_a
</div>
</foreignobject>
<foreignobject
class="gl-overflow-visible"
height="58px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="8"
y="183.00000000000003"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 58px; text-align: right;"
>
test_b
</div>
</foreignobject>
<foreignobject
class="gl-overflow-visible"
height="58.00000000000006px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="8"
y="266"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 58.00000000000006px; text-align: right;"
>
post_test_a
</div>
</foreignobject>
<foreignobject
class="gl-overflow-visible"
height="58.00000000000006px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="8"
y="349.00000000000006"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 58.00000000000006px; text-align: right;"
>
post_test_b
</div>
</foreignobject>
<foreignobject
class="gl-overflow-visible"
height="25px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="630.6666666666666"
y="75.0000000000002"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 25px; text-align: right;"
>
post_test_c
</div>
</foreignobject>
<foreignobject
class="gl-overflow-visible"
height="25px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="369.3333333333333"
y="215.99999999999977"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 25px; text-align: left;"
>
staging_a
</div>
</foreignobject>
<foreignobject
class="gl-overflow-visible"
height="25px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="369.3333333333333"
y="298.99999999999994"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 25px; text-align: left;"
>
staging_b
</div>
</foreignobject>
<foreignobject
class="gl-overflow-visible"
height="25px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="630.6666666666666"
y="216.00000000000009"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 25px; text-align: right;"
>
canary_a
</div>
</foreignobject>
<foreignobject
class="gl-overflow-visible"
height="25px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="630.6666666666666"
y="357"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 25px; text-align: right;"
>
canary_c
</div>
</foreignobject>
<foreignobject
class="gl-overflow-visible"
height="58px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="908"
y="241.18907251056908"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 58px; text-align: left;"
>
production_a
</div>
</foreignobject>
<foreignobject
class="gl-overflow-visible"
height="58px"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="84"
x="908"
y="382"
>
<div
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
style="height: 58px; text-align: left;"
>
production_d
</div>
</foreignobject>
</g>
</svg>
`;

View File

@ -1,98 +0,0 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DagAnnotations from '~/ci/pipeline_details/dag/components/dag_annotations.vue';
import { singleNote, multiNote } from '../mock_data';
describe('The DAG annotations', () => {
let wrapper;
const getColorBlock = () => wrapper.find('[data-testid="dag-color-block"]');
const getAllColorBlocks = () => wrapper.findAll('[data-testid="dag-color-block"]');
const getTextBlock = () => wrapper.find('[data-testid="dag-note-text"]');
const getAllTextBlocks = () => wrapper.findAll('[data-testid="dag-note-text"]');
const getToggleButton = () => wrapper.findComponent(GlButton);
const createComponent = (propsData = {}, method = shallowMount) => {
wrapper = method(DagAnnotations, {
propsData,
data() {
return {
showList: true,
};
},
});
};
describe('when there is one annotation', () => {
const currentNote = singleNote['dag-link103'];
beforeEach(() => {
createComponent({ annotations: singleNote });
});
it('displays the color block', () => {
expect(getColorBlock().exists()).toBe(true);
});
it('displays the text block', () => {
expect(getTextBlock().exists()).toBe(true);
expect(getTextBlock().text()).toBe(`${currentNote.source.name}${currentNote.target.name}`);
});
it('does not display the list toggle link', () => {
expect(getToggleButton().exists()).toBe(false);
});
});
describe('when there are multiple annoataions', () => {
beforeEach(() => {
createComponent({ annotations: multiNote });
});
it('displays a color block for each link', () => {
expect(getAllColorBlocks().length).toBe(Object.keys(multiNote).length);
});
it('displays a text block for each link', () => {
expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length);
Object.values(multiNote).forEach((item, idx) => {
expect(getAllTextBlocks().at(idx).text()).toBe(`${item.source.name}${item.target.name}`);
});
});
it('displays the list toggle link', () => {
expect(getToggleButton().exists()).toBe(true);
expect(getToggleButton().text()).toBe('Hide list');
});
});
describe('the list toggle', () => {
beforeEach(() => {
createComponent({ annotations: multiNote }, mount);
});
describe('clicking hide', () => {
it('hides listed items and changes text to show', async () => {
expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length);
expect(getToggleButton().text()).toBe('Hide list');
getToggleButton().trigger('click');
await nextTick();
expect(getAllTextBlocks().length).toBe(0);
expect(getToggleButton().text()).toBe('Show list');
});
});
describe('clicking show', () => {
it('shows listed items and changes text to hide', async () => {
getToggleButton().trigger('click');
getToggleButton().trigger('click');
await nextTick();
expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length);
expect(getToggleButton().text()).toBe('Hide list');
});
});
});
});

View File

@ -1,209 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/ci/pipeline_details/dag/constants';
import DagGraph from '~/ci/pipeline_details/dag/components/dag_graph.vue';
import { createSankey } from '~/ci/pipeline_details/dag/utils/drawing_utils';
import { highlightIn, highlightOut } from '~/ci/pipeline_details/dag/utils/interactions';
import { removeOrphanNodes } from '~/ci/pipeline_details/utils/parsing_utils';
import { parsedData } from '../mock_data';
describe('The DAG graph', () => {
let wrapper;
const getGraph = () => wrapper.find('.dag-graph-container > svg');
const getAllLinks = () => wrapper.findAll(`.${LINK_SELECTOR}`);
const getAllNodes = () => wrapper.findAll(`.${NODE_SELECTOR}`);
const getAllLabels = () => wrapper.findAll('foreignObject');
const createComponent = (propsData = {}) => {
if (wrapper?.destroy) {
wrapper.destroy();
}
wrapper = shallowMount(DagGraph, {
attachTo: document.body,
propsData,
data() {
return {
color: () => {},
width: 0,
height: 0,
};
},
});
};
beforeEach(() => {
createComponent({ graphData: parsedData });
});
describe('in the basic case', () => {
beforeEach(() => {
/*
The graph uses random to offset links. To keep the snapshot consistent,
we mock Math.random. Wheeeee!
*/
const randomNumber = jest.spyOn(global.Math, 'random');
randomNumber.mockImplementation(() => 0.2);
createComponent({ graphData: parsedData });
});
it('renders the graph svg', () => {
expect(getGraph().exists()).toBe(true);
expect(getGraph().html()).toMatchSnapshot();
});
});
describe('links', () => {
it('renders the expected number of links', () => {
expect(getAllLinks()).toHaveLength(parsedData.links.length);
});
it('renders the expected number of gradients', () => {
expect(wrapper.findAll('linearGradient')).toHaveLength(parsedData.links.length);
});
it('renders the expected number of clip paths', () => {
expect(wrapper.findAll('clipPath')).toHaveLength(parsedData.links.length);
});
});
describe('nodes and labels', () => {
const sankeyNodes = createSankey()(parsedData).nodes;
const processedNodes = removeOrphanNodes(sankeyNodes);
describe('nodes', () => {
it('renders the expected number of nodes', () => {
expect(getAllNodes()).toHaveLength(processedNodes.length);
});
});
describe('labels', () => {
it('renders the expected number of labels as foreignObjects', () => {
expect(getAllLabels()).toHaveLength(processedNodes.length);
});
it('renders the title as text', () => {
expect(getAllLabels().at(0).text()).toBe(parsedData.nodes[0].name);
});
});
});
describe('interactions', () => {
const strokeOpacity = (opacity) => `stroke-opacity: ${opacity};`;
const baseOpacity = () => wrapper.vm.$options.viewOptions.baseOpacity;
describe('links', () => {
const liveLink = () => getAllLinks().at(4);
const otherLink = () => getAllLinks().at(1);
describe('on hover', () => {
it('sets the link opacity to baseOpacity and background links to 0.2', () => {
liveLink().trigger('mouseover');
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
});
it('reverts the styles on mouseout', () => {
liveLink().trigger('mouseover');
liveLink().trigger('mouseout');
expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
});
});
describe('on click', () => {
describe('toggles link liveness', () => {
it('turns link on', () => {
liveLink().trigger('click');
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
});
it('turns link off on second click', () => {
liveLink().trigger('click');
liveLink().trigger('click');
expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
});
});
it('the link remains live even after mouseout', () => {
liveLink().trigger('click');
liveLink().trigger('mouseout');
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
});
it('preserves state when multiple links are toggled on and off', () => {
const anotherLiveLink = () => getAllLinks().at(2);
liveLink().trigger('click');
anotherLiveLink().trigger('click');
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
anotherLiveLink().trigger('click');
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightOut));
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
liveLink().trigger('click');
expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
});
});
});
describe('nodes', () => {
const liveNode = () => getAllNodes().at(10);
const anotherLiveNode = () => getAllNodes().at(5);
const nodesNotHighlighted = () => getAllNodes().filter((n) => !n.classes(IS_HIGHLIGHTED));
const linksNotHighlighted = () => getAllLinks().filter((n) => !n.classes(IS_HIGHLIGHTED));
const nodesHighlighted = () => getAllNodes().filter((n) => n.classes(IS_HIGHLIGHTED));
const linksHighlighted = () => getAllLinks().filter((n) => n.classes(IS_HIGHLIGHTED));
describe('on click', () => {
it('highlights the clicked node and predecessors', () => {
liveNode().trigger('click');
expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true);
expect(linksNotHighlighted().length < getAllLinks().length).toBe(true);
linksHighlighted().wrappers.forEach((link) => {
expect(link.attributes('style')).toBe(strokeOpacity(highlightIn));
});
nodesHighlighted().wrappers.forEach((node) => {
expect(node.attributes('stroke')).not.toBe('#f2f2f2');
});
linksNotHighlighted().wrappers.forEach((link) => {
expect(link.attributes('style')).toBe(strokeOpacity(highlightOut));
});
nodesNotHighlighted().wrappers.forEach((node) => {
expect(node.attributes('stroke')).toBe('#f2f2f2');
});
});
it('toggles path off on second click', () => {
liveNode().trigger('click');
liveNode().trigger('click');
expect(nodesNotHighlighted().length).toBe(getAllNodes().length);
expect(linksNotHighlighted().length).toBe(getAllLinks().length);
});
it('preserves state when multiple nodes are toggled on and off', () => {
anotherLiveNode().trigger('click');
liveNode().trigger('click');
anotherLiveNode().trigger('click');
expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true);
expect(linksNotHighlighted().length < getAllLinks().length).toBe(true);
});
});
});
});
});

View File

@ -1,187 +0,0 @@
import { GlEmptyState } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/ci/pipeline_details/dag/constants';
import Dag from '~/ci/pipeline_details/dag/dag.vue';
import DagAnnotations from '~/ci/pipeline_details/dag/components/dag_annotations.vue';
import DagGraph from '~/ci/pipeline_details/dag/components/dag_graph.vue';
import { PARSE_FAILURE, UNSUPPORTED_DATA } from '~/ci/pipeline_details/constants';
import {
mockParsedGraphQLNodes,
tooSmallGraph,
unparseableGraph,
graphWithoutDependencies,
singleNote,
multiNote,
} from './mock_data';
describe('Pipeline DAG graph wrapper', () => {
let wrapper;
const getDeprecationAlert = () => wrapper.findByTestId('deprecation-alert');
const getFailureAlert = () => wrapper.findByTestId('failure-alert');
const getAllFailureAlerts = () => wrapper.findAllByTestId('failure-alert');
const getGraph = () => wrapper.findComponent(DagGraph);
const getNotes = () => wrapper.findComponent(DagAnnotations);
const getErrorText = (type) => wrapper.vm.$options.errorTexts[type];
const getEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = ({
graphData = mockParsedGraphQLNodes,
provideOverride = {},
method = shallowMountExtended,
} = {}) => {
wrapper = method(Dag, {
provide: {
pipelineProjectPath: 'root/abc-dag',
pipelineIid: '1',
emptySvgPath: '/my-svg',
dagDocPath: '/my-doc',
...provideOverride,
},
data() {
return {
graphData,
showFailureAlert: false,
};
},
});
};
describe('deprecation alert', () => {
beforeEach(() => {
createComponent();
});
it('renders the deprecation alert', () => {
expect(getDeprecationAlert().exists()).toBe(true);
});
it('dismisses the deprecation alert properly', async () => {
getDeprecationAlert().vm.$emit('dismiss');
await nextTick();
expect(getDeprecationAlert().exists()).toBe(false);
});
});
describe('when a query argument is undefined', () => {
beforeEach(() => {
createComponent({
provideOverride: { pipelineProjectPath: undefined },
graphData: null,
});
});
it('does not render the graph', () => {
expect(getGraph().exists()).toBe(false);
});
it('does not render the empty state', () => {
expect(getEmptyState().exists()).toBe(false);
});
});
describe('when all query variables are defined', () => {
describe('but the parse fails', () => {
beforeEach(() => {
createComponent({
graphData: unparseableGraph,
});
});
it('shows the PARSE_FAILURE alert and not the graph', () => {
expect(getFailureAlert().exists()).toBe(true);
expect(getFailureAlert().text()).toBe(getErrorText(PARSE_FAILURE));
expect(getGraph().exists()).toBe(false);
});
it('does not render the empty state', () => {
expect(getEmptyState().exists()).toBe(false);
});
});
describe('parse succeeds', () => {
beforeEach(() => {
createComponent({ method: mount });
});
it('shows the graph', () => {
expect(getGraph().exists()).toBe(true);
});
it('does not render the empty state', () => {
expect(getEmptyState().exists()).toBe(false);
});
});
describe('parse succeeds, but the resulting graph is too small', () => {
beforeEach(() => {
createComponent({
graphData: tooSmallGraph,
});
});
it('shows the UNSUPPORTED_DATA alert and not the graph', () => {
expect(getFailureAlert().exists()).toBe(true);
expect(getFailureAlert().text()).toBe(getErrorText(UNSUPPORTED_DATA));
expect(getGraph().exists()).toBe(false);
});
it('does not show the empty dag graph state', () => {
expect(getEmptyState().exists()).toBe(false);
});
});
describe('the returned data is empty', () => {
beforeEach(() => {
createComponent({
method: shallowMountExtended,
graphData: graphWithoutDependencies,
});
});
it('does not render an error alert or the graph', () => {
expect(getAllFailureAlerts().length).toBe(0);
expect(getGraph().exists()).toBe(false);
});
it('shows the empty dag graph state', () => {
expect(getEmptyState().exists()).toBe(true);
});
});
});
describe('annotations', () => {
beforeEach(() => {
createComponent();
});
it('toggles on link mouseover and mouseout', async () => {
const currentNote = singleNote['dag-link103'];
expect(getNotes().exists()).toBe(false);
getGraph().vm.$emit('update-annotation', { type: ADD_NOTE, data: currentNote });
await nextTick();
expect(getNotes().exists()).toBe(true);
getGraph().vm.$emit('update-annotation', { type: REMOVE_NOTE, data: currentNote });
await nextTick();
expect(getNotes().exists()).toBe(false);
});
it('toggles on node and link click', async () => {
expect(getNotes().exists()).toBe(false);
getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: multiNote });
await nextTick();
expect(getNotes().exists()).toBe(true);
getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: {} });
await nextTick();
expect(getNotes().exists()).toBe(false);
});
});
});

View File

@ -1,6 +1,6 @@
import { createSankey } from '~/ci/pipeline_details/dag/utils/drawing_utils';
import { createSankey } from '~/ci/pipeline_details/utils/drawing_utils';
import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
import { mockParsedGraphQLNodes } from '../mock_data';
import { mockParsedGraphQLNodes } from './mock_data';
describe('DAG visualization drawing utilities', () => {
const parsed = parseData(mockParsedGraphQLNodes);

View File

@ -1,5 +1,4 @@
import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
import { createSankey } from '~/ci/pipeline_details/dag/utils/drawing_utils';
import {
makeLinksFromNodes,
filterByAncestors,
@ -7,15 +6,14 @@ import {
keepLatestDownstreamPipelines,
listByLayers,
parseData,
removeOrphanNodes,
getMaxNodes,
} from '~/ci/pipeline_details/utils/parsing_utils';
import { createNodeDict } from '~/ci/pipeline_details/utils';
import { mockDownstreamPipelinesRest } from '../../../vue_merge_request_widget/mock_data';
import { mockDownstreamPipelinesGraphql } from '../../../commit/mock_data';
import { mockParsedGraphQLNodes, missingJob } from '../dag/mock_data';
import { generateResponse } from '../graph/mock_data';
import { mockParsedGraphQLNodes, missingJob } from './mock_data';
describe('DAG visualization parsing utilities', () => {
const nodeDict = createNodeDict(mockParsedGraphQLNodes);
@ -82,27 +80,6 @@ describe('DAG visualization parsing utilities', () => {
});
});
describe('removeOrphanNodes', () => {
it('removes sankey nodes that have no needs and are not needed', () => {
const layoutSettings = {
width: 200,
height: 200,
nodeWidth: 10,
nodePadding: 20,
paddingForLabels: 100,
};
const sankeyLayout = createSankey(layoutSettings)(parsed);
const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);
/*
These lengths are determined by the mock data.
If the data changes, the numbers may also change.
*/
expect(parsed.nodes).toHaveLength(mockParsedGraphQLNodes.length);
expect(cleanedNodes).toHaveLength(12);
});
});
describe('getMaxNodes', () => {
it('returns the number of nodes in the most populous generation', () => {
const layerNodes = [

View File

@ -77,6 +77,21 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillOrDropCiPipelineOnProjectId,
end
end
context 'when backfill will create an invalid record' do
before do
table(:ci_pipelines, primary_key: :id, database: :ci)
.create!(id: 100, iid: 100, partition_id: 100, project_id: 137)
pipeline_with_builds.update!(iid: 100)
end
it 'deletes the pipeline instead' do
migration.perform
expect { pipeline_with_builds.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'when associations are invalid as well' do
let!(:pipeline_with_bad_build) do
table(:ci_pipelines, primary_key: :id, database: :ci).create!(id: 5, partition_id: 100)

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe FixNonNullableSnippets, migration: :gitlab_main, feature_category: :source_code_management do
let(:migration) { described_class.new }
let(:snippets) { table(:snippets) }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
let!(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
let!(:personal_snippet) do
snippets.create!(
type: 'PersonalSnippet', author_id: 1, project_id: nil, title: 'Snippet1', organization_id: 1
)
end
let!(:project_snippet_with_organization) do
snippets.create!(
type: 'ProjectSnippet', author_id: 1, project_id: project.id, title: 'Snippet2', organization_id: 1
)
end
let!(:project_snippet_without_organization) do
snippets.create!(
type: 'ProjectSnippet', author_id: 1, project_id: project.id, title: 'Snippet3', organization_id: nil
)
end
describe '#up' do
context 'when GitLab.com', :saas do
it 'nullfies organization_id for project snippets' do
expect { migrate! }
.to change { project_snippet_with_organization.reload.organization_id }.from(1).to(nil)
.and not_change { personal_snippet.reload.organization_id }.from(1)
.and not_change { project_snippet_without_organization.reload.organization_id }.from(nil)
end
end
context 'when self-managed' do
it 'does not update organization_id for project snippets' do
expect { migrate! }
.to not_change { project_snippet_with_organization.reload.organization_id }.from(1)
.and not_change { personal_snippet.reload.organization_id }.from(1)
.and not_change { project_snippet_without_organization.reload.organization_id }.from(nil)
end
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe FinalizeNullifyOrganizationIdForSnippets, feature_category: :source_code_management, migration_version: 20240906133341 do
describe '#up' do
it 'ensures the migration is completed for self-managed instances' do
QueueNullifyOrganizationIdForSnippets.new.up
migration = Gitlab::Database::BackgroundMigration::BatchedMigration.where(
job_class_name: 'NullifyOrganizationIdForSnippets',
table_name: 'snippets'
).first
expect(migration.status).not_to eq(6)
migrate!
expect(migration.reload.status).to eq(6)
QueueNullifyOrganizationIdForSnippets.new.down
end
end
end

View File

@ -61,6 +61,18 @@ RSpec.describe JwtController, feature_category: :system_access do
end
end
context 'POST /jwt/auth when in maintenance mode' do
before do
stub_maintenance_mode_setting(true)
end
it 'returns 404' do
post '/jwt/auth'
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'authenticating against container registry' do
context 'existing service' do
subject! { get '/jwt/auth', params: parameters }

View File

@ -89,6 +89,18 @@ RSpec.describe Packages::CleanupPackageFileWorker, feature_category: :package_re
.and change { Packages::Package.count }.by(-1)
end
end
describe 'removing the last package file in an ML model package' do
let_it_be_with_reload(:package) { create(:ml_model_package) }
let_it_be(:package_file) { create(:package_file, :pending_destruction, package: package) }
it 'deletes the package file but keeps the package' do
expect(worker).to receive(:log_extra_metadata_on_done).twice
expect { subject }.to change { Packages::PackageFile.count }.by(-1)
.and change { Packages::Package.count }.by(0)
end
end
end
describe '#max_running_jobs' do

View File

@ -14334,10 +14334,10 @@ vite-plugin-ruby@^5.1.0:
debug "^4.3.4"
fast-glob "^3.3.2"
vite@^5.4.5:
version "5.4.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.5.tgz#e4ab27709de46ff29bd8db52b0c51606acba893b"
integrity sha512-pXqR0qtb2bTwLkev4SE3r4abCNioP3GkjvIDLlzziPpXtHgiJIjuKl+1GN6ESOT3wMjG3JTeARopj2SwYaHTOA==
vite@^5.4.6:
version "5.4.6"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.6.tgz#85a93a1228a7fb5a723ca1743e337a2588ed008f"
integrity sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"