Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-05-01 15:13:48 +00:00
parent 1a9340ff71
commit 3901ed083c
33 changed files with 809 additions and 312 deletions

View File

@ -1 +1 @@
93490767b16e3e4fafdd9a6ee3579bab33f79faa
5ce562f1608201580a29260861728a6e0a9bd087

View File

@ -43,7 +43,11 @@ export default {
},
},
},
inject: ['localMutations'],
inject: {
localMutations: {
default: null,
},
},
props: {
checkable: {
type: Boolean,

View File

@ -1,69 +0,0 @@
<script>
import { GlBadge, GlTab } from '@gitlab/ui';
import { I18N_FETCH_ERROR } from '~/ci/runner/constants';
import { createAlert } from '~/alert';
import { fetchPolicies } from '~/lib/graphql';
import allRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners.query.graphql';
import RunnerName from '~/ci/runner/components/runner_name.vue';
export default {
components: {
GlBadge,
GlTab,
RunnerName,
},
data() {
return {
runners: {
items: [],
pageInfo: {},
},
};
},
apollo: {
runners: {
query: allRunnersQuery,
fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return {
type: 'INSTANCE_TYPE',
};
},
update(data) {
const { runners } = data;
return {
items: runners?.nodes || [],
pageInfo: runners?.pageInfo || {},
};
},
error() {
createAlert({ message: I18N_FETCH_ERROR });
},
},
},
computed: {
runnersItems() {
return this.runners.items;
},
runnersItemCount() {
return this.runnersItems.length;
},
},
};
</script>
<template>
<gl-tab>
<template #title>
<div class="gl-flex gl-gap-2">
{{ __('Instance') }}
<gl-badge>{{ runnersItemCount }}</gl-badge>
</div>
</template>
<ul>
<li v-for="runner in runnersItems" :key="runner.key">
<runner-name :key="runner.key" :runner="runner" />
</li>
</ul>
</gl-tab>
</template>

View File

@ -1,75 +1,77 @@
<script>
import { GlBadge, GlTab } from '@gitlab/ui';
import { __ } from '~/locale';
import { I18N_FETCH_ERROR } from '~/ci/runner/constants';
import { createAlert } from '~/alert';
import { fetchPolicies } from '~/lib/graphql';
import groupRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners.query.graphql';
import { GlLink, GlTab, GlBadge } from '@gitlab/ui';
import RunnerList from '~/ci/runner/components/runner_list.vue';
import RunnerName from '~/ci/runner/components/runner_name.vue';
export const QUERY_TYPES = {
project: 'PROJECT_TYPE',
group: 'GROUP_TYPE',
};
import { fetchPolicies } from '~/lib/graphql';
import projectRunnersQuery from '~/ci/runner/graphql/list/project_runners.query.graphql';
export default {
name: 'RunnersTab',
components: {
GlBadge,
GlLink,
GlTab,
GlBadge,
RunnerList,
RunnerName,
},
props: {
projectFullPath: {
type: String,
required: true,
},
title: {
type: String,
required: false,
default: __('Project'),
required: true,
},
type: {
type: String,
required: false,
default: 'project',
},
groupFullPath: {
runnerType: {
type: String,
required: true,
},
},
emits: ['error'],
data() {
return {
loading: 0, // Initialized to 0 as this is used by a "loadingKey". See https://apollo.vuejs.org/api/smart-query.html#options
runners: {
count: null,
items: [],
urlsById: {},
pageInfo: {},
},
};
},
apollo: {
runners: {
query: groupRunnersQuery,
query: projectRunnersQuery,
fetchPolicy: fetchPolicies.NETWORK_ONLY,
loadingKey: 'loading',
variables() {
return {
type: QUERY_TYPES[this.type],
groupFullPath: this.groupFullPath,
};
return this.variables;
},
update(data) {
const { edges = [], pageInfo = {} } = data?.group?.runners || {};
const items = edges.map(({ node }) => node);
return { items, pageInfo };
const { edges = [], count } = data?.project?.runners || {};
const items = edges.map(({ node, webUrl }) => ({ ...node, webUrl }));
return {
count,
items,
};
},
error() {
createAlert({ message: I18N_FETCH_ERROR });
error(error) {
this.$emit('error', error);
},
},
},
computed: {
runnersItems() {
return this.runners.items;
variables() {
return {
fullPath: this.projectFullPath,
type: this.runnerType,
};
},
runnersItemsCount() {
return this.runnersItems.length;
isLoading() {
return Boolean(this.loading);
},
isEmpty() {
return !this.runners.items?.length && !this.loading;
},
},
};
@ -79,14 +81,19 @@ export default {
<template #title>
<div class="gl-flex gl-gap-2">
{{ title }}
<gl-badge>{{ runnersItemsCount }}</gl-badge>
<gl-badge v-if="runners.count !== null">{{ runners.count }}</gl-badge>
</div>
</template>
<ul>
<li v-for="runner in runnersItems" :key="runner.key">
<runner-name :key="runner.key" :runner="runner" />
</li>
</ul>
<p v-if="isEmpty" data-testid="empty-message" class="gl-px-5 gl-pt-5 gl-text-subtle">
<slot name="empty"></slot>
</p>
<runner-list v-else :runners="runners.items" :loading="isLoading">
<template #runner-name="{ runner }">
<gl-link data-testid="runner-link" :href="runner.webUrl">
<runner-name :runner="runner" />
</gl-link>
</template>
</runner-list>
</gl-tab>
</template>

View File

@ -0,0 +1,66 @@
<script>
import { GlTabs } from '@gitlab/ui';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import RunnersTab from './runners_tab.vue';
export default {
name: 'RunnersTabs',
components: {
GlTabs,
RunnersTab,
},
props: {
projectFullPath: {
type: String,
required: true,
},
},
emits: ['error'],
methods: {
onError(error) {
this.$emit('error', error);
},
},
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
};
</script>
<template>
<gl-tabs>
<runners-tab
:title="s__('Runners|Project')"
:runner-type="$options.PROJECT_TYPE"
:project-full-path="projectFullPath"
@error="onError"
>
<template #empty>
{{
s__(
'Runners|No project runners found, you can create one by selecting "New project runner".',
)
}}
</template>
</runners-tab>
<runners-tab
:title="s__('Runners|Group')"
:runner-type="$options.GROUP_TYPE"
:project-full-path="projectFullPath"
@error="onError"
>
<template #empty>
{{ s__('Runners|No group runners found.') }}
</template>
</runners-tab>
<runners-tab
:title="s__('Runners|Instance')"
:runner-type="$options.INSTANCE_TYPE"
:project-full-path="projectFullPath"
@error="onError"
>
<template #empty>
{{ s__('Runners|No instance runners found.') }}
</template>
</runners-tab>
</gl-tabs>
</template>

View File

@ -22,7 +22,7 @@ export const initProjectRunnersSettings = (selector = '#js-project-runners-setti
allowRegistrationToken,
registrationToken,
newProjectRunnerPath,
groupFullPath,
projectFullPath,
} = el.dataset;
return new Vue({
@ -35,7 +35,7 @@ export const initProjectRunnersSettings = (selector = '#js-project-runners-setti
allowRegistrationToken: parseBoolean(allowRegistrationToken),
registrationToken,
newProjectRunnerPath,
groupFullPath,
projectFullPath,
},
});
},

View File

@ -1,19 +1,19 @@
<script>
import { GlButton, GlTabs } from '@gitlab/ui';
import { GlButton, GlAlert } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import { I18N_FETCH_ERROR } from '~/ci/runner/constants';
import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
import RunnersTab from '~/ci/runner/project_runners_settings/components/runners_tab.vue';
import InstanceRunnersTab from '~/ci/runner/project_runners_settings/components/instance_runners_tab.vue';
import RunnersTabs from '~/ci/runner/project_runners_settings/components/runners_tabs.vue';
export default {
name: 'ProjectRunnersSettingsApp',
components: {
GlAlert,
GlButton,
GlTabs,
CrudComponent,
RegistrationDropdown,
RunnersTab,
InstanceRunnersTab,
RunnersTabs,
},
props: {
canCreateRunner: {
@ -34,31 +34,46 @@ export default {
required: false,
default: null,
},
groupFullPath: {
projectFullPath: {
type: String,
required: true,
},
},
data() {
return {
hasFetchError: false,
};
},
methods: {
onError(error) {
this.hasFetchError = true;
Sentry.captureException(error);
},
onDismissError() {
this.hasFetchError = false;
},
},
I18N_FETCH_ERROR,
};
</script>
<template>
<crud-component :title="s__('Runners|Runners')" body-class="!gl-m-0">
<template #actions>
<gl-button v-if="canCreateRunner" size="small" :href="newProjectRunnerPath">{{
s__('Runners|New project runner')
}}</gl-button>
<registration-dropdown
size="small"
type="PROJECT_TYPE"
:allow-registration-token="allowRegistrationToken"
:registration-token="registrationToken"
/>
</template>
<gl-tabs>
<runners-tab :title="__('Project')" type="project" :group-full-path="groupFullPath" />
<runners-tab :title="__('Group')" type="group" :group-full-path="groupFullPath" />
<instance-runners-tab />
</gl-tabs>
</crud-component>
<div>
<gl-alert v-if="hasFetchError" class="gl-mb-4" variant="danger" @dismiss="onDismissError">
{{ $options.I18N_FETCH_ERROR }}
</gl-alert>
<crud-component :title="s__('Runners|Available Runners')" body-class="!gl-m-0">
<template #actions>
<gl-button v-if="canCreateRunner" size="small" :href="newProjectRunnerPath">{{
s__('Runners|New project runner')
}}</gl-button>
<registration-dropdown
size="small"
type="PROJECT_TYPE"
:allow-registration-token="allowRegistrationToken"
:registration-token="registrationToken"
/>
</template>
<runners-tabs :project-full-path="projectFullPath" @error="onError" />
</crud-component>
</div>
</template>

View File

@ -26,7 +26,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
isLoggedIn: isLoggedIn(),
props: {
workItemId: {
type: String,
@ -48,6 +47,9 @@ export default {
notificationIcon() {
return this.subscribedToNotifications ? ICON_ON : ICON_OFF;
},
isLoggedIn() {
return isLoggedIn();
},
},
methods: {
toggleNotifications(subscribed) {
@ -82,6 +84,7 @@ export default {
<template>
<gl-button
v-if="isLoggedIn"
ref="tooltip"
v-gl-tooltip.hover
category="secondary"

View File

@ -131,7 +131,7 @@ module Ci
can_create_runner: can?(current_user, :create_runner, project).to_s,
allow_registration_token: project.namespace.allow_runner_registration_token?.to_s,
registration_token: can?(current_user, :read_runners_registration_token, project) ? project.runners_token : nil,
group_full_path: project.group&.full_path,
project_full_path: project.full_path,
new_project_runner_path: new_project_runner_path(project)
}
end

View File

@ -92,7 +92,7 @@ module Ci
end
# We require the published_by to be the same as the release author because
# creating a release and publishing a version must be done in a single session via release-cli.
# creating a release and publishing a version must be done in a single session via CLI tools.
def validate_published_by_is_release_author
return if published_by == release.author

View File

@ -23,8 +23,21 @@ module ContainerRegistry
end
protection_rule =
project.container_registry_protection_tag_rules.create(params.slice(*ALLOWED_ATTRIBUTES))
project.container_registry_protection_tag_rules.new(params.slice(*ALLOWED_ATTRIBUTES))
if protection_rule.immutable?
unless Feature.enabled?(:container_registry_immutable_tags, project)
return service_response_error(message: _('Not available'))
end
unless can?(current_user, :create_container_registry_protection_immutable_tag_rule, project)
return service_response_error(
message: _('Unauthorized to create an immutable protection rule for container image tags')
)
end
end
protection_rule.save
return service_response_error(message: protection_rule.errors.full_messages) unless protection_rule.persisted?
ServiceResponse.success(payload: { container_protection_tag_rule: protection_rule })

View File

@ -6,4 +6,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/524346
milestone: '17.11'
group: group::pipeline authoring
type: beta
default_enabled: false
default_enabled: true

View File

@ -142,7 +142,7 @@ integrates with external vendor LLM providers, including:
- [Fireworks AI](https://fireworks.ai/)
- [Google Vertex](https://cloud.google.com/vertex-ai/)
These LLMs communicate through the [GitLab Cloud Connector](../../development/cloud_connector/_index.md),
These LLMs communicate through the GitLab Cloud Connector,
offering a ready-to-use AI solution without the need for on-premise infrastructure.
For licensing, you must have a GitLab Ultimate subscription, and [GitLab Duo Enterprise](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial). To get access to your purchased subscription, request a license through the [Customers Portal](../../subscriptions/customers_portal.md)

View File

@ -109,11 +109,8 @@ You control a subset of these logs by turning AI Logs on and off through the Duo
When AI Logs are enabled, the [`llm.log` file](../logs/_index.md#llmlog) in your GitLab Self-Managed instance, code generation and Chat events that occur through your instance are captured. The log file does not capture anything when it is not enabled. Code completion logs are captured directly in the AI gateway. These logs are not transmitted to GitLab, and are only visible on your GitLab Self-Managed infrastructure.
For more information on:
- Logged events and their properties, see the [logged event documentation](../../development/ai_features/logged_events.md).
- How to rotate, manage, export, and visualize the logs in `llm.log`, see the [log system documentation](../logs/_index.md).
- The log file location (for example, so you can delete logs), see [LLM input and output logging](../logs/_index.md#llm-input-and-output-logging).
- [Rotate, manage, export, and visualize the logs in `llm.log`](../logs/_index.md).
- [View the log file location (for example, so you can delete logs)](../logs/_index.md#llm-input-and-output-logging).
### Logs in your AI gateway container
@ -309,7 +306,6 @@ The AI logs control whether additional debugging information, including prompts
- **GitLab Self-Managed and self-hosted AI gateway**: The feature flag enables detailed logging to `llm.log` on the self-hosted instance, capturing inputs and outputs for AI models.
- **GitLab Self-Managed and GitLab-managed AI gateway**: The feature flag enables logging on your GitLab Self-Managed instance. However, the flag does **not** activate expanded logging for the GitLab-managed AI gateway side. Logging remains disabled for the cloud-connected AI gateway to protect sensitive data.
For more information, see the [Feature Flag section under Privacy Considerations](../../development/ai_features/logging.md#privacy-considerations) documentation.
### Logging in cloud-connected AI gateways

View File

@ -43350,6 +43350,8 @@ Values for sorting dependencies.
| Value | Description |
| ----- | ----------- |
| <a id="dependencysortlicense_asc"></a>`LICENSE_ASC` | License by ascending order. |
| <a id="dependencysortlicense_desc"></a>`LICENSE_DESC` | License by descending order. |
| <a id="dependencysortname_asc"></a>`NAME_ASC` | Name by ascending order. |
| <a id="dependencysortname_desc"></a>`NAME_DESC` | Name by descending order. |
| <a id="dependencysortpackager_asc"></a>`PACKAGER_ASC` | Packager by ascending order. |

View File

@ -262,17 +262,49 @@ For more information, see [issue 480328](https://gitlab.com/gitlab-org/gitlab/-/
1. Retry running the failed migration. It should now succeed.
- Runner tags missing when upgrading to GitLab 17.8, see [issue 524402](https://gitlab.com/gitlab-org/gitlab/-/issues/524402).
Upgrading first to GitLab 17.6 sidesteps the issue. The bug is fixed in GitLab 17.11.
**Affected releases**:
| Affected minor releases | Affected patch releases | Fixed in |
| ----------------------- | ----------------------- | -------- |
| 17.8 | 17.8.0 - 17.8.6 | 17.8.7 |
| 17.9 | 17.9.0 - 17.9.4 | 17.9.5 |
| 17.10 | 17.10.0 - 17.10.4 | 17.10.5 |
When upgrading from 17.5 through a version older than the patch releases mentioned in the table,
there is a chance of the runner tags table becoming empty.
Run the following PostgreSQL query on the `ci` database to check the runner tags table to determine if you are
affected:
```sql
SELECT 'OK, ci_runner_taggings is populated.' FROM ci_runner_taggings LIMIT 1;
```
If the query returns an empty result instead of `OK, ci_runner_taggings is populated.`,
see the [workaround](https://gitlab.com/gitlab-org/gitlab/-/issues/524402#workaround) in the related issue.
## Issues to be aware of when upgrading to 17.9
- In GitLab 17.8, three new secrets have been added to support the new encryption framework (started to be used in 17.9).
If you have a multi-node configuration, you must [ensure these secrets are the same on all nodes](#unify-new-encryption-secrets).
- Runner tags missing when upgrading to GitLab 17.9
- Runner tags missing when upgrading to GitLab 17.9, see [issue 524402](https://gitlab.com/gitlab-org/gitlab/-/issues/524402).
Upgrading first to GitLab 17.6 sidesteps the issue. The bug is fixed in GitLab 17.11.
When upgrading from 17.5 through a version older than the 17.8.6 or 17.9.3 patch releases, there is a chance of the
runner tags table becoming empty. Upgrading first to GitLab 17.6 sidesteps the issue.
**Affected releases**:
The 17.9.5 patch release addresses the migration problem.
| Affected minor releases | Affected patch releases | Fixed in |
| ----------------------- | ----------------------- | -------- |
| 17.8 | 17.8.0 - 17.8.6 | 17.8.7 |
| 17.9 | 17.9.0 - 17.9.4 | 17.9.5 |
| 17.10 | 17.10.0 - 17.10.4 | 17.10.5 |
When upgrading from 17.5 through a version older than the patch releases mentioned in the table,
there is a chance of the runner tags table becoming empty.
Run the following PostgreSQL query on the `ci` database to check the runner tags table to determine if you are
affected:
@ -289,12 +321,19 @@ For more information, see [issue 480328](https://gitlab.com/gitlab-org/gitlab/-/
- In GitLab 17.8, three new secrets have been added to support the new encryption framework (started to be used in 17.9).
If you have a multi-node configuration, you must [ensure these secrets are the same on all nodes](#unify-new-encryption-secrets).
- Runner tags missing when upgrading to GitLab 17.10
- Runner tags missing when upgrading to GitLab 17.10, see [issue 524402](https://gitlab.com/gitlab-org/gitlab/-/issues/524402).
Upgrading first to GitLab 17.6 sidesteps the issue. The bug is fixed in GitLab 17.11.
When upgrading from 17.5 through a version older than the 17.8.6 or 17.9.5 patch releases, there is a chance of the
runner tags table becoming empty. Upgrading first to GitLab 17.6 sidesteps the issue.
**Affected releases**:
The 17.10.5 patch release addresses the migration problem.
| Affected minor releases | Affected patch releases | Fixed in |
| ----------------------- | ----------------------- | -------- |
| 17.8 | 17.8.0 - 17.8.6 | 17.8.7 |
| 17.9 | 17.9.0 - 17.9.4 | 17.9.5 |
| 17.10 | 17.10.0 - 17.10.4 | 17.10.5 |
When upgrading from 17.5 through a version older than the patch releases mentioned in the table,
there is a chance of the runner tags table becoming empty.
Run the following PostgreSQL query on the `ci` database to check the runner tags table to determine if you are
affected:
@ -815,53 +854,53 @@ If you have a multi-node configuration, you must ensure these secrets are the sa
On GitLab >= 18.0.0, >= 17.11.2, >= 17.10.6, or >= 17.9.8, run:
```shell
gitlab-rake gitlab:doctor:encryption_keys
```
```shell
gitlab-rake gitlab:doctor:encryption_keys
```
If you're using other versions:
If you're using other versions, run:
```shell
gitlab-rails runner 'require_relative Pathname(Dir.pwd).join("encryption_keys.rb"); Gitlab::Doctor::EncryptionKeys.new(Logger.new($stdout)).run!'
```
```shell
gitlab-rails runner 'require_relative Pathname(Dir.pwd).join("encryption_keys.rb"); Gitlab::Doctor::EncryptionKeys.new(Logger.new($stdout)).run!'
```
All reported keys usage are for the same key ID. For example, on node 1:
```shell
Gathering existing encryption keys:
- active_record_encryption_primary_key: ID => `bb32`; truncated secret => `bEt...eBU`
- active_record_encryption_deterministic_key: ID => `445f`; truncated secret => `MJo...yg5`
```shell
Gathering existing encryption keys:
- active_record_encryption_primary_key: ID => `bb32`; truncated secret => `bEt...eBU`
- active_record_encryption_deterministic_key: ID => `445f`; truncated secret => `MJo...yg5`
[... snipped for brevity ...]
[... snipped for brevity ...]
Encryption keys usage for VirtualRegistries::Packages::Maven::Upstream: NONE
Encryption keys usage for Ai::ActiveContext::Connection: NONE
Encryption keys usage for CloudConnector::Keys:
- `bb32` => 1
Encryption keys usage for DependencyProxy::GroupSetting:
- `bb32` => 8
Encryption keys usage for Ci::PipelineScheduleInput:
- `bb32` => 1
```
Encryption keys usage for VirtualRegistries::Packages::Maven::Upstream: NONE
Encryption keys usage for Ai::ActiveContext::Connection: NONE
Encryption keys usage for CloudConnector::Keys:
- `bb32` => 1
Encryption keys usage for DependencyProxy::GroupSetting:
- `bb32` => 8
Encryption keys usage for Ci::PipelineScheduleInput:
- `bb32` => 1
```
And for example, on node 2 (you should not see any `(UNKNOWN KEY!)` this time):
And for example, on node 2 (you should not see any `(UNKNOWN KEY!)` this time):
```shell
Gathering existing encryption keys:
- active_record_encryption_primary_key: ID => `bb32`; truncated secret => `bEt...eBU`
- active_record_encryption_deterministic_key: ID => `445f`; truncated secret => `MJo...yg5`
```shell
Gathering existing encryption keys:
- active_record_encryption_primary_key: ID => `bb32`; truncated secret => `bEt...eBU`
- active_record_encryption_deterministic_key: ID => `445f`; truncated secret => `MJo...yg5`
[... snipped for brevity ...]
[... snipped for brevity ...]
Encryption keys usage for VirtualRegistries::Packages::Maven::Upstream: NONE
Encryption keys usage for Ai::ActiveContext::Connection: NONE
Encryption keys usage for CloudConnector::Keys:
- `bb32` => 1
Encryption keys usage for DependencyProxy::GroupSetting:
- `bb32` => 8
Encryption keys usage for Ci::PipelineScheduleInput:
- `bb32` => 1
```
Encryption keys usage for VirtualRegistries::Packages::Maven::Upstream: NONE
Encryption keys usage for Ai::ActiveContext::Connection: NONE
Encryption keys usage for CloudConnector::Keys:
- `bb32` => 1
Encryption keys usage for DependencyProxy::GroupSetting:
- `bb32` => 8
Encryption keys usage for Ci::PipelineScheduleInput:
- `bb32` => 1
```
1. Remove the `encryption_keys.rb` file if you downloaded it previously:
@ -873,15 +912,15 @@ If you have a multi-node configuration, you must ensure these secrets are the sa
For GitLab >= 17.10:
```shell
gitlab-rake cloud_connector:keys:create
```
```shell
gitlab-rake cloud_connector:keys:create
```
For GitLab 17.9:
```shell
gitlab-rails runner 'CloudConnector::Keys.create!(secret_key: OpenSSL::PKey::RSA.new(2048).to_pem)'
```
```shell
gitlab-rails runner 'CloudConnector::Keys.create!(secret_key: OpenSSL::PKey::RSA.new(2048).to_pem)'
```
1. [Disable maintenance mode](../../administration/maintenance_mode/_index.md#disable-maintenance-mode).

View File

@ -7,7 +7,7 @@ title: AI gateway
The [AI gateway](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/ai_gateway/) is a standalone service that gives access to AI-native GitLab Duo features.
GitLab operates an instance of AI gateway that is used by GitLab Self-Managed, GitLab Dedicated, and GitLab.com through the [Cloud Connector](../../development/cloud_connector/_index.md).
GitLab operates an instance of AI gateway that is used by GitLab Self-Managed, GitLab Dedicated, and GitLab.com through the Cloud Connector.
On GitLab Self-Managed, this GitLab instance of AI gateway applies regardless of whether you are using the
cloud-based AI gateway hosted by GitLab, or using [GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/_index.md) to self-host the AI gateway.

View File

@ -662,7 +662,6 @@ The `solution/` directory provides two possible solutions.
GitLab Duo usage focuses on contributing to the GitLab codebase, and how customers can contribute more efficiently.
The GitLab codebase is large, and requires to understand sometimes complex algorithms or application specific implementations.
Review the [architecture components](../../development/architecture.md) to learn more.
### Contribute to frontend: Profile Settings

View File

@ -5,102 +5,51 @@ info: To determine the technical writer assigned to the Stage/Group associated w
title: GitLab Release CLI tool
---
{{< alert type="warning" >}}
**The `release-cli` is in maintenance mode**.
The `release-cli` does not accept new features.
All new feature development happens in the `glab` CLI,
so you should use the [`glab` CLI](../../../editor_extensions/gitlab_cli/_index.md) whenever possible.
The `release-cli` is in maintenance mode, and [issue cli#7450](https://gitlab.com/gitlab-org/cli/-/issues/7450) proposes to deprecate it as the `glab` CLI matures.
You can use [the feedback issue](https://gitlab.com/gitlab-org/cli/-/issues/7859) to share any comments.
## Switch from `release-cli` to `glab` CLI
- For API usage details, see [the `glab` CLI project documentation](https://gitlab.com/gitlab-org/cli).
- With a CI/CD job and the [`release`](../../../ci/yaml/_index.md#release) keyword,
change the job's `image` to use the `cli:latest` image. For example:
```yaml
release_job:
stage: release
image: registry.gitlab.com/gitlab-org/cli:latest
rules:
- if: $CI_COMMIT_TAG
script:
- echo "Running the release job."
release:
tag_name: $CI_COMMIT_TAG
name: 'Release $CI_COMMIT_TAG'
description: 'Release created using the cli.'
```
## Fall back to `release-cli`
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/524346) in GitLab 18.0, [with a flag](../../../administration/feature_flags.md) named `ci_glab_for_release`. Enabled by default.
{{< /history >}}
{{< alert type="flag" >}}
The availability of this feature is controlled by a feature flag. For more information, see the history.
{{< /alert >}}
The [GitLab Release CLI (`release-cli`)](https://gitlab.com/gitlab-org/release-cli)
is a command-line tool for managing releases from the command line or from a CI/CD pipeline.
You can use the release CLI to create, update, modify, and delete releases.
CI/CD jobs that use the `release` keyword use a script that falls back to using `release-cli`
if the required `glab` version is not available on the runner. The fallback logic
is a safe-guard to ensure that projects that have not yet migrated to use `glab` CLI
can continue working.
When you [use a CI/CD job to create a release](_index.md#creating-a-release-by-using-a-cicd-job),
the `release` keyword entries are transformed into Bash commands and sent to the Docker
container containing the `release-cli` tool. The tool then creates the release.
You can also call the `release-cli` tool directly from a [`script`](../../../ci/yaml/_index.md#script).
For example:
```shell
release-cli create --name "Release $CI_COMMIT_SHA" --description \
"Created using the release-cli $EXTRA_DESCRIPTION" \
--tag-name "v${MAJOR}.${MINOR}.${REVISION}" --ref "$CI_COMMIT_SHA" \
--released-at "2020-07-15T08:00:00Z" --milestone "m1" --milestone "m2" --milestone "m3" \
--assets-link "{\"name\":\"asset1\",\"url\":\"https://example.com/assets/1\",\"link_type\":\"other\"}"
```
## Install the `release-cli` for the Shell executor
{{< details >}}
- Tier: Free, Premium, Ultimate
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
{{< /details >}}
The `release-cli` binaries are [available in the package registry](https://gitlab.com/gitlab-org/release-cli/-/packages).
When you use a runner with the Shell executor, you can download and install
the `release-cli` manually for your [supported OS and architecture](https://gitlab.com/gitlab-org/release-cli/-/packages).
Once installed, [the `release` keyword](../../../ci/yaml/_index.md#release) is available to use in your CI/CD jobs.
### Install on Unix/Linux
1. Download the binary for your system from the GitLab package registry.
For example, if you use an amd64 system:
```shell
curl --location --output /usr/local/bin/release-cli "https://gitlab.com/api/v4/projects/gitlab-org%2Frelease-cli/packages/generic/release-cli/latest/release-cli-linux-amd64"
```
1. Give it permissions to execute:
```shell
sudo chmod +x /usr/local/bin/release-cli
```
1. Verify `release-cli` is available:
```shell
$ release-cli -v
release-cli version 0.15.0
```
### Install on Windows PowerShell
1. Create a folder somewhere in your system, for example `C:\GitLab\Release-CLI\bin`
```shell
New-Item -Path 'C:\GitLab\Release-CLI\bin' -ItemType Directory
```
1. Download the executable file:
```shell
PS C:\> Invoke-WebRequest -Uri "https://gitlab.com/api/v4/projects/gitlab-org%2Frelease-cli/packages/generic/release-cli/latest/release-cli-windows-amd64.exe" -OutFile "C:\GitLab\Release-CLI\bin\release-cli.exe"
Directory: C:\GitLab\Release-CLI
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 3/16/2021 4:17 AM bin
```
1. Add the directory to your `$env:PATH`:
```shell
$env:PATH += ";C:\GitLab\Release-CLI\bin"
```
1. Verify `release-cli` is available:
```shell
PS C:\> release-cli -v
release-cli version 0.15.0
```
This fallback is [scheduled to be removed](https://gitlab.com/gitlab-org/gitlab/-/issues/537919)
in GitLab 19.0 with the removal of `release-cli`.

View File

@ -26,7 +26,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
route_setting :authorization, job_token_policies: :admin_releases
# Note: This endpoint should only be used by `release-cli` and should be authenticated with a job token.
# Note: This endpoint should only be used by CLI tools and should be authenticated with a job token.
# For this reason, we should not document the endpoint in the API docs.
post ':id/catalog/publish' do
release = user_project.releases.find_by_tag!(params[:version])

View File

@ -51517,6 +51517,9 @@ msgstr ""
msgid "Runners|Available"
msgstr ""
msgid "Runners|Available Runners"
msgstr ""
msgid "Runners|Available to all projects"
msgstr ""
@ -51876,12 +51879,21 @@ msgstr ""
msgid "Runners|No description"
msgstr ""
msgid "Runners|No group runners found."
msgstr ""
msgid "Runners|No instance runners found."
msgstr ""
msgid "Runners|No jobs have been run by group runners assigned to this group in the past 3 hours."
msgstr ""
msgid "Runners|No jobs have been run by instance runners in the past 3 hours."
msgstr ""
msgid "Runners|No project runners found, you can create one by selecting \"New project runner\"."
msgstr ""
msgid "Runners|No spot. Default choice for Windows Shell executor."
msgstr ""
@ -55723,7 +55735,7 @@ msgstr ""
msgid "SecurityReports|Identifier"
msgstr ""
msgid "SecurityReports|If you were expecting vulnerabilities to be shown here, check that you've completed the %{linkStart}security scanning prerequisites%{linkEnd}, or check the other vulnerability types in the tabs above."
msgid "SecurityReports|If you were expecting vulnerabilities to be shown here, check that you've completed the %{linkStart}security scanning prerequisites%{linkEnd}."
msgstr ""
msgid "SecurityReports|Image"
@ -64180,6 +64192,9 @@ msgstr ""
msgid "Unauthorized to create an environment"
msgstr ""
msgid "Unauthorized to create an immutable protection rule for container image tags"
msgstr ""
msgid "Unauthorized to delete a container registry protection rule"
msgstr ""

View File

@ -8,6 +8,7 @@ import runnersCountData from 'test_fixtures/graphql/ci/runner/list/all_runners_c
import groupRunnersData from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.json';
import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.paginated.json';
import groupRunnersCountData from 'test_fixtures/graphql/ci/runner/list/group_runners_count.query.graphql.json';
import projectRunnersData from 'test_fixtures/graphql/ci/runner/list/project_runners.query.graphql.json';
// Register runner queries
import runnerForRegistration from 'test_fixtures/graphql/ci/runner/register/runner_for_registration.query.graphql.json';
@ -471,6 +472,7 @@ export {
groupRunnersData,
groupRunnersDataPaginated,
groupRunnersCountData,
projectRunnersData,
emptyPageInfo,
runnerData,
runnerJobCountData,

View File

@ -0,0 +1,141 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlTab, GlBadge } from '@gitlab/ui';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import RunnerList from '~/ci/runner/components/runner_list.vue';
import { PROJECT_TYPE } from '~/ci/runner/constants';
import { projectRunnersData, runnerJobCountData } from 'jest/ci/runner/mock_data';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import projectRunnersQuery from '~/ci/runner/graphql/list/project_runners.query.graphql';
import runnerJobCountQuery from '~/ci/runner/graphql/list/runner_job_count.query.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnersTab from '~/ci/runner/project_runners_settings/components/runners_tab.vue';
Vue.use(VueApollo);
const mockRunners = projectRunnersData.data.project.runners.edges;
const mockRunnerId = getIdFromGraphQLId(mockRunners[0].node.id);
const mockRunnerSha = mockRunners[0].node.shortSha;
describe('RunnersTab', () => {
let wrapper;
let projectRunnersHandler;
let runnerJobCountHandler;
const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnersTab, {
propsData: {
projectFullPath: 'group/project',
title: 'Project',
runnerType: PROJECT_TYPE,
...props,
},
apolloProvider: createMockApollo([
[projectRunnersQuery, projectRunnersHandler],
[runnerJobCountQuery, runnerJobCountHandler],
]),
stubs: {
GlTab,
},
slots: {
empty: 'No runners found',
},
});
return waitForPromises();
};
beforeEach(() => {
projectRunnersHandler = jest.fn().mockResolvedValue(projectRunnersData);
runnerJobCountHandler = jest.fn().mockResolvedValue(runnerJobCountData);
});
const findTab = () => wrapper.findComponent(GlTab);
const findBadge = () => wrapper.findComponent(GlBadge);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findEmptyMessage = () => wrapper.findByTestId('empty-message');
describe('when rendered', () => {
beforeEach(() => {
createComponent();
});
it('fetches data', () => {
expect(projectRunnersHandler).toHaveBeenCalledTimes(1);
expect(projectRunnersHandler).toHaveBeenCalledWith({
fullPath: 'group/project',
type: PROJECT_TYPE,
});
});
it('renders the tab with the correct title', () => {
expect(findTab().text()).toContain('Project');
});
it('does not show badge when count is null', () => {
expect(findBadge().exists()).toBe(false);
});
it('does not render empty state', () => {
expect(findEmptyMessage().exists()).toBe(false);
});
it('shows runner list in loading state', () => {
expect(findRunnerList().props('loading')).toBe(true);
});
});
describe('when data is fetched', () => {
beforeEach(async () => {
await createComponent();
});
it('shows badge with count when available', () => {
expect(findBadge().text()).toBe('2');
});
it('does not render empty state', () => {
expect(findEmptyMessage().exists()).toBe(false);
});
it('shows runner list when runners are available', () => {
expect(findRunnerList().props('loading')).toBe(false);
expect(findRunnerList().props('runners')).toEqual([
expect.objectContaining({ ...mockRunners[0].node }),
expect.objectContaining({ ...mockRunners[1].node }),
]);
});
it('shows link to runner', async () => {
await createComponent({ mountFn: mountExtended });
expect(wrapper.findByTestId('runner-link').attributes('href')).toBe(mockRunners[0].webUrl);
expect(wrapper.findByTestId('runner-link').text()).toBe(
`#${mockRunnerId} (${mockRunnerSha})`,
);
});
});
it('shows empty message with no runners', async () => {
projectRunnersHandler.mockResolvedValue({
data: {},
});
await createComponent();
expect(findEmptyMessage().exists()).toBe(true);
expect(findRunnerList().exists()).toBe(false);
});
it('emits error event when apollo query fails', async () => {
const error = new Error('Network error');
projectRunnersHandler.mockRejectedValue(error);
await createComponent();
expect(findEmptyMessage().exists()).toBe(true);
expect(wrapper.emitted('error')).toEqual([[error]]);
});
});

View File

@ -0,0 +1,92 @@
import { shallowMount } from '@vue/test-utils';
import { GlTabs } from '@gitlab/ui';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import RunnersTabs from '~/ci/runner/project_runners_settings/components/runners_tabs.vue';
import RunnersTab from '~/ci/runner/project_runners_settings/components/runners_tab.vue';
const error = new Error('Test error');
describe('RunnersTabs', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(RunnersTabs, {
propsData: {
projectFullPath: 'group/project',
...props,
},
stubs: {
GlTabs,
},
});
};
const findTabs = () => wrapper.findComponent(GlTabs);
const findRunnerTabs = () => wrapper.findAllComponents(RunnersTab);
const findRunnerTabAt = (i) => findRunnerTabs().at(i);
beforeEach(() => {
createComponent();
});
it('renders tabs container', () => {
expect(findTabs().exists()).toBe(true);
});
it('renders the correct number of tabs', () => {
expect(findRunnerTabs()).toHaveLength(3);
});
describe('Project tab', () => {
it('renders the tab content', () => {
expect(findRunnerTabAt(0).props()).toMatchObject({
title: 'Project',
runnerType: PROJECT_TYPE,
projectFullPath: 'group/project',
});
expect(findRunnerTabAt(0).text()).toBe(
'No project runners found, you can create one by selecting "New project runner".',
);
});
it('emits an error event', () => {
findRunnerTabAt(0).vm.$emit('error', error);
expect(wrapper.emitted().error[0]).toEqual([error]);
});
});
describe('Group tab', () => {
it('renders the tab content', () => {
expect(findRunnerTabAt(1).props()).toMatchObject({
title: 'Group',
runnerType: GROUP_TYPE,
projectFullPath: 'group/project',
});
expect(findRunnerTabAt(1).text()).toBe('No group runners found.');
});
it('emits an error event', () => {
findRunnerTabAt(1).vm.$emit('error', error);
expect(wrapper.emitted().error[0]).toEqual([error]);
});
});
describe('Instance tab', () => {
it('renders the tab content', () => {
expect(findRunnerTabAt(2).props()).toMatchObject({
title: 'Instance',
runnerType: INSTANCE_TYPE,
projectFullPath: 'group/project',
});
expect(findRunnerTabAt(2).text()).toBe('No instance runners found.');
});
it('emits an error event', () => {
findRunnerTabAt(2).vm.$emit('error', error);
expect(wrapper.emitted().error[0]).toEqual([error]);
});
});
});

View File

@ -0,0 +1,99 @@
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlAlert } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
import RunnersTabs from '~/ci/runner/project_runners_settings/components/runners_tabs.vue';
import ProjectRunnersSettingsApp from '~/ci/runner/project_runners_settings/project_runners_settings_app.vue';
jest.mock('~/sentry/sentry_browser_wrapper');
describe('ProjectRunnersSettingsApp', () => {
let wrapper;
const createComponent = ({ props } = {}) => {
wrapper = shallowMount(ProjectRunnersSettingsApp, {
propsData: {
canCreateRunner: true,
allowRegistrationToken: true,
registrationToken: 'token123',
newProjectRunnerPath: '/runners/new',
projectFullPath: 'group/project',
...props,
},
stubs: {
CrudComponent,
},
});
};
const findAlert = () => wrapper.findComponent(GlAlert);
const findCrudComponent = () => wrapper.findComponent(CrudComponent);
const findNewRunnerButton = () => wrapper.findComponent(GlButton);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnersTabs = () => wrapper.findComponent(RunnersTabs);
beforeEach(() => {
createComponent();
});
it('renders the crud component with correct title', () => {
expect(findCrudComponent().props('title')).toBe('Available Runners');
});
it('renders new runner button when canCreateRunner is true', () => {
expect(findNewRunnerButton().attributes('href')).toBe('/runners/new');
expect(findNewRunnerButton().text()).toBe('New project runner');
});
it('does not render new runner button when canCreateRunner is false', () => {
createComponent({
props: { canCreateRunner: false },
});
expect(findNewRunnerButton().exists()).toBe(false);
});
it('renders registration dropdown with correct props', () => {
expect(findRegistrationDropdown().props()).toMatchObject({
type: 'PROJECT_TYPE',
allowRegistrationToken: true,
registrationToken: 'token123',
});
});
it('renders runners tabs with correct props', () => {
expect(findRunnersTabs().props('projectFullPath')).toBe('group/project');
});
it('does not show error alert by default', () => {
expect(findAlert().exists()).toBe(false);
});
describe('when an error occurs', () => {
const error = new Error('Test error');
beforeEach(async () => {
findRunnersTabs().vm.$emit('error', error);
await nextTick();
});
it('shows error alert', () => {
expect(findAlert().text()).toBe('Something went wrong while fetching runner data.');
expect(Sentry.captureException).toHaveBeenCalledWith(error);
});
it('dismisses error alert', async () => {
expect(findAlert().exists()).toBe(true);
findAlert().vm.$emit('dismiss');
await nextTick();
expect(findAlert().exists()).toBe(false);
});
});
});

View File

@ -261,4 +261,28 @@ RSpec.describe 'Runner (JavaScript fixtures)', feature_category: :fleet_visibili
end
end
end
describe 'as project maintainer', GraphQL::Query do
let_it_be(:project_maintainer) { create(:user) }
before_all do
project.add_maintainer(project_maintainer)
end
describe 'project_runners.query.graphql', type: :request do
project_runners_query = 'list/project_runners.query.graphql'
let_it_be(:query) do
get_graphql_query_as_string("#{query_path}#{project_runners_query}")
end
it "#{fixtures_path}#{project_runners_query}.json" do
post_graphql(query, current_user: project_maintainer, variables: {
fullPath: project.full_path
})
expect_graphql_errors_to_be_empty
end
end
end
end

View File

@ -80,10 +80,16 @@ describe('WorkItemActions component', () => {
expect(findNotificationsButton().exists()).toBe(true);
});
it('does not render button if user is not logged in', () => {
isLoggedIn.mockReturnValue(false);
createComponent();
expect(findNotificationsButton().exists()).toBe(false);
});
describe('notifications action', () => {
beforeEach(() => {
createComponent();
isLoggedIn.mockReturnValue(true);
});
it.each`

View File

@ -223,8 +223,7 @@ RSpec.describe Ci::RunnersHelper, feature_category: :fleet_visibility do
end
describe '#project_runners_settings_data' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:project) { create(:project) }
subject(:result) { helper.project_runners_settings_data(project) }
@ -234,7 +233,6 @@ RSpec.describe Ci::RunnersHelper, feature_category: :fleet_visibility do
context 'when the user has all permissions' do
before do
allow(helper).to receive(:can?).with(user, :admin_group, group).and_return(true)
allow(helper).to receive(:can?).with(user, :create_runner, project).and_return(true)
allow(helper).to receive(:can?).with(user, :read_runners_registration_token, project).and_return(true)
allow(project.namespace).to receive(:allow_runner_registration_token?).and_return(true)
@ -245,7 +243,7 @@ RSpec.describe Ci::RunnersHelper, feature_category: :fleet_visibility do
can_create_runner: 'true',
allow_registration_token: 'true',
registration_token: project.runners_token,
group_full_path: group.full_path,
project_full_path: project.full_path,
new_project_runner_path: new_project_runner_path(project)
)
end
@ -253,7 +251,6 @@ RSpec.describe Ci::RunnersHelper, feature_category: :fleet_visibility do
context 'when user cannot manage runners' do
before do
allow(helper).to receive(:can?).with(user, :admin_group, group).and_return(false)
allow(helper).to receive(:can?).with(user, :create_runner, project).and_return(false)
allow(helper).to receive(:can?).with(user, :read_runners_registration_token, project).and_return(false)
allow(project.namespace).to receive(:allow_runner_registration_token?).and_return(false)
@ -264,7 +261,7 @@ RSpec.describe Ci::RunnersHelper, feature_category: :fleet_visibility do
can_create_runner: 'false',
allow_registration_token: 'false',
registration_token: nil,
group_full_path: group.full_path,
project_full_path: project.full_path,
new_project_runner_path: new_project_runner_path(project)
)
end

View File

@ -9074,7 +9074,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
it_behaves_like 'it has loose foreign keys' do
it_behaves_like 'it has loose foreign keys', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/526190' do
let(:factory_name) { :project }
end

View File

@ -4092,6 +4092,33 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
describe 'creating container registry protection immutable tag rules' do
using RSpec::Parameterized::TableSyntax
where(:user_role, :expected_result) do
:admin | :be_allowed
:owner | :be_allowed
:maintainer | :be_disallowed
:developer | :be_disallowed
:reporter | :be_disallowed
:planner | :be_disallowed
:guest | :be_disallowed
:anonymous | :be_disallowed
end
with_them do
let(:current_user) do
public_send(user_role)
end
before do
enable_admin_mode!(current_user) if user_role == :admin
end
it { is_expected.to send(expected_result, :create_container_registry_protection_immutable_tag_rule) }
end
end
private
def project_subject(project_type)

View File

@ -95,10 +95,19 @@ RSpec.describe 'Creating the container registry tag protection rule', :aggregate
it_behaves_like 'returning a mutation error', 'Access levels should either both be present or both be nil'
end
context 'with both access levels blank' do
context 'with an immutable tag rule (both access levels blank)' do
let(:input) { super().merge(minimum_access_level_for_delete: nil, minimum_access_level_for_push: nil) }
it_behaves_like 'a successful response'
context 'with an authorized user' do
let_it_be(:current_user) { create(:user, owner_of: project) }
it_behaves_like 'a successful response'
end
context 'with an unauthorized user' do
it_behaves_like 'returning a mutation error',
'Unauthorized to create an immutable protection rule for container image tags'
end
end
context 'with blank input field `tagNamePattern`' do

View File

@ -28,8 +28,8 @@ RSpec.describe ContainerRegistry::Protection::CreateTagRuleService, '#execute',
be_a(ContainerRegistry::Protection::TagRule)
.and(have_attributes(
tag_name_pattern: params[:tag_name_pattern],
minimum_access_level_for_push: params[:minimum_access_level_for_push].to_s,
minimum_access_level_for_delete: params[:minimum_access_level_for_delete].to_s
minimum_access_level_for_push: params[:minimum_access_level_for_push]&.to_s,
minimum_access_level_for_delete: params[:minimum_access_level_for_delete]&.to_s
))
}
)
@ -146,6 +146,68 @@ RSpec.describe ContainerRegistry::Protection::CreateTagRuleService, '#execute',
message: 'Maximum number of protection rules have been reached.'
end
describe 'user roles' do
using RSpec::Parameterized::TableSyntax
where(:user_role, :success) do
:owner | true
:maintainer | true
:developer | false
:reporter | false
:guest | false
end
with_them do
before do
project.send(:"add_#{user_role}", current_user)
end
if params[:success]
it_behaves_like 'a successful service response'
else
it_behaves_like 'an erroneous service response',
message: 'Unauthorized to create a protection rule for container image tags'
end
end
context 'when the current user is an admin', :enable_admin_mode do
let(:current_user) { build_stubbed(:admin) }
it_behaves_like 'a successful service response'
end
context 'when the protection rule is immutable' do
let(:params) { attributes_for(:container_registry_protection_tag_rule, :immutable, project: project) }
context 'when the current user is the maintainer' do
it_behaves_like 'an erroneous service response',
message: 'Unauthorized to create an immutable protection rule for container image tags'
end
context 'when the current user is the owner' do
before do
project.send(:add_owner, current_user)
end
it_behaves_like 'a successful service response'
end
context 'when the current user is an admin', :enable_admin_mode do
let(:current_user) { build_stubbed(:admin) }
it_behaves_like 'a successful service response'
end
context 'when the feature container_registry_immutable_tags is disabled' do
before do
stub_feature_flags(container_registry_immutable_tags: false)
end
it_behaves_like 'an erroneous service response', message: 'Not available'
end
end
end
context 'when the GitLab API is not supported' do
before do
stub_gitlab_api_client_to_support_gitlab_api(supported: false)

View File

@ -3529,7 +3529,6 @@
- './spec/frontend/fixtures/projects_json.rb'
- './spec/frontend/fixtures/raw.rb'
- './spec/frontend/fixtures/releases.rb'
- './spec/frontend/fixtures/runner.rb'
- './spec/frontend/fixtures/search.rb'
- './spec/frontend/fixtures/sessions.rb'
- './spec/frontend/fixtures/snippet.rb'