Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-08-23 15:09:07 +00:00
parent 8d4eff3fd9
commit 51aa153c0d
36 changed files with 7475 additions and 225 deletions

View File

@ -81,6 +81,10 @@ export const BASE_ROLES = [
},
];
export const BASE_ROLES_WITHOUT_MINIMAL_ACCESS = BASE_ROLES.filter(
({ accessLevel }) => accessLevel !== ACCESS_LEVEL_MINIMAL_ACCESS_INTEGER,
);
export const ACCESS_LEVEL_LABELS = {
[ACCESS_LEVEL_NO_ACCESS_INTEGER]: ACCESS_LEVEL_NO_ACCESS,
[ACCESS_LEVEL_MINIMAL_ACCESS_INTEGER]: ACCESS_LEVEL_MINIMAL_ACCESS,

View File

@ -1,5 +1,6 @@
<script>
import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { GlSkeletonLoader, GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
@ -32,7 +33,10 @@ const HIDE_TD_ON_MOBILE = '!gl-hidden lg:!gl-table-cell';
*/
export default {
name: 'PipelinesTable',
cellHeight: 50,
components: {
GlSkeletonLoader,
GlTableLite,
LegacyPipelineMiniGraph,
PipelineFailedJobsWidget,
@ -51,15 +55,15 @@ export default {
},
},
props: {
pipelines: {
type: Array,
required: true,
},
updateGraphDropdown: {
isCreatingPipeline: {
type: Boolean,
required: false,
default: false,
},
pipelines: {
type: Array,
required: true,
},
pipelineIdType: {
type: String,
required: false,
@ -68,8 +72,16 @@ export default {
return value === PIPELINE_IID_KEY || value === PIPELINE_ID_KEY;
},
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
isMobile() {
return ['md', 'sm', 'xs'].includes(GlBreakpointInstance.getBreakpointSize());
},
tableFields() {
return [
{
@ -112,13 +124,19 @@ export default {
return this.useFailedJobsWidget ? '!gl-pb-0 !gl-border-none' : 'pl-p-5!';
},
pipelinesWithDetails() {
let { pipelines } = this;
if (this.isCreatingPipeline) {
pipelines = [{ isLoading: true }, ...this.pipelines];
}
if (this.useFailedJobsWidget) {
return this.pipelines.map((p) => {
pipelines = pipelines.map((p) => {
return { ...p, _showDetails: true };
});
}
return this.pipelines;
return pipelines;
},
},
methods: {
@ -126,6 +144,9 @@ export default {
const downstream = pipeline.triggered;
return keepLatestDownstreamPipelines(downstream);
},
cellWidth(ref) {
return this.$refs[ref]?.offsetWidth;
},
getProjectPath(item) {
return cleanLeadingSeparator(item.project.full_path);
},
@ -144,6 +165,13 @@ export default {
onCancelPipeline(pipeline) {
this.$emit('cancel-pipeline', pipeline);
},
setLoaderPosition(ref) {
if (this.isMobile) {
return this.cellWidth(ref) / 2;
}
return 0;
},
trackPipelineMiniGraph() {
this.track('click_minigraph', { label: TRACKING_CATEGORIES.table });
},
@ -172,11 +200,30 @@ export default {
</template>
<template #cell(status)="{ item }">
<pipeline-status-badge :pipeline="item" />
<div v-if="item.isLoading" ref="status">
<gl-skeleton-loader :height="$options.cellHeight" :width="cellWidth('status')">
<rect height="30" rx="4" ry="4" :width="cellWidth('status')" />
</gl-skeleton-loader>
</div>
<pipeline-status-badge v-else :pipeline="item" />
</template>
<template #cell(pipeline)="{ item }">
<div v-if="item.isLoading" ref="pipeline">
<gl-skeleton-loader :height="$options.cellHeight" :width="cellWidth('pipeline')">
<rect height="14" rx="4" ry="4" :width="cellWidth('pipeline')" />
<rect
height="10"
rx="4"
ry="4"
:width="cellWidth('pipeline') / 2"
:x="setLoaderPosition('pipeline')"
y="20"
/>
</gl-skeleton-loader>
</div>
<pipeline-url
v-else
:pipeline="item"
:pipeline-id-type="pipelineIdType"
ref-color="gl-text-default"
@ -184,11 +231,22 @@ export default {
</template>
<template #cell(triggerer)="{ item }">
<pipeline-triggerer :pipeline="item" />
<div v-if="item.isLoading" ref="triggerer" class="gl-ml-3">
<gl-skeleton-loader :height="$options.cellHeight" :width="cellWidth('triggerer')">
<rect :height="34" rx="50" ry="50" :width="34" />
</gl-skeleton-loader>
</div>
<pipeline-triggerer v-else :pipeline="item" />
</template>
<template #cell(stages)="{ item }">
<div v-if="item.isLoading" ref="stages">
<gl-skeleton-loader :height="$options.cellHeight" :width="cellWidth('stages')">
<rect height="20" rx="10" ry="10" :width="cellWidth('stages')" />
</gl-skeleton-loader>
</div>
<legacy-pipeline-mini-graph
v-else
:downstream-pipelines="getDownstreamPipelines(item)"
:pipeline-path="item.path"
:stages="getStages(item)"
@ -200,6 +258,7 @@ export default {
<template #cell(actions)="{ item }">
<pipeline-operations
v-if="!item.isLoading"
:pipeline="item"
@cancel-pipeline="onCancelPipeline"
@refresh-pipelines-table="onRefreshPipelinesTable"
@ -209,7 +268,7 @@ export default {
<template #row-details="{ item }">
<pipeline-failed-jobs-widget
v-if="useFailedJobsWidget"
v-if="useFailedJobsWidget && !item.isLoading"
:failed-jobs-count="failedJobsCount(item)"
:is-pipeline-active="item.active"
:pipeline-iid="item.iid"

View File

@ -296,10 +296,11 @@ export default {
</gl-button>
<pipelines-table
:is-creating-pipeline="state.isRunningMergeRequestPipeline"
:pipeline-id-type="$options.pipelineIdKey"
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:view-type="viewType"
:pipeline-id-type="$options.pipelineIdKey"
@cancel-pipeline="onCancelPipeline"
@refresh-pipelines-table="onRefreshPipelinesTable"
@retry-pipeline="onRetryPipeline"

View File

@ -220,7 +220,7 @@ class GfmAutoComplete {
tpl += ' <small class="params"><%- params.join(" ") %></small>';
}
if (value.warning && value.icon && value.icon === 'confidential') {
tpl += `<small class="description gl-display-flex gl-align-items-center">${spriteIcon(
tpl += `<small class="description gl-flex gl-items-center">${spriteIcon(
'eye-slash',
's16 gl-mr-2',
)}<em><%- warning %></em></small>`;

View File

@ -50,7 +50,7 @@ div.innerHTML = `
</defs>
</svg>
<h2>Don't want to see this message anymore?</h2>
<p class="gl-text-body">
<p class="gl-text-primary">
Follow the documentation to switch to using Vite.<br />
Vite compiles frontend assets faster and eliminates the need for this message.
</p>

View File

@ -28,7 +28,7 @@ import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item
const ALLOWED_ICONS = ['issue-close'];
const ICON_COLORS = {
'issue-close': '!gl-bg-blue-100 gl-text-blue-700',
'issue-close': '!gl-bg-blue-100 gl-text-blue-700 icon-info',
};
export default {
@ -115,23 +115,27 @@ export default {
<template>
<timeline-entry-item
:id="noteAnchorId"
:class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }"
class="note system-note note-wrapper"
:class="{
target: isTargetNote,
'pr-0': shouldShowDescriptionVersion,
}"
class="system-note-v2"
>
<div
:class="[
getIconColor,
{
'system-note-icon gl-bg-gray-50 gl-text-gray-600': isAllowedIcon,
'system-note-tiny-dot !gl-bg-gray-900': !isAllowedIcon,
'system-note-icon-v2 gl-h-6 gl-w-6 gl-ml-2 -gl-mt-1': isAllowedIcon,
'system-note-dot gl-h-3 gl-w-3 gl-mt-3 -gl-top-1 gl-ml-4 gl-border-2 gl-border-gray-50 gl-border-solid gl-bg-gray-900':
!isAllowedIcon,
},
]"
class="gl-relative gl-float-left gl-flex gl-items-center gl-justify-center gl-rounded-full"
>
<gl-icon v-if="isAllowedIcon" :size="12" :name="note.systemNoteIconName" />
<gl-icon v-if="isAllowedIcon" :size="14" :name="note.systemNoteIconName" />
</div>
<div class="timeline-content">
<div class="note-header">
<div class="gl-ml-7">
<div class="gl-flex gl-justify-between gl-items-start">
<note-header
:author="note.author"
:created-at="note.createdAt"
@ -153,11 +157,8 @@ export default {
</template>
</note-header>
</div>
<div class="note-body">
<div
v-if="shouldShowDescriptionVersion"
class="description-version gl-relative !gl-pt-3 gl-pl-4"
>
<div class="note-body-v2 gl-pl-3 gl-pb-3">
<div v-if="shouldShowDescriptionVersion" class="gl-relative !gl-pt-3">
<pre v-if="isLoadingDescriptionVersion" class="loading-state">
<gl-skeleton-loader />
</pre>
@ -165,7 +166,7 @@ export default {
v-else
v-safe-html="descriptionVersion"
data-testid="description-version-diff"
class="wrapper gl-mt-3 gl-whitespace-pre-wrap gl-pr-7"
class="gl-mt-3 gl-whitespace-pre-wrap gl-pr-7"
></pre>
<gl-button
v-if="displayDeleteButton"

View File

@ -1,66 +0,0 @@
/**
Shared styles for system note dot and icon styles used for MR, Issue, Work Item
*/
.system-note-tiny-dot {
width: 8px;
height: 8px;
margin-top: 6px;
margin-left: 12px;
margin-right: 8px;
border: 2px solid var(--gray-50, $gray-50);
.gl-dark .modal-body & {
border-color: var(--gray-100, $gray-100);
}
}
.system-note-icon {
width: 20px;
height: 20px;
margin-left: 6px;
// stylelint-disable-next-line gitlab/no-gl-class
&.gl-bg-green-100 {
--bg-color: var(--green-100, #{$green-100});
}
// stylelint-disable-next-line gitlab/no-gl-class
&.gl-bg-red-100 {
--bg-color: var(--red-100, #{$red-100});
}
// stylelint-disable-next-line gitlab/no-gl-class
&.gl-bg-blue-100 {
--bg-color: var(--blue-100, #{$blue-100});
}
}
.system-note-icon:not(.mr-system-note-empty)::before {
content: '';
display: block;
position: absolute;
left: calc(50% - 1px);
bottom: 100%;
width: 2px;
height: 20px;
background: linear-gradient(to bottom, transparent, var(--bg-color));
.system-note:first-child & {
display: none;
}
}
.system-note-icon:not(.mr-system-note-empty)::after {
content: '';
display: block;
position: absolute;
left: calc(50% - 1px);
top: 100%;
width: 2px;
height: 20px;
background: linear-gradient(to bottom, var(--bg-color), transparent);
.system-note:last-child & {
display: none;
}
}

View File

@ -1,5 +1,4 @@
@import 'mixins_and_variables_and_functions';
@import 'system_note_styles';
@import './notes/system_notes_v2';
.issuable-details {

View File

@ -1,5 +1,4 @@
@import 'mixins_and_variables_and_functions';
@import 'system_note_styles';
@import './notes/system_notes_v2';
$work-item-field-inset-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-200, $gray-200) !important;

View File

@ -28,7 +28,8 @@ class ApplicationController < BaseActionController
include CheckRateLimit
include RequestPayloadLogger
include StrongPaginationParams
include Gitlab::HttpRouterRuleContext
include Gitlab::HttpRouter::RuleContext
include Gitlab::HttpRouter::RuleMetrics
before_action :authenticate_user!, except: [:route_not_found]
before_action :set_current_organization
@ -42,6 +43,7 @@ class ApplicationController < BaseActionController
before_action :active_user_check, unless: :devise_controller?
before_action :set_usage_stats_consent_flag
before_action :check_impersonation_availability
before_action :increment_http_router_metrics
# Make sure the `auth_user` is memoized so it can be logged, we do this after
# all other before filters that could have set the user.

View File

@ -59,6 +59,12 @@ module TodosHelper
if todo.resource_parent.is_a?(Group)
todo.resource_parent.name
else
# Note: A todo with neither project nor group is invalid.
# But still we heard from users with such todos in their database,
# and for these users the /dashboard/todos page returned 500.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/388051
return unless todo.project.present?
title = content_tag(:span, todo.project.name, class: 'project-name')
namespace = content_tag(:span, "#{todo.project.namespace.human_name} / ", class: 'namespace-name')

File diff suppressed because it is too large Load Diff

View File

@ -188,6 +188,7 @@ The following metrics are available:
| `gitlab_keeparound_refs_created_total` | Counter | 16.10 | Counts the number of keep-around refs actually created | `source` |
| `search_advanced_index_repair_total` | Counter | 17.3 | Counts the number of index repair operations | `document_type` |
| `search_advanced_boolean_settings` | Gauge | 17.3 | Current state of Advanced search boolean settings | `name` |
| `gitlab_http_router_rule_total` | Counter | 17.4 | Counts occurrences of HTTP Router rule's `rule_action` and `rule_type` | `rule_action`, `rule_type` |
## Metrics controlled by a feature flag

View File

@ -1,7 +1,7 @@
---
stage: Fulfillment
group: Subscription management
description: Seat assignment, GitLab Duo Pro add-on
description: Seat assignment, GitLab Duo add-on
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
---
@ -16,29 +16,30 @@ Subscription add-ons are purchased as additional seats in your subscription.
Access to features provided by subscription add-ons is managed through seat assignment. Subscription
add-ons can be assigned to billable users only.
## Purchase GitLab Duo Pro seats
## Purchase GitLab Duo seats
You can purchase additional GitLab Duo Pro seats for your group namespace or self-managed instance. After you complete the purchase,
you must assign the seats to billable users so that they can use GitLab Duo Pro.
You can purchase additional GitLab Duo Pro or GitLab Duo Enterprise seats for your group namespace or self-managed instance. After you complete the purchase, you must assign the seats to billable users so that they can use GitLab Duo.
To purchase GitLab Duo Pro seats, you can use the Customers Portal, or you can contact the [GitLab Sales team](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/).
To purchase GitLab Duo Pro seats, you can use the Customers Portal, or you can contact the [GitLab Sales team](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/). To purchase GitLab Duo Enterprise, contact the [GitLab Sales team](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/).
1. Sign in to the [GitLab Customers Portal](https://customers.gitlab.com/).
1. On the subscription card, select the vertical ellipsis (**{ellipsis_v}**).
1. Select **Buy GitLab Duo Pro**.
1. Enter the number of seats for GitLab Duo Pro.
1. Enter the number of seats for GitLab Duo.
1. Review the **Purchase summary** section.
1. From the **Payment method** dropdown list, select your payment method.
1. Select **Purchase seats**.
## Assign GitLab Duo Pro seats
## Assign GitLab Duo seats
Prerequisites:
- You must purchase the GitLab Duo Pro add-on, or have the GitLab Duo Pro trial.
- For self-managed and GitLab Dedicated, the GitLab Duo Pro add-on is available for GitLab 16.8 and later only.
- You must purchase a GitLab Duo add-on, or have an active GitLab Duo trial.
- For self-managed and GitLab Dedicated:
- The GitLab Duo Pro add-on is available in GitLab 16.8 and later.
- The GitLab Duo Enterprise add-on is only available in GitLab 17.3 and later.
After you purchase GitLab Duo Pro, you can assign seats to billable users to grant access to the add-on.
After you purchase GitLab Duo, you can assign seats to billable users to grant access to the add-on.
### For GitLab.com
@ -46,7 +47,7 @@ After you purchase GitLab Duo Pro, you can assign seats to billable users to gra
1. Select **Settings > GitLab Duo**.
1. To the right of the user, turn on the toggle to assign GitLab Duo Pro.
To use Code Suggestions in any project or group, a user must be assigned a seat in at least one top-level group.
To use GitLab Duo features in any project or group, a user must be assigned a seat in at least one top-level group.
### For self-managed
@ -61,14 +62,14 @@ Prerequisites:
1. On the left sidebar, select **Subscription**.
1. In **Subscription details**, to the right of **Last sync**, select
synchronize subscription (**{retry}**).
1. To the right of the user, turn on the toggle to assign GitLab Duo Pro.
1. To the right of the user, turn on the toggle to assign GitLab Duo.
#### Configure network and proxy settings
For self-managed instances, to enable GitLab Duo features,
you must [enable network connectivity](../user/ai_features_enable.md).
## Assign and remove GitLab Duo Pro seats in bulk
## Assign and remove GitLab Duo seats in bulk
You can assign or remove seats in bulk for multiple users.
@ -88,13 +89,13 @@ You can assign or remove seats in bulk for multiple users.
Administrators of self-managed instances can use a [Rake task](../raketasks/user_management.md#bulk-assign-users-to-gitlab-duo-pro) to assign or remove seats in bulk.
## Purchase additional GitLab Duo Pro seats
## Purchase additional GitLab Duo seats
You can purchase additional GitLab Duo Pro seats for your group namespace or self-managed instance. After you complete the purchase, the seats are added to the total number of GitLab Duo Pro seats in your subscription.
You can purchase additional GitLab Duo Pro or GitLab Duo Enterprise seats for your group namespace or self-managed instance. After you complete the purchase, the seats are added to the total number of GitLab Duo seats in your subscription.
Prerequisites:
- You must purchase the GitLab Duo Pro add-on.
- You must purchase the GitLab Duo Pro or GitLab Duo Enterprise add-on.
### For GitLab.com
@ -141,7 +142,7 @@ Prerequisites:
1. Select **Continue**.
1. If prompted, select the group that the trial should be applied to.
1. Select **Activate my trial**.
1. [Assign seats](#assign-gitlab-duo-pro-seats) to the users who need access.
1. [Assign seats](#assign-gitlab-duo-seats) to the users who need access.
### On Self-managed and GitLab Dedicated
@ -162,11 +163,11 @@ Prerequisites:
- Ensure the email you submit for trial registration matches the email of the [subscription contact](customers_portal.md#change-your-subscription-contact).
1. Select **Submit**.
The trial automatically syncs to your instance within 24 hours. After the trial has synced, [assign seats](#assign-gitlab-duo-pro-seats) to users that you want to access GitLab Duo Pro.
The trial automatically syncs to your instance within 24 hours. After the trial has synced, [assign seats](#assign-gitlab-duo-seats) to users that you want to access GitLab Duo.
## Automatic seat removal for seat overages
If your quantity of purchased GitLab Duo Pro seats is reduced, seat assignments are automatically removed to match the seat quantity available in the subscription.
If your quantity of purchased GitLab Duo add-on seats is reduced, seat assignments are automatically removed to match the seat quantity available in the subscription.
For example:

View File

@ -19,7 +19,7 @@ GitLab Duo features that are generally available are automatically turned on for
- For the best experience, you should upgrade to the [latest version of GitLab](https://about.gitlab.com/releases/categories/releases/).
- If you have GitLab Dedicated, you must have [GitLab Duo Pro or Enterprise](../../subscriptions/subscription-add-ons.md).
- For some generally available features, like [Code Suggestions](../project/repository/code_suggestions/index.md),
[you must assign seats](../../subscriptions/subscription-add-ons.md#assign-gitlab-duo-pro-seats)
[you must assign seats](../../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats)
to the users you want to have access.
GitLab Duo features that are experimental or beta are turned off by default
@ -235,8 +235,8 @@ You can use command-line tools such as `curl` to verify the connectivity.
In addition to [turning on GitLab Duo features](turn_on_off.md#prerequisites),
you can also do the following:
1. Verify that [subscription seats have been purchased](../../subscriptions/subscription-add-ons.md#purchase-gitlab-duo-pro-seats).
1. Ensure that [seats are assigned to users](../../subscriptions/subscription-add-ons.md#assign-gitlab-duo-pro-seats).
1. Verify that [subscription seats have been purchased](../../subscriptions/subscription-add-ons.md#purchase-gitlab-duo-seats).
1. Ensure that [seats are assigned to users](../../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats).
1. For IDE users with the [GitLab Duo extension](../../user/project/repository/code_suggestions/supported_extensions.md#supported-editor-extensions):
- Verify that the extension is up-to-date.
- Run extension setting health checks, and test the authentication.

View File

@ -160,6 +160,7 @@ Project Owners can do any listed action, and also can delete pipelines:
| Run CI/CD job | | | | ✓ | ✓ | |
| Run CI/CD pipeline for a protected branch | | | | ✓ | ✓ | Developers and maintainers: Only if the user is [allowed to merge or push to the protected branch](../ci/pipelines/index.md#pipeline-security-on-protected-branches). |
| Stop [environments](../ci/environments/index.md) | | | | ✓ | ✓ | |
| Delete [environments](../ci/environments/index.md) | | | | ✓ | ✓ | |
| View a job with [debug logging](../ci/variables/index.md#enable-debug-logging) | | | | ✓ | ✓ | |
| Use pipeline editor | | | | ✓ | ✓ | |
| Run [interactive web terminals](../ci/interactive_web_terminal/index.md) | | | | ✓ | ✓ | |
@ -352,7 +353,7 @@ Project permissions for [GitLab Duo](gitlab_duo/index.md):
| Action | Non-member | Guest | Reporter | Developer | Maintainer | Owner | Notes |
|-------------------------------------------------------------------------------------------------------------|------------|-------|----------|-----------|------------|-------|-------|
| <br>Configure [Duo feature availability](gitlab_duo/turn_on_off.md#turn-off-for-a-project) | | | | | ✓ | ✓ | |
| <br>Use Duo features | | ✓ | ✓ | ✓ | ✓ | ✓ | Code Suggestions requires a [user being assigned a seat to gain access to a Duo add-on](../subscriptions/subscription-add-ons.md#assign-gitlab-duo-pro-seats). |
| <br>Use Duo features | | ✓ | ✓ | ✓ | ✓ | ✓ | Code Suggestions requires a [user being assigned a seat to gain access to a Duo add-on](../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats). |
## Group members permissions
@ -527,11 +528,11 @@ Group permissions for [GitLab Duo](../user/gitlab_duo/index.md):
| Action | Non-member | Guest | Reporter | Developer | Maintainer | Owner | Notes |
|-------------------------------------------------------------------------------------------------------------|------------|-------|----------|-----------|------------|-------|-------|
| <br>Purchase [Duo seats](../subscriptions/subscription-add-ons.md#purchase-additional-gitlab-duo-pro-seats) | | | | | | ✓ | |
| <br>Purchase [Duo seats](../subscriptions/subscription-add-ons.md#purchase-additional-gitlab-duo-seats) | | | | | | ✓ | |
| <br>Configure [Duo feature availability](gitlab_duo/turn_on_off.md#turn-off-for-a-group) | | | | | ✓ | ✓ | |
| <br>Configure [self-hosted models](../administration/self_hosted_models/configure_duo_features.md) | | | | | | ✓ | |
| <br>Enable [beta and experimental features](gitlab_duo/turn_on_off.md#turn-on-beta-and-experimental-features) | | | | | | ✓ | |
| <br>Use Duo features | | | ✓ | ✓ | ✓ | ✓ | Requires [user being assigned a seat to gain access to a Duo add-on](../subscriptions/subscription-add-ons.md#assign-gitlab-duo-pro-seats). |
| <br>Use Duo features | | | ✓ | ✓ | ✓ | ✓ | Requires [user being assigned a seat to gain access to a Duo add-on](../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats). |
## Users with Minimal Access

View File

@ -59,7 +59,7 @@ Prerequisites:
- You must have [one of the supported IDE extensions](supported_extensions.md#supported-editor-extensions).
- Your organization must have purchased the GitLab Duo Pro add-on and
[assigned you a seat](../../../../subscriptions/subscription-add-ons.md#assign-gitlab-duo-pro-seats).
[assigned you a seat](../../../../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats).
- For self-managed GitLab, you must have GitLab 16.8 or later, and have
[configured proxy settings](../../../../subscriptions/subscription-add-ons.md#configure-network-and-proxy-settings).
To use Code Suggestions:

View File

@ -31,7 +31,7 @@ A flash message with Code Suggestions check status is displayed at the top of th
If suggestions are not displayed, follow these steps:
1. Ensure you have [installed a supported IDE extension](supported_extensions.md#supported-editor-extensions)
1. Ensure your administrator has [assigned you a seat](../../../../subscriptions/subscription-add-ons.md#assign-gitlab-duo-pro-seats).
1. Ensure your administrator has [assigned you a seat](../../../../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats).
If suggestions are still not displayed, try the following troubleshooting steps.

View File

@ -89,6 +89,8 @@ module API
feature_category: feature_category,
**http_router_rule_context
)
increment_http_router_metrics
end
before do
@ -201,7 +203,8 @@ module API
helpers ::API::Helpers::CommonHelpers
helpers ::API::Helpers::PerformanceBarHelpers
helpers ::API::Helpers::RateLimiter
helpers Gitlab::HttpRouterRuleContext
helpers Gitlab::HttpRouter::RuleContext
helpers Gitlab::HttpRouter::RuleMetrics
namespace do
after do

View File

@ -472,6 +472,47 @@ module Gitlab
current_request.path.match(%r{access_tokens/\d+/rotate$}) ||
current_request.path.match(%r{/personal_access_tokens/self/rotate$})
end
# To prevent Rack Attack from incorrectly rate limiting
# authenticated Git activity, we need to authenticate the user
# from other means (e.g. HTTP Basic Authentication) only if the
# request originated from a Git or Git LFS
# request. Repositories::GitHttpClientController or
# Repositories::LfsApiController normally does the authentication,
# but Rack Attack runs before those controllers.
def find_user_for_git_or_lfs_request
return unless git_or_lfs_request?
find_user_from_lfs_token || find_user_from_basic_auth_password
end
def find_user_from_personal_access_token_for_api_or_git
return unless api_request? || git_or_lfs_request?
find_user_from_personal_access_token
end
def find_user_from_dependency_proxy_token
return unless dependency_proxy_request?
token, _ = ActionController::HttpAuthentication::Token.token_and_options(current_request)
return unless token
user_or_deploy_token = ::DependencyProxy::AuthTokenService.user_or_deploy_token_from_jwt(token)
# Do not return deploy tokens
# See https://gitlab.com/gitlab-org/gitlab/-/issues/342481
return unless user_or_deploy_token.is_a?(::User)
user_or_deploy_token
rescue ActiveRecord::RecordNotFound
nil # invalid id used return no user
end
def dependency_proxy_request?
Gitlab::PathRegex.dependency_proxy_route_regex.match?(current_request.path)
end
end
end
end

View File

@ -50,25 +50,6 @@ module Gitlab
(user&.project_bot? || user&.service_account?) && api_request?
end
# To prevent Rack Attack from incorrectly rate limiting
# authenticated Git activity, we need to authenticate the user
# from other means (e.g. HTTP Basic Authentication) only if the
# request originated from a Git or Git LFS
# request. Repositories::GitHttpClientController or
# Repositories::LfsApiController normally does the authentication,
# but Rack Attack runs before those controllers.
def find_user_for_git_or_lfs_request
return unless git_or_lfs_request?
find_user_from_lfs_token || find_user_from_basic_auth_password
end
def find_user_from_personal_access_token_for_api_or_git
return unless api_request? || git_or_lfs_request?
find_user_from_personal_access_token
end
def valid_access_token?(scopes: [])
# We may just be checking whether the user has :admin_mode access, so
# don't construe an auth failure as a real failure.
@ -131,28 +112,6 @@ module Gitlab
deploy_token_allowed: api_request? || git_request?
}
end
def find_user_from_dependency_proxy_token
return unless dependency_proxy_request?
token, _ = ActionController::HttpAuthentication::Token.token_and_options(current_request)
return unless token
user_or_deploy_token = ::DependencyProxy::AuthTokenService.user_or_deploy_token_from_jwt(token)
# Do not return deploy tokens
# See https://gitlab.com/gitlab-org/gitlab/-/issues/342481
return unless user_or_deploy_token.is_a?(::User)
user_or_deploy_token
rescue ActiveRecord::RecordNotFound
nil # invalid id used return no user
end
def dependency_proxy_request?
Gitlab::PathRegex.dependency_proxy_route_regex.match?(current_request.path)
end
end
end
end

View File

@ -6,7 +6,7 @@ module Gitlab
module Sbom
module Validators
class CyclonedxSchemaValidator
SUPPORTED_SPEC_VERSIONS = %w[1.4 1.5].freeze
SUPPORTED_SPEC_VERSIONS = %w[1.4 1.5 1.6].freeze
SCHEMA_BASE_PATH = Rails.root.join('app', 'validators', 'json_schemas', 'cyclonedx').freeze

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
module Gitlab
module HttpRouter
module RuleContext
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
# This module is used to log the headers set by the HTTP Router.
# Refer: https://gitlab.com/gitlab-org/cells/http-router/-/blob/main/src/header.ts
# to obtain the list of headers.
#
# Usage:
# 1. Include this concern in base controller:
# include Gitlab::HttpRouter::RuleContext
# Or, in the API layer as a helper
# helpers Gitlab::HttpRouter::RuleContext
#
# 2. Use the `http_router_rule_context` method when pushing to Gitlab::ApplicationContext:
# Gitlab::ApplicationContext.push(**router_rule_context)
# These values should be kept in sync with the values in the HTTP Router.
# https://gitlab.com/gitlab-org/cells/http-router/-/blob/main/src/rules/types.d.ts
ALLOWED_ROUTER_RULE_ACTIONS = %w[classify proxy].freeze
# We do not expect a type for `proxy` rules
ROUTER_RULE_ACTIONS_WITHOUT_TYPE = %w[proxy].freeze
ALLOWED_ROUTER_RULE_TYPES = %w[FIRST_CELL SESSION_PREFIX].freeze
private
def http_router_rule_context
{
http_router_rule_action: sanitized_http_router_rule_action,
http_router_rule_type: sanitized_http_router_rule_type
}
end
def sanitized_http_router_rule_action
sanitize_value(
request.headers['X-Gitlab-Http-Router-Rule-Action'],
ALLOWED_ROUTER_RULE_ACTIONS
)
end
strong_memoize_attr :sanitized_http_router_rule_action
def sanitized_http_router_rule_type
sanitize_router_rule_type(
request.headers['X-Gitlab-Http-Router-Rule-Type'],
sanitized_http_router_rule_action,
ALLOWED_ROUTER_RULE_TYPES
)
end
def sanitize_value(value, allowed_values)
return if value.blank?
allowed_values.include?(value) ? value : nil
end
def sanitize_router_rule_type(value, sanitized_http_router_rule_action, allowed_values)
# Considerations:
# - `type` cannot exist without an `action`
# - Some actions (`proxy`) are not expected to have a corresponding `type`, so we perform an early return.
return if sanitized_http_router_rule_action.blank?
return if ROUTER_RULE_ACTIONS_WITHOUT_TYPE.include?(sanitized_http_router_rule_action)
sanitize_value(value, allowed_values)
end
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module Gitlab
module HttpRouter
module RuleMetrics
extend ActiveSupport::Concern
include HttpRouter::RuleContext
include Gitlab::Utils::StrongMemoize
def increment_http_router_metrics
context = http_router_rule_context
increment_http_router_rule_counter(context[:http_router_rule_action], context[:http_router_rule_type])
end
private
def increment_http_router_rule_counter(http_router_rule_action, http_router_rule_type)
# `action` should be present, but `type` is optional
return if http_router_rule_action.blank?
labels = {
rule_action: http_router_rule_action,
rule_type: http_router_rule_type
}
http_router_rule_counter.increment(labels)
end
def http_router_rule_counter
name = :gitlab_http_router_rule_total
comment = 'Total number of HTTP router rule invocations'
::Gitlab::Metrics.counter(name, comment)
end
strong_memoize_attr :http_router_rule_counter
end
end
end

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
module Gitlab
module HttpRouterRuleContext
extend ActiveSupport::Concern
# This module is used to log the headers set by the HTTP Router.
# Refer: https://gitlab.com/gitlab-org/cells/http-router/-/blob/main/src/header.ts
# to obtain the list of headers.
#
# Usage:
# 1. Include this concern in base controller:
# include Gitlab::HttpRouterRuleContext
# Or, in the API layer as a helper
# helpers Gitlab::HttpRouterRuleContext
#
# 2. Use the `http_router_rule_context` method when pushing to Gitlab::ApplicationContext:
# Gitlab::ApplicationContext.push(**router_rule_context)
private
def http_router_rule_context
{
http_router_rule_action: request.headers['X-Gitlab-Http-Router-Rule-Action'],
http_router_rule_type: request.headers['X-Gitlab-Http-Router-Rule-Type']
}
end
end
end

View File

@ -32824,6 +32824,9 @@ msgstr ""
msgid "MemberRole|Custom role"
msgstr ""
msgid "MemberRole|Custom role created on %{dateTime}"
msgstr ""
msgid "MemberRole|Custom roles"
msgstr ""
@ -32854,6 +32857,9 @@ msgstr ""
msgid "MemberRole|Failed to delete role. %{error}"
msgstr ""
msgid "MemberRole|Failed to fetch role."
msgstr ""
msgid "MemberRole|Failed to fetch roles."
msgstr ""
@ -32944,9 +32950,15 @@ msgstr ""
msgid "MemberRole|This role has been manually selected and will not sync to the LDAP sync role."
msgstr ""
msgid "MemberRole|This role is available by default and cannot be changed."
msgstr ""
msgid "MemberRole|To delete custom role, remove role from all group members."
msgstr ""
msgid "MemberRole|To delete custom role, remove role from all users."
msgstr ""
msgid "MemberRole|Update role"
msgstr ""

View File

@ -141,7 +141,7 @@
"colord": "^2.9.3",
"compression-webpack-plugin": "^5.0.2",
"copy-webpack-plugin": "^6.4.1",
"core-js": "^3.38.0",
"core-js": "^3.38.1",
"cron-validator": "^1.1.1",
"cronstrue": "^1.122.0",
"cropperjs": "^1.6.1",

View File

@ -1,4 +1,4 @@
import { GlTableLite } from '@gitlab/ui';
import { GlTableLite, GlSkeletonLoader } from '@gitlab/ui';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@ -57,6 +57,7 @@ describe('Pipelines Table', () => {
});
};
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findGlTableLite = () => wrapper.findComponent(GlTableLite);
const findCiIcon = () => wrapper.findComponent(CiIcon);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
@ -216,6 +217,36 @@ describe('Pipelines Table', () => {
});
});
});
describe('async pipeline creation', () => {
describe('when isCreatingPipeline is enabled', () => {
beforeEach(() => {
createComponent({ props: { isCreatingPipeline: true } });
});
it('Adds an additional loader row to the pipelines table', () => {
expect(findTableRows()).toHaveLength(pipelines.length + 1);
});
it('renders the skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(true);
});
});
describe('when isCreatingPipeline is disabled', () => {
beforeEach(() => {
createComponent();
});
it('does not add a loader row to the pipelines table', () => {
expect(findTableRows()).toHaveLength(pipelines.length);
});
it('does not render skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(false);
});
});
});
});
describe('events', () => {

View File

@ -252,6 +252,14 @@ describe('Pipelines table in Commits and Merge requests', () => {
expect(findRunPipelineBtn().props('disabled')).toBe(false);
});
it('sets isCreatingPipeline to true in pipelines table', async () => {
expect(findPipelinesTable().props('isCreatingPipeline')).toBe(false);
await findRunPipelineBtn().trigger('click');
expect(findPipelinesTable().props('isCreatingPipeline')).toBe(true);
});
});
describe('when asyncMergeRequestPipelineCreation is disabled', () => {

View File

@ -11,6 +11,7 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import PageHeading from '~/vue_shared/components/page_heading.vue';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
import RuleView from '~/projects/settings/branch_rules/components/view/index.vue';
import RuleDrawer from '~/projects/settings/branch_rules/components/view/rule_drawer.vue';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
@ -125,8 +126,7 @@ describe('View branch rules', () => {
const findBranchName = () => wrapper.findByTestId('branch');
const findAllBranches = () => wrapper.findByTestId('all-branches');
const findBranchProtectionCrud = () => wrapper.findByTestId('status-checks');
const findBranchProtectionTitle = () => wrapper.findByTestId('crud-title');
const findSettingsSection = () => wrapper.findComponent(SettingsSection);
const findAllowedToMerge = () => wrapper.findByTestId('allowed-to-merge-content');
const findAllowedToPush = () => wrapper.findByTestId('allowed-to-push-content');
const findAllowForcePushToggle = () => wrapper.findByTestId('force-push-content');
@ -176,14 +176,13 @@ describe('View branch rules', () => {
});
it('renders a branch protection title', () => {
expect(findBranchProtectionTitle().exists()).toBe(true);
expect(findSettingsSection().attributes('heading')).toBe('Protect branch');
});
it('renders a branch protection component for push rules', () => {
expect(findAllowedToPush().props()).toMatchObject({
header: sprintf(I18N.allowedToPushHeader, {
total: 2,
}),
header: 'Allowed to push and merge',
count: 2,
...protectionMockProps,
});
});
@ -224,9 +223,8 @@ describe('View branch rules', () => {
it('renders a branch protection component for merge rules', () => {
expect(findAllowedToMerge().props()).toMatchObject({
header: sprintf(I18N.allowedToMergeHeader, {
total: 2,
}),
header: 'Allowed to merge',
count: 2,
...protectionMockProps,
});
});
@ -385,7 +383,7 @@ describe('View branch rules', () => {
});
it('does not render Protect Branch section', () => {
expect(findBranchProtectionCrud().exists()).toBe(false);
expect(findSettingsSection().exists()).toBe(false);
});
});

View File

@ -30,7 +30,7 @@ const createComponent = (propsData = issuableTitleProps) =>
describe('IssuableTitle', () => {
let wrapper;
const findStickyHeader = () => wrapper.findComponent('[data-testid="header"]');
const findStickyHeader = () => wrapper.find('[data-testid="header"]');
beforeEach(() => {
wrapper = createComponent();
@ -58,11 +58,13 @@ describe('IssuableTitle', () => {
describe('template', () => {
it('renders issuable title', async () => {
const titleHtml = '<b>Sample</b> title';
const wrapperWithTitle = createComponent({
...mockIssuableShowProps,
issuable: {
...mockIssuable,
titleHtml: '<b>Sample</b> title',
titleHtml,
},
});
@ -70,9 +72,7 @@ describe('IssuableTitle', () => {
const titleEl = wrapperWithTitle.find('[data-testid="issuable-title"]');
expect(titleEl.exists()).toBe(true);
expect(titleEl.html()).toBe(
'<h1 dir="auto" data-testid="issuable-title" class="title gl-text-size-h-display"><b>Sample</b> title</h1>',
);
expect(titleEl.element.innerHTML).toBe('<b>Sample</b> title');
wrapperWithTitle.destroy();
});

View File

@ -447,6 +447,14 @@ RSpec.describe TodosHelper do
end
context 'when todo resource parent is not a group' do
context 'when todo belongs to no project either' do
let(:todo) { build(:todo, group: nil, project: nil, user: user) }
subject(:result) { helper.todo_parent_path(todo) }
it { expect(result).to eq(nil) }
end
it 'returns project title with namespace' do
result = helper.todo_parent_path(project_access_request_todo)

View File

@ -219,7 +219,7 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Validators::CyclonedxSchemaValidator,
end
context 'when spec version is supported' do
where(:spec_version) { %w[1.4 1.5] }
where(:spec_version) { %w[1.4 1.5 1.6] }
with_them do
it_behaves_like 'a validator that performs the expected validations'

View File

@ -114,6 +114,80 @@ RSpec.describe API::API, feature_category: :system_access do
end
end
describe 'counter metrics', :aggregate_failures do
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { project.first_owner }
let_it_be(:http_router_rule_counter) { Gitlab::Metrics.counter(:gitlab_http_router_rule_total, 'description') }
let(:perform_request) { get(api("/projects/#{project.id}", user), headers: headers) }
context 'when the headers are present' do
context 'for classify action' do
let(:headers) do
{
'X-Gitlab-Http-Router-Rule-Action' => 'classify',
'X-Gitlab-Http-Router-Rule-Type' => 'FIRST_CELL'
}
end
it 'increments the counter' do
expect { perform_request }
.to change { http_router_rule_counter.get(rule_action: 'classify', rule_type: 'FIRST_CELL') }.by(1)
end
end
context 'for proxy action' do
let(:headers) do
{
'X-Gitlab-Http-Router-Rule-Action' => 'proxy'
}
end
it 'increments the counter' do
expect { perform_request }
.to change { http_router_rule_counter.get(rule_action: 'proxy', rule_type: nil) }.by(1)
end
end
end
context 'for invalid action and type' do
let(:headers) do
{
'X-Gitlab-Http-Router-Rule-Action' => 'invalid',
'X-Gitlab-Http-Router-Rule-Type' => 'invalid'
}
end
it 'does not increment the counter' do
expect { perform_request }
.to change { http_router_rule_counter.get(rule_action: 'invalid', rule_type: 'invalid') }.by(0)
end
end
context 'when action is not present and type is present' do
let(:headers) do
{
'X-Gitlab-Http-Router-Rule-Type' => 'FIRST_CELL'
}
end
it 'does not increment the counter' do
expect { perform_request }.to change {
http_router_rule_counter.get(rule_action: nil, rule_type: 'FIRST_CELL')
}.by(0)
end
end
context 'when the headers are absent' do
let(:headers) { {} }
it 'does not increment the counter' do
expect { perform_request }
.to change { http_router_rule_counter.get(rule_action: nil, rule_type: nil) }.by(0)
end
end
end
describe 'logging', :aggregate_failures do
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { project.first_owner }
@ -132,14 +206,14 @@ RSpec.describe API::API, feature_category: :system_access do
'meta.client_id' => a_string_matching(%r{\Auser/.+}),
'meta.feature_category' => 'team_planning',
'meta.http_router_rule_action' => 'classify',
'meta.http_router_rule_type' => 'FirstCell',
'meta.http_router_rule_type' => 'FIRST_CELL',
'route' => '/api/:version/projects/:id/issues'
)
end
get(api("/projects/#{project.id}/issues", user), headers: {
'X-Gitlab-Http-Router-Rule-Action' => 'classify',
'X-Gitlab-Http-Router-Rule-Type' => 'FirstCell'
'X-Gitlab-Http-Router-Rule-Type' => 'FIRST_CELL'
})
expect(response).to have_gitlab_http_status(:ok)

View File

@ -91,6 +91,17 @@ RSpec.describe ApplicationController, type: :request, feature_category: :shared
sign_in(user)
end
let(:headers) do
{
'X-Gitlab-Http-Router-Rule-Action' => 'classify',
'X-Gitlab-Http-Router-Rule-Type' => 'FIRST_CELL'
}
end
subject(:perform_request) do
get root_path, headers: headers
end
it 'includes the HTTP ROUTER headers in ApplicationContext' do
expect_next_instance_of(RootController) do |controller|
expect(controller).to receive(:index).and_wrap_original do |m, *args|
@ -98,16 +109,80 @@ RSpec.describe ApplicationController, type: :request, feature_category: :shared
expect(Gitlab::ApplicationContext.current).to include(
'meta.http_router_rule_action' => 'classify',
'meta.http_router_rule_type' => 'FirstCell'
'meta.http_router_rule_type' => 'FIRST_CELL'
)
end
end
get root_path, headers: {
'X-Gitlab-Http-Router-Rule-Action' => 'classify',
'X-Gitlab-Http-Router-Rule-Type' => 'FirstCell'
perform_request
end
context 'for counters' do
let(:http_router_rule_counter) { Gitlab::Metrics.counter(:gitlab_http_router_rule_total, 'description') }
context 'when the headers are present' do
context 'for classify action' do
it 'increments the counter' do
expect { perform_request }.to change {
http_router_rule_counter.get(rule_action: 'classify', rule_type: 'FIRST_CELL')
}.by(1)
end
end
context 'for proxy action' do
let(:headers) do
{
'X-Gitlab-Http-Router-Rule-Action' => 'proxy'
}
end
it 'increments the counter' do
expect { perform_request }.to change {
http_router_rule_counter.get(rule_action: 'proxy', rule_type: nil)
}.by(1)
end
end
context 'for invalid action and type' do
let(:headers) do
{
'X-Gitlab-Http-Router-Rule-Action' => 'invalid',
'X-Gitlab-Http-Router-Rule-Type' => 'invalid'
}
end
it 'does not increment the counter' do
expect { perform_request }.to change {
http_router_rule_counter.get(rule_action: 'invalid', rule_type: 'invalid')
}.by(0)
end
end
context 'when action is not present and type is present' do
let(:headers) do
{
'X-Gitlab-Http-Router-Rule-Type' => 'FIRST_CELL'
}
end
it 'does not increment the counter' do
expect { perform_request }.to change {
http_router_rule_counter.get(rule_action: nil, rule_type: 'FIRST_CELL')
}.by(0)
end
end
end
context 'when the headers are absent' do
let(:headers) { {} }
it 'does not increment the counter' do
expect { perform_request }.to change {
http_router_rule_counter.get(rule_action: nil, rule_type: nil)
}.by(0)
end
end
end
end
describe 'static context middleware', feature_category: :error_budgets do

View File

@ -5017,10 +5017,10 @@ core-js-pure@^3.30.2:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.35.0.tgz#4660033304a050215ae82e476bd2513a419fbb34"
integrity sha512-f+eRYmkou59uh7BPcyJ8MC76DiGhspj1KMxVIcF24tzP8NA9HVa1uC7BTW2tgx7E1QVCzDzsgp7kArrzhlz8Ew==
core-js@^3.29.1, core-js@^3.38.0, core-js@^3.6.5:
version "3.38.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.0.tgz#8acb7c050bf2ccbb35f938c0d040132f6110f636"
integrity sha512-XPpwqEodRljce9KswjZShh95qJ1URisBeKCjUdq27YdenkslVe7OO0ZJhlYXAChW7OhXaRLl8AAba7IBfoIHug==
core-js@^3.29.1, core-js@^3.38.1, core-js@^3.6.5:
version "3.38.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.1.tgz#aa375b79a286a670388a1a363363d53677c0383e"
integrity sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==
core-util-is@~1.0.0:
version "1.0.3"