Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1a9340ff71
commit
3901ed083c
|
|
@ -1 +1 @@
|
|||
93490767b16e3e4fafdd9a6ee3579bab33f79faa
|
||||
5ce562f1608201580a29260861728a6e0a9bd087
|
||||
|
|
|
|||
|
|
@ -43,7 +43,11 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
inject: ['localMutations'],
|
||||
inject: {
|
||||
localMutations: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
checkable: {
|
||||
type: Boolean,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in New Issue