Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-06-18 21:12:36 +00:00
parent 172925dab8
commit 0ad218275c
74 changed files with 1637 additions and 53 deletions

View File

@ -105,8 +105,6 @@ gdk:compile-test-assets:
extends:
- compile-test-assets
- .build-images:rules:build-gdk-image
variables:
BABEL_ENV: "istanbul"
update-assets-compile-production-cache:
extends:

View File

@ -1144,6 +1144,10 @@
- !reference [".qa:rules:e2e-test-never-run", rules]
- <<: *if-default-branch-schedule-nightly # already executed in the 2-hourly schedule
when: never
- <<: *if-dot-com-gitlab-org-schedule
variables:
BABEL_ENV: "istanbul"
CACHE_ASSETS_AS_PACKAGE: "false"
- <<: *if-default-branch-refs
- <<: *if-merge-request-labels-run-all-e2e
- <<: *if-merge-request-labels-run-cs-evaluation

View File

@ -1,5 +1,5 @@
<script>
import { GlCollapsibleListbox } from '@gitlab/ui';
import { GlCollapsibleListbox, GlButton } from '@gitlab/ui';
import { isEqual, debounce } from 'lodash';
import EMPTY_VARIABLES_SVG from '@gitlab/svgs/dist/illustrations/variables-sm.svg';
import { s__ } from '~/locale';
@ -10,6 +10,7 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import InputsTableSkeletonLoader from './pipeline_inputs_table/inputs_table_skeleton_loader.vue';
import PipelineInputsTable from './pipeline_inputs_table/pipeline_inputs_table.vue';
import getPipelineInputsQuery from './graphql/queries/pipeline_creation_inputs.query.graphql';
import PipelineInputsPreviewDrawer from './pipeline_inputs_preview_drawer.vue';
const ARRAY_TYPE = 'ARRAY';
@ -20,6 +21,8 @@ export default {
InputsTableSkeletonLoader,
PipelineInputsTable,
GlCollapsibleListbox,
GlButton,
PipelineInputsPreviewDrawer,
},
inject: ['projectPath'],
props: {
@ -53,6 +56,7 @@ export default {
inputs: [],
selectedInputNames: [],
searchTerm: '',
showPreviewDrawer: false,
};
},
apollo: {
@ -285,6 +289,16 @@ export default {
:title="s__('Pipelines|Inputs')"
>
<template #actions>
<gl-button
category="secondary"
variant="confirm"
size="small"
:disabled="!hasInputs"
@click="showPreviewDrawer = true"
>
{{ s__('Pipelines|Preview inputs') }}
</gl-button>
<gl-collapsible-listbox
v-model="selectedInputNames"
:items="filteredInputsList"
@ -328,5 +342,11 @@ export default {
{{ s__('Pipelines|There are no inputs for this configuration.') }}
</div>
</template>
<pipeline-inputs-preview-drawer
:open="showPreviewDrawer"
:inputs="inputs"
@close="showPreviewDrawer = false"
/>
</crud-component>
</template>

View File

@ -0,0 +1,67 @@
<script>
import { GlDrawer } from '@gitlab/ui';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { formatInputsForDisplay } from './utils';
export default {
components: {
GlDrawer,
},
props: {
open: {
type: Boolean,
required: true,
},
inputs: {
type: Array,
required: true,
},
},
computed: {
getDrawerHeaderHeight() {
return getContentWrapperHeight();
},
formattedLines() {
return formatInputsForDisplay(this.inputs);
},
},
methods: {
onClose() {
this.$emit('close');
},
},
DRAWER_Z_INDEX,
};
</script>
<template>
<gl-drawer
:open="open"
:header-height="getDrawerHeaderHeight"
:z-index="$options.DRAWER_Z_INDEX"
@close="onClose"
>
<template #title>
<h2 class="gl-my-0 gl-text-size-h2 gl-leading-24">
{{ s__('Pipelines|Preview your inputs') }}
</h2>
</template>
<div>
<p>{{ s__('Pipelines|The pipeline will run with these inputs:') }}</p>
<div class="file-content code" data-testid="inputs-code-block">
<pre class="!gl-border-1 !gl-border-solid !gl-border-subtle gl-p-3"><div
v-for="(line, index) in formattedLines"
:key="index"
:class="{
'gl-text-danger': line.type === 'old',
'gl-text-success': line.type === 'new'
}"
class="line"
data-testid="inputs-code-line"
>{{ line.content }}</div></pre>
</div>
</div>
</gl-drawer>
</template>

View File

@ -1,3 +1,5 @@
import { isEqual } from 'lodash';
/**
* Generates skeleton rect props for the skeleton loader based on column and row indices
*
@ -15,3 +17,124 @@ export const getSkeletonRectProps = (columnIndex, rowIndex) => {
ry: 2,
};
};
/**
* Formats a value for display in the input preview
* @param {*} value - The value to format
* @returns {string} - Formatted string representation
*/
export function formatValue(value) {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (typeof value === 'string') return `"${value}"`;
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
}
/**
* Checks if an input value has changed from its default
* @param {*} value - Current value
* @param {*} defaultValue - Default value
* @returns {boolean} - True if values are different
*/
export function hasValueChanged(value, defaultValue) {
if (typeof value === 'object' || typeof defaultValue === 'object') {
return !isEqual(value, defaultValue);
}
return value !== defaultValue;
}
/**
* Formats value lines for diff display
* @param {Object} input - Input object with value, default, etc.
* @param {boolean} isChanged - Whether the value has changed
* @returns {Array} - Array of line objects
*/
export function formatValueLines(input, isChanged) {
const lines = [];
const formattedValue = formatValue(input.value);
const formattedDefault = formatValue(input.default);
if (isChanged) {
lines.push({
// eslint-disable-next-line @gitlab/require-i18n-strings
content: `- value: ${formattedDefault}`,
type: 'old',
});
lines.push({
// eslint-disable-next-line @gitlab/require-i18n-strings
content: `+ value: ${formattedValue}`,
type: 'new',
});
} else {
lines.push({
// eslint-disable-next-line @gitlab/require-i18n-strings
content: ` value: ${formattedValue}`,
type: '',
});
}
return lines;
}
/**
* Formats metadata lines (type, description)
* @param {Object} input - Input object
* @returns {Array} - Array of line objects
*/
export function formatMetadataLines(input) {
const lines = [];
if (input.type) {
lines.push({
// eslint-disable-next-line @gitlab/require-i18n-strings
content: ` type: "${input.type}"`,
type: '',
});
}
if (input.description) {
lines.push({
// eslint-disable-next-line @gitlab/require-i18n-strings
content: ` description: "${input.description}"`,
type: '',
});
}
return lines;
}
/**
* Formats a single input into display lines
* @param {Object} input - Input object
* @returns {Array} - Array of line objects
*/
export function formatInputLines(input) {
const lines = [];
const isChanged = hasValueChanged(input.value, input.default);
lines.push({
content: `${input.name}:`,
type: '',
});
lines.push(...formatValueLines(input, isChanged));
lines.push(...formatMetadataLines(input));
lines.push({
content: '',
type: '',
});
return lines;
}
/**
* Formats all inputs into display lines
* @param {Array} inputs - Array of input objects
* @returns {Array} - Array of line objects for display
*/
export function formatInputsForDisplay(inputs) {
return inputs.flatMap(formatInputLines);
}

View File

@ -132,7 +132,7 @@ export default {
data-testid="ci-action-button"
@click.prevent="onActionButtonClick"
>
<gl-loading-icon v-if="isLoading" size="sm" class="gl-m-2" />
<gl-loading-icon v-if="isLoading" size="sm" />
<gl-icon v-else :name="jobAction.icon" :size="12" />
</gl-button>
<job-action-modal

View File

@ -7,7 +7,11 @@ import Api from '~/api';
import Tracking from '~/tracking';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { n__, sprintf } from '~/locale';
import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils';
import {
memberName,
triggerExternalAlert,
baseBindingAttributes,
} from 'ee_else_ce/invite_members/utils/member_utils';
import { responseFromSuccess } from 'ee_else_ce/invite_members/utils/response_message_parser';
import { captureException } from '~/ci/runner/sentry_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
@ -201,6 +205,9 @@ export default {
primaryButtonText() {
return this.hasBsoEnabled ? BLOCKED_SEAT_OVERAGES_CTA_DOCS : BLOCKED_SEAT_OVERAGES_CTA;
},
baseBindingAttributes() {
return baseBindingAttributes(this.hasInvalidMembers);
},
},
watch: {
isEmptyInvites: {
@ -389,6 +396,7 @@ export default {
:root-group-id="rootId"
:users-limit-dataset="usersLimitDataset"
:full-path="fullPath"
v-bind="baseBindingAttributes"
@close="onClose"
@cancel="onCancel"
@reset="resetFields"

View File

@ -18,3 +18,7 @@ export function searchUsers(url, search) {
export function triggerExternalAlert() {
return false;
}
export function baseBindingAttributes() {
return {};
}

View File

@ -107,6 +107,7 @@ export async function mountIssuesListApp() {
releasesPath,
resetPath,
rssPath,
projectNamespaceFullPath,
showNewIssueLink,
signInPath,
wiCanAdminLabel,
@ -179,6 +180,7 @@ export async function mountIssuesListApp() {
newProjectPath,
releasesPath,
rssPath,
projectNamespaceFullPath,
showNewIssueLink: parseBoolean(showNewIssueLink),
signInPath,
// For CsvImportExportButtons component

View File

@ -244,10 +244,13 @@ export default {
<template v-if="isSubmodule">
@ <gl-link :href="submoduleTreeUrl" class="commit-sha">{{ shortSha }}</gl-link>
</template>
<!-- The z-index of the lock must be higher than tree-item-link::before in files.scss -->
<gl-icon
v-if="hasLockLabel"
v-gl-tooltip
class="gl-relative gl-z-1"
:title="commitData.lockLabel"
:aria-label="commitData.lockLabel"
name="lock"
:size="12"
/>

View File

@ -266,7 +266,7 @@ export default {
>
</template>
<template #append>
<gl-button type="cancel" data-testid="snippet-cancel-btn" :href="cancelButtonHref">
<gl-button data-testid="snippet-cancel-btn" :href="cancelButtonHref">
{{ __('Cancel') }}
</gl-button>
</template>

View File

@ -64,6 +64,7 @@ export default {
size="small"
placement="bottom-end"
searchable
data-testid="deploy-url-menu"
@search="search"
>
<template #list-item="{ item }">

View File

@ -103,7 +103,7 @@ export default {
<gl-button
v-for="(btn, index) in tertiaryButtons"
:id="btn.id"
:key="index"
:key="btn.id || index"
v-gl-tooltip.hover
:title="setTooltip(btn)"
:href="btn.href"

View File

@ -116,7 +116,17 @@ export default {
PageHeading,
},
mixins: [glFeatureFlagMixin()],
inject: ['groupPath', 'workItemPlanningViewEnabled'],
inject: {
groupPath: {
default: '',
},
projectNamespaceFullPath: {
default: '',
},
workItemPlanningViewEnabled: {
default: false,
},
},
i18n: {
suggestionTitle: s__('WorkItem|Similar items'),
similarWorkItemHelpText: s__(
@ -946,6 +956,7 @@ export default {
:full-path="fullPath"
:is-group="isGroup"
:current-project-name="namespaceFullName"
:project-namespace-full-path="projectNamespaceFullPath"
toggle-id="create-work-item-project"
/>
</gl-form-group>

View File

@ -44,6 +44,11 @@ export default {
required: false,
default: undefined,
},
projectNamespaceFullPath: {
required: false,
type: String,
default: '',
},
},
data() {
return {
@ -60,7 +65,9 @@ export default {
},
variables() {
return {
fullPath: this.fullPath,
// The `projectNamespaceFullPath` is available in case
// project belongs to user's personal namespace.
fullPath: this.projectNamespaceFullPath || this.fullPath,
projectSearch: this.searchKey,
};
},

View File

@ -60,6 +60,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType, withTabs } = {}
hasLinkedItemsEpicsFeature,
canCreateProjects,
newProjectPath,
projectNamespaceFullPath,
hasIssueDateFilterFeature,
timeTrackingLimitToHours,
hasStatusFeature,
@ -133,6 +134,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType, withTabs } = {}
canCreateProjects: parseBoolean(canCreateProjects),
newIssuePath: '',
newProjectPath,
projectNamespaceFullPath,
hasIssueDateFilterFeature: parseBoolean(hasIssueDateFilterFeature),
timeTrackingLimitToHours: parseBoolean(timeTrackingLimitToHours),
hasStatusFeature: parseBoolean(hasStatusFeature),

View File

@ -192,6 +192,7 @@ module IssuesHelper
quick_actions_help_path: help_page_path('user/project/quick_actions.md'),
releases_path: project_releases_path(project, format: :json),
reset_path: new_issuable_address_project_path(project, issuable_type: 'issue'),
project_namespace_full_path: project.namespace.full_path,
show_new_issue_link: show_new_issue_link?(project).to_s,
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s
)

View File

@ -26,6 +26,8 @@ module WorkItemsHelper
show_new_work_item: can?(current_user, :create_work_item, resource_parent).to_s,
can_create_projects: can?(current_user, :create_projects, group).to_s,
new_project_path: new_project_path(namespace_id: group&.id),
project_namespace_full_path:
resource_parent.is_a?(Project) ? resource_parent.namespace.full_path : resource_parent.full_path,
group_id: group&.id,
has_issue_date_filter_feature: has_issue_date_filter_feature?(resource_parent, current_user).to_s,
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s

View File

@ -0,0 +1,116 @@
# frozen_string_literal: true
module Ci
module Slsa
class ProvenanceStatement
include ActiveModel::Serializers::JSON
attr_accessor :_type, :subject, :predicate_type, :predicate
def self.from_build(build)
raise ArgumentError, "runner manager information not available in build" unless build.runner_manager
archives = build.job_artifacts.filter { |artifact| artifact.file_type == "archive" }
raise ArgumentError, 'artifacts associated with build do not contain a single archive' if archives.length != 1
archive = archives[0]
archive_resource = ResourceDescriptor.new(name: archive.file.filename, digest: { sha256: archive.file_sha256 })
provenance_statement = ProvenanceStatement.new
provenance_statement.subject = [archive_resource]
provenance_statement.predicate.build_definition = BuildDefinition.from_build(build)
provenance_statement.predicate.run_details = RunDetails.from_build(build)
provenance_statement
end
def initialize
@predicate = Predicate.new
@_type = "https://in-toto.io/Statement/v1"
@predicate_type = "https://slsa.dev/provenance/v1"
end
def as_json(options = nil)
json = super
exceptions = ["_type"]
json.deep_transform_keys do |k|
next k if exceptions.include?(k)
k.camelize(:lower)
end
end
def attributes
{ '_type' => nil, 'subject' => nil, 'predicate_type' => nil, 'predicate' => nil }
end
class BuildDefinition
include ActiveModel::Model
attr_accessor :build_type, :external_parameters, :internal_parameters, :resolved_dependencies
def self.from_build(build)
# TODO: update buildType as part of https://gitlab.com/gitlab-org/gitlab/-/issues/426764
build_type = "https://gitlab.com/gitlab-org/gitlab/-/issues/546150"
external_parameters = { variables: build.variables.map(&:key) }
internal_parameters = {
architecture: build.runner_manager.architecture,
executor: build.runner_manager.executor_type,
job: build.id,
name: build.runner.display_name
}
build_resource = ResourceDescriptor.new(uri: Gitlab::Routing.url_helpers.project_url(build.project),
digest: { gitCommit: build.sha })
resolved_dependencies = [build_resource]
BuildDefinition.new(build_type: build_type, external_parameters: external_parameters,
internal_parameters: internal_parameters,
resolved_dependencies: resolved_dependencies)
end
end
class RunDetails
include ActiveModel::Model
attr_accessor :builder, :metadata, :byproducts
def self.from_build(build)
builder = {
id: Gitlab::Routing.url_helpers.group_runner_url(build.runner.owner, build.runner),
version: {
"gitlab-runner": build.runner_manager.revision
}
}
metadata = {
invocationId: build.id,
# https://github.com/in-toto/attestation/blob/7aefca35a0f74a6e0cb397a8c4a76558f54de571/spec/v1/field_types.md#timestamp
startedOn: build.started_at&.utc&.rfc3339,
finishedOn: build.finished_at&.utc&.rfc3339
}
RunDetails.new(builder: builder, metadata: metadata)
end
end
class Builder
include ActiveModel::Model
attr_accessor :id, :builder_dependencies, :version
end
class BuildMetadata
include ActiveModel::Model
attr_accessor :invocation_id, :started_on, :finished_on
end
class Predicate
include ActiveModel::Model
attr_accessor :build_definition, :run_details
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Ci
module Slsa
class ResourceDescriptor
include ActiveModel::Model
attr_accessor :name, :digest, :uri
end
end
end

View File

@ -0,0 +1,170 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SLSA Provenance v1.0 Statement",
"description": "SLSA Provenance Statement conforming to v1.0 specification",
"type": "object",
"required": [
"_type",
"subject",
"predicateType",
"predicate"
],
"properties": {
"_type": {
"type": "string",
"const": "https://in-toto.io/Statement/v1"
},
"subject": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": [
"name",
"digest"
],
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"digest": {
"type": "object",
"required": [
"sha256"
],
"properties": {
"sha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
}
},
"additionalProperties": true
}
},
"additionalProperties": false
}
},
"predicateType": {
"type": "string",
"const": "https://slsa.dev/provenance/v1"
},
"predicate": {
"type": "object",
"required": [
"buildDefinition",
"runDetails"
],
"properties": {
"buildDefinition": {
"type": "object",
"required": [
"buildType",
"externalParameters"
],
"properties": {
"buildType": {
"type": "string",
"format": "uri"
},
"externalParameters": {
"type": "object"
},
"internalParameters": {
"type": "object"
},
"resolvedDependencies": {
"type": "array",
"items": {
"type": "object",
"properties": {
"uri": {
"type": "string",
"format": "uri"
},
"digest": {
"type": "object",
"properties": {
"sha256": {
"type": "string",
"pattern": "^[a-f0-9]{40,64}$"
}
}
}
}
}
}
},
"additionalProperties": false
},
"runDetails": {
"type": "object",
"required": [
"builder",
"metadata"
],
"properties": {
"builder": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"format": "uri"
},
"version": {
"type": "object"
},
"builderDependencies": {
"type": "array"
}
},
"additionalProperties": true
},
"metadata": {
"type": "object",
"required": [
"invocationId"
],
"properties": {
"invocationId": {
"oneOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"startedOn": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"finishedOn": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
"additionalProperties": true
},
"byproducts": {
"type": "array"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}

View File

@ -9,7 +9,7 @@
= header_message
= render 'peek/bar'
= render 'layouts/published_experiments'
= render "layouts/header/empty"
= render "layouts/header/empty" unless @hide_empty_navbar
.layout-page.gl-h-full.borderless.gl-flex.gl-flex-wrap
.content-wrapper.gl-pt-6{ class: 'md:!gl-pt-11' }
%div{ class: container_class }

View File

@ -57,7 +57,11 @@ namespace :admin do
put 'renew', on: :member
end
resources :groups, only: [:index, :new, :create]
resources :groups, only: [:index, :new, :create] do
collection do
get :all, :inactive, to: 'groups#index'
end
end
resources :organizations, only: [:index]

View File

@ -4,7 +4,6 @@ group: Pipeline Execution
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
description: Learn about building and testing your application.
title: Get started with GitLab CI/CD
description: Overview of how CI/CD features fit together.
---
{{< details >}}

View File

@ -326,6 +326,32 @@ To define the indexing timeout for a project:
(for example, `30m` (30 minutes), `2h` (two hours), or `1d` (one day)).
1. Select **Save changes**.
## Set the maximum number of files in a project to be indexed
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/539526) in GitLab 18.2.
{{< /history >}}
Prerequisites:
- You must have administrator access to the instance.
You can set the maximum number of files in a project that can be indexed.
Projects with more files than this limit in the default branch are not indexed.
The default value is `500,000`.
You can adjust this value based on the node's performance and workload.
To set the maximum number of files in a project to be indexed:
1. On the left sidebar, at the bottom, select **Admin**.
1. Select **Settings > Search**.
1. Expand **Exact code search configuration**.
1. In the **Maximum number of files per project to be indexed** text box, enter a number greater than zero.
1. Select **Save changes**.
## Define the retry interval for failed namespaces
{{< history >}}

View File

@ -4,7 +4,6 @@ group: Source Code
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
description: Learn about the Git version control system.
title: Get started with Git
description: Understand Git, install, common commands, and tutorial.
---
Git is a version control system you use to track changes to your code and collaborate with others.

View File

@ -4,7 +4,6 @@ group: Secret Detection
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
description: Learn about testing and resolving vulnerabilities.
title: Get started securing your application
description: Overview of how features fit together.
---
Identify and remediate vulnerabilities in your application's source code.

View File

@ -4,7 +4,6 @@ group: none
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
description: Learn how to deploy and manage dependencies.
title: Get started deploying and releasing your application
description: Overview of how features fit together.
---
Start with previewing your application, and end with deploying

View File

@ -4,7 +4,6 @@ group: Source Code
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
description: Learn how to build, track, and deliver the code for your project.
title: Get started managing code
description: Overview of how features fit together.
---
GitLab provides tools for the full software development lifecycle,

View File

@ -2,7 +2,7 @@
stage: Deploy
group: Environments
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
description: Overview of how features fit together.
description: Learn how to employ best practices for managing your infrastructure.
title: Get started managing your infrastructure
---

View File

@ -4,7 +4,6 @@ group: Platform Insights
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
description: Learn how to monitor your app and respond to incidents.
title: Get started with monitoring your application in GitLab
description: Overview of how features fit together.
---
Monitoring is a crucial part of maintaining and optimizing your applications.

View File

@ -4,7 +4,6 @@ group: Project Management
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
description: Learn how to plan and execute on work.
title: Get started planning work
description: Overview of how features fit together.
---
GitLab has tools to help you plan, execute, and track your work.

View File

@ -4,7 +4,6 @@ group: Organizations
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
description: Learn how to configure projects to suit your organization.
title: Get started organizing work with projects
description: Overview of how features fit together.
---
Projects in GitLab organize all the data for a specific development project.

View File

@ -1,11 +1,11 @@
---
redirect_to: '../_index.md'
redirect_to: '_index.md#epics-as-work-items'
remove_date: '2025-09-13'
---
<!-- markdownlint-disable -->
This document was moved to [another location](../_index.md).
This document was moved to [another location](_index.md#epics-as-work-items).
<!-- This redirect file can be deleted after <2025-09-13>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->

View File

@ -123,6 +123,75 @@ To enable `@GitLabDuo` to automatically review merge requests:
1. In the **GitLab Duo Code Review** section, select **Enable automatic reviews by GitLab Duo**.
1. Select **Save changes**.
## Customize instructions for GitLab Duo Code Review
{{< details >}}
- Tier: Premium, Ultimate
- Add-on: GitLab Duo Enterprise
- Offering: GitLab.com
- Status: Beta
- LLM: Anthropic [Claude 4.0 Sonnet](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-sonnet-4?inv=1&invt=Ab0dPw&project=ai-enablement-dev-69497ba7)
{{< /details >}}
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/545136) in GitLab 18.2 as a [beta](../../../policy/development_stages_support.md#beta) [with a flag](../../../administration/feature_flags/_index.md) named `duo_code_review_custom_instructions`. Disabled by default.
{{< /history >}}
{{< alert type="flag" >}}
The availability of this feature is controlled by a feature flag.
For more information, see the history.
{{< /alert >}}
GitLab Duo Code Review can help ensure consistent code review standards in your project.
Define a glob pattern for files, and create custom instructions for files matching that
pattern. For example, enforce Ruby style conventions only on Ruby files, and Go style
conventions on Go files. GitLab Duo appends your custom instructions to its standard review
criteria.
To configure custom instructions:
1. In the root of your repository, create a `.gitlab/duo` directory if it doesn't already exist.
1. In the `.gitlab/duo` directory, create a file named `mr-review-instructions.yaml`.
1. Add your custom instructions using this format:
```yaml
instructions:
- name: <instruction_group_name>
fileFilters:
- <glob_pattern_1>
- <glob_pattern_2>
instructions: |
<your_custom_review_instructions>
```
For example:
```yaml
instructions:
- name: Ruby Style Guide
fileFilters:
- "*.rb"
- "lib/**/*.rb"
instructions: |
1. Ensure all methods have proper documentation
2. Follow Ruby style guide conventions
3. Prefer symbols over strings for hash keys
- name: Test Coverage
fileFilters:
- "spec/**/*_spec.rb"
instructions: |
1. Test both happy paths and edge cases
2. Include error scenarios
3. Use shared examples to reduce duplication
```
## Summarize a code review
{{< details >}}

View File

@ -1587,6 +1587,9 @@ msgstr ""
msgid "%{title} webhook (for example, `%{example}`)."
msgstr ""
msgid "%{title}: %{exceptions}"
msgstr ""
msgid "%{tooltipText} image"
msgstr ""
@ -1754,6 +1757,9 @@ msgid_plural "(%d closed)"
msgstr[0] ""
msgstr[1] ""
msgid "(%{count}) %{exceptions}"
msgstr ""
msgid "(%{mrCount} merged)"
msgstr ""
@ -25696,6 +25702,11 @@ msgstr ""
msgid "Except policy:"
msgstr ""
msgid "Exception"
msgid_plural "Exceptions"
msgstr[0] ""
msgstr[1] ""
msgid "Exceptions"
msgstr ""
@ -31661,6 +31672,9 @@ msgstr ""
msgid "IdentityVerification|Country or region"
msgstr ""
msgid "IdentityVerification|Email Verification"
msgstr ""
msgid "IdentityVerification|Email update is only offered once."
msgstr ""
@ -31709,6 +31723,12 @@ msgstr ""
msgid "IdentityVerification|Maximum login attempts exceeded. Wait %{interval} and try again."
msgstr ""
msgid "IdentityVerification|Payment Method Verification"
msgstr ""
msgid "IdentityVerification|Phone Number Verification"
msgstr ""
msgid "IdentityVerification|Phone number"
msgstr ""
@ -31745,6 +31765,9 @@ msgstr ""
msgid "IdentityVerification|Something went wrong. Please try again."
msgstr ""
msgid "IdentityVerification|Step %{stepIndex} of %{totalSteps}"
msgstr ""
msgid "IdentityVerification|Step %{stepNumber}: Verify a payment method"
msgstr ""
@ -37619,6 +37642,9 @@ msgstr ""
msgid "Maximum number of comments exceeded"
msgstr ""
msgid "Maximum number of files per project to be indexed"
msgstr ""
msgid "Maximum number of mirrors that can be synchronizing at the same time."
msgstr ""
@ -45733,6 +45759,12 @@ msgstr ""
msgid "Pipelines|Pipeline syntax is correct. %{linkStart}Learn more%{linkEnd}"
msgstr ""
msgid "Pipelines|Preview inputs"
msgstr ""
msgid "Pipelines|Preview your inputs"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
msgstr ""
@ -45805,6 +45837,9 @@ msgstr ""
msgid "Pipelines|The branch or tag does not exist"
msgstr ""
msgid "Pipelines|The pipeline will run with these inputs:"
msgstr ""
msgid "Pipelines|There are currently no finished pipelines."
msgstr ""

View File

@ -65,7 +65,7 @@
"@gitlab/fonts": "^1.3.0",
"@gitlab/query-language-rust": "0.8.5",
"@gitlab/svgs": "3.134.0",
"@gitlab/ui": "113.7.0",
"@gitlab/ui": "114.0.1",
"@gitlab/vue-router-vue3": "npm:vue-router@4.5.1",
"@gitlab/vuex-vue3": "npm:vuex@4.1.0",
"@gitlab/web-ide": "^0.0.1-dev-20250611141528",

View File

@ -5,6 +5,8 @@ module QA
name: :ci_release_cli_catalog_publish_option
} do
describe 'CI catalog release with glab', :skip_live_env do
let(:glab_version) { 'v1.59.1' }
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
let!(:project) do
@ -168,7 +170,7 @@ module QA
tags: ["#{executor}"]
create-release-with-existing-tag:
image: registry.gitlab.com/gitlab-org/cli:latest
image: registry.gitlab.com/gitlab-org/cli:#{glab_version}
script:
- echo "Creating release $CI_COMMIT_TAG"
rules:
@ -189,7 +191,7 @@ module QA
- if: $CI_COMMIT_TAG != "v9.0.2" # to prevent creating a new pipeline because of the tag created in the test
create-release-with-new-tag-filled-with-information:
image: registry.gitlab.com/gitlab-org/cli:latest
image: registry.gitlab.com/gitlab-org/cli:#{glab_version}
script:
- echo "Creating release $CI_COMMIT_TAG"
rules:

View File

@ -46,14 +46,12 @@ ee/spec/frontend/related_items_tree/components/tree_root_spec.js
ee/spec/frontend/roadmap/components/roadmap_shell_spec.js
ee/spec/frontend/roles_and_permissions/components/role_selector_spec.js
ee/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
ee/spec/frontend/status_checks/components/modal_create_spec.js
ee/spec/frontend/status_checks/mount_spec.js
ee/spec/frontend/usage_quotas/transfer/components/usage_by_month_spec.js
ee/spec/frontend/users/identity_verification/components/international_phone_input_spec.js
ee/spec/frontend/users/identity_verification/components/verify_phone_verification_code_spec.js
spec/frontend/__helpers__/vue_test_utils_helper_spec.js
spec/frontend/access_tokens/index_spec.js
spec/frontend/admin/abuse_report/components/reported_content_spec.js
spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js
spec/frontend/admin/broadcast_messages/components/base_spec.js
spec/frontend/alert_management/components/alert_management_table_spec.js
@ -80,14 +78,12 @@ spec/frontend/clusters/components/new_cluster_spec.js
spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
spec/frontend/clusters_list/components/delete_agent_button_spec.js
spec/frontend/content_editor/components/wrappers/paragraph_spec.js
spec/frontend/custom_emoji/components/list_spec.js
spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
spec/frontend/design_management/components/design_overlay_spec.js
spec/frontend/design_management/pages/design/index_spec.js
spec/frontend/design_management/pages/index_spec.js
spec/frontend/editor/components/source_editor_toolbar_spec.js
spec/frontend/editor/extensions/source_editor_toolbar_ext_spec.js
spec/frontend/error_tracking/components/error_details_spec.js
spec/frontend/error_tracking/components/error_tracking_list_spec.js
spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
spec/frontend/feature_flags/components/strategy_spec.js
@ -101,7 +97,6 @@ spec/frontend/helpers/init_simple_app_helper_spec.js
spec/frontend/integrations/edit/components/trigger_field_spec.js
spec/frontend/integrations/edit/components/trigger_fields_spec.js
spec/frontend/invite_members/components/members_token_select_spec.js
spec/frontend/issuable/components/issuable_by_email_spec.js
spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
spec/frontend/issues/list/components/issues_list_app_spec.js
spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js
@ -116,7 +111,6 @@ spec/frontend/members/components/table/max_role_spec.js
spec/frontend/members/components/table/members_table_spec.js
spec/frontend/ml/model_registry/components/model_edit_spec.js
spec/frontend/notes/components/discussion_notes_spec.js
spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js

View File

@ -362,8 +362,8 @@ FactoryBot.define do
trait :artifacts do
after(:create) do |build, evaluator|
create(:ci_job_artifact, :archive, :public, job: build, expire_at: build.artifacts_expire_at)
create(:ci_job_artifact, :metadata, :public, job: build, expire_at: build.artifacts_expire_at)
create(:ci_job_artifact, :mocked_checksum, :archive, :public, job: build, expire_at: build.artifacts_expire_at)
create(:ci_job_artifact, :mocked_checksum, :metadata, :public, job: build, expire_at: build.artifacts_expire_at)
build.reload
end
end

View File

@ -502,6 +502,12 @@ FactoryBot.define do
end
end
trait :mocked_checksum do
after(:build) do |artifact, evaluator|
artifact.file_sha256 = Digest::SHA256.hexdigest("mocked")
end
end
trait :annotations do
file_type { :annotations }
file_format { :gzip }

View File

@ -0,0 +1,80 @@
# frozen_string_literal: true
# https://slsa.dev/spec/v1.1/provenance
FactoryBot.define do
factory :provenance_statement, class: 'Ci::Slsa::ProvenanceStatement' do
_type { "https://in-toto.io/Statement/v1" }
subject { [association(:resource_descriptor), association(:resource_descriptor)] }
predicate_type { "https://slsa.dev/provenance/v1" }
predicate { association(:predicate) }
skip_create
end
factory :predicate, class: 'Ci::Slsa::ProvenanceStatement::Predicate' do
build_definition { association(:build_definition) }
run_details { association(:run_details) }
skip_create
end
factory :resource_descriptor, class: 'Ci::Slsa::ResourceDescriptor' do
sequence(:name) { |n| "resource_#{n}" }
sequence(:digest) do |n|
{
sha256: Digest::SHA256.hexdigest("resource_#{n}")
}
end
skip_create
end
factory :build_definition, class: 'Ci::Slsa::ProvenanceStatement::BuildDefinition' do
build_type { "https://gitlab.com/gitlab-org/gitlab-runner/-/blob/15/PROVENANCE.md" }
# Arbitrary JSON object according to spec.
external_parameters do
{
repository: "https://gitlab.com/tanuki/hello-world",
ref: "refs/heads/main"
}
end
# Arbitrary JSON object according to spec.
internal_parameters do
{
doc_ref: "https://gitlab.com/tanuki/hello-world",
ref_ref: "refs/heads/main"
}
end
resolved_dependencies do
[association(:resource_descriptor), association(:resource_descriptor), association(:resource_descriptor)]
end
skip_create
end
factory :run_details, class: 'Ci::Slsa::ProvenanceStatement::RunDetails' do
builder { association(:builder) }
metadata { association(:build_metadata) }
byproducts { [association(:resource_descriptor)] }
skip_create
end
factory :build_metadata, class: 'Ci::Slsa::ProvenanceStatement::BuildMetadata' do
sequence(:invocation_id) { |nb| "build_#{nb}" }
started_on { "2025-06-09T08:48:14Z" }
finished_on { "2025-06-10T08:48:14Z" }
skip_create
end
factory :builder, class: 'Ci::Slsa::ProvenanceStatement::Builder' do
id { "https://gitlab.com/gitlab-org/gitlab-runner/-/blob/15/RUN_TYPE.md" }
builder_dependencies { [association(:resource_descriptor)] }
version { { "gitlab-runner": "4d7093e1" } }
skip_create
end
end

View File

@ -102,6 +102,7 @@ exports[`Alert integration settings form default state should match the default
data-testid="save-changes-button"
icon=""
size="medium"
tag="button"
type="submit"
variant="confirm"
>

View File

@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlCollapsibleListbox } from '@gitlab/ui';
import { GlCollapsibleListbox, GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -10,6 +10,7 @@ import CrudComponent from '~/vue_shared/components/crud_component.vue';
import InputsTableSkeletonLoader from '~/ci/common/pipeline_inputs/pipeline_inputs_table/inputs_table_skeleton_loader.vue';
import PipelineInputsForm from '~/ci/common/pipeline_inputs/pipeline_inputs_form.vue';
import PipelineInputsTable from '~/ci/common/pipeline_inputs/pipeline_inputs_table/pipeline_inputs_table.vue';
import PipelineInputsPreviewDrawer from '~/ci/common/pipeline_inputs/pipeline_inputs_preview_drawer.vue';
import getPipelineInputsQuery from '~/ci/common/pipeline_inputs/graphql/queries/pipeline_creation_inputs.query.graphql';
/** mock data to be replaced with fixtures - https://gitlab.com/gitlab-org/gitlab/-/issues/525243 */
import {
@ -95,6 +96,8 @@ describe('PipelineInputsForm', () => {
const findEmptyState = () => wrapper.findByText('There are no inputs for this configuration.');
const findEmptySelectionState = () => wrapper.findByTestId('empty-selection-state');
const findInputsSelector = () => wrapper.findComponent(GlCollapsibleListbox);
const findPreviewButton = () => wrapper.findComponent(GlButton);
const findPreviewDrawer = () => wrapper.findComponent(PipelineInputsPreviewDrawer);
const selectInputs = async (inputs = ['deploy_environment', 'api_token', 'tags']) => {
findInputsSelector().vm.$emit('select', inputs);
@ -119,6 +122,15 @@ describe('PipelineInputsForm', () => {
it('renders a loading state', () => {
expect(findSkeletonLoader().exists()).toBe(true);
});
it('renders preview button', () => {
expect(findPreviewButton().exists()).toBe(true);
expect(findPreviewButton().text()).toBe('Preview inputs');
});
it('renders preview drawer', () => {
expect(findPreviewDrawer().exists()).toBe(true);
});
});
describe('GraphQL query', () => {
@ -295,6 +307,32 @@ describe('PipelineInputsForm', () => {
expect(otherInputs.every((i) => !i.isSelected)).toBe(true);
});
});
describe('inputs preview', () => {
it('enables preview button', () => {
expect(findPreviewButton().props('disabled')).toBe(false);
});
it('opens drawer when preview button is clicked', async () => {
expect(findPreviewDrawer().props('open')).toBe(false);
await findPreviewButton().vm.$emit('click');
expect(findPreviewDrawer().props('open')).toBe(true);
});
it('passes inputs to drawer', () => {
expect(findPreviewDrawer().props('inputs')).toEqual(expectedInputs);
});
it('closes drawer when close event is emitted', async () => {
await findPreviewButton().vm.$emit('click');
expect(findPreviewDrawer().props('open')).toBe(true);
await findPreviewDrawer().vm.$emit('close');
expect(findPreviewDrawer().props('open')).toBe(false);
});
});
});
describe('with no inputs', () => {
@ -318,6 +356,14 @@ describe('PipelineInputsForm', () => {
it('does not display the empty selection state message', () => {
expect(findEmptySelectionState().exists()).toBe(false);
});
it('disables inputs selector', () => {
expect(findInputsSelector().props('disabled')).toBe(true);
});
it('disables preview button', () => {
expect(findPreviewButton().props('disabled')).toBe(true);
});
});
describe('with empty ref (error case)', () => {

View File

@ -0,0 +1,252 @@
import { GlDrawer } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineInputsPreviewDrawer from '~/ci/common/pipeline_inputs/pipeline_inputs_preview_drawer.vue';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
describe('PipelineInputsPreviewDrawer', () => {
let wrapper;
const defaultProps = {
open: false,
inputs: [],
};
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(PipelineInputsPreviewDrawer, {
propsData: { ...defaultProps, ...props },
stubs: { GlDrawer },
});
};
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findCodeBlock = () => wrapper.findByTestId('inputs-code-block');
const findCodeLines = () => wrapper.findAllByTestId('inputs-code-line');
describe('mounted', () => {
beforeEach(() => {
createComponent({ open: true });
});
it('renders drawer with correct props', () => {
const drawer = findDrawer();
expect(drawer.props()).toMatchObject({
open: true,
zIndex: DRAWER_Z_INDEX,
});
});
it('displays correct title', () => {
expect(wrapper.text()).toContain('Preview your inputs');
});
it('displays correct description', () => {
expect(wrapper.text()).toContain('The pipeline will run with these inputs:');
});
it('renders code block container', () => {
expect(findCodeBlock().exists()).toBe(true);
});
it('emits close event when drawer closes', async () => {
await findDrawer().vm.$emit('close');
expect(wrapper.emitted('close')).toHaveLength(1);
});
});
describe('input formatting', () => {
describe('with unchanged inputs', () => {
beforeEach(() => {
createComponent({
open: true,
inputs: [
{
name: 'environment',
value: 'production',
default: 'production',
type: 'STRING',
description: 'Target environment',
},
],
});
});
it('displays input without diff styling', () => {
const line = (at) => findCodeLines().at(at);
expect(line(0).text()).toBe('environment:');
expect(line(1).text()).toBe('value: "production"');
expect(line(2).text()).toBe('type: "STRING"');
expect(line(3).text()).toBe('description: "Target environment"');
});
it('does not apply diff colors to unchanged values', () => {
const valueLine = findCodeLines().wrappers.find((line) =>
line.text().includes('value: "production"'),
);
expect(valueLine.classes()).not.toContain('gl-text-danger');
expect(valueLine.classes()).not.toContain('gl-text-success');
});
});
describe('with changed inputs', () => {
beforeEach(() => {
createComponent({
open: true,
inputs: [
{
name: 'environment',
value: 'staging',
default: 'production',
type: 'STRING',
description: 'Target environment',
},
],
});
});
it('displays input with diff styling', () => {
const line = (at) => findCodeLines().at(at);
expect(line(0).text()).toBe('environment:');
expect(line(1).text()).toBe('- value: "production"');
expect(line(2).text()).toBe('+ value: "staging"');
expect(line(3).text()).toBe('type: "STRING"');
expect(line(4).text()).toBe('description: "Target environment"');
});
it('applies correct diff colors', () => {
const removedLine = findCodeLines().wrappers.find((line) =>
line.text().includes('- value: "production"'),
);
const addedLine = findCodeLines().wrappers.find((line) =>
line.text().includes('+ value: "staging"'),
);
expect(removedLine.classes()).toContain('gl-text-danger');
expect(addedLine.classes()).toContain('gl-text-success');
});
});
describe('with minimal input data', () => {
beforeEach(() => {
createComponent({
open: true,
inputs: [
{
name: 'simple',
value: 'test',
default: 'test',
},
],
});
});
it('handles inputs without type or description', () => {
const line = (at) => findCodeLines().at(at);
const lineTexts = findCodeLines().wrappers.map((item) => item.text());
expect(line(0).text()).toBe('simple:');
expect(line(1).text()).toBe('value: "test"');
expect(lineTexts.filter((text) => text.includes('type:'))).toHaveLength(0);
expect(lineTexts.filter((text) => text.includes('description:'))).toHaveLength(0);
});
});
});
describe('value type formatting', () => {
describe.each([
{
name: 'string values',
inputName: 'string_input',
value: 'hello world',
default: 'default_string',
type: 'STRING',
expectedValue: '"hello world"',
expectedDefault: '"default_string"',
},
{
name: 'number values',
inputName: 'number_input',
value: 42,
default: 0,
type: 'NUMBER',
expectedValue: '42',
expectedDefault: '0',
},
{
name: 'boolean values',
inputName: 'boolean_input',
value: true,
default: false,
type: 'BOOLEAN',
expectedValue: 'true',
expectedDefault: 'false',
},
{
name: 'object values',
inputName: 'object_input',
value: { key: 'value', count: 42 },
default: { key: 'default' },
type: 'OBJECT',
expectedValue: '{"key":"value","count":42}',
expectedDefault: '{"key":"default"}',
},
{
name: 'array values',
inputName: 'array_input',
value: ['item1', 'item2', 42],
default: [],
type: 'ARRAY',
expectedValue: '["item1","item2",42]',
expectedDefault: '[]',
},
{
name: 'empty string values',
inputName: 'empty_string',
value: '',
default: 'not_empty',
type: 'STRING',
expectedValue: '""',
expectedDefault: '"not_empty"',
},
{
name: 'null values',
inputName: 'null_input',
value: 'actual_value',
default: null,
type: 'STRING',
expectedValue: '"actual_value"',
expectedDefault: 'null',
},
])(
'$name',
({ inputName, value, default: defaultValue, type, expectedValue, expectedDefault }) => {
beforeEach(() => {
createComponent({
open: true,
inputs: [
{
name: inputName,
value,
default: defaultValue,
type,
},
],
});
});
it('formats values correctly with diff styling', () => {
const line = (at) => findCodeLines().at(at);
expect(line(0).text()).toBe(`${inputName}:`);
expect(line(1).text()).toBe(`- value: ${expectedDefault}`);
expect(line(2).text()).toBe(`+ value: ${expectedValue}`);
expect(line(3).text()).toBe(`type: "${type}"`);
});
},
);
});
});

View File

@ -1,4 +1,12 @@
import { getSkeletonRectProps } from '~/ci/common/pipeline_inputs/utils';
import {
getSkeletonRectProps,
formatValue,
hasValueChanged,
formatValueLines,
formatMetadataLines,
formatInputLines,
formatInputsForDisplay,
} from '~/ci/common/pipeline_inputs/utils';
describe('Skeleton utils', () => {
describe('getSkeletonRectProps', () => {
@ -25,3 +33,275 @@ describe('Skeleton utils', () => {
);
});
});
describe('Input Formatter Utils', () => {
describe('formatValue', () => {
it.each([
{ input: 'hello world', expected: '"hello world"', description: 'regular string' },
{ input: '', expected: '""', description: 'empty string' },
{ input: 42, expected: '42', description: 'positive integer' },
{ input: 0, expected: '0', description: 'zero' },
{ input: -1, expected: '-1', description: 'negative integer' },
{ input: 3.14, expected: '3.14', description: 'decimal number' },
{ input: true, expected: 'true', description: 'boolean true' },
{ input: false, expected: 'false', description: 'boolean false' },
{ input: null, expected: 'null', description: 'null value' },
{ input: undefined, expected: 'undefined', description: 'undefined value' },
{ input: { key: 'value' }, expected: '{"key":"value"}', description: 'simple object' },
{ input: {}, expected: '{}', description: 'empty object' },
{
input: { nested: { count: 42 } },
expected: '{"nested":{"count":42}}',
description: 'nested object',
},
{ input: [1, 2, 3], expected: '[1,2,3]', description: 'number array' },
{ input: [], expected: '[]', description: 'empty array' },
{ input: ['a', 'b'], expected: '["a","b"]', description: 'string array' },
{
input: [{ id: 1 }, { id: 2 }],
expected: '[{"id":1},{"id":2}]',
description: 'object array',
},
])('formats $description correctly', ({ input, expected }) => {
expect(formatValue(input)).toBe(expected);
});
});
describe('hasValueChanged', () => {
describe('primitive values', () => {
it.each([
{
value: 'new-value',
defaultValue: 'default-value',
expected: true,
description: 'different values',
},
{
value: 'saved-value',
defaultValue: 'saved-value',
expected: false,
description: 'unchanged values',
},
])('returns $expected for $description', ({ value, defaultValue, expected }) => {
expect(hasValueChanged(value, defaultValue)).toBe(expected);
});
});
describe('object values', () => {
it.each([
{
value: { key: 'value' },
defaultValue: { key: 'different' },
expected: true,
description: 'different object values',
},
{
value: [1, 2, 3],
defaultValue: [1, 2, 4],
expected: true,
description: 'different arrays',
},
{
value: { key: 'value' },
defaultValue: { key: 'value' },
expected: false,
description: 'same object values',
},
{
value: [1, 2, 3],
defaultValue: [1, 2, 3],
expected: false,
description: 'same arrays',
},
{
value: { nested: { count: 42 } },
defaultValue: { nested: { count: 42 } },
expected: false,
description: 'same nested objects',
},
])('returns $expected for $description', ({ value, defaultValue, expected }) => {
expect(hasValueChanged(value, defaultValue)).toBe(expected);
});
});
});
describe('formatValueLines', () => {
it('returns diff lines for changed values', () => {
const input = {
value: 'new_value',
default: 'old_value',
};
const result = formatValueLines(input, true);
expect(result).toEqual([
{ content: '- value: "old_value"', type: 'old' },
{ content: '+ value: "new_value"', type: 'new' },
]);
});
it('returns single value line for unchanged values', () => {
const input = {
value: 'same_value',
default: 'same_value',
};
const result = formatValueLines(input, false);
expect(result).toEqual([{ content: ' value: "same_value"', type: '' }]);
});
});
describe('formatMetadataLines', () => {
it.each([
{
description: 'both type and description when present',
input: {
type: 'STRING',
description: 'A test input',
},
expected: [
{ content: ' type: "STRING"', type: '' },
{ content: ' description: "A test input"', type: '' },
],
},
{
description: 'only type when description is missing',
input: {
type: 'NUMBER',
},
expected: [{ content: ' type: "NUMBER"', type: '' }],
},
{
description: 'only description when type is missing',
input: {
description: 'No type specified',
},
expected: [{ content: ' description: "No type specified"', type: '' }],
},
{
description: 'empty array when both are missing',
input: {},
expected: [],
},
])('returns $description', ({ input, expected }) => {
const result = formatMetadataLines(input);
expect(result).toEqual(expected);
});
});
describe('formatInputLines', () => {
it('formats complete input with all fields', () => {
const input = {
name: 'test_input',
value: 'new_value',
default: 'old_value',
type: 'STRING',
description: 'Test description',
};
const result = formatInputLines(input);
expect(result).toEqual([
{ content: 'test_input:', type: '' },
{ content: '- value: "old_value"', type: 'old' },
{ content: '+ value: "new_value"', type: 'new' },
{ content: ' type: "STRING"', type: '' },
{ content: ' description: "Test description"', type: '' },
{ content: '', type: '' },
]);
});
it('formats input with minimal fields', () => {
const input = {
name: 'simple_input',
value: 'value',
default: 'value',
};
const result = formatInputLines(input);
expect(result).toEqual([
{ content: 'simple_input:', type: '' },
{ content: ' value: "value"', type: '' },
{ content: '', type: '' },
]);
});
});
describe('formatInputsForDisplay', () => {
it('formats multiple inputs correctly', () => {
const inputs = [
{
name: 'first_input',
value: 'new',
default: 'old',
type: 'STRING',
},
{
name: 'second_input',
value: 42,
default: 42,
type: 'NUMBER',
description: 'A number input',
},
];
const result = formatInputsForDisplay(inputs);
expect(result).toEqual([
{ content: 'first_input:', type: '' },
{ content: '- value: "old"', type: 'old' },
{ content: '+ value: "new"', type: 'new' },
{ content: ' type: "STRING"', type: '' },
{ content: '', type: '' },
{ content: 'second_input:', type: '' },
{ content: ' value: 42', type: '' },
{ content: ' type: "NUMBER"', type: '' },
{ content: ' description: "A number input"', type: '' },
{ content: '', type: '' },
]);
});
it('handles empty inputs array', () => {
const result = formatInputsForDisplay([]);
expect(result).toEqual([]);
});
});
describe('edge cases', () => {
it('handles inputs with null/undefined values', () => {
const input = {
name: 'null_input',
value: null,
default: undefined,
};
const result = formatInputLines(input);
expect(result).toEqual([
{ content: 'null_input:', type: '' },
{ content: '- value: undefined', type: 'old' },
{ content: '+ value: null', type: 'new' },
{ content: '', type: '' },
]);
});
it('handles inputs with empty strings vs null', () => {
const input = {
name: 'empty_vs_null',
value: '',
default: null,
};
const result = formatInputLines(input);
expect(result).toEqual([
{ content: 'empty_vs_null:', type: '' },
{ content: '- value: null', type: 'old' },
{ content: '+ value: ""', type: 'new' },
{ content: '', type: '' },
]);
});
});
});

View File

@ -136,7 +136,7 @@ describe('JobActionButton', () => {
});
it('passes correct props', () => {
expect(findActionButton().props()).toStrictEqual({
expect(findActionButton().props()).toMatchObject({
block: false,
buttonTextClasses: '',
category: 'primary',

View File

@ -62,7 +62,9 @@ exports[`Code navigation popover component renders popover 1`] = `
href="http://gitlab.com/test.js"
icon=""
size="medium"
tag="button"
target="_blank"
type="button"
variant="default"
>
Go to definition

View File

@ -1,14 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`content_editor/components/toolbar_button displays tertiary, medium button with a provided label and icon 1`] = `
<b-button-stub
<button
aria-label="Bold"
class="btn-default-tertiary btn-icon gl-button gl-mr-2"
size="sm"
tag="button"
class="btn btn-default btn-default-tertiary btn-icon btn-sm gl-button gl-mr-2"
title="Bold"
type="button"
variant="default"
>
<gl-icon-stub
class="gl-button-icon"
@ -16,5 +13,5 @@ exports[`content_editor/components/toolbar_button displays tertiary, medium butt
size="16"
variant="current"
/>
</b-button-stub>
</button>
`;

View File

@ -18,7 +18,6 @@ exports[`Custom emoji settings list component renders table of custom emoji 1`]
class="btn btn-confirm btn-md gl-button"
data-testid="action-primary"
href="/new"
to="/new"
>
<span
class="gl-button-text"
@ -35,7 +34,6 @@ exports[`Custom emoji settings list component renders table of custom emoji 1`]
class="btn btn-confirm btn-md gl-button"
data-testid="action-primary"
href="/new"
to="/new"
>
<span
class="gl-button-text"

View File

@ -59,7 +59,9 @@ exports[`Design management toolbar component renders design and updated data 1`]
href="/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d"
icon="download"
size="medium"
tag="button"
title="Download design"
type="button"
variant="default"
/>
<delete-button-stub
@ -80,7 +82,9 @@ exports[`Design management toolbar component renders design and updated data 1`]
data-testid="toggle-design-sidebar"
icon="comments"
size="medium"
tag="button"
title="Show comments"
type="button"
variant="default"
/>
<design-navigation-stub

View File

@ -9,7 +9,9 @@ exports[`Design management upload button component renders inverted upload desig
category="secondary"
icon=""
size="small"
tag="button"
title="Adding a design with the same filename replaces the file in a new version."
type="button"
variant="confirm"
>
Upload designs
@ -31,7 +33,9 @@ exports[`Design management upload button component renders upload design button
category="secondary"
icon=""
size="small"
tag="button"
title="Adding a design with the same filename replaces the file in a new version."
type="button"
variant="confirm"
>
Upload designs

View File

@ -53,6 +53,8 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
role="button"
size="medium"
tabindex="0"
tag="button"
type="button"
variant="default"
>
Reset webhook URL

View File

@ -203,6 +203,14 @@ describe('InviteMembersModal', () => {
);
});
});
describe('hasErrorDuringInvite prop', () => {
it('does not pass hasErrorDuringInvite prop when function returns null', () => {
createInviteMembersToProjectWrapper();
expect(findBase().props('hasErrorDuringInvite')).toBeUndefined();
});
});
});
describe('rendering with tracking considerations', () => {

View File

@ -1,5 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import { memberName, searchUsers, triggerExternalAlert } from '~/invite_members/utils/member_utils';
import {
memberName,
searchUsers,
triggerExternalAlert,
baseBindingAttributes,
} from '~/invite_members/utils/member_utils';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
@ -40,3 +45,9 @@ describe('Trigger External Alert', () => {
expect(triggerExternalAlert()).toBe(false);
});
});
describe('baseBindingAttributes', () => {
it('returns empty object', () => {
expect(baseBindingAttributes()).toEqual({});
});
});

View File

@ -38,7 +38,9 @@ exports[`Merge request dashboard collapsible section renders section 1`] = `
class="gl-mr-2 gl-self-center"
icon="information-o"
size="medium"
tag="button"
title=""
type="button"
variant="link"
/>
<div
@ -54,7 +56,9 @@ exports[`Merge request dashboard collapsible section renders section 1`] = `
data-testid="crud-collapse-toggle"
icon=""
size="small"
tag="button"
title="Collapse"
type="button"
variant="default"
>
<gl-animated-chevron-lg-down-up-icon-stub

View File

@ -19,7 +19,9 @@ exports[`FileSha renders 1`] = `
icon="copy-to-clipboard"
id="reference-0"
size="small"
tag="button"
title="Copy SHA"
type="button"
variant="default"
/>
</div>

View File

@ -82,7 +82,9 @@ exports[`packages_list_row renders 1`] = `
data-testid="action-delete"
icon="remove"
size="medium"
tag="button"
title="Remove package"
type="button"
variant="danger"
/>
</div>

View File

@ -19,7 +19,9 @@ exports[`FileSha renders 1`] = `
icon="copy-to-clipboard"
id="reference-0"
size="small"
tag="button"
title="Copy SHA"
type="button"
variant="default"
/>
</div>

View File

@ -28,6 +28,8 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
data-testid="delete-button"
icon=""
size="medium"
tag="button"
type="button"
variant="danger"
>
Delete project

View File

@ -30,6 +30,8 @@ exports[`Repository last commit component renders commit widget 1`] = `
icon=""
label="true"
size="medium"
tag="button"
type="button"
variant="default"
>
12345678
@ -52,6 +54,8 @@ exports[`Repository last commit component renders commit widget 1`] = `
href="/history"
icon=""
size="medium"
tag="button"
type="button"
variant="default"
>
History

View File

@ -29,6 +29,8 @@ exports[`Repository table row component renders a symlink table row 1`] = `
/>
</a>
<gl-icon-stub
arialabel="Locked by Root"
class="gl-relative gl-z-1"
name="lock"
size="12"
title="Locked by Root"
@ -87,6 +89,8 @@ exports[`Repository table row component renders table row 1`] = `
/>
</a>
<gl-icon-stub
arialabel="Locked by Root"
class="gl-relative gl-z-1"
name="lock"
size="12"
title="Locked by Root"
@ -145,6 +149,8 @@ exports[`Repository table row component renders table row for path with special
/>
</a>
<gl-icon-stub
arialabel="Locked by Root"
class="gl-relative gl-z-1"
name="lock"
size="12"
title="Locked by Root"

View File

@ -9,7 +9,9 @@ exports[`Clear Icon Button renders successfully 1`] = `
icon="clear"
name="clear"
size="small"
tag="button"
title="Tooltip Text"
type="button"
variant="default"
/>
`;

View File

@ -10,6 +10,7 @@ exports[`SidebarTodo template renders component container element with proper da
data-issuable-type="epic"
icon=""
size="small"
tag="button"
type="button"
variant="default"
>

View File

@ -29,7 +29,8 @@ describe('Deployment View App button', () => {
const findReviewAppLink = () => wrapper.findComponent(ReviewAppLink);
const findMrWidgetDeploymentDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findDeployUrlMenuItems = () => wrapper.findAllComponents(GlLink);
const findDeployUrlMenuItems = () =>
wrapper.findByTestId('deploy-url-menu').findAllComponents(GlLink);
describe('text', () => {
it('renders text as passed', () => {

View File

@ -217,6 +217,7 @@ describe('Test report extension', () => {
});
await findCopyFailedSpecsBtn().trigger('click');
await nextTick();
// tooltip text is replaced for 1 second
expect(findCopyFailedSpecsBtn().attributes()).toMatchObject({

View File

@ -118,6 +118,7 @@ describe('Create work item component', () => {
apolloProvider: mockApollo,
propsData: {
fullPath: 'full-path',
projectNamespaceFullPath: 'full-path',
preselectedWorkItemType,
...props,
},

View File

@ -9,7 +9,9 @@ exports[`Design management upload button component renders upload design button
category="primary"
icon="media"
size="medium"
tag="button"
title="Adding a design with the same filename replaces the file in a new version."
type="button"
variant="default"
>
Add design

View File

@ -41,11 +41,12 @@ describe('WorkItemProjectsListbox', () => {
const findAllDropdownItemsFor = (fullPath) => wrapper.findAllByTestId(`listbox-item-${fullPath}`);
const findDropdownToggle = () => wrapper.findByTestId('base-dropdown-toggle');
const createComponent = async (
const createComponent = async ({
isGroup = true,
fullPath = 'group-a',
selectedProjectFullPath = null,
) => {
projectNamespaceFullPath = '',
} = {}) => {
wrapper = mountExtended(WorkItemProjectsListbox, {
apolloProvider: createMockApollo([
[namespaceProjectsForLinksWidgetQuery, namespaceProjectsFormLinksWidgetResolver],
@ -54,6 +55,7 @@ describe('WorkItemProjectsListbox', () => {
fullPath,
isGroup,
selectedProjectFullPath,
projectNamespaceFullPath,
},
});
@ -143,7 +145,11 @@ describe('WorkItemProjectsListbox', () => {
describe('project level work items', () => {
beforeEach(async () => {
await createComponent(false, 'group-a/example-project-a', 'group-a/example-project-a');
await createComponent({
isGroup: false,
fullPath: 'group-a/example-project-a',
selectedProjectFullPath: 'group-a/example-project-a',
});
gon.current_username = 'root';
});
@ -164,6 +170,24 @@ describe('WorkItemProjectsListbox', () => {
expect(findDropdownToggle().text()).toBe(namespaceProjectsData[0].name);
});
it('auto-selects the current project for user-namespace when projectNamespaceFullPath is present', async () => {
const projectNamespaceFullPath = 'root';
await createComponent({
isGroup: false,
fullPath: 'root/example-project-a',
projectNamespaceFullPath,
});
await nextTick();
// Since `beforeEach` is already running the query once, we need to look
// for second call with expected variables.
expect(namespaceProjectsFormLinksWidgetResolver).toHaveBeenNthCalledWith(2, {
fullPath: projectNamespaceFullPath,
projectSearch: '',
});
expect(findDropdownToggle().text()).toBe(namespaceProjectsData[0].name);
});
it('supports selecting a project', async () => {
removeLocalstorageFrequentItems();

View File

@ -246,6 +246,7 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
releases_path: project_releases_path(project, format: :json),
reset_path: new_issuable_address_project_path(project, issuable_type: 'issue'),
rss_path: '#',
project_namespace_full_path: project.namespace.full_path,
show_new_issue_link: 'true',
sign_in_path: new_user_session_path,
time_tracking_limit_to_hours: "false"

View File

@ -31,6 +31,7 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
group_path: nil,
issues_list_path: project_issues_path(project),
labels_manage_path: project_labels_path(project),
project_namespace_full_path: project.namespace.full_path,
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
sign_in_path: user_session_path(redirect_to_referer: 'yes'),
new_comment_template_paths: include({ text: "Your comment templates",
@ -68,6 +69,7 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
{
issues_list_path: issues_group_path(group),
labels_manage_path: group_labels_path(group),
project_namespace_full_path: group.full_path,
default_branch: nil
}
)

View File

@ -0,0 +1,162 @@
# frozen_string_literal: true
require 'spec_helper'
SLSA_PROVENANCE_V1_SCHEMA = 'app/validators/json_schemas/slsa/in_toto_v1/provenance_v1.json'
RSpec.describe Ci::Slsa::ProvenanceStatement, type: :model, feature_category: :continuous_integration do
let(:parsed) { Gitlab::Json.parse(subject.to_json) }
describe 'when ProvenanceStatement is correctly instantiated' do
subject(:provenance_statement) { create(:provenance_statement) }
it 'initializes without crashing' do
expect(parsed['_type']).to eq('https://in-toto.io/Statement/v1')
expect(parsed['predicateType']).to eq('https://slsa.dev/provenance/v1')
end
it 'has the correct subject' do
subject = parsed['subject']
expect(subject.length).to eq(2)
expect(subject[0]['name']).to start_with('resource_')
expect(subject[1]['name']).to start_with('resource_')
expect(subject[0]['digest']['sha256'].length).to eq(64)
expect(subject[1]['digest']['sha256'].length).to eq(64)
end
it 'has the correct predicate build definition' do
build_definition = parsed['predicate']['buildDefinition']
expect(build_definition['buildType']).to eq('https://gitlab.com/gitlab-org/gitlab-runner/-/blob/15/PROVENANCE.md')
expect(build_definition['internalParameters']).to be_a(Hash)
expect(build_definition['externalParameters']).to be_a(Hash)
expect(build_definition['resolvedDependencies'].length).to eq(3)
end
it 'has the correct run details' do
run_details = parsed['predicate']['runDetails']
builder = run_details['builder']
metadata = run_details['metadata']
byproducts = run_details['byproducts']
expect(builder['id']).to eq('https://gitlab.com/gitlab-org/gitlab-runner/-/blob/15/RUN_TYPE.md')
expect(builder['version']['gitlab-runner']).to eq("4d7093e1")
expect(builder['builderDependencies'].length).to eq(1)
expect(metadata['invocationId']).to start_with('build_')
expect(metadata['startedOn']).to eq('2025-06-09T08:48:14Z')
expect(metadata['finishedOn']).to eq('2025-06-10T08:48:14Z')
expect(byproducts.length).to eq(1)
end
describe 'and we check the schema' do
let(:schema) do
JSONSchemer.schema(Pathname.new(SLSA_PROVENANCE_V1_SCHEMA))
end
let(:errors) { schema.validate(parsed).map { |e| JSONSchemer::Errors.pretty(e) } }
it 'conforms to specification' do
expect(errors).to eq([])
end
end
end
describe '#from_build' do
subject(:provenance_statement) { described_class.from_build(build) }
let_it_be(:user) { create(:user) }
let_it_be(:group, reload: true) { create_default(:group, :allow_runner_registration_token) }
let_it_be(:project, reload: true) { create_default(:project, :repository, group: group) }
let_it_be(:pipeline, reload: true) do
create_default(
:ci_pipeline,
project: project,
sha: project.commit.id,
ref: project.default_branch,
status: 'success'
)
end
let_it_be(:runner) { create(:ci_runner, :hosted_runner) }
let_it_be(:runner_manager) { create(:ci_runner_machine, runner: runner) }
context 'when a valid build is passed as a parameter' do
let_it_be(:build) { create(:ci_build, :artifacts, :finished, runner_manager: runner_manager, pipeline: pipeline) }
it 'returns the appropriate JSON object' do
expect(parsed['_type']).to eq('https://in-toto.io/Statement/v1')
expect(parsed['predicateType']).to eq('https://slsa.dev/provenance/v1')
end
it 'has the correct subject' do
subject = parsed['subject']
expect(subject.length).to eq(1)
expect(subject[0]['name']).to eq('ci_build_artifacts.zip')
expect(subject[0]['digest']['sha256']).to eq('3d4a07bcbf2eaec380ad707451832924bee1197fbdf43d20d6d4bc96c8284268')
end
it 'has the correct predicate build definition' do
build_definition = parsed['predicate']['buildDefinition']
# TODO: update buildType as part of https://gitlab.com/gitlab-org/gitlab/-/issues/426764
expect(build_definition['buildType']).to eq('https://gitlab.com/gitlab-org/gitlab/-/issues/546150')
expect(build_definition['externalParameters']['variables']).to include("GITLAB_CI")
expect(build_definition['internalParameters']['name']).to start_with("My runner")
expect(build_definition['resolvedDependencies'].length).to eq(1)
end
it 'has the correct run details' do
run_details = parsed['predicate']['runDetails']
builder = run_details['builder']
metadata = run_details['metadata']
expect(builder['id']).to start_with('http://localhost/groups/GitLab-Admin-Bot/-/runners/')
expect(metadata['invocationId']).to eq(build.id)
expect(metadata['startedOn']).to eq(build.started_at.utc.try(:rfc3339))
expect(metadata['finishedOn']).to eq(build.finished_at.utc.try(:rfc3339))
end
describe 'and we check the schema' do
let(:schema) do
JSONSchemer.schema(Pathname.new(SLSA_PROVENANCE_V1_SCHEMA))
end
let(:errors) { schema.validate(parsed).map { |e| JSONSchemer::Errors.pretty(e) } }
it 'conforms to specification' do
expect(errors).to eq([])
end
end
end
context 'when a build is invalid' do
context 'when it does not have a build_runner_manager' do
let_it_be(:build) { create(:ci_build, :artifacts, :finished, pipeline: pipeline) }
it 'raises an exception' do
expect { provenance_statement }.to raise_error(ArgumentError)
end
end
context 'when artifact type is not specifically of "archive" file type' do
let_it_be(:build) do
create(:ci_build, :codequality_report, :finished, runner_manager: runner_manager, pipeline: pipeline)
end
it 'raises an exception' do
expect { provenance_statement }.to raise_error(ArgumentError)
end
end
end
end
end

View File

@ -170,6 +170,8 @@ RSpec.describe Admin::GroupsController, "routing" do
it "to #index" do
expect(get("/admin/groups")).to route_to('admin/groups#index')
expect(get("/admin/groups/all")).to route_to('admin/groups#index')
expect(get("/admin/groups/inactive")).to route_to('admin/groups#index')
end
it "to #show" do

View File

@ -1457,10 +1457,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.134.0.tgz#d377ed04560e096155e6f2ff96532b32f65f5db9"
integrity sha512-j80CQRNCdBIF0bykqWHCafYh/NaZg67Z5aZ9Whq28V+Od9l1KTn4Qb1SEd71hRLfDEkFQibdBc4L+CVVR98Q/w==
"@gitlab/ui@113.7.0":
version "113.7.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-113.7.0.tgz#d7e6659213829f1ba954fd59c0e6c3320cba4546"
integrity sha512-myhEaxW9Vg0knjviiLPV0PWH62tDsnZwXcsLcdEMI1mXXmIdcSrJRVFGT3Us7FlM0K+aZJPpqgXyV+GUOXeY8g==
"@gitlab/ui@114.0.1":
version "114.0.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-114.0.1.tgz#847f1ab2ce56bcb504b95c9c7535799fb4defa0c"
integrity sha512-JvXGyIqye/MuM7s2NqGWfd6rsPYv2+o/r//nxzS7ginRM/yJQbSNIJo+yPuxOREqnONmzuqCpeigYyBcPE2PHw==
dependencies:
"@floating-ui/dom" "1.7.0"
echarts "^5.3.2"