From af2edb33e5ff07cec81c8261e0d73cb07e65dc92 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 18 Sep 2024 18:12:22 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo/gitlab/bounded_contexts.yml | 3 + .../ci/pipeline_details/constants.js | 4 - .../dag/components/dag_annotations.vue | 69 -- .../dag/components/dag_graph.vue | 317 -------- .../ci/pipeline_details/dag/constants.js | 9 - .../ci/pipeline_details/dag/dag.vue | 306 -------- .../queries/get_dag_vis_data.query.graphql | 33 - .../dag/utils/drawing_utils.js | 134 ---- .../dag/utils/interactions.js | 156 ---- .../ci/pipeline_details/pipeline_tabs.js | 2 - .../javascripts/ci/pipeline_details/routes.js | 3 - .../pipeline_details/utils/drawing_utils.js | 29 + .../pipeline_details/utils/parsing_utils.js | 12 +- app/assets/stylesheets/framework/layout.scss | 2 +- app/controllers/jwt_controller.rb | 3 + .../layouts/header/_openssl_callout.html.haml | 2 +- .../packages/cleanup_package_file_worker.rb | 3 + .../nullify_organization_id_for_snippets.yml | 2 +- ...133020_add_temporary_index_for_snippets.rb | 21 + ...0240906133040_fix_non_nullable_snippets.rb | 31 + ...050_remove_temporary_index_for_snippets.rb | 21 + ...ze_nullify_organization_id_for_snippets.rb | 18 + db/schema_migrations/20240906133020 | 1 + db/schema_migrations/20240906133040 | 1 + db/schema_migrations/20240906133050 | 1 + db/schema_migrations/20240906133341 | 1 + .../configure_duo_features.md | 33 +- .../self_hosted_models/index.md | 23 +- .../install_infrastructure.md | 134 ++-- .../self_hosted_models/troubleshooting.md | 103 ++- doc/api/plan_limits.md | 2 +- doc/api/projects.md | 54 ++ .../img/dag_graph_example_clicked_v13_1.png | Bin 54491 -> 0 bytes .../img/dag_graph_example_v13_1.png | Bin 54919 -> 0 bytes doc/ci/directed_acyclic_graph/index.md | 95 +-- doc/ci/pipelines/index.md | 2 +- doc/ci/pipelines/pipeline_architectures.md | 2 +- doc/ci/pipelines/pipeline_efficiency.md | 2 +- doc/ci/quick_start/index.md | 2 +- doc/ci/runners/configure_runners.md | 2 +- doc/ci/yaml/index.md | 6 +- doc/ci/yaml/needs.md | 88 +++ doc/development/custom_models/index.md | 2 +- doc/integration/google_cloud_iam.md | 2 +- doc/subscriptions/subscription-add-ons.md | 6 + doc/tutorials/secure_application.md | 1 + doc/user/custom_roles/abilities.md | 2 +- doc/user/project/code_intelligence.md | 32 +- .../repository/code_suggestions/index.md | 7 + lib/api/projects.rb | 2 +- ...kfill_or_drop_ci_pipeline_on_project_id.rb | 2 + locale/gitlab.pot | 33 - package.json | 2 +- .../gitlab_migration_large_project_spec.rb | 18 +- .../__snapshots__/dag_graph_spec.js.snap | 743 ------------------ .../dag/components/dag_annotations_spec.js | 98 --- .../dag/components/dag_graph_spec.js | 209 ----- .../ci/pipeline_details/dag/dag_spec.js | 187 ----- .../{dag => }/utils/drawing_utils_spec.js | 4 +- .../{dag => utils}/mock_data.js | 0 .../utils/parsing_utils_spec.js | 25 +- ..._or_drop_ci_pipeline_on_project_id_spec.rb | 15 + ...06133040_fix_non_nullable_snippets_spec.rb | 51 ++ ...llify_organization_id_for_snippets_spec.rb | 24 + spec/requests/jwt_controller_spec.rb | 12 + .../cleanup_package_file_worker_spec.rb | 12 + yarn.lock | 8 +- 67 files changed, 626 insertions(+), 2603 deletions(-) delete mode 100644 app/assets/javascripts/ci/pipeline_details/dag/components/dag_annotations.vue delete mode 100644 app/assets/javascripts/ci/pipeline_details/dag/components/dag_graph.vue delete mode 100644 app/assets/javascripts/ci/pipeline_details/dag/constants.js delete mode 100644 app/assets/javascripts/ci/pipeline_details/dag/dag.vue delete mode 100644 app/assets/javascripts/ci/pipeline_details/dag/graphql/queries/get_dag_vis_data.query.graphql delete mode 100644 app/assets/javascripts/ci/pipeline_details/dag/utils/drawing_utils.js delete mode 100644 app/assets/javascripts/ci/pipeline_details/dag/utils/interactions.js create mode 100644 db/post_migrate/20240906133020_add_temporary_index_for_snippets.rb create mode 100644 db/post_migrate/20240906133040_fix_non_nullable_snippets.rb create mode 100644 db/post_migrate/20240906133050_remove_temporary_index_for_snippets.rb create mode 100644 db/post_migrate/20240906133341_finalize_nullify_organization_id_for_snippets.rb create mode 100644 db/schema_migrations/20240906133020 create mode 100644 db/schema_migrations/20240906133040 create mode 100644 db/schema_migrations/20240906133050 create mode 100644 db/schema_migrations/20240906133341 delete mode 100644 doc/ci/directed_acyclic_graph/img/dag_graph_example_clicked_v13_1.png delete mode 100644 doc/ci/directed_acyclic_graph/img/dag_graph_example_v13_1.png create mode 100644 doc/ci/yaml/needs.md delete mode 100644 spec/frontend/ci/pipeline_details/dag/components/__snapshots__/dag_graph_spec.js.snap delete mode 100644 spec/frontend/ci/pipeline_details/dag/components/dag_annotations_spec.js delete mode 100644 spec/frontend/ci/pipeline_details/dag/components/dag_graph_spec.js delete mode 100644 spec/frontend/ci/pipeline_details/dag/dag_spec.js rename spec/frontend/ci/pipeline_details/{dag => }/utils/drawing_utils_spec.js (93%) rename spec/frontend/ci/pipeline_details/{dag => utils}/mock_data.js (100%) create mode 100644 spec/migrations/db/post_migrate/20240906133040_fix_non_nullable_snippets_spec.rb create mode 100644 spec/migrations/db/post_migrate/20240906133341_finalize_nullify_organization_id_for_snippets_spec.rb diff --git a/.rubocop_todo/gitlab/bounded_contexts.yml b/.rubocop_todo/gitlab/bounded_contexts.yml index 3d4d492ac13..f9fc939376b 100644 --- a/.rubocop_todo/gitlab/bounded_contexts.yml +++ b/.rubocop_todo/gitlab/bounded_contexts.yml @@ -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' diff --git a/app/assets/javascripts/ci/pipeline_details/constants.js b/app/assets/javascripts/ci/pipeline_details/constants.js index b59125fffee..3f69fe88919 100644 --- a/app/assets/javascripts/ci/pipeline_details/constants.js +++ b/app/assets/javascripts/ci/pipeline_details/constants.js @@ -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, diff --git a/app/assets/javascripts/ci/pipeline_details/dag/components/dag_annotations.vue b/app/assets/javascripts/ci/pipeline_details/dag/components/dag_annotations.vue deleted file mode 100644 index 5022557f3d1..00000000000 --- a/app/assets/javascripts/ci/pipeline_details/dag/components/dag_annotations.vue +++ /dev/null @@ -1,69 +0,0 @@ - - diff --git a/app/assets/javascripts/ci/pipeline_details/dag/components/dag_graph.vue b/app/assets/javascripts/ci/pipeline_details/dag/components/dag_graph.vue deleted file mode 100644 index 02dd2b1ed01..00000000000 --- a/app/assets/javascripts/ci/pipeline_details/dag/components/dag_graph.vue +++ /dev/null @@ -1,317 +0,0 @@ - - diff --git a/app/assets/javascripts/ci/pipeline_details/dag/constants.js b/app/assets/javascripts/ci/pipeline_details/dag/constants.js deleted file mode 100644 index cd89055737f..00000000000 --- a/app/assets/javascripts/ci/pipeline_details/dag/constants.js +++ /dev/null @@ -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'; diff --git a/app/assets/javascripts/ci/pipeline_details/dag/dag.vue b/app/assets/javascripts/ci/pipeline_details/dag/dag.vue deleted file mode 100644 index 9af967ceddf..00000000000 --- a/app/assets/javascripts/ci/pipeline_details/dag/dag.vue +++ /dev/null @@ -1,306 +0,0 @@ - - - diff --git a/app/assets/javascripts/ci/pipeline_details/dag/graphql/queries/get_dag_vis_data.query.graphql b/app/assets/javascripts/ci/pipeline_details/dag/graphql/queries/get_dag_vis_data.query.graphql deleted file mode 100644 index 2a0b13dd0cc..00000000000 --- a/app/assets/javascripts/ci/pipeline_details/dag/graphql/queries/get_dag_vis_data.query.graphql +++ /dev/null @@ -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 - } - } - } - } - } - } - } - } - } - } -} diff --git a/app/assets/javascripts/ci/pipeline_details/dag/utils/drawing_utils.js b/app/assets/javascripts/ci/pipeline_details/dag/utils/drawing_utils.js deleted file mode 100644 index 3cd09d57ffb..00000000000 --- a/app/assets/javascripts/ci/pipeline_details/dag/utils/drawing_utils.js +++ /dev/null @@ -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', - }; -}; diff --git a/app/assets/javascripts/ci/pipeline_details/dag/utils/interactions.js b/app/assets/javascripts/ci/pipeline_details/dag/utils/interactions.js deleted file mode 100644 index c0b590af6a1..00000000000 --- a/app/assets/javascripts/ci/pipeline_details/dag/utils/interactions.js +++ /dev/null @@ -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); -}; diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_tabs.js b/app/assets/javascripts/ci/pipeline_details/pipeline_tabs.js index e33a8c8a546..f71cba2b3ba 100644 --- a/app/assets/javascripts/ci/pipeline_details/pipeline_tabs.js +++ b/app/assets/javascripts/ci/pipeline_details/pipeline_tabs.js @@ -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, diff --git a/app/assets/javascripts/ci/pipeline_details/routes.js b/app/assets/javascripts/ci/pipeline_details/routes.js index aa4293f48c0..8935bbc1394 100644 --- a/app/assets/javascripts/ci/pipeline_details/routes.js +++ b/app/assets/javascripts/ci/pipeline_details/routes.js @@ -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 }, diff --git a/app/assets/javascripts/ci/pipeline_details/utils/drawing_utils.js b/app/assets/javascripts/ci/pipeline_details/utils/drawing_utils.js index d6d9ea94c13..07f27a7d7f6 100644 --- a/app/assets/javascripts/ci/pipeline_details/utils/drawing_utils.js +++ b/app/assets/javascripts/ci/pipeline_details/utils/drawing_utils.js @@ -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}`; diff --git a/app/assets/javascripts/ci/pipeline_details/utils/parsing_utils.js b/app/assets/javascripts/ci/pipeline_details/utils/parsing_utils.js index 0a2a6d16498..4a7229e433f 100644 --- a/app/assets/javascripts/ci/pipeline_details/utils/parsing_utils.js +++ b/app/assets/javascripts/ci/pipeline_details/utils/parsing_utils.js @@ -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 diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 23dc6139459..6d546f3fd63 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -50,7 +50,7 @@ html { } .alert-wrapper { - .gl-alert:first-child { + &:not(:empty) { @apply gl-mt-3; } diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 0fa33e20a73..0df806ae70b 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -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 diff --git a/app/views/layouts/header/_openssl_callout.html.haml b/app/views/layouts/header/_openssl_callout.html.haml index 0963ff21d14..fa9282f7f0c 100644 --- a/app/views/layouts/header/_openssl_callout.html.haml +++ b/app/views/layouts/header/_openssl_callout.html.haml @@ -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| diff --git a/app/workers/packages/cleanup_package_file_worker.rb b/app/workers/packages/cleanup_package_file_worker.rb index 68a145920ca..5264d8736d9 100644 --- a/app/workers/packages/cleanup_package_file_worker.rb +++ b/app/workers/packages/cleanup_package_file_worker.rb @@ -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? diff --git a/db/docs/batched_background_migrations/nullify_organization_id_for_snippets.yml b/db/docs/batched_background_migrations/nullify_organization_id_for_snippets.yml index 39602957ce7..119f6d8ff8a 100644 --- a/db/docs/batched_background_migrations/nullify_organization_id_for_snippets.yml +++ b/db/docs/batched_background_migrations/nullify_organization_id_for_snippets.yml @@ -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' diff --git a/db/post_migrate/20240906133020_add_temporary_index_for_snippets.rb b/db/post_migrate/20240906133020_add_temporary_index_for_snippets.rb new file mode 100644 index 00000000000..89be9fc31af --- /dev/null +++ b/db/post_migrate/20240906133020_add_temporary_index_for_snippets.rb @@ -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 diff --git a/db/post_migrate/20240906133040_fix_non_nullable_snippets.rb b/db/post_migrate/20240906133040_fix_non_nullable_snippets.rb new file mode 100644 index 00000000000..67d485986c1 --- /dev/null +++ b/db/post_migrate/20240906133040_fix_non_nullable_snippets.rb @@ -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 diff --git a/db/post_migrate/20240906133050_remove_temporary_index_for_snippets.rb b/db/post_migrate/20240906133050_remove_temporary_index_for_snippets.rb new file mode 100644 index 00000000000..d1ee30f5b2d --- /dev/null +++ b/db/post_migrate/20240906133050_remove_temporary_index_for_snippets.rb @@ -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 diff --git a/db/post_migrate/20240906133341_finalize_nullify_organization_id_for_snippets.rb b/db/post_migrate/20240906133341_finalize_nullify_organization_id_for_snippets.rb new file mode 100644 index 00000000000..b5f620250f7 --- /dev/null +++ b/db/post_migrate/20240906133341_finalize_nullify_organization_id_for_snippets.rb @@ -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 diff --git a/db/schema_migrations/20240906133020 b/db/schema_migrations/20240906133020 new file mode 100644 index 00000000000..ca120bfc98c --- /dev/null +++ b/db/schema_migrations/20240906133020 @@ -0,0 +1 @@ +ed8265250fec53324c839f5ea5e9405ec8a74ab1777010b3a93b5f63d37c646f \ No newline at end of file diff --git a/db/schema_migrations/20240906133040 b/db/schema_migrations/20240906133040 new file mode 100644 index 00000000000..7164ae95d13 --- /dev/null +++ b/db/schema_migrations/20240906133040 @@ -0,0 +1 @@ +151fef83e897583bb108b74a181063f8e73e18d2bafda6db6c8eff158263a89e \ No newline at end of file diff --git a/db/schema_migrations/20240906133050 b/db/schema_migrations/20240906133050 new file mode 100644 index 00000000000..272471f066f --- /dev/null +++ b/db/schema_migrations/20240906133050 @@ -0,0 +1 @@ +e40f25686e1352ddff17405937fb1b973d879bdab9d10c8cb080b5d5d9868139 \ No newline at end of file diff --git a/db/schema_migrations/20240906133341 b/db/schema_migrations/20240906133341 new file mode 100644 index 00000000000..6eb3c8ea67f --- /dev/null +++ b/db/schema_migrations/20240906133341 @@ -0,0 +1 @@ +b33ed25f04eb775aa69aa1e27b6da5ba2a46d5c8754de16092091c65e8a93c89 \ No newline at end of file diff --git a/doc/administration/self_hosted_models/configure_duo_features.md b/doc/administration/self_hosted_models/configure_duo_features.md index 986115ef0e5..9c038477495 100644 --- a/doc/administration/self_hosted_models/configure_duo_features.md +++ b/doc/administration/self_hosted_models/configure_duo_features.md @@ -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**. diff --git a/doc/administration/self_hosted_models/index.md b/doc/administration/self_hosted_models/index.md index 4d6a9dbb2eb..baa91ad1c9a 100644 --- a/doc/administration/self_hosted_models/index.md +++ b/doc/administration/self_hosted_models/index.md @@ -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 diff --git a/doc/administration/self_hosted_models/install_infrastructure.md b/doc/administration/self_hosted_models/install_infrastructure.md index 2cf500e12ae..96bd42d8d81 100644 --- a/doc/administration/self_hosted_models/install_infrastructure.md +++ b/doc/administration/self_hosted_models/install_infrastructure.md @@ -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) + +For an installation video guide, see [Self-Hosted Models Deployment](https://youtu.be/UNmD9-sgUvw). + -## Step 1: Install LLM serving infrastructure + +For an installation video guide in French, see [Self-Hosted Models Deployment (French Language version)](https://youtu.be/UNmD9-sgUvw). + + +## 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:// ``` -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= ``` -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:///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 image’s 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= -``` +For more information on other actions to take, see the +[troubleshooting documentation](troubleshooting.md). diff --git a/doc/administration/self_hosted_models/troubleshooting.md b/doc/administration/self_hosted_models/troubleshooting.md index bb69a773d84..eeee10486b4 100644 --- a/doc/administration/self_hosted_models/troubleshooting.md +++ b/doc/administration/self_hosted_models/troubleshooting.md @@ -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 = "" @@ -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("").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"] == "" ``` -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('/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: -- `` with the name of the model you are using, for example `mistral` or `codegemma`. +- `` with the name of the model you are using. For example `mistral` or `codegemma`. - `` 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": "", "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 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 image’s 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= +``` diff --git a/doc/api/plan_limits.md b/doc/api/plan_limits.md index b0eae280f41..cc445d8c4e3 100644 --- a/doc/api/plan_limits.md +++ b/doc/api/plan_limits.md @@ -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. | diff --git a/doc/api/projects.md b/doc/api/projects.md index df11ca3bafa..12cc38e63bc 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -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: " \ + --header "Content-Type: application/json" \ + --data '{ + "file_path":"src/main.c", + "content":"#include\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: diff --git a/doc/ci/directed_acyclic_graph/img/dag_graph_example_clicked_v13_1.png b/doc/ci/directed_acyclic_graph/img/dag_graph_example_clicked_v13_1.png deleted file mode 100644 index 3610d471ef4bfd75ada7be809209e6f97aeade1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54491 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 z 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 NIy?TltJt0^_&?gf6g&U` diff --git a/doc/ci/directed_acyclic_graph/img/dag_graph_example_v13_1.png b/doc/ci/directed_acyclic_graph/img/dag_graph_example_v13_1.png deleted file mode 100644 index bd5215d31c86934bee0a89afb91c8fbd7d372785..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54919 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 U+VDKz*#8S{ZF$Jzi5WfNf7k_asQ>@~ diff --git a/doc/ci/directed_acyclic_graph/index.md b/doc/ci/directed_acyclic_graph/index.md index d2ef02e0e06..86001ecd5bb 100644 --- a/doc/ci/directed_acyclic_graph/index.md +++ b/doc/ci/directed_acyclic_graph/index.md @@ -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. - - -## 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) - + + + + diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md index 7581f0f4e9f..44103b9fddc 100644 --- a/doc/ci/pipelines/index.md +++ b/doc/ci/pipelines/index.md @@ -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). diff --git a/doc/ci/pipelines/pipeline_architectures.md b/doc/ci/pipelines/pipeline_architectures.md index 2728b05ad99..cdfe49c79c8 100644 --- a/doc/ci/pipelines/pipeline_architectures.md +++ b/doc/ci/pipelines/pipeline_architectures.md @@ -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. diff --git a/doc/ci/pipelines/pipeline_efficiency.md b/doc/ci/pipelines/pipeline_efficiency.md index 55904dd70c5..a97f3234015 100644 --- a/doc/ci/pipelines/pipeline_efficiency.md +++ b/doc/ci/pipelines/pipeline_efficiency.md @@ -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. diff --git a/doc/ci/quick_start/index.md b/doc/ci/quick_start/index.md index 1ff6de3170c..37cd2cd0197 100644 --- a/doc/ci/quick_start/index.md +++ b/doc/ci/quick_start/index.md @@ -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. diff --git a/doc/ci/runners/configure_runners.md b/doc/ci/runners/configure_runners.md index e4efc307ef2..3db5c403431 100644 --- a/doc/ci/runners/configure_runners.md +++ b/doc/ci/runners/configure_runners.md @@ -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/). diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 52e12fd5d43..cf95c6357fa 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -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`. diff --git a/doc/ci/yaml/needs.md b/doc/ci/yaml/needs.md new file mode 100644 index 00000000000..78fe1fb2706 --- /dev/null +++ b/doc/ci/yaml/needs.md @@ -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. + + +## 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. + diff --git a/doc/development/custom_models/index.md b/doc/development/custom_models/index.md index 03db4657f6a..7cfdbf6aa22 100644 --- a/doc/development/custom_models/index.md +++ b/doc/development/custom_models/index.md @@ -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. diff --git a/doc/integration/google_cloud_iam.md b/doc/integration/google_cloud_iam.md index 35b4227a065..e8750933ed8 100644 --- a/doc/integration/google_cloud_iam.md +++ b/doc/integration/google_cloud_iam.md @@ -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 diff --git a/doc/subscriptions/subscription-add-ons.md b/doc/subscriptions/subscription-add-ons.md index ef6a8c4e986..7c3c162fa5d 100644 --- a/doc/subscriptions/subscription-add-ons.md +++ b/doc/subscriptions/subscription-add-ons.md @@ -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, diff --git a/doc/tutorials/secure_application.md b/doc/tutorials/secure_application.md index d44673e3879..0dabcaec383 100644 --- a/doc/tutorials/secure_application.md +++ b/doc/tutorials/secure_application.md @@ -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. | | diff --git a/doc/user/custom_roles/abilities.md b/doc/user/custom_roles/abilities.md index a9ffbd4252b..6a9e6046266 100644 --- a/doc/user/custom_roles/abilities.md +++ b/doc/user/custom_roles/abilities.md @@ -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 diff --git a/doc/user/project/code_intelligence.md b/doc/user/project/code_intelligence.md index 7b9eb5dcae5..0229d0cbd56 100644 --- a/doc/user/project/code_intelligence.md +++ b/doc/user/project/code_intelligence.md @@ -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 diff --git a/doc/user/project/repository/code_suggestions/index.md b/doc/user/project/repository/code_suggestions/index.md index cd9dd830aa5..bdc03929f04 100644 --- a/doc/user/project/repository/code_suggestions/index.md +++ b/doc/user/project/repository/code_suggestions/index.md @@ -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. diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 16f3d4c06be..0cb85a92a1a 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -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]) diff --git a/lib/gitlab/background_migration/backfill_or_drop_ci_pipeline_on_project_id.rb b/lib/gitlab/background_migration/backfill_or_drop_ci_pipeline_on_project_id.rb index 66794ccba7d..6010f13af75 100644 --- a/lib/gitlab/background_migration/backfill_or_drop_ci_pipeline_on_project_id.rb +++ b/lib/gitlab/background_migration/backfill_or_drop_ci_pipeline_on_project_id.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 321ac01bcbb..f69a8651eed 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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, you’ll 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 "" diff --git a/package.json b/package.json index 1d275f544ae..27884bc2799 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb index dab1b379ee6..f3778f06334 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb @@ -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] diff --git a/spec/frontend/ci/pipeline_details/dag/components/__snapshots__/dag_graph_spec.js.snap b/spec/frontend/ci/pipeline_details/dag/components/__snapshots__/dag_graph_spec.js.snap deleted file mode 100644 index fe22a89ff68..00000000000 --- a/spec/frontend/ci/pipeline_details/dag/components/__snapshots__/dag_graph_spec.js.snap +++ /dev/null @@ -1,743 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`The DAG graph in the basic case renders the graph svg 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- build_a -
-
- -
- test_a -
-
- -
- test_b -
-
- -
- post_test_a -
-
- -
- post_test_b -
-
- -
- post_test_c -
-
- -
- staging_a -
-
- -
- staging_b -
-
- -
- canary_a -
-
- -
- canary_c -
-
- -
- production_a -
-
- -
- production_d -
-
-
-
-`; diff --git a/spec/frontend/ci/pipeline_details/dag/components/dag_annotations_spec.js b/spec/frontend/ci/pipeline_details/dag/components/dag_annotations_spec.js deleted file mode 100644 index d1c338e50c6..00000000000 --- a/spec/frontend/ci/pipeline_details/dag/components/dag_annotations_spec.js +++ /dev/null @@ -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'); - }); - }); - }); -}); diff --git a/spec/frontend/ci/pipeline_details/dag/components/dag_graph_spec.js b/spec/frontend/ci/pipeline_details/dag/components/dag_graph_spec.js deleted file mode 100644 index aff83c00e79..00000000000 --- a/spec/frontend/ci/pipeline_details/dag/components/dag_graph_spec.js +++ /dev/null @@ -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); - }); - }); - }); - }); -}); diff --git a/spec/frontend/ci/pipeline_details/dag/dag_spec.js b/spec/frontend/ci/pipeline_details/dag/dag_spec.js deleted file mode 100644 index 37fbc486bc7..00000000000 --- a/spec/frontend/ci/pipeline_details/dag/dag_spec.js +++ /dev/null @@ -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); - }); - }); -}); diff --git a/spec/frontend/ci/pipeline_details/dag/utils/drawing_utils_spec.js b/spec/frontend/ci/pipeline_details/utils/drawing_utils_spec.js similarity index 93% rename from spec/frontend/ci/pipeline_details/dag/utils/drawing_utils_spec.js rename to spec/frontend/ci/pipeline_details/utils/drawing_utils_spec.js index aea8e894bd4..39c776f726f 100644 --- a/spec/frontend/ci/pipeline_details/dag/utils/drawing_utils_spec.js +++ b/spec/frontend/ci/pipeline_details/utils/drawing_utils_spec.js @@ -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); diff --git a/spec/frontend/ci/pipeline_details/dag/mock_data.js b/spec/frontend/ci/pipeline_details/utils/mock_data.js similarity index 100% rename from spec/frontend/ci/pipeline_details/dag/mock_data.js rename to spec/frontend/ci/pipeline_details/utils/mock_data.js diff --git a/spec/frontend/ci/pipeline_details/utils/parsing_utils_spec.js b/spec/frontend/ci/pipeline_details/utils/parsing_utils_spec.js index 9390f076d3d..d182cd213e5 100644 --- a/spec/frontend/ci/pipeline_details/utils/parsing_utils_spec.js +++ b/spec/frontend/ci/pipeline_details/utils/parsing_utils_spec.js @@ -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 = [ diff --git a/spec/lib/gitlab/background_migration/backfill_or_drop_ci_pipeline_on_project_id_spec.rb b/spec/lib/gitlab/background_migration/backfill_or_drop_ci_pipeline_on_project_id_spec.rb index f9a4dfd6519..2aea40ad51e 100644 --- a/spec/lib/gitlab/background_migration/backfill_or_drop_ci_pipeline_on_project_id_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_or_drop_ci_pipeline_on_project_id_spec.rb @@ -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) diff --git a/spec/migrations/db/post_migrate/20240906133040_fix_non_nullable_snippets_spec.rb b/spec/migrations/db/post_migrate/20240906133040_fix_non_nullable_snippets_spec.rb new file mode 100644 index 00000000000..3d8212714d2 --- /dev/null +++ b/spec/migrations/db/post_migrate/20240906133040_fix_non_nullable_snippets_spec.rb @@ -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 diff --git a/spec/migrations/db/post_migrate/20240906133341_finalize_nullify_organization_id_for_snippets_spec.rb b/spec/migrations/db/post_migrate/20240906133341_finalize_nullify_organization_id_for_snippets_spec.rb new file mode 100644 index 00000000000..34448704791 --- /dev/null +++ b/spec/migrations/db/post_migrate/20240906133341_finalize_nullify_organization_id_for_snippets_spec.rb @@ -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 diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 5a48c52a1f0..bc2bd90bed2 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -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 } diff --git a/spec/workers/packages/cleanup_package_file_worker_spec.rb b/spec/workers/packages/cleanup_package_file_worker_spec.rb index 6e42565abbc..7146199df2f 100644 --- a/spec/workers/packages/cleanup_package_file_worker_spec.rb +++ b/spec/workers/packages/cleanup_package_file_worker_spec.rb @@ -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 diff --git a/yarn.lock b/yarn.lock index 7b94097efda..a9b281c6065 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"