Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
172925dab8
commit
0ad218275c
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -18,3 +18,7 @@ export function searchUsers(url, search) {
|
|||
export function triggerExternalAlert() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function baseBindingAttributes() {
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ export default {
|
|||
size="small"
|
||||
placement="bottom-end"
|
||||
searchable
|
||||
data-testid="deploy-url-menu"
|
||||
@search="search"
|
||||
>
|
||||
<template #list-item="{ item }">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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 >}}
|
||||
|
|
|
|||
|
|
@ -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 >}}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
|
|
|
|||
|
|
@ -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 >}}
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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)', () => {
|
||||
|
|
|
|||
|
|
@ -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}"`);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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: '' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ describe('JobActionButton', () => {
|
|||
});
|
||||
|
||||
it('passes correct props', () => {
|
||||
expect(findActionButton().props()).toStrictEqual({
|
||||
expect(findActionButton().props()).toMatchObject({
|
||||
block: false,
|
||||
buttonTextClasses: '',
|
||||
category: 'primary',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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({});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ describe('Create work item component', () => {
|
|||
apolloProvider: mockApollo,
|
||||
propsData: {
|
||||
fullPath: 'full-path',
|
||||
projectNamespaceFullPath: 'full-path',
|
||||
preselectedWorkItemType,
|
||||
...props,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue