Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-02 18:12:38 +00:00
parent ca1dd21a37
commit 344ece1971
38 changed files with 782 additions and 330 deletions

View File

@ -82,7 +82,7 @@ eslint:
- |
function eslint_script() {
yarn_install_script
yarn run lint:eslint:all --format gitlab
./tooling/ci/changed_files.rb eslint
}
run_with_custom_exit_code eslint_script

View File

@ -28,9 +28,13 @@ export default {
},
computed: {
alertInfoMessage() {
return sprintf(this.$options.i18n.alertInfoMessage, {
accessTokenType: this.accessTokenType,
});
return sprintf(
this.$options.i18n.alertInfoMessage,
{
accessTokenType: this.accessTokenType,
},
false,
);
},
alertDangerTitle() {
return n__(
@ -50,7 +54,13 @@ export default {
};
},
label() {
return sprintf(this.$options.i18n.label, { accessTokenType: this.accessTokenType });
return sprintf(
this.$options.i18n.label,
{
accessTokenType: this.accessTokenType,
},
false,
);
},
isNameOrScopesSet() {
const urlParams = new URLSearchParams(window.location.search);

View File

@ -24,7 +24,7 @@ import { issuableInitialDataById, isLegacyIssueType } from './show/utils/issuabl
const feedback = {};
if (gon.features?.workItemViewForIssues) {
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/463598';
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/523713';
feedback.feedbackIssueText = __('Provide feedback on the experience');
feedback.content = __(
'Weve introduced some improvements to the issue page such as real time updates, additional features, and a refreshed design. Have questions or thoughts on the changes?',

View File

@ -129,6 +129,7 @@ export const getSortOptions = ({
hasIssueWeightsFeature,
hasManualSort = true,
hasMergedDate = false,
hasDueDate = true,
} = {}) => {
const sortOptions = [
{
@ -171,7 +172,7 @@ export const getSortOptions = ({
descending: MILESTONE_DUE_DESC,
},
},
{
hasDueDate && {
id: 6,
title: __('Due date'),
sortDirection: {

View File

@ -480,7 +480,11 @@ export default {
);
},
sortOptions() {
return getSortOptions({ hasManualSort: false, hasMergedDate: this.state === STATUS_MERGED });
return getSortOptions({
hasManualSort: false,
hasMergedDate: this.state === STATUS_MERGED,
hasDueDate: false,
});
},
tabCounts() {
const { openedMergeRequests, closedMergeRequests, mergedMergeRequests, allMergeRequests } =

View File

@ -13,7 +13,7 @@ initWorkItemsRoot();
const feedback = {};
if (gon.features.workItemViewForIssues) {
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/463598';
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/523713';
feedback.feedbackIssueText = __('Provide feedback on the experience');
feedback.content = __(
'Weve introduced some improvements to the issue page such as real time updates, additional features, and a refreshed design. Have questions or thoughts on the changes?',

View File

@ -13,7 +13,7 @@ new IssuableTemplateSelectors({ warnTemplateOverride: true, editor: mountMarkdow
const feedback = {};
if (gon.features.workItemViewForIssues) {
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/463598';
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/523713';
feedback.feedbackIssueText = __('Provide feedback on the experience');
feedback.content = __(
'Weve introduced some improvements to the issue page such as real time updates, additional features, and a refreshed design. Have questions or thoughts on the changes?',

View File

@ -308,20 +308,30 @@ export default {
},
confidentialItem() {
return {
text: this.isConfidential
? this.$options.i18n.disableConfidentiality
: this.$options.i18n.enableConfidentiality,
extraAttrs: {
disabled: this.isParentConfidential,
},
text: this.confidentialItemText,
extraAttrs: { disabled: this.isParentConfidential },
};
},
confidentialItemText() {
return this.isConfidential
? this.$options.i18n.disableConfidentiality
: this.$options.i18n.enableConfidentiality;
},
confidentialItemIcon() {
return this.isConfidential ? 'eye' : 'eye-slash';
},
confidentialItemIconVariant() {
return this.isParentConfidential ? 'current' : 'subtle';
},
confidentialTooltip() {
return this.isParentConfidential ? this.$options.i18n.confidentialParentTooltip : '';
},
lockDiscussionText() {
return this.isDiscussionLocked ? __('Unlock discussion') : __('Lock discussion');
},
lockDiscussionIcon() {
return this.isDiscussionLocked ? 'lock-open' : 'lock';
},
objectiveWorkItemTypeId() {
return this.workItemTypes.find((type) => type.name === WORK_ITEM_TYPE_NAME_OBJECTIVE).id;
},
@ -545,12 +555,20 @@ export default {
<template #list-item>
<gl-toggle
:value="subscribedToNotifications"
:label="$options.i18n.notifications"
label-position="left"
data-testid="notifications-toggle"
class="work-item-dropdown-toggle gl-justify-between"
@change="toggleNotifications($event)"
/>
>
<template #label>
<span :title="$options.i18n.notifications" class="gl-flex gl-gap-3 gl-pt-1">
<gl-icon name="notifications" variant="subtle" />
<span class="gl-max-w-[154px] gl-truncate">{{
$options.i18n.notifications
}}</span>
</span>
</template>
</gl-toggle>
</template>
</gl-disclosure-dropdown-item>
<gl-dropdown-divider />
@ -575,7 +593,10 @@ export default {
data-testid="new-related-work-item"
@action="isCreateWorkItemModalVisible = true"
>
<template #list-item>{{ newRelatedItemLabel }}</template>
<template #list-item>
<gl-icon name="plus" class="gl-mr-2" variant="subtle" />
{{ newRelatedItemLabel }}
</template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
@ -583,7 +604,10 @@ export default {
data-testid="promote-action"
@action="promoteToObjective"
>
<template #list-item>{{ __('Promote to objective') }}</template>
<template #list-item>
<gl-icon name="level-up" class="gl-mr-2" variant="subtle" />
{{ __('Promote to objective') }}
</template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
@ -591,7 +615,10 @@ export default {
data-testid="change-type-action"
@action="showChangeTypeModal"
>
<template #list-item>{{ $options.i18n.changeWorkItemType }}</template>
<template #list-item>
<gl-icon name="issue-type-issue" class="gl-mr-2" variant="subtle" />
{{ $options.i18n.changeWorkItemType }}
</template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
@ -599,7 +626,10 @@ export default {
data-testid="move-action"
@action="isMoveWorkItemModalVisible = true"
>
<template #list-item>{{ __('Move') }}</template>
<template #list-item>
<gl-icon name="long-arrow" class="gl-mr-2" variant="subtle" />
{{ __('Move') }}
</template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
@ -608,7 +638,8 @@ export default {
@action="toggleDiscussionLock"
>
<template #list-item>
<gl-loading-icon v-if="isLockDiscussionUpdating" class="gl-mr-1" inline />
<gl-loading-icon v-if="isLockDiscussionUpdating" class="gl-mr-2" inline />
<gl-icon :name="lockDiscussionIcon" class="gl-mr-2" variant="subtle" />
{{ lockDiscussionText }}
</template>
</gl-disclosure-dropdown-item>
@ -619,7 +650,16 @@ export default {
:item="confidentialItem"
data-testid="confidentiality-toggle-action"
@action="handleToggleWorkItemConfidentiality"
/>
>
<template #list-item>
<gl-icon
:name="confidentialItemIcon"
class="gl-mr-2"
:variant="confidentialItemIconVariant"
/>
{{ confidentialItemText }}
</template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
data-testid="copy-reference-action"
@ -627,7 +667,10 @@ export default {
class="shortcut-copy-reference"
@action="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
>
<template #list-item>{{ $options.i18n.copyReference }}</template>
<template #list-item>
<gl-icon name="copy-to-clipboard" class="gl-mr-2" variant="subtle" />
{{ $options.i18n.copyReference }}
</template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
@ -636,7 +679,10 @@ export default {
:data-clipboard-text="workItemCreateNoteEmail"
@action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
>
<template #list-item>{{ i18n.copyCreateNoteEmail }}</template>
<template #list-item>
<gl-icon name="copy-to-clipboard" class="gl-mr-2" variant="subtle" />
{{ i18n.copyCreateNoteEmail }}
</template>
</gl-disclosure-dropdown-item>
<gl-dropdown-divider />
@ -646,7 +692,10 @@ export default {
data-testid="report-abuse-action"
@action="handleToggleReportAbuseModal"
>
<template #list-item>{{ $options.i18n.reportAbuse }}</template>
<template #list-item>
<gl-icon name="review-warning" class="gl-mr-2" variant="subtle" />
{{ $options.i18n.reportAbuse }}
</template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
@ -662,7 +711,10 @@ export default {
@action="handleDelete"
>
<template #list-item>
<span>{{ i18n.deleteWorkItem }}</span>
<span>
<gl-icon name="remove" class="gl-mr-2" variant="current" />
{{ i18n.deleteWorkItem }}
</span>
</template>
</gl-disclosure-dropdown-item>
</template>
@ -686,10 +738,21 @@ export default {
<template #list-item>
<gl-toggle
:value="truncationEnabled"
:label="s__('WorkItem|Truncate descriptions')"
label-position="left"
class="work-item-dropdown-toggle gl-justify-between"
/>
>
<template #label>
<span
:title="s__('WorkItem|Truncate descriptions')"
class="gl-flex gl-gap-3 gl-pt-1"
>
<gl-icon name="text-description" variant="subtle" />
<span class="gl-max-w-[154px] gl-truncate">{{
s__('WorkItem|Truncate descriptions')
}}</span>
</span>
</template>
</gl-toggle>
</template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
@ -699,7 +762,10 @@ export default {
>
<template #list-item>
<div class="gl-flex gl-items-center gl-justify-between">
<span>{{ toggleSidebarLabel }}</span>
<span>
<gl-icon name="sidebar-right" class="gl-mr-2" variant="subtle" />
{{ toggleSidebarLabel }}
</span>
<kbd v-if="toggleSidebarKeys" class="flat">{{ toggleSidebarKeys }}</kbd>
</div>
</template>

View File

@ -1,5 +1,12 @@
<script>
import { GlButton, GlDisclosureDropdownItem, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import {
GlIcon,
GlButton,
GlDisclosureDropdownItem,
GlLoadingIcon,
GlModal,
GlLink,
} from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { __, s__ } from '~/locale';
@ -22,6 +29,7 @@ import workItemOpenChildCountQuery from '../graphql/open_child_count.query.graph
export default {
components: {
GlIcon,
GlButton,
GlDisclosureDropdownItem,
GlLoadingIcon,
@ -169,6 +177,9 @@ export default {
}
return sprintfWorkItem(baseText, this.workItemType);
},
toggleWorkItemStateIcon() {
return this.isWorkItemOpen ? 'issue-close' : 'issue-open-m';
},
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
@ -291,6 +302,7 @@ export default {
{{ toggleInProgressText }}
</template>
<template v-else>
<gl-icon :name="toggleWorkItemStateIcon" class="gl-mr-2" variant="subtle" />
{{ toggleWorkItemStateText }}
</template>
</template>

View File

@ -90,7 +90,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType, withTabs } = {}
const feedback = {};
if (gon.features.workItemViewForIssues) {
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/463598';
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/523713';
feedback.feedbackIssueText = __('Provide feedback on the experience');
feedback.content = __(
'Weve introduced some improvements to the issue page such as real time updates, additional features, and a refreshed design. Have questions or thoughts on the changes?',

View File

@ -0,0 +1,15 @@
- project = local_assigns[:project].nil? || local_assigns[:project]
- assigned = false unless local_assigns[:assigned] == true
%li
.gl-flex.gl-gap-3
.gl-w-6
- if assigned
= sprite_icon('status-success', variant: 'success', css_class: 'gl-mt-3')
= render Pajamas::AvatarComponent.new(project, size: 32, avatar_options: { aria: { hidden: "true" } })
.gl-flex.gl-flex-col.gl-gap-1.gl-grow
%h3.gl-text-base.gl-mt-1.gl-mb-0
= project.full_name
%p.gl-text-sm.gl-text-subtle.gl-mb-0
= project.description
= yield

View File

@ -7,50 +7,35 @@
#js-admin-runner-edit{ data: {runner_id: @runner.id, runner_path: admin_runner_path(@runner) } }
- if @runner.project_type?
.gl-overflow-auto
%h4.gl-text-lg.gl-my-5= _('Restrict projects for this runner')
= render ::Layouts::SettingsSectionComponent.new(_('Restrict projects for this runner'), options: { class: 'gl-mt-7 gl-pt-6 gl-border-t' }) do |c|
- c.with_body do
.gl-flex.gl-flex-col.gl-gap-5
= render ::Layouts::CrudComponent.new(_('Assigned projects')) do |c|
- c.with_body do
- if @runner.runner_projects.any?
%ul.content-list{ data: { testid: 'assigned-projects' } }
- @runner.runner_projects.each do |runner_project|
- project = runner_project.project
- if project
= render "project", project: project, assigned: true do
= render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, href: admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, button_options: { class: 'gl-self-center' }) do
= _('Disable')
- if @runner.runner_projects.any?
%table.table{ data: { testid: 'assigned-projects' } }
%thead
%tr
%th= _('Assigned projects')
- @runner.runner_projects.each do |runner_project|
- project = runner_project.project
- if project
%tr
%td
= render Pajamas::AlertComponent.new(variant: :danger,
dismissible: false,
title: project.full_name) do |c|
- c.with_actions do
= render Pajamas::ButtonComponent.new(variant: :confirm, href: admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete) do
= _('Disable')
= render ::Layouts::CrudComponent.new(s_('Runners|Select projects to assign to this runner')) do |c|
- c.with_body do
= form_tag edit_admin_runner_path(@runner), id: 'runner-projects-search', class: 'gl-w-full gl-p-5', method: :get do
.input-group
= search_field_tag :search, params[:search], class: 'form-control gl-form-input', spellcheck: false
.input-group-append
= render Pajamas::ButtonComponent.new(type: 'submit', variant: :default, icon: 'search', button_options: { 'aria-label': _('Search') })
%table.table{ data: { testid: 'unassigned-projects' } }
%thead
%tr
%th= s_('Runners|Select projects to assign to this runner')
%th
%ul.content-list.gl-border-t.gl-border-t-section{ data: { testid: 'unassigned-projects' } }
- @projects.each do |project|
= render "project", project: project do
= gitlab_ui_form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post, html: { class: 'gl-self-center' } do |f|
= f.hidden_field :runner_id, value: @runner.id
= render Pajamas::ButtonComponent.new(size: :small, type: :submit) do
= _('Enable')
%tr
%td
= form_tag edit_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do
.input-group
= search_field_tag :search, params[:search], class: 'form-control gl-form-input', spellcheck: false
.input-group-append
= render Pajamas::ButtonComponent.new(type: 'submit', variant: :default) do
= _('Search')
%td
- @projects.each do |project|
%tr
%td
= project.full_name
%td
.gl-float-right
= gitlab_ui_form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post do |f|
= f.hidden_field :runner_id, value: @runner.id
= render Pajamas::ButtonComponent.new(size: :small, type: :submit) do
= _('Enable')
= paginate_without_count @projects
- c.with_pagination do
= paginate_without_count @projects

View File

@ -1,7 +1,7 @@
- title: "Rename `setPreReceiveSecretDetection` GraphQL mutation to `setSecretPushProtection`"
removal_milestone: "18.0"
announcement_milestone: "17.7"
breaking_change: true
breaking_change: false
window: 3
reporter: abellucci
stage: application_security_testing

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class DropUnwantedSequenceForProjectIdForeignKey < Gitlab::Database::Migration[2.2]
milestone '17.11'
disable_ddl_transaction!
def up
drop_sequence(
:project_compliance_framework_settings,
:project_id,
:project_compliance_framework_settings_project_id_seq
)
end
def down
# We don't want to restore the sequence as it was a design flaw
# Having a sequence on a foreign key column is an incident waiting to happen.
# https://gitlab.com/gitlab-org/gitlab/-/issues/526909
end
end

View File

@ -0,0 +1 @@
b79f0ecfad2c8ac31210d60ed100ba2b778bd2adf2aeaee17f233e1b09e9e438

View File

@ -20608,15 +20608,6 @@ CREATE SEQUENCE project_compliance_framework_settings_id_seq
ALTER SEQUENCE project_compliance_framework_settings_id_seq OWNED BY project_compliance_framework_settings.id;
CREATE SEQUENCE project_compliance_framework_settings_project_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE project_compliance_framework_settings_project_id_seq OWNED BY project_compliance_framework_settings.project_id;
CREATE TABLE project_compliance_standards_adherence (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -27390,8 +27381,6 @@ ALTER TABLE ONLY project_ci_cd_settings ALTER COLUMN id SET DEFAULT nextval('pro
ALTER TABLE ONLY project_ci_feature_usages ALTER COLUMN id SET DEFAULT nextval('project_ci_feature_usages_id_seq'::regclass);
ALTER TABLE ONLY project_compliance_framework_settings ALTER COLUMN project_id SET DEFAULT nextval('project_compliance_framework_settings_project_id_seq'::regclass);
ALTER TABLE ONLY project_compliance_framework_settings ALTER COLUMN id SET DEFAULT nextval('project_compliance_framework_settings_id_seq'::regclass);
ALTER TABLE ONLY project_compliance_standards_adherence ALTER COLUMN id SET DEFAULT nextval('project_compliance_standards_adherence_id_seq'::regclass);

View File

@ -8,7 +8,7 @@ title: Applications API
{{< details >}}
- Tier: Free, Premium, Ultimate
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
- Offering: GitLab Self-Managed, GitLab Dedicated
{{< /details >}}

View File

@ -2,6 +2,7 @@
stage: Application Security Testing
group: Composition Analysis
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: Access the Dependencies API to retrieve project dependency information, including package details, versions, vulnerabilities, and licenses for supported package managers.
title: Dependencies API
---

View File

@ -68,9 +68,9 @@ for Conan recipes.
| --------- | ---- | -------- | ----------- |
| `id` | string | yes | The project ID or full project path. |
## Ping
## Verify availability of a Conan repository
Ping the GitLab Conan repository to verify availability:
Verifies the availability of the GitLab Conan repository.
```plaintext
GET <route-prefix>/ping
@ -86,9 +86,9 @@ Example response:
""
```
## Search
## Search for a Conan package
Search the instance for Conan packages by name:
Searches the instance for a Conan package with a specified name.
```plaintext
GET <route-prefix>/conans/search
@ -118,9 +118,9 @@ Example response:
}
```
## Authenticate
## Create an authentication token
Returns a JWT to be used for Conan requests in a Bearer header:
Creates a JSON Web Token (JWT) for use as a Bearer header in other requests.
```shell
"Authorization: Bearer <token>
@ -142,9 +142,9 @@ Example response:
eyJhbGciOiJIUzI1NiIiheR5cCI6IkpXVCJ9.eyJhY2Nlc3NfdG9rZW4iOjMyMTQyMzAsqaVzZXJfaWQiOjQwNTkyNTQsImp0aSI6IjdlNzBiZTNjLWFlNWQtNDEyOC1hMmIyLWZiOThhZWM0MWM2OSIsImlhd3r1MTYxNjYyMzQzNSwibmJmIjoxNjE2NjIzNDMwLCJleHAiOjE2MTY2MjcwMzV9.QF0Q3ZIB2GW5zNKyMSIe0HIFOITjEsZEioR-27Rtu7E
```
## Check Credentials
## Verify authentication credentials
Checks the validity of Basic Auth credentials or a Conan JWT generated from [`/authenticate`](#authenticate).
Verifies the validity of Basic Auth credentials or a Conan JWT generated from the [`/authenticate`](#create-an-authentication-token) endpoint.
```plaintext
GET <route-prefix>/users/check_credentials
@ -160,10 +160,10 @@ Example response:
ok
```
## Recipe Snapshot
## Get a recipe snapshot
This returns the snapshot of the recipe files for the specified Conan recipe. The snapshot is a list
of filenames with their associated md5 hash.
Gets a snapshot of the files for a specified Conan recipe. The snapshot is a list of filenames
with their associated MD5 hash.
```plaintext
GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel
@ -190,10 +190,10 @@ Example response:
}
```
## Package Snapshot
## Get a package snapshot
This returns the snapshot of the package files for the specified Conan recipe with the specified
Conan reference. The snapshot is a list of filenames with their associated md5 hash.
Gets a snapshot of the files for a specified Conan package and reference. The snapshot is a list of filenames
with their associated MD5 hash.
```plaintext
GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/packages/:conan_package_reference
@ -221,9 +221,9 @@ Example response:
}
```
## Recipe Manifest
## Get a recipe manifest
The manifest is a list of recipe filenames with their associated download URLs.
Gets a manifest that includes a list of files and associated download URLs for a specified recipe.
```plaintext
GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/digest
@ -253,9 +253,9 @@ Example response:
The URLs in the response have the same route prefix used to request them. If you request them with
the project-level route, the returned URLs contain `/projects/:id`.
## Package Manifest
## Get a package manifest
The manifest is a list of package filenames with their associated download URLs.
Gets a manifest that includes a list of files and associated download URLs for a specified package.
```plaintext
GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/packages/:conan_package_reference/digest
@ -286,10 +286,10 @@ Example response:
The URLs in the response have the same route prefix used to request them. If you request them with
the project-level route, the returned URLs contain `/projects/:id`.
## Recipe Download URLs
## List all recipe download URLs
Recipe download URLs return a list of recipe filenames with their associated download URLs.
This attribute is the same payload as the [recipe manifest](#recipe-manifest) endpoint.
Lists all files and associated download URLs for a specified recipe.
Returns the same payload as the [recipe manifest](#get-a-recipe-manifest) endpoint.
```plaintext
GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/download_urls
@ -319,10 +319,10 @@ Example response:
The URLs in the response have the same route prefix used to request them. If you request them with
the project-level route, the returned URLs contain `/projects/:id`.
## Package Download URLs
## List all package download URLs
Package download URLs return a list of package filenames with their associated download URLs.
This URL is the same payload as the [package manifest](#package-manifest) endpoint.
Lists all files and associated download URLs for a specified package.
Returns the same payload as the [package manifest](#get-a-package-manifest) endpoint.
```plaintext
GET <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/packages/:conan_package_reference/download_urls
@ -353,9 +353,10 @@ Example response:
The URLs in the response have the same route prefix used to request them. If you request them with
the project-level route, the returned URLs contain `/projects/:id`.
## Recipe Upload URLs
## List all recipe upload URLs
Given a list of recipe filenames and file sizes, a list of URLs to upload each file is returned.
Lists the upload URLs for a specified collection of recipe files. The request must include a JSON object
with the name and size of the individual files.
```plaintext
POST <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/upload_urls
@ -370,6 +371,8 @@ POST <route-prefix>/conans/:package_name/:package_version/:package_username/:pac
Example request JSON payload:
The payload must include both the name and size of the file.
```json
{
"conanfile.py": 410,
@ -397,9 +400,10 @@ Example response:
The URLs in the response have the same route prefix used to request them. If you request them with
the project-level route, the returned URLs contain `/projects/:id`.
## Package Upload URLs
## List all package upload URLs
Given a list of package filenames and file sizes, a list of URLs to upload each file is returned.
Lists the upload URLs for a specified collection of package files. The request must include a JSON object
with the name and size of the individual files.
```plaintext
POST <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls
@ -415,6 +419,8 @@ POST <route-prefix>/conans/:package_name/:package_version/:package_username/:pac
Example request JSON payload:
The payload must include both the name and size of the file.
```json
{
"conan_package.tgz": 5412,
@ -444,11 +450,10 @@ Example response:
The URLs in the response have the same route prefix used to request them. If you request them with
the project-level route, the returned URLs contain `/projects/:id`.
## Download a Recipe file
## Get a recipe file
Download a recipe file to the package registry. You must use a download URL that the
[recipe download URLs endpoint](#recipe-download-urls)
returned.
Gets a recipe file from the package registry. You must use the download URL returned from the
[recipe download URLs](#list-all-recipe-download-urls) endpoint.
```plaintext
GET packages/conan/v1/files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision/export/:file_name
@ -475,11 +480,10 @@ curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.examp
This example writes to `conanfile.py` in the current directory.
## Upload a Recipe file
## Upload a recipe file
Upload a recipe file to the package registry. You must use an upload URL that the
[recipe upload URLs endpoint](#recipe-upload-urls)
returned.
Uploads a specified recipe file in the package registry. You must use the upload URL returned from the
[recipe upload URLs](#list-all-recipe-upload-urls) endpoint.
```plaintext
PUT packages/conan/v1/files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision/export/:file_name
@ -503,11 +507,10 @@ curl --request PUT \
"https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/export/conanfile.py"
```
## Download a Package file
## Get a package file
Download a package file to the package registry. You must use a download URL that the
[package download URLs endpoint](#package-download-urls)
returned.
Gets a package file from the package registry. You must use the download URL returned from the
[package download URLs](#list-all-package-download-urls) endpoint.
```plaintext
GET packages/conan/v1/files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name
@ -536,11 +539,10 @@ curl --header "Authorization: Bearer <authenticate_token>" "https://gitlab.examp
This example writes to `conaninfo.txt` in the current directory.
## Upload a Package file
## Upload a package file
Upload a package file to the package registry. You must use an upload URL that the
[package upload URLs endpoint](#package-upload-urls)
returned.
Uploads a specified package file in the package registry. You must use the upload URL returned from the
[package upload URLs](#list-all-package-upload-urls) endpoint.
```plaintext
PUT packages/conan/v1/files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision/package/:conan_package_reference/:package_revision/:file_name
@ -566,9 +568,9 @@ curl --request PUT \
"https://gitlab.example.com/api/v4/packages/conan/v1/files/my-package/1.0/my-group+my-project/stable/0/package/103f6067a947f366ef91fc1b7da351c588d1827f/0/conaninfo.txt"
```
## Delete a Package (delete a Conan recipe)
## Delete a recipe and package
Delete the Conan recipe and package files from the registry:
Deletes a specified Conan recipe and the associated package files from the package registry.
```plaintext
DELETE <route-prefix>/conans/:package_name/:package_version/:package_username/:package_channel

View File

@ -635,7 +635,7 @@ a few different methods, based on where the variable is created or defined.
### Pass YAML-defined CI/CD variables
You can use the `variables` keyword to pass CI/CD variables to a downstream pipeline.
These variables are "trigger variables" for [variable precedence](../variables/_index.md#cicd-variable-precedence).
These variables are pipeline variables for [variable precedence](../variables/_index.md#cicd-variable-precedence).
For example:

View File

@ -1151,6 +1151,41 @@ As a workaround you can either:
- If a single large variable is larger than `ARG_MAX`, try using [Secure Files](../secure_files/_index.md), or
bring the file to the job through some other mechanism.
### `Insufficient permissions to set pipeline variables` error for a downstream pipeline
When triggering a downstream pipeline, you might get this error unexpectedly:
```plaintext
Failed - (downstream pipeline can not be created, Insufficient permissions to set pipeline variables)
```
This error occurs when a downstream project has [restricted pipeline variables](#restrict-pipeline-variables) and the trigger job either:
- Has variables defined. For example:
```yaml
trigger-job:
variables:
VAR_FOR_DOWNSTREAM: "test"
trigger: my-group/my-project
```
- Receives variables from [default variables](../yaml/_index.md#default-variables) defined in a top-level `variables` section. For example:
```yaml
variables:
DEFAULT_VAR: "test"
trigger-job:
trigger: my-group/my-project
```
Variables passed to a downstream pipeline in a trigger job are [pipeline variables](#use-pipeline-variables),
so the workaround is to either:
- Remove the `variables` defined in the trigger job to avoid passing variables.
- [Prevent default variables from being passed to the downstream pipeline](../pipelines/downstream_pipelines.md#prevent-default-variables-from-being-passed).
### Default variable doesn't expand in job variable of the same name
You cannot use a default variable's value in a job variable of the same name. A default variable

View File

@ -2,6 +2,7 @@
stage: Application Security Testing
group: Composition Analysis
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 dependency scanning, detect vulnerabilities in your project dependencies, and resolve them through practical step-by-step guidance.
title: 'Tutorial: Set up dependency scanning'
---

View File

@ -2,6 +2,7 @@
stage: Application Security Testing
group: Composition Analysis
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 generate and export a Software Bill of Materials (SBOM) in CycloneDX format for your project dependencies and save it as a CI/CD artifact.
title: 'Tutorial: Export dependency list in SBOM format'
---
@ -92,7 +93,7 @@ Set up Dependency Scanning. For detailed instructions, follow [the Dependency Sc
- apk add --update jq curl
stage: .post
script:
- |
- |
curl --header "Authorization: Bearer $PRIVATE_TOKEN" --output export.sh --url "https://gitlab.com/api/v4/snippets/<SNIPPET_ID>/raw"
- /bin/sh export.sh
artifacts:

View File

@ -83,7 +83,6 @@ This window takes place on May 5 - 7, 2025 from 09:00 UTC to 22:00 UTC.
| [GraphQL `target` field for to-do items replaced with `targetEntity`](https://gitlab.com/gitlab-org/gitlab/-/issues/484987) | Low | Foundations | Project |
| [`ciJobTokenScopeAddProject` GraphQL mutation is deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/474175) | Low | Govern | Project |
| [Removal of `migrationState` field in `ContainerRepository` GraphQL API](https://gitlab.com/gitlab-org/gitlab/-/issues/459869) | Low | Package | Project |
| [Rename `setPreReceiveSecretDetection` GraphQL mutation to `setSecretPushProtection`](https://gitlab.com/gitlab-org/gitlab/-/issues/514414) | Medium | Application_security_testing | Project |
| [Updated tooling to release CI/CD components to the Catalog](https://gitlab.com/groups/gitlab-org/-/epics/12788) | High | Verify | Instance |
| [Increased default security for use of pipeline variables](https://gitlab.com/gitlab-org/gitlab/-/issues/502382) | Medium | Verify | Project |
| [Amazon S3 Signature Version 2](https://gitlab.com/gitlab-org/container-registry/-/issues/1449) | Low | Package | Project |

View File

@ -1839,14 +1839,14 @@ In 18.0 we are removing the `duoProAssignedUsersCount` GraphQL field. Users may
</div>
<div class="deprecation breaking-change" data-milestone="18.0">
<div class="deprecation " data-milestone="18.0">
### Rename `setPreReceiveSecretDetection` GraphQL mutation to `setSecretPushProtection`
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone">17.7</span>
- Removal in GitLab <span class="milestone">18.0</span> ([breaking change](https://docs.gitlab.com/update/terminology/#breaking-change))
- Removal in GitLab <span class="milestone">18.0</span>
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/514414).
</div>

View File

@ -21,8 +21,8 @@ title: Code Suggestions
- [Removed support for GitLab native model](https://gitlab.com/groups/gitlab-org/-/epics/10752) in GitLab 16.2.
- [Introduced support for Code Generation](https://gitlab.com/gitlab-org/gitlab/-/issues/415583) in GitLab 16.3.
- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/435271) in GitLab 16.7.
- Subscription changed to require GitLab Duo Pro on February 15, 2024.
- Changed to require GitLab Duo add-on in GitLab 17.6 and later.
- [Changed](https://gitlab.com/gitlab-org/fulfillment/meta/-/issues/2031) to require the GitLab Duo Pro add-on on February 15, 2024. Previously, this feature was included with Premium and Ultimate subscriptions.
- [Changed](https://gitlab.com/gitlab-org/fulfillment/meta/-/issues/2031) to require the GitLab Duo Pro or GitLab Duo Enterprise add-on for all supported GitLab versions starting October 17, 2024.
- [Introduced support for Fireworks AI-hosted Qwen2.5 code completion model](https://gitlab.com/groups/gitlab-org/-/epics/15850) in GitLab 17.6, with a flag named `fireworks_qwen_code_completion`.
{{< /history >}}
@ -42,9 +42,16 @@ you want to use to manage Code Suggestions requests:
[View a click-through demo](https://gitlab.navattic.com/code-suggestions).
<!-- Video published on 2023-12-09 --> <!-- Demo published on 2024-02-01 -->
## Prerequisites
To use Code Suggestions, you need:
- A Premium or Ultimate subscription with the GitLab Duo Pro or Enterprise add-on.
- An assigned seat in your GitLab Duo subscription.
{{< alert type="note" >}}
GitLab Duo requires GitLab 17.2 and later for the best user experience and results. Earlier versions may continue to work, however the experience may be degraded. You should [upgrade to the latest version of GitLab](../../../../update/_index.md#upgrade-gitlab) for the best experience.
GitLab Duo requires GitLab 17.2 and later for the best user experience and results. Earlier versions may continue to work, however, the experience may be degraded. You should [upgrade to the latest version of GitLab](../../../../update/_index.md#upgrade-gitlab) for the best experience.
{{< /alert >}}

View File

@ -53198,6 +53198,9 @@ msgstr ""
msgid "SecurityInventory|Security inventory"
msgstr ""
msgid "SecurityInventory|Tool coverage: %{coverage}%%"
msgstr ""
msgid "SecurityInventory|View security coverage and vulnerabilities for all the projects in this group."
msgstr ""
@ -65427,6 +65430,9 @@ msgstr ""
msgid "Vulnerabilities|Vulnerability report export"
msgstr ""
msgid "Vulnerabilities|changed vulnerability status to Needs Triage because it was redetected in pipeline %{pipeline_link}"
msgstr ""
msgid "Vulnerability"
msgstr ""

View File

@ -1,38 +0,0 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Plan', :smoke, :health_check, product_group: :project_management do
let!(:user) do
create(:user,
name: "QA User <img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;",
password: "pw_#{SecureRandom.hex(12)}",
api_client: Runtime::API::Client.as_admin)
end
let!(:project) { create(:project, name: 'xss-test-for-mentions-project') }
describe 'check xss occurence in @mentions in issues', :requires_admin do
before do
Flow::Login.sign_in
project.add_member(user)
create(:issue, project: project).visit!
end
it 'mentions a user in a comment', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347949' do
work_item_enabled = Page::Project::Issue::Show.perform(&:work_item_enabled?)
page_type = work_item_enabled ? Page::Project::WorkItem::Show : Page::Project::Issue::Show
page_type.perform do |show|
show.select_all_activities_filter
show.comment("cc-ing you here @#{user.username}")
expect do
expect(show).to have_comment("cc-ing you here")
end.not_to raise_error # Selenium::WebDriver::Error::UnhandledAlertError
end
end
end
end
end

View File

@ -151,7 +151,6 @@ 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
spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js
spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js

View File

@ -127,7 +127,7 @@ RSpec.describe "Admin manages runner in admin section", :js, feature_category: :
shared_examples 'assignable runner' do
it 'enables a runner for a project' do
within_testid('unassigned-projects') do
within('tr', text: project2.full_name) do
within('li', text: project2.full_name) do
click_on 'Enable'
end
end

View File

@ -63,6 +63,21 @@ RSpec.describe "User comments on issue", :js, feature_category: :team_planning d
expect(page).not_to have_content('Continue editing')
end
context "with a user whose name contains XSS" do
let_it_be(:xss_user) { create(:user, name: "User <img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;") }
before do
project.add_guest(xss_user)
end
it "escapes username when mentioning user" do
mention = "@#{xss_user.username} check this out"
expect { add_note(mention) }.not_to raise_error
expect(page).to have_content(mention)
end
end
end
context "when editing comments" do

View File

@ -7,147 +7,10 @@ RSpec.describe WorkItems::WorkItemsFinder, feature_category: :team_planning do
it_behaves_like 'issues or work items finder', :work_item, '{Issues|WorkItems}Finder#execute context'
context 'when group parameter is present' do
context 'with group parameter' do
include_context '{Issues|WorkItems}Finder#execute context', :work_item
let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group, author: user) }
let_it_be(:group_confidential_work_item) do
create(:work_item, :confidential, :group_level, namespace: group, author: user2)
end
let_it_be(:subgroup_work_item) { create(:work_item, :group_level, namespace: subgroup, author: user) }
let_it_be(:subgroup_confidential_work_item) do
create(:work_item, :confidential, :group_level, namespace: subgroup, author: user2)
end
let_it_be(:subgroup2) { create(:group, :private, parent: group) }
let_it_be(:subgroup2_work_item) { create(:work_item, :group_level, namespace: subgroup2, author: user) }
let_it_be(:subgroup2_confidential_work_item) do
create(:work_item, :confidential, :group_level, namespace: subgroup2, author: user2)
end
let(:params) { { group_id: group } }
let(:scope) { 'all' }
context 'when namespace_level_work_items is disabled' do
before do
stub_feature_flags(namespace_level_work_items: false)
end
it 'does not return group level work items' do
expect(items).to contain_exactly(item1, item5)
end
end
it 'returns group level work items' do
expect(items).to contain_exactly(group_work_item)
end
context 'when user has access to confidential items' do
before do
group.add_reporter(user)
end
it 'includes confidential group-level items' do
expect(items).to contain_exactly(group_work_item, group_confidential_work_item)
end
end
context 'when include_descendants is true' do
before do
params[:include_descendants] = true
end
context 'when user does not have access to all subgroups' do
it 'includes work items from subgroups and child projects with access' do
expect(items).to contain_exactly(group_work_item, subgroup_work_item, item1, item4, item5)
end
end
context 'when user has read access to all subgroups' do
before_all do
subgroup2.add_guest(user)
end
it 'includes work items from subgroups and child projects with access' do
expect(items).to contain_exactly(
group_work_item,
subgroup_work_item,
subgroup2_work_item,
item1,
item4,
item5
)
end
end
context 'when user can access all confidential items' do
before_all do
group.add_reporter(user)
end
it 'includes confidential items from subgroups and child projects' do
expect(items).to contain_exactly(
group_work_item,
group_confidential_work_item,
subgroup_work_item,
subgroup_confidential_work_item,
subgroup2_work_item,
subgroup2_confidential_work_item,
item1,
item4,
item5
)
end
end
context 'when user can access confidential issues of certain subgroups only' do
before_all do
subgroup2.add_reporter(user)
end
it 'includes confidential items from subgroups and child projects with access' do
expect(items).to contain_exactly(
group_work_item,
subgroup_work_item,
subgroup2_work_item,
subgroup2_confidential_work_item,
item1,
item4,
item5
)
end
end
context 'when exclude_projects is true' do
before do
params[:exclude_projects] = true
end
it 'does not include work items from projects' do
expect(items).to contain_exactly(group_work_item, subgroup_work_item)
end
end
end
context 'when include_ancestors is true' do
let(:params) { { group_id: subgroup, include_ancestors: true } }
it 'includes work items from ancestor groups' do
expect(items).to contain_exactly(group_work_item, subgroup_work_item)
end
end
context 'when both include_descendants and include_ancestors are true' do
let_it_be(:sub_subgroup) { create(:group, parent: subgroup) }
let_it_be(:sub_subgroup_work_item) { create(:work_item, :group_level, namespace: sub_subgroup, author: user) }
let(:params) { { group_id: subgroup, include_descendants: true, include_ancestors: true } }
it 'includes work items from ancestor groups, subgroups, and child projects' do
expect(items).to contain_exactly(group_work_item, subgroup_work_item, sub_subgroup_work_item, item4)
end
end
it_behaves_like 'work items finder group parameter'
end
context 'with start and end date filtering' do

View File

@ -145,7 +145,7 @@ describe('Merge requests list app', () => {
expect(findIssuableList().props()).toMatchObject({
namespace: 'gitlab-org/gitlab',
recentSearchesStorageKey: 'merge_requests',
sortOptions: getSortOptions({ hasManualSort: false }),
sortOptions: getSortOptions({ hasManualSort: false, hasDueDate: false }),
initialSortBy: 'CREATED_DESC',
issuableSymbol: '!',
issuables: getQueryResponse.data.namespace.mergeRequests.nodes,

View File

@ -1,6 +1,7 @@
import { nextTick } from 'vue';
import { GlFilteredSearchToken } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { sortableFields } from '~/packages_and_registries/package_registry/utils';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
@ -42,7 +43,7 @@ describe('Package Search', () => {
it('has a registry search component', async () => {
mountComponent();
await nextTick();
await waitForPromises();
expect(findPersistedSearch().exists()).toBe(true);
});
@ -84,7 +85,7 @@ describe('Package Search', () => {
`('in a $page page binds the right props', async ({ isGroupPage }) => {
mountComponent(isGroupPage);
await nextTick();
await waitForPromises();
expect(findPersistedSearch().props()).toMatchObject({
tokens: expect.arrayContaining([
@ -124,7 +125,7 @@ describe('Package Search', () => {
mountComponent();
await nextTick();
await waitForPromises();
findPersistedSearch().vm.$emit('update', payload);
@ -156,7 +157,7 @@ describe('Package Search', () => {
mountComponent();
await nextTick();
await waitForPromises();
findPersistedSearch().vm.$emit('update', payload);

View File

@ -231,7 +231,7 @@ describe('WorkItemActions component', () => {
expect(findDropdownItemsActual()).toEqual([
{
testId: 'notifications-toggle-form',
text: '',
text: 'Notifications',
},
{
divider: true,
@ -289,7 +289,7 @@ describe('WorkItemActions component', () => {
},
{
testId: 'truncation-toggle-action',
text: '',
text: 'Truncate descriptions',
},
{
testId: 'sidebar-toggle-action',

View File

@ -0,0 +1,158 @@
# frozen_string_literal: true
RSpec.shared_examples 'work items finder group parameter' do
context 'when group parameter is present' do
let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group, author: user) }
let_it_be(:group_confidential_work_item) do
create(:work_item, :confidential, :group_level, namespace: group, author: user2)
end
let_it_be(:subgroup_work_item) { create(:work_item, :group_level, namespace: subgroup, author: user) }
let_it_be(:subgroup_confidential_work_item) do
create(:work_item, :confidential, :group_level, namespace: subgroup, author: user2)
end
let_it_be(:subgroup2) { create(:group, :private, parent: group) }
let_it_be(:subgroup2_work_item) { create(:work_item, :group_level, namespace: subgroup2, author: user) }
let_it_be(:subgroup2_confidential_work_item) do
create(:work_item, :confidential, :group_level, namespace: subgroup2, author: user2)
end
let(:params) { { group_id: group } }
let(:scope) { 'all' }
before do
stub_licensed_features(epics: true)
end
context 'when namespace_level_work_items and work_item_epics is disabled' do
before do
stub_feature_flags(namespace_level_work_items: false, work_item_epics: false)
end
it 'does not return group level work items' do
expect(items).to contain_exactly(item1, item5)
end
end
context 'when work_item_epics is disabled' do
before do
stub_feature_flags(work_item_epics: false)
end
it 'returns group level work items' do
expect(items).to contain_exactly(group_work_item)
end
end
it 'returns group level work items' do
expect(items).to contain_exactly(group_work_item)
end
context 'when user has access to confidential items' do
before do
group.add_reporter(user)
end
it 'includes confidential group-level items' do
expect(items).to contain_exactly(group_work_item, group_confidential_work_item)
end
end
context 'when include_descendants is true' do
before do
params[:include_descendants] = true
end
context 'when user does not have access to all subgroups' do
it 'includes work items from subgroups and child projects with access' do
expect(items).to contain_exactly(group_work_item, subgroup_work_item, item1, item4, item5)
end
end
context 'when user has read access to all subgroups' do
before_all do
subgroup2.add_guest(user)
end
it 'includes work items from subgroups and child projects with access' do
expect(items).to contain_exactly(
group_work_item,
subgroup_work_item,
subgroup2_work_item,
item1,
item4,
item5
)
end
end
context 'when user can access all confidential items' do
before_all do
group.add_reporter(user)
end
it 'includes confidential items from subgroups and child projects' do
expect(items).to contain_exactly(
group_work_item,
group_confidential_work_item,
subgroup_work_item,
subgroup_confidential_work_item,
subgroup2_work_item,
subgroup2_confidential_work_item,
item1,
item4,
item5
)
end
end
context 'when user can access confidential issues of certain subgroups only' do
before_all do
subgroup2.add_reporter(user)
end
it 'includes confidential items from subgroups and child projects with access' do
expect(items).to contain_exactly(
group_work_item,
subgroup_work_item,
subgroup2_work_item,
subgroup2_confidential_work_item,
item1,
item4,
item5
)
end
end
context 'when exclude_projects is true' do
before do
params[:exclude_projects] = true
end
it 'does not include work items from projects' do
expect(items).to contain_exactly(group_work_item, subgroup_work_item)
end
end
end
context 'when include_ancestors is true' do
let(:params) { { group_id: subgroup, include_ancestors: true } }
it 'includes work items from ancestor groups' do
expect(items).to contain_exactly(group_work_item, subgroup_work_item)
end
end
context 'when both include_descendants and include_ancestors are true' do
let_it_be(:sub_subgroup) { create(:group, parent: subgroup) }
let_it_be(:sub_subgroup_work_item) { create(:work_item, :group_level, namespace: sub_subgroup, author: user) }
let(:params) { { group_id: subgroup, include_descendants: true, include_ancestors: true } }
it 'includes work items from ancestor groups, subgroups, and child projects' do
expect(items).to contain_exactly(group_work_item, subgroup_work_item, sub_subgroup_work_item, item4)
end
end
end
end

View File

@ -0,0 +1,216 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../../tooling/ci/changed_files'
RSpec.describe CI::ChangedFiles, feature_category: :tooling do
let(:instance) { described_class.new }
describe '#should_run_checks_for_changed_files' do
# The mock values are based on allowed values from the docs
# https://docs.gitlab.com/ci/variables/predefined_variables/
let(:pipeline_source) { 'merge_request_event' }
let(:merge_request_event_type) { 'merged_result' }
let(:commit_ref_name) { 'feature-branch' }
before do
stub_env('CI_PIPELINE_SOURCE', pipeline_source)
stub_env('CI_MERGE_REQUEST_EVENT_TYPE', merge_request_event_type)
stub_env('CI_COMMIT_REF_NAME', commit_ref_name)
end
context 'when in a valid merge request environment' do
it 'returns true when no tier labels exist' do
stub_env('CI_MERGE_REQUEST_LABELS', nil)
expect(instance.should_run_checks_for_changed_files).to be true
end
it 'returns true when tier-1 label exists' do
stub_env('CI_MERGE_REQUEST_LABELS', 'pipeline::tier-1,other-label')
expect(instance.should_run_checks_for_changed_files).to be true
end
it 'returns false when other tier label exists' do
stub_env('CI_MERGE_REQUEST_LABELS', 'pipeline::tier-2,other-label')
expect(instance.should_run_checks_for_changed_files).to be false
end
end
context 'when not in a valid merge request environment' do
context 'when the merge request event type is merge_train' do
let(:merge_request_event_type) { 'merge_train' }
it 'returns false' do
expect(instance.should_run_checks_for_changed_files).to be false
end
end
context 'when the pipeline source is not merged_request_event' do
let(:pipeline_source) { 'push' }
it 'returns false when pipeline source is push' do
expect(instance.should_run_checks_for_changed_files).to be false
end
end
context 'when the current branch is CI_DEFAULT_BRANCH' do
let(:commit_ref_name) { 'master' }
it 'returns false' do
stub_env('CI_DEFAULT_BRANCH', 'master')
expect(instance.should_run_checks_for_changed_files).to be false
end
end
context 'when the environment variables are not set' do
let(:pipeline_source) { nil }
let(:merge_request_event_type) { nil }
let(:commit_ref_name) { nil }
it 'returns false' do
expect(instance.should_run_checks_for_changed_files).to be false
end
end
end
end
describe '#get_changed_files_in_merged_results_pipeline' do
let(:git_diff_output) { "file1.js\nfile2.rb\nfile3.vue" }
before do
allow(instance).to receive(:`)
.with('git diff --name-only --diff-filter=d HEAD~..HEAD')
.and_return(git_diff_output)
end
context 'when git diff is run in a merged results pipeline' do
it 'returns an array when there are changed files' do
expect(instance.get_changed_files_in_merged_results_pipeline)
.to match_array(['file1.js', 'file2.rb', 'file3.vue'])
end
context "when there are no changed files" do
let(:git_diff_output) { "" }
it 'returns an empty array' do
expect(instance.get_changed_files_in_merged_results_pipeline).to eq([])
end
end
end
end
describe '#filter_and_get_changed_files_in_mr' do
context 'when checks should run for changed files' do \
let(:changed_files_output) { ['file1.js', 'file2.rb', 'file3.vue'] }
before do
allow(instance).to receive_messages(
should_run_checks_for_changed_files: true,
get_changed_files_in_merged_results_pipeline: changed_files_output
)
end
context 'when changed files exist' do
it 'returns filtered files' do
expect(instance.filter_and_get_changed_files_in_mr(filter_pattern: /\.(js|vue)$/))
.to match_array(['file1.js', 'file3.vue'])
end
it 'returns all files when filter is empty' do
expect(instance.filter_and_get_changed_files_in_mr)
.to match_array(changed_files_output)
end
it 'returns empty array and prints warning when no files match filter' do
allow(instance).to receive(:get_changed_files_in_merged_results_pipeline).and_return(['file1.txt',
'file2.rb'])
expect(instance).to receive(:puts).with('No files were changed. Skipping...')
expect(instance.filter_and_get_changed_files_in_mr(filter_pattern: /\.(js|vue)$/)).to eq([])
end
end
end
context 'when checks should not run for changed files' do
before do
allow(instance).to receive(:should_run_checks_for_changed_files).and_return(false)
end
it 'returns ["."] and prints warning' do
expect(instance).to receive(:puts).with("Changed file criteria didn't match... Command will run for all files")
expect(instance.filter_and_get_changed_files_in_mr(filter_pattern: /\.(js|vue)$/)).to eq(['.'])
end
end
end
describe '#run_eslint_for_changed_files' do
let(:files) { ['file1.js', 'file2.vue'] }
let(:eslint_command) { ['yarn', 'run', 'lint:eslint', '--format', 'gitlab', 'file1.js', 'file2.vue'] }
before do
allow(instance).to receive(:puts).with('Running ESLint...')
end
context 'when there are changed files to lint' do
before do
allow(instance).to receive(:filter_and_get_changed_files_in_mr).and_return(files)
end
it 'runs eslint with the correct arguments and returns exit 0 on success' do
expect(instance).to receive(:system).with(*eslint_command).and_return(true)
status = instance_double(Process::Status, exitstatus: 0)
allow(instance).to receive(:last_command_status).and_return(status)
expect(instance.run_eslint_for_changed_files).to eq(0)
end
it 'runs eslint with the correct arguments and returns exit 1 on failure' do
expect(instance).to receive(:system).with(*eslint_command).and_return(false)
status = instance_double(Process::Status, exitstatus: 1)
allow(instance).to receive(:last_command_status).and_return(status)
expect(instance.run_eslint_for_changed_files).to eq(1)
end
end
context 'when there are no changed files to lint' do
it 'does not run eslint and returns exit code 0' do
allow(instance).to receive(:filter_and_get_changed_files_in_mr).and_return([])
expect(instance).not_to receive(:system)
expect(instance.run_eslint_for_changed_files).to eq(0)
end
end
end
describe 'Run CLI commands' do
it 'returns 0 for empty args' do
allow(ARGV).to receive(:empty?).and_return(true)
expect(instance.process_command_and_determine_exit_status).to eq(0)
end
it 'returns 0 when eslint succeeds' do
allow(ARGV).to receive(:first).and_return('eslint')
allow(instance).to receive(:run_eslint_for_changed_files).and_return(0)
expect(instance.process_command_and_determine_exit_status).to eq(0)
end
it 'returns exit code when eslint fails' do
allow(ARGV).to receive(:first).and_return('eslint')
allow(instance).to receive(:run_eslint_for_changed_files).and_return(11)
expect(instance.process_command_and_determine_exit_status).to eq(11)
end
it 'returns 1 for unknown commands' do
allow(ARGV).to receive(:first).and_return('unknown')
expect(instance.process_command_and_determine_exit_status).to eq(1)
end
end
end

83
tooling/ci/changed_files.rb Executable file
View File

@ -0,0 +1,83 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
module CI
class ChangedFiles
FRONTEND_FILES_FILTER = /\.(js|cjs|mjs|vue)$/
def initialize(env: ENV, args: ARGV)
@env = env
@args = args
end
private attr_reader :env, :args
def should_run_checks_for_changed_files
is_valid_mr_event = env['CI_PIPELINE_SOURCE'] == 'merge_request_event' &&
env['CI_MERGE_REQUEST_EVENT_TYPE'] != 'merge_train'
labels = env['CI_MERGE_REQUEST_LABELS']
is_tier_1_pipeline = labels.nil? || labels.include?('pipeline::tier-1')
is_not_master_branch = env['CI_COMMIT_REF_NAME'] != env['CI_DEFAULT_BRANCH']
is_valid_mr_event && is_tier_1_pipeline && is_not_master_branch
end
# See: https://gitlab.com/groups/gitlab-org/-/epics/16845#note_2370956250
# for why we use `HEAD~` to compare
def get_changed_files_in_merged_results_pipeline
`git diff --name-only --diff-filter=d HEAD~..HEAD`.split("\n")
end
def filter_and_get_changed_files_in_mr(filter_pattern: //)
changed_files =
if should_run_checks_for_changed_files
get_changed_files_in_merged_results_pipeline.grep(filter_pattern)
else
puts "Changed file criteria didn't match... Command will run for all files"
['.']
end
puts 'No files were changed. Skipping...' if changed_files.empty?
changed_files
end
def run_eslint_for_changed_files
puts 'Running ESLint...'
files = filter_and_get_changed_files_in_mr(filter_pattern: FRONTEND_FILES_FILTER)
return 0 if files.empty?
command = ["yarn", "run", "lint:eslint", "--format", "gitlab", *files]
system(*command)
last_command_status.exitstatus
end
def last_command_status
$?
end
def process_command_and_determine_exit_status
return 0 if args.empty?
command = args.first
case command
when "eslint"
run_eslint_for_changed_files
else
warn "Unknown command: #{command}"
1
end
end
end
end
if __FILE__ == $PROGRAM_NAME
runner = CI::ChangedFiles.new
exit runner.process_command_and_determine_exit_status
end