Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-23 21:12:09 +00:00
parent 961bdc8763
commit eb5145f05d
36 changed files with 496 additions and 165 deletions

View File

@ -85,7 +85,7 @@
{"name":"colored2","version":"3.1.2","platform":"ruby","checksum":"b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a"},
{"name":"commonmarker","version":"0.23.11","platform":"ruby","checksum":"9d1d35d358740151bce29235aebfecc63314fb57dd89a83e72d4061b4fe3d2bf"},
{"name":"concurrent-ruby","version":"1.2.3","platform":"ruby","checksum":"82fdd3f8a0816e28d513e637bb2b90a45d7b982bdf4f3a0511722d2e495801e2"},
{"name":"connection_pool","version":"2.5.0","platform":"ruby","checksum":"233b92f8d38e038c1349ccea65dd3772727d669d6d2e71f9897c8bf5cd53ebfc"},
{"name":"connection_pool","version":"2.5.1","platform":"ruby","checksum":"ae802a90a4b5a081101b39d618e69921a9a50bea9ac3420a5b8c71f1befa3e9c"},
{"name":"console","version":"1.29.2","platform":"ruby","checksum":"afd9b75a1b047059dda22df0e3c0a386e96f50f6752c87c4b00b1a9fcbe77cd6"},
{"name":"cork","version":"0.3.0","platform":"ruby","checksum":"a0a0ac50e262f8514d1abe0a14e95e71c98b24e3378690e5d044daf0013ad4bc"},
{"name":"cose","version":"1.3.0","platform":"ruby","checksum":"63247c66a5bc76e53926756574fe3724cc0a88707e358c90532ae2a320e98601"},

View File

@ -453,7 +453,7 @@ GEM
colored2 (3.1.2)
commonmarker (0.23.11)
concurrent-ruby (1.2.3)
connection_pool (2.5.0)
connection_pool (2.5.1)
console (1.29.2)
fiber-annotation
fiber-local (~> 1.1)

View File

@ -85,7 +85,7 @@
{"name":"colored2","version":"3.1.2","platform":"ruby","checksum":"b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a"},
{"name":"commonmarker","version":"0.23.11","platform":"ruby","checksum":"9d1d35d358740151bce29235aebfecc63314fb57dd89a83e72d4061b4fe3d2bf"},
{"name":"concurrent-ruby","version":"1.2.3","platform":"ruby","checksum":"82fdd3f8a0816e28d513e637bb2b90a45d7b982bdf4f3a0511722d2e495801e2"},
{"name":"connection_pool","version":"2.5.0","platform":"ruby","checksum":"233b92f8d38e038c1349ccea65dd3772727d669d6d2e71f9897c8bf5cd53ebfc"},
{"name":"connection_pool","version":"2.5.1","platform":"ruby","checksum":"ae802a90a4b5a081101b39d618e69921a9a50bea9ac3420a5b8c71f1befa3e9c"},
{"name":"console","version":"1.29.2","platform":"ruby","checksum":"afd9b75a1b047059dda22df0e3c0a386e96f50f6752c87c4b00b1a9fcbe77cd6"},
{"name":"cork","version":"0.3.0","platform":"ruby","checksum":"a0a0ac50e262f8514d1abe0a14e95e71c98b24e3378690e5d044daf0013ad4bc"},
{"name":"cose","version":"1.3.0","platform":"ruby","checksum":"63247c66a5bc76e53926756574fe3724cc0a88707e358c90532ae2a320e98601"},

View File

@ -465,7 +465,7 @@ GEM
colored2 (3.1.2)
commonmarker (0.23.11)
concurrent-ruby (1.2.3)
connection_pool (2.5.0)
connection_pool (2.5.1)
console (1.29.2)
fiber-annotation
fiber-local (~> 1.1)

View File

@ -5,6 +5,7 @@ import {
GlDisclosureDropdownGroup,
GlLoadingIcon,
GlTooltipDirective,
GlSearchBoxByType,
} from '@gitlab/ui';
import { createAlert } from '~/alert';
import { s__, __, sprintf } from '~/locale';
@ -17,6 +18,8 @@ import { PIPELINE_POLL_INTERVAL_DEFAULT, FAILED_STATUS } from '~/ci/constants';
import JobDropdownItem from '~/ci/common/private/job_dropdown_item.vue';
import getPipelineStageJobsQuery from './graphql/queries/get_pipeline_stage_jobs.query.graphql';
const searchItemsThreshold = 12;
export default {
name: 'PipelineStageDropdown',
components: {
@ -25,6 +28,7 @@ export default {
GlDisclosureDropdown,
GlDisclosureDropdownGroup,
GlLoadingIcon,
GlSearchBoxByType,
JobDropdownItem,
},
directives: {
@ -46,6 +50,7 @@ export default {
return {
isDropdownOpen: false,
stageJobs: [],
search: '',
};
},
apollo: {
@ -99,6 +104,14 @@ export default {
passedJobs() {
return this.stageJobs.filter((job) => job.detailedStatus.group !== FAILED_STATUS);
},
searchedJobs() {
return this.stageJobs.filter((job) =>
job.name.toLowerCase().includes(this.search.toLowerCase()),
);
},
searchVisible() {
return !this.isLoading && this.stageJobs.length > searchItemsThreshold;
},
},
mounted() {
toggleQueryPollingByVisibility(this.$apollo.queries.stageJobs);
@ -107,6 +120,7 @@ export default {
onHideDropdown() {
this.isDropdownOpen = false;
this.$apollo.queries.stageJobs.stopPolling();
this.search = '';
},
onShowDropdown() {
this.isDropdownOpen = true;
@ -149,6 +163,7 @@ export default {
>
<span>{{ dropdownHeaderText }}</span>
</div>
<gl-search-box-by-type v-if="searchVisible" v-model="search" class="gl-m-2" borderless />
</template>
<div v-if="isLoading" class="gl-flex gl-gap-3 gl-px-4 gl-py-3">
@ -161,27 +176,43 @@ export default {
data-testid="pipeline-mini-graph-dropdown-menu-list"
@click.stop
>
<gl-disclosure-dropdown-group v-if="hasFailedJobs">
<template #group-label>{{ s__('Pipelines|Failed jobs') }}</template>
<job-dropdown-item
v-for="job in failedJobs"
:key="job.id"
:job="job"
@jobActionExecuted="$emit('jobActionExecuted')"
/>
</gl-disclosure-dropdown-group>
<gl-disclosure-dropdown-group
v-if="hasPassedJobs"
:bordered="hasFailedJobs"
data-testid="passed-jobs"
>
<job-dropdown-item
v-for="job in passedJobs"
:key="job.id"
:job="job"
@jobActionExecuted="$emit('jobActionExecuted')"
/>
</gl-disclosure-dropdown-group>
<template v-if="search">
<gl-disclosure-dropdown-group
v-if="searchedJobs.length"
key="searched-jobs"
data-testid="searched-jobs"
>
<job-dropdown-item
v-for="job in searchedJobs"
:key="job.id + 'searched'"
:job="job"
@jobActionExecuted="$emit('jobActionExecuted')"
/>
</gl-disclosure-dropdown-group>
</template>
<template v-else>
<gl-disclosure-dropdown-group v-if="hasFailedJobs">
<template #group-label>{{ s__('Pipelines|Failed jobs') }}</template>
<job-dropdown-item
v-for="job in failedJobs"
:key="job.id"
:job="job"
@jobActionExecuted="$emit('jobActionExecuted')"
/>
</gl-disclosure-dropdown-group>
<gl-disclosure-dropdown-group
v-if="hasPassedJobs"
:bordered="hasFailedJobs"
data-testid="passed-jobs"
>
<job-dropdown-item
v-for="job in passedJobs"
:key="job.id"
:job="job"
@jobActionExecuted="$emit('jobActionExecuted')"
/>
</gl-disclosure-dropdown-group>
</template>
</ul>
<template #footer>

View File

@ -2,10 +2,11 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
export default {
ORGANIZATIONS_PER_PAGE: 25, // Same value as PAGE_LENGTH in `app/controllers/import/github_groups_controller.rb`
components: {
GlCollapsibleListbox,
},
@ -17,15 +18,22 @@ export default {
},
},
data() {
return { organizationsLoading: true, organizations: [], organizationFilter: '' };
return {
page: 1,
hasMoreOrganizations: false,
isLoading: true,
isLoadingMore: false,
organizations: [],
organizationFilter: '',
};
},
computed: {
toggleText() {
return this.value || this.$options.i18n.allOrganizations;
return this.value || s__('ImportProjects|All organizations');
},
dropdownItems() {
return [
{ text: this.$options.i18n.allOrganizations, value: '' },
{ text: s__('ImportProjects|All organizations'), value: '' },
...this.organizations
.filter((entry) =>
entry.name.toLowerCase().includes(this.organizationFilter.toLowerCase()),
@ -38,36 +46,64 @@ export default {
},
},
async mounted() {
try {
this.organizationsLoading = true;
const {
data: { provider_groups: organizations },
} = await axios.get(this.statusImportGithubGroupPath);
this.organizations = organizations;
} catch (e) {
createAlert({
message: __('Something went wrong on our end.'),
});
Sentry.captureException(e);
} finally {
this.organizationsLoading = false;
}
this.loadInitialOrganizations();
},
i18n: {
allOrganizations: s__('ImportProjects|All organizations'),
methods: {
async fetchOrganizations(page = this.page) {
try {
const {
data: { provider_groups: organizations },
} = await axios.get(this.statusImportGithubGroupPath, {
params: { page },
});
this.hasMoreOrganizations = organizations.length === this.$options.ORGANIZATIONS_PER_PAGE;
return organizations;
} catch (e) {
createAlert({
message: s__('GithubImporter|Something went wrong while fetching GitHub organizations.'),
});
Sentry.captureException(e);
this.hasMoreOrganizations = false; // Stop loading more after error
return [];
}
},
async loadInitialOrganizations() {
this.organizations = await this.fetchOrganizations();
this.isLoading = false;
},
async loadMoreOrganizations() {
if (!this.hasMoreOrganizations) {
return;
}
this.isLoadingMore = true;
const nextPageOrganizations = await this.fetchOrganizations(this.page + 1);
if (nextPageOrganizations.length > 0) {
this.organizations.push(...nextPageOrganizations);
this.page += 1;
}
this.isLoadingMore = false;
},
},
};
</script>
<template>
<gl-collapsible-listbox
:loading="organizationsLoading"
:loading="isLoading"
:toggle-text="toggleText"
:header-text="s__('ImportProjects|Organizations')"
:items="dropdownItems"
searchable
:infinite-scroll="hasMoreOrganizations"
:infinite-scroll-loading="isLoadingMore"
role="button"
tabindex="0"
@search="organizationFilter = $event"
@select="$emit('input', $event)"
@bottom-reached="loadMoreOrganizations"
/>
</template>

View File

@ -66,7 +66,7 @@ export default {
<import-projects-table v-bind="$attrs">
<template #filter="{ importAllButtonText, showImportAllModal }">
<gl-tabs v-model="selectedRelationTypeTabIdx" content-class="!gl-py-0 gl-mb-3">
<gl-tab v-for="tab in $options.relationTypes" :key="tab.title" :title="tab.title">
<gl-tab v-for="tab in $options.relationTypes" :key="tab.title" :title="tab.title" lazy>
<div
class="gl-flex gl-flex-wrap gl-justify-between gl-gap-3 gl-border-0 gl-border-b-1 gl-border-solid gl-border-b-default gl-bg-subtle gl-p-5"
>

View File

@ -17,6 +17,7 @@ import {
PACKAGE_ERROR_STATUS,
PACKAGE_DEFAULT_STATUS,
PACKAGE_DEPRECATED_STATUS,
PACKAGE_TYPE_CONAN,
WARNING_TEXT,
} from '~/packages_and_registries/package_registry/constants';
import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
@ -103,6 +104,9 @@ export default {
'gl-font-normal': this.errorStatusRow,
};
},
isPackageTypeConan() {
return this.packageEntity.packageType === PACKAGE_TYPE_CONAN;
},
},
i18n: {
createdAt: __('Created %{timestamp}'),
@ -127,18 +131,30 @@ export default {
</template>
<template #left-primary>
<div class="gl-mr-5 gl-flex gl-min-w-0 gl-items-center gl-gap-3" data-testid="package-name">
<router-link
v-if="containsWebPathLink"
:class="errorPackageStyle"
class="gl-min-w-0 gl-break-all gl-text-default"
data-testid="details-link"
:to="{ name: 'details', params: { id: packageId } }"
>
{{ packageEntity.name }}
</router-link>
<span v-else :class="errorPackageStyle">
{{ packageEntity.name }}
</span>
<div class="gl-gap-2 sm:gl-flex">
<router-link
v-if="containsWebPathLink"
:class="errorPackageStyle"
class="gl-min-w-0 gl-break-all gl-text-default"
data-testid="details-link"
:to="{ name: 'details', params: { id: packageId } }"
>
{{ packageEntity.name }}
</router-link>
<span v-else :class="errorPackageStyle">
{{ packageEntity.name }}
</span>
<div v-if="isPackageTypeConan" data-testid="conan-metadata">
<span class="gl-hidden sm:gl-inline">&middot;</span>
<span class="gl-font-normal gl-text-subtle" data-testid="package-username">
{{ packageEntity.metadata.packageUsername }}
</span>
<gl-badge class="gl-ml-2" variant="info" data-testid="package-channel">
{{ packageEntity.metadata.packageChannel }}
</gl-badge>
</div>
</div>
<div v-if="showBadges" class="gl-flex gl-gap-3">
<package-tags

View File

@ -31,6 +31,13 @@ query getPackages(
count
nodes {
...PackageData
metadata {
... on ConanMetadata {
id
packageChannel
packageUsername
}
}
}
pageInfo {
...PageInfo
@ -58,6 +65,13 @@ query getPackages(
name
webUrl
}
metadata {
... on ConanMetadata {
id
packageChannel
packageUsername
}
}
}
pageInfo {
...PageInfo

View File

@ -127,12 +127,15 @@ export default {
async mounted() {
eventHub.$on('convert-task-list-item', this.convertTaskListItem);
eventHub.$on('delete-task-list-item', this.deleteTaskListItem);
window.addEventListener('hashchange', (e) => this.truncateOrScrollToAnchor(e));
await this.$nextTick();
this.truncateOrScrollToAnchor();
},
beforeDestroy() {
eventHub.$off('convert-task-list-item', this.convertTaskListItem);
eventHub.$off('delete-task-list-item', this.deleteTaskListItem);
window.removeEventListener('hashchange', this.truncateOrScrollToAnchor);
this.removeAllPointerEventListeners();
},
methods: {
@ -158,13 +161,18 @@ export default {
* If yes, it will prevent description from truncating and will scroll the page to the anchor.
* If no, it will truncate the description as per default behaviour.
*/
truncateOrScrollToAnchor() {
truncateOrScrollToAnchor({ type } = {}) {
const hash = getLocationHash();
const hashSelector = `href="#${hash}"`;
const isLocationHashAnchoredInDescription =
hash && this.descriptionHtml?.includes(hashSelector);
if (isLocationHashAnchoredInDescription) {
// Link was referred from current page itself,
// so we expand the description and then scroll to it.
if (type === 'hashchange') {
this.showAll();
}
handleLocationHash();
} else {
this.truncateLongDescription();

View File

@ -7,6 +7,8 @@ module Import
before_action :provider_auth, only: [:status]
feature_category :importers
# Same value as ORGANIZATIONS_PER_PAGE in
# `app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue`
PAGE_LENGTH = 25
def status

View File

@ -9,6 +9,7 @@ module Snippets
cannot_be_used_together: 'and snippet files cannot be used together',
invalid_data: 'have invalid data'
}.freeze
SNIPPET_ACCESS_ERROR = :snippet_access_error
CreateRepositoryError = Class.new(StandardError)
@ -32,7 +33,7 @@ module Snippets
def forbidden_visibility_error(snippet)
deny_visibility_level(snippet)
snippet_error_response(snippet, 403)
snippet_error_response(snippet, SNIPPET_ACCESS_ERROR)
end
def valid_params?
@ -50,7 +51,6 @@ module Snippets
snippet.errors.add(:snippet_actions, INVALID_PARAMS_MESSAGES[:invalid_data])
end
# Callers need to interpret into 422
snippet_error_response(snippet, INVALID_PARAMS_ERROR)
end

View File

@ -2,7 +2,7 @@
removal_milestone: '18.0'
announcement_milestone: '17.4'
breaking_change: true
window: 1
window: 2
reporter: lwanko
stage: Fulfillment
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/476858

View File

@ -2,7 +2,7 @@
removal_milestone: '18.0'
announcement_milestone: '17.5'
breaking_change: true
window: 1
window: 2
reporter: lwanko
stage: Fulfillment
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/489850

View File

@ -0,0 +1,19 @@
- title: "Resource owner password credentials grant is deprecated"
# The milestones for the deprecation announcement, and the removal.
removal_milestone: "19.0"
announcement_milestone: "18.0"
# Change breaking_change to false if needed.
breaking_change: true
window: # Can be 1, 2, or 3 - The window when the breaking change will be deployed on GitLab.com
reporter: hsutor # The GitLab username of the person reporting the change
stage: software_supply_chain_security
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/457353
# Use the impact calculator https://gitlab-com.gitlab.io/gl-infra/breaking-change-impact-calculator/?
impact: medium
scope: instance
resolution_role: Admin
manual_task: true
body: | # (required) Don't change this line.
Using the resource owner password credentials (ROPC) grant as an OAuth flow is deprecated, and support will be fully removed in GitLab 19.0. We have added a setting that can be enabled or disabled by administrators to use this grant type with client credentials only, in their instances. This allows users who would like to opt out of using ROPC without client credentials to do so prior to 19.0. ROPC will be completely removed in 19.0 and cannot be used even with client credentials after that point.
GitLab has [required client authentication for ROPC on GitLab.com](https://about.gitlab.com/blog/2025/04/01/improving-oauth-ropc-security-on-gitlab-com/) since April 8, 2025 for security reasons. Fully removing ROPC support keeps security in line with the OAuth RFC version 2.1.

View File

@ -80,6 +80,7 @@ Valid reports are:
- `api_fuzzing`
- `coverage_fuzzing`
- `sast`
- `secret_detection`
For example, here is the definition of a SAST job that generates a file named `gl-sast-report.json`,
and uploads it as a SAST report:

View File

@ -174,14 +174,25 @@ For more information, see [GitLab Duo add-on seat management with LDAP](../admin
## View assigned GitLab Duo users
{{< history >}}
- Last GitLab Duo activity field [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/455761) in GitLab 18.0.
{{< /history >}}
Prerequisites:
- You must purchase a GitLab Duo add-on, or have an active GitLab Duo trial.
After you purchase GitLab Duo, you can assign seats to users to grant access to the add-on.
After you purchase GitLab Duo, you can assign seats to users to grant access to the add-on. Then you can view details of assigned GitLab Duo users.
To retrieve the **Last Duo activity** date for assigned GitLab Duo users,
you can also use the [GraphQL API](../api/graphql/reference/_index.md#addonuser) with the `AddOnUser` type.
The GitLab Duo seat utilization page shows the following information for each user:
- User's full name and username
- Seat assignment status
- Public email address
- Last GitLab activity
- Last GitLab Duo activity
### For GitLab.com

View File

@ -26,8 +26,6 @@ This window takes place on April 21 - 23, 2025 from 09:00 UTC to 22:00 UTC.
|-------------|--------|-------|-------|------------------------|
| [CI/CD job token - **Limit access from your project** setting removal](deprecations.md#cicd-job-token---limit-access-from-your-project-setting-removal) | High | Software supply chain security | Project | Refer to the [Understanding this change](https://gitlab.com/gitlab-org/gitlab/-/issues/395708#understanding-this-change) section for details. |
| [CI/CD job token - **Authorized groups and projects** allowlist enforcement](deprecations.md#cicd-job-token---authorized-groups-and-projects-allowlist-enforcement) | High | Software supply chain security | Project | Refer to the [Understanding this change](https://gitlab.com/gitlab-org/gitlab/-/issues/383084#understanding-this-change) section for details. |
| [Replace `add_on_purchase` GraphQL field with `add_on_purchases`](deprecations.md#replace-add_on_purchase-graphql-field-with-add_on_purchases) | Low | Fulfillment | Instance, group | |
| [Replace namespace `add_on_purchase` GraphQL field with `add_on_purchases`](deprecations.md#replace-namespace-add_on_purchase-graphql-field-with-add_on_purchases) | Low | Fulfillment | Instance, group | |
| [Deprecation of `name` field in `ProjectMonthlyUsageType` GraphQL API](deprecations.md#deprecation-of-name-field-in-projectmonthlyusagetype-graphql-api) | Low | Fulfillment | Project | |
| [Deprecation of `STORAGE` enum in `NamespaceProjectSortEnum` GraphQL API](deprecations.md#deprecation-of-storage-enum-in-namespaceprojectsortenum-graphql-api) | Low | Fulfillment | Group | |
| [DAST `dast_devtools_api_timeout` will have a lower default value](deprecations.md#dast-dast_devtools_api_timeout-will-have-a-lower-default-value) | Low | Application security testing | Project | |
@ -40,6 +38,8 @@ This window takes place on April 28 - 30, 2025 from 09:00 UTC to 22:00 UTC.
| Deprecation | Impact | Stage | Scope | Check potential impact |
|-------------|--------|-------|-------|------------------------|
| [Replace `add_on_purchase` GraphQL field with `add_on_purchases`](deprecations.md#replace-add_on_purchase-graphql-field-with-add_on_purchases) | Low | Fulfillment | Instance, group | |
| [Replace namespace `add_on_purchase` GraphQL field with `add_on_purchases`](deprecations.md#replace-namespace-add_on_purchase-graphql-field-with-add_on_purchases) | Low | Fulfillment | Instance, group | |
| [Limit number of scan execution policy actions allowed per policy](deprecations.md#limit-number-of-scan-execution-policy-actions-allowed-per-policy) | Low | Security risk management | Instance, group, project | |
| [Behavior change for Upcoming and Started milestone filters](deprecations.md#behavior-change-for-upcoming-and-started-milestone-filters) | Low | Plan | Group, project | |

View File

@ -762,6 +762,24 @@ A replacement feature is planned as part of the [Auto Remediation vision](https:
<div class="deprecation breaking-change" data-milestone="19.0">
### Resource owner password credentials grant is deprecated
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone">18.0</span>
- Removal in GitLab <span class="milestone">19.0</span> ([breaking change](https://docs.gitlab.com/update/terminology/#breaking-change))
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/457353).
</div>
Using the resource owner password credentials (ROPC) grant as an OAuth flow is deprecated, and support will be fully removed in GitLab 19.0. We have added a setting that can be enabled or disabled by administrators to use this grant type with client credentials only, in their instances. This allows users who would like to opt out of using ROPC without client credentials to do so prior to 19.0. ROPC will be completely removed in 19.0 and cannot be used even with client credentials after that point.
GitLab has [required client authentication for ROPC on GitLab.com](https://about.gitlab.com/blog/2025/04/01/improving-oauth-ropc-security-on-gitlab-com/) since April 8, 2025 for security reasons. Fully removing ROPC support keeps security in line with the OAuth RFC version 2.1.
</div>
<div class="deprecation breaking-change" data-milestone="19.0">
### S3 storage driver (AWS SDK v1) for the container registry
<div class="deprecation-notes">

View File

@ -181,10 +181,13 @@ Dependency paths are supported for the following package managers only when usin
If the [Dependency Scanning](../dependency_scanning/_index.md) CI job is configured,
[discovered licenses](../../compliance/license_scanning_of_cyclonedx_files/_index.md) are displayed on this page.
## Download the dependency list
## Export
You can download the full list of dependencies and their details in JSON, CSV, or CycloneDX format.
The dependency list shows only the results of the last successful pipeline that ran on the default branch.
You can export the dependency list in:
- JSON
- CSV
- CycloneDX format (for projects only)
To download the dependency list:
@ -192,6 +195,12 @@ To download the dependency list:
1. Select **Secure > Dependency list**.
1. Select **Export**.
{{< alert type="note" >}}
CycloneDX export is available on the project dependency list.
{{< /alert >}}
## Troubleshooting
### License appears as 'unknown'

View File

@ -28244,6 +28244,9 @@ msgstr ""
msgid "GithubImporter|Release attachments"
msgstr ""
msgid "GithubImporter|Something went wrong while fetching GitHub organizations."
msgstr ""
msgid "GithubImporter|Your import of GitHub gists into GitLab snippets is complete."
msgstr ""
@ -35357,6 +35360,9 @@ msgstr ""
msgid "Last Accessed On"
msgstr ""
msgid "Last GitLab Duo activity"
msgstr ""
msgid "Last GitLab activity"
msgstr ""

View File

@ -3,7 +3,7 @@
source 'https://rubygems.org'
gem 'gitlab-qa', '~> 15', '>= 15.4.0', require: 'gitlab/qa'
gem 'gitlab_quality-test_tooling', '~> 2.9.0', require: false
gem 'gitlab_quality-test_tooling', '~> 2.10.0', require: false
gem 'gitlab-utils', path: '../gems/gitlab-utils'
gem 'activesupport', '~> 7.0.8.7' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.26.0'

View File

@ -128,7 +128,7 @@ GEM
rainbow (>= 3, < 4)
table_print (= 1.5.7)
zeitwerk (>= 2, < 3)
gitlab_quality-test_tooling (2.9.0)
gitlab_quality-test_tooling (2.10.0)
activesupport (>= 7.0, < 7.3)
amatch (~> 0.4.1)
fog-google (~> 1.24, >= 1.24.1)
@ -369,7 +369,7 @@ DEPENDENCIES
gitlab-orchestrator!
gitlab-qa (~> 15, >= 15.4.0)
gitlab-utils!
gitlab_quality-test_tooling (~> 2.9.0)
gitlab_quality-test_tooling (~> 2.10.0)
googleauth (~> 1.9.0)
influxdb-client (~> 3.2)
junit_merge (~> 0.1.2)

View File

@ -43,7 +43,7 @@ module RuboCop
def on_casgn(node)
# we want to make sure that we are running the cop only on
# ConfigFiles::Constants::CONFIG_FILES_CONSTANTS
return unless config_files_constants?(node.parent.parent)
return unless config_files_constants?(node&.parent&.parent)
_matcher, constants_array = config_file_classes(node)

View File

@ -54,7 +54,6 @@ ee/spec/frontend/requirements/components/requirements_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/security_configuration/components/app_spec.js
ee/spec/frontend/security_configuration/dast_profiles/components/dast_profiles_list_spec.js
ee/spec/frontend/security_dashboard/components/shared/vulnerability_report/vulnerability_report_tabs_spec.js
ee/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
ee/spec/frontend/status_checks/components/modal_create_spec.js

View File

@ -3,7 +3,7 @@
require 'spec_helper'
require_relative './shared_context_and_examples'
RSpec.describe 'CI configuration validation - branch pipelines', feature_category: :tooling do
RSpec.describe 'CI configuration validation - branch pipelines', feature_category: :tooling, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/535543' do
include ProjectForksHelper
include CiConfigurationValidationHelper

View File

@ -177,7 +177,7 @@ export const mockPipelineJob = {
};
// for `pipeline_stage_spec.js`
export const mockPipelineStageJobs = {
export const createMockPipelineStageJobs = () => ({
data: {
ciPipelineStage: {
__typename: 'CiStage',
@ -237,7 +237,9 @@ export const mockPipelineStageJobs = {
},
},
},
};
});
export const mockPipelineStageJobs = createMockPipelineStageJobs();
export const singlePipeline = {
id: 'gid://gitlab/Ci::Pipeline/610',

View File

@ -1,6 +1,6 @@
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlButton, GlDisclosureDropdown, GlLoadingIcon } from '@gitlab/ui';
import { GlButton, GlDisclosureDropdown, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createAlert } from '~/alert';
@ -13,7 +13,12 @@ import JobDropdownItem from '~/ci/common/private/job_dropdown_item.vue';
import PipelineStageDropdown from '~/ci/pipeline_mini_graph/pipeline_stage_dropdown.vue';
import getPipelineStageJobsQuery from '~/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stage_jobs.query.graphql';
import { mockPipelineStageJobs, pipelineStage, pipelineStageJobsFetchError } from './mock_data';
import {
createMockPipelineStageJobs,
mockPipelineStageJobs,
pipelineStage,
pipelineStageJobsFetchError,
} from './mock_data';
Vue.use(VueApollo);
jest.mock('~/alert');
@ -57,15 +62,19 @@ describe('PipelineStageDropdown', () => {
const clickStageDropdown = async () => {
await findDropdownButton().trigger('click');
await waitForPromises;
await waitForPromises();
await nextTick();
};
beforeEach(() => {
pipelineStageResponse = jest.fn();
createComponent();
pipelineStageResponse = jest.fn().mockResolvedValue(mockPipelineStageJobs);
});
describe('when mounted', () => {
beforeEach(() => {
createComponent();
});
it('renders the dropdown', () => {
expect(findStageDropdown().exists()).toBe(true);
});
@ -76,6 +85,10 @@ describe('PipelineStageDropdown', () => {
});
describe('dropdown appearance', () => {
beforeEach(() => {
createComponent();
});
it('renders the icon', () => {
expect(findCiIcon().exists()).toBe(true);
});
@ -94,24 +107,22 @@ describe('PipelineStageDropdown', () => {
});
describe('when dropdown is clicked', () => {
beforeEach(async () => {
await createComponent();
pipelineStageResponse.mockResolvedValue(mockPipelineStageJobs);
});
it('has the correct header title', async () => {
await createComponent();
await clickStageDropdown();
expect(findDropdownHeader().text()).toBe('Stage: build');
});
it('emits miniGraphStageClick', async () => {
await createComponent();
await clickStageDropdown();
expect(wrapper.emitted('miniGraphStageClick')).toHaveLength(1);
});
it('has fired the stage query', async () => {
await createComponent();
await clickStageDropdown();
const { stage } = defaultProps;
@ -120,16 +131,25 @@ describe('PipelineStageDropdown', () => {
describe('and query is loading', () => {
it('renders a loading icon and no list', async () => {
let res;
pipelineStageResponse.mockImplementationOnce(
() =>
new Promise((resolve) => {
res = resolve;
}),
);
createComponent();
await clickStageDropdown();
expect(findLoadingIcon().exists()).toBe(true);
expect(findJobList().exists()).toBe(false);
res();
});
});
describe('and query is successful', () => {
beforeEach(async () => {
await createComponent();
await clickStageDropdown();
});
@ -147,6 +167,34 @@ describe('PipelineStageDropdown', () => {
findJobDropdownItems().at(0).vm.$emit('jobActionExecuted');
expect(wrapper.emitted('jobActionExecuted')).toHaveLength(1);
});
it('does not show search', () => {
expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(false);
});
});
describe('with too many items', () => {
let jobs;
beforeEach(async () => {
jobs = createMockPipelineStageJobs();
jobs.data.ciPipelineStage.jobs.nodes = new Array(13)
.fill(jobs.data.ciPipelineStage.jobs.nodes[0])
.map((node, i) => ({ ...node, name: node.name + i, id: node.id + i }));
await createComponent({ pipelineStageHandler: jest.fn().mockResolvedValue(jobs) });
await clickStageDropdown();
});
it('displays search', () => {
expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true);
});
it('searches items', async () => {
const { name } = jobs.data.ciPipelineStage.jobs.nodes[5];
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', name);
await nextTick();
expect(findJobDropdownItems().length).toBe(1);
});
});
describe('and query is not successful', () => {
@ -155,8 +203,6 @@ describe('PipelineStageDropdown', () => {
it('throws an error for the pipeline query', async () => {
await createComponent({ pipelineStageHandler: failedHandler });
await clickStageDropdown();
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({ message: pipelineStageJobsFetchError });
});
});
@ -164,10 +210,8 @@ describe('PipelineStageDropdown', () => {
describe('when there are failed jobs', () => {
beforeEach(async () => {
pipelineStageResponse.mockResolvedValue(mockPipelineStageJobs);
await createComponent();
await clickStageDropdown();
await waitForPromises();
});
it('renders failed jobs title', () => {
@ -189,7 +233,6 @@ describe('PipelineStageDropdown', () => {
pipelineStageResponse.mockResolvedValue(withoutFailedJob);
await createComponent();
await clickStageDropdown();
await waitForPromises();
});
it('does not render failed jobs title', () => {
@ -206,7 +249,6 @@ describe('PipelineStageDropdown', () => {
pipelineStageResponse.mockResolvedValue(mockPipelineStageJobs);
await createComponent();
await clickStageDropdown();
await waitForPromises();
});
it('starts polling when dropdown is open', () => {
@ -225,7 +267,6 @@ describe('PipelineStageDropdown', () => {
expect(pipelineStageResponse).toHaveBeenCalledTimes(2);
await clickStageDropdown();
await waitForPromises();
jest.advanceTimersByTime(8000);
@ -246,7 +287,6 @@ describe('PipelineStageDropdown', () => {
props: { isMergeTrain: true },
});
await clickStageDropdown();
await waitForPromises();
expect(findMergeTrainMessage().exists()).toBe(true);
});

View File

@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { captureException } from '~/sentry/sentry_browser_wrapper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@ -13,7 +14,7 @@ import GithubOrganizationsBox from '~/import_entities/import_projects/components
jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/alert');
const MOCK_RESPONSE = {
const mockResponse = {
provider_groups: [{ name: 'alpha-1' }, { name: 'alpha-2' }, { name: 'beta-1' }],
};
@ -38,27 +39,20 @@ describe('GithubOrganizationsBox component', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_OK, MOCK_RESPONSE);
});
afterEach(() => {
mockAxios.restore();
});
it('has underlying listbox as loading while loading organizations', () => {
it('renders listbox as loading', () => {
createComponent();
expect(findListbox().props('loading')).toBe(true);
});
it('clears underlying listbox when loading is complete', async () => {
createComponent();
await axios.waitForAll();
expect(findListbox().props('loading')).toBe(false);
});
it('sets toggle-text to all organizations when selection is not provided', () => {
createComponent({ value: '' });
expect(findListbox().props('toggleText')).toBe(GithubOrganizationsBox.i18n.allOrganizations);
expect(findListbox().props('toggleText')).toBe('All organizations');
});
it('sets toggle-text to organization name when it is provided', () => {
@ -75,23 +69,91 @@ describe('GithubOrganizationsBox component', () => {
expect(wrapper.emitted('input').at(-1)).toStrictEqual(['org-id']);
});
it('filters list for underlying listbox', async () => {
createComponent();
await axios.waitForAll();
describe('when request is successful', () => {
beforeEach(async () => {
mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_OK, mockResponse);
findListbox().vm.$emit('search', 'alpha');
await nextTick();
createComponent();
await waitForPromises();
});
// 2 matches + 'All organizations'
expect(findListbox().props('items')).toHaveLength(3);
it('renders listbox as loaded', () => {
expect(findListbox().props('loading')).toBe(false);
});
it('filters organizations on search', async () => {
findListbox().vm.$emit('search', 'alpha');
await nextTick();
// 2 items matching search; 1 item for 'All organizations'
expect(findListbox().props('items')).toHaveLength(3);
});
});
it('reports error to sentry on load', async () => {
mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
await axios.waitForAll();
describe('when request fails', () => {
it('reports error to sentry', async () => {
mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(captureException).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalled();
createComponent();
await waitForPromises();
expect(captureException).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalled();
});
});
describe('infinite scroll', () => {
describe('when less than 25 organizations are returned', () => {
it('does not enable infinite scroll', async () => {
createComponent();
await waitForPromises();
expect(findListbox().props('infiniteScroll')).toBe(false);
});
});
describe('when 25 organizations are returned', () => {
const mockResponsePage1 = {
provider_groups: Array(25)
.fill()
.map((_, i) => ({ name: `org-${i}` })),
};
beforeEach(async () => {
mockAxios.onGet(mockGithubGroupPath).replyOnce(HTTP_STATUS_OK, mockResponsePage1);
createComponent();
await waitForPromises();
});
it('enables infinite scroll', () => {
expect(findListbox().props('infiniteScroll')).toBe(true);
});
describe('when bottom is reached', () => {
beforeEach(() => {
mockAxios
.onGet(mockGithubGroupPath, { params: { page: 2 } })
.replyOnce(HTTP_STATUS_OK, mockResponse);
findListbox().vm.$emit('bottom-reached');
});
it('loads more organizations', async () => {
expect(findListbox().props('infiniteScrollLoading')).toBe(true);
await waitForPromises();
expect(findListbox().props('infiniteScrollLoading')).toBe(false);
// 25 items on page 1; 3 items on page 2; 1 item for 'All organizations'
expect(findListbox().props('items')).toHaveLength(29);
});
it('disables infinite scroll when page 2 has less than 25 organizations', async () => {
await waitForPromises();
expect(findListbox().props('infiniteScroll')).toBe(false);
});
});
});
});
});

View File

@ -85,26 +85,6 @@ describe('GithubStatusTable', () => {
});
});
it('renders name filter disabled when tab with organization filter is selected and organization is not set', async () => {
const NEW_ACTIVE_TAB_IDX = GithubStatusTable.relationTypes.findIndex(
(entry) => entry.showOrganizationFilter,
);
await selectTab(NEW_ACTIVE_TAB_IDX);
expect(findFilterField().props('disabled')).toBe(true);
});
it('enables name filter disabled when organization is set', async () => {
const NEW_ACTIVE_TAB_IDX = GithubStatusTable.relationTypes.findIndex(
(entry) => entry.showOrganizationFilter,
);
await selectTab(NEW_ACTIVE_TAB_IDX);
wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', 'some-org');
await nextTick();
expect(findFilterField().props('disabled')).toBe(false);
});
it('updates filter when search box is changed', async () => {
const NEW_FILTER = 'test';
findFilterField().vm.$emit('submit', NEW_FILTER);
@ -115,12 +95,32 @@ describe('GithubStatusTable', () => {
});
});
it('updates organization_login filter when GithubOrganizationsBox emits input', () => {
const NEW_ORG = 'some-org';
wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', NEW_ORG);
describe('when "Organization" tab is selected', () => {
beforeEach(async () => {
const organizationTabIndex = GithubStatusTable.relationTypes.findIndex(
(entry) => entry.showOrganizationFilter,
);
await selectTab(organizationTabIndex);
});
expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), {
organization_login: NEW_ORG,
it('renders disabled name filter when organization is not set', () => {
expect(findFilterField().props('disabled')).toBe(true);
});
it('enables name filter when organization is set', async () => {
wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', 'some-org');
await nextTick();
expect(findFilterField().props('disabled')).toBe(false);
});
it('updates organization_login filter when GithubOrganizationsBox emits input', () => {
const NEW_ORG = 'some-org';
wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', NEW_ORG);
expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), {
organization_login: NEW_ORG,
});
});
});
});

View File

@ -29,12 +29,16 @@ exports[`packages_list_row renders 1`] = `
class="gl-flex gl-gap-3 gl-items-center gl-min-w-0 gl-mr-5"
data-testid="package-name"
>
<a
class="gl-break-all gl-min-w-0 gl-text-default"
data-testid="details-link"
<div
class="gl-gap-2 sm:gl-flex"
>
@gitlab-org/package-15
</a>
<a
class="gl-break-all gl-min-w-0 gl-text-default"
data-testid="details-link"
>
@gitlab-org/package-15
</a>
</div>
</div>
</div>
<div

View File

@ -16,6 +16,7 @@ import {
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import {
conanMetadata,
linksData,
packageData,
packagePipelines,
@ -51,6 +52,9 @@ describe('packages_list_row', () => {
const findListItem = () => wrapper.findComponent(ListItem);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
const findPackageName = () => wrapper.findByTestId('package-name');
const findConanMetadata = () => wrapper.findByTestId('conan-metadata');
const findPackageUsername = () => wrapper.findByTestId('package-username');
const findPackageChannel = () => wrapper.findByTestId('package-channel');
const mountComponent = ({
packageEntity = packageWithoutTags,
@ -406,4 +410,26 @@ describe('packages_list_row', () => {
});
});
});
describe('conan metadata', () => {
it('does not render `packageUsername` and `packageChannel` for any packages other than conan', () => {
mountComponent();
expect(findConanMetadata().exists()).toBe(false);
});
it('renders correct `packageUsername` and `packageChannel` when package type is conan', () => {
mountComponent({
packageEntity: {
...packageWithoutTags,
packageType: 'CONAN',
metadata: conanMetadata(),
},
});
expect(findConanMetadata().exists()).toBe(true);
expect(findPackageUsername().text()).toBe('gitlab-org+gitlab-test');
expect(findPackageChannel().text()).toBe('stable');
});
});
});

View File

@ -169,6 +169,7 @@ export const packageData = (extend) => ({
protectionRuleExists: false,
...userPermissionsData,
...extend,
metadata: null,
});
export const conanMetadata = () => ({
@ -236,13 +237,7 @@ export const packageDetailsQuery = ({ extendPackage = {} } = {}) => ({
data: {
package: {
...packageData(),
metadata: {
...conanMetadata(),
...composerMetadata(),
...pypiMetadata(),
...mavenMetadata(),
...nugetMetadata(),
},
metadata: packageTypeMetadataQueryMapping[extendPackage.packageType]?.() || null,
project: {
id: '1',
path: 'projectPath',

View File

@ -108,12 +108,7 @@ describe('WorkItemDescriptionRendered', () => {
describe('with anchor to description item', () => {
const anchorHash = '#description-anchor';
afterAll(() => {
window.location.hash = '';
});
it('scrolls matching link into view', async () => {
window.location.hash = anchorHash;
const setupComponent = async () => {
createComponent({
workItemDescription: {
description: 'This is a long description',
@ -128,10 +123,39 @@ describe('WorkItemDescriptionRendered', () => {
jest.spyOn(wrapper.vm, 'truncateLongDescription');
await nextTick();
};
afterAll(() => {
window.location.hash = '';
});
it('scrolls matching link into view when opened with hash present', async () => {
window.location.hash = anchorHash;
await setupComponent();
// Check if page loaded with hash present scrolls hash into view.
// In order to scroll, description must not have been truncated.
expect(handleLocationHash).toHaveBeenCalled();
expect(wrapper.vm.truncateLongDescription).not.toHaveBeenCalled();
});
it('expands description and then scrolls to matching link into view on user navigation', async () => {
window.location.hash = '';
await setupComponent();
// Check if page loaded with no hash present shows truncated description.
expect(handleLocationHash).not.toHaveBeenCalled();
// Simulate user clicking on an anchor hash within the description.
window.location.hash = anchorHash;
window.dispatchEvent(new Event('hashchange'));
await nextTick();
// Check if description is expanded and hash is scrolled into view.
expect(handleLocationHash).toHaveBeenCalled();
expect(findReadMore().exists()).toBe(false);
});
});
describe('`disableHeadingAnchors` prop', () => {

View File

@ -52,6 +52,10 @@ RSpec.describe Snippets::CreateService, feature_category: :source_code_managemen
expect(subject).to be_error
end
it 'responds with a reason' do
expect(subject.reason).to eq(described_class::SNIPPET_ACCESS_ERROR)
end
it 'does not create a public snippet' do
expect(subject.message).to match('has been restricted')
end

View File

@ -46,6 +46,10 @@ RSpec.describe Snippets::UpdateService, feature_category: :source_code_managemen
expect(subject).to be_error
end
it 'responds with a reason' do
expect(subject.reason).to eq(described_class::SNIPPET_ACCESS_ERROR)
end
it 'does not update snippet to public visibility' do
original_visibility = snippet.visibility_level