Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-02-28 21:13:37 +00:00
parent 76747b143e
commit 2113bb8ffe
28 changed files with 649 additions and 447 deletions

View File

@ -30,7 +30,6 @@ Gitlab/ServiceResponse:
- 'app/services/snippets/base_service.rb'
- 'app/services/snippets/bulk_destroy_service.rb'
- 'app/services/snippets/destroy_service.rb'
- 'app/services/snippets/repository_validation_service.rb'
- 'app/services/timelogs/base_service.rb'
- 'app/services/work_items/create_and_link_service.rb'
- 'app/services/work_items/create_from_task_service.rb'

View File

@ -0,0 +1,215 @@
<script>
import {
GlLoadingIcon,
GlFormInputGroup,
GlInputGroupText,
GlFormInput,
GlButton,
GlLink,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import { cloneDeep, uniqueId } from 'lodash';
import { createAlert } from '~/alert';
import { reportToSentry } from '~/ci/utils';
import { JOB_GRAPHQL_ERRORS } from '~/ci/constants';
import { fetchPolicies } from '~/lib/graphql';
import { helpPagePath } from '~/helpers/help_page_helper';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_COMMIT_STATUS } from '~/graphql_shared/constants';
import GetJob from '../graphql/queries/get_job.query.graphql';
export default {
name: 'JobVariablesForm',
components: {
GlLoadingIcon,
GlFormInputGroup,
GlInputGroupText,
GlFormInput,
GlButton,
GlLink,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
},
clearBtnSharedClasses: ['gl-flex-grow-0 gl-basis-0 !gl-m-0 !gl-ml-3'],
variableSettings: helpPagePath('ci/variables/_index', { anchor: 'for-a-project' }),
inputTypes: {
key: 'key',
value: 'value',
},
inject: ['projectPath'],
apollo: {
variables: {
query: GetJob,
variables() {
return {
fullPath: this.projectPath,
id: convertToGraphQLId(TYPENAME_COMMIT_STATUS, this.jobId),
};
},
skip() {
// variables list always contains one empty variable
// skip refetch if form already has non-empty variables
return this.variables.length > 1;
},
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
update(data) {
const jobVariables = cloneDeep(data?.project?.job?.manualVariables?.nodes);
return [...jobVariables.reverse(), ...this.variables];
},
error(error) {
createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
reportToSentry(this.$options.name, error);
},
},
},
props: {
jobId: {
type: Number,
required: true,
},
},
data() {
return {
variables: [
{
id: uniqueId(),
key: '',
value: '',
},
],
};
},
watch: {
variables: {
handler(newValue) {
this.$emit('update-variables', newValue);
},
deep: true,
},
},
methods: {
inputRef(type, id) {
return `${this.$options.inputTypes[type]}-${id}`;
},
addEmptyVariable() {
const lastVar = this.variables[this.variables.length - 1];
if (lastVar.key === '') {
return;
}
this.variables.push({
id: uniqueId(),
key: '',
value: '',
});
},
canRemove(index) {
return index < this.variables.length - 1;
},
deleteVariable(id) {
this.variables.splice(
this.variables.findIndex((el) => el.id === id),
1,
);
},
},
};
</script>
<template>
<gl-loading-icon v-if="$apollo.queries.variables.loading" class="gl-mt-5" size="lg" />
<div v-else class="gl-mx-auto gl-mt-5">
<label>{{ s__('CiVariables|Variables') }}</label>
<div
v-for="(variable, index) in variables"
:key="variable.id"
class="gl-mb-5 gl-flex gl-items-center"
data-testid="ci-variable-row"
>
<gl-form-input-group class="gl-mr-4 gl-grow">
<template #prepend>
<gl-input-group-text>
{{ s__('CiVariables|Key') }}
</gl-input-group-text>
</template>
<gl-form-input
:ref="inputRef('key', variable.id)"
v-model="variable.key"
:placeholder="s__('CiVariables|Input variable key')"
data-testid="ci-variable-key"
@change="addEmptyVariable"
/>
</gl-form-input-group>
<gl-form-input-group class="gl-grow-2">
<template #prepend>
<gl-input-group-text>
{{ s__('CiVariables|Value') }}
</gl-input-group-text>
</template>
<gl-form-input
:ref="inputRef('value', variable.id)"
v-model="variable.value"
:placeholder="s__('CiVariables|Input variable value')"
data-testid="ci-variable-value"
/>
</gl-form-input-group>
<gl-button
v-if="canRemove(index)"
v-gl-tooltip
:aria-label="s__('CiVariables|Remove inputs')"
:title="s__('CiVariables|Remove inputs')"
:class="$options.clearBtnSharedClasses"
category="tertiary"
icon="remove"
data-testid="delete-variable-btn"
@click="deleteVariable(variable.id)"
/>
<!-- Placeholder button to keep the layout fixed -->
<gl-button
v-else
class="gl-pointer-events-none gl-opacity-0"
:class="$options.clearBtnSharedClasses"
data-testid="delete-variable-btn-placeholder"
category="tertiary"
icon="remove"
/>
</div>
<div class="gl-mt-5 gl-text-center">
<gl-sprintf
:message="
s__(
'CiVariables|Specify variable values to be used in this run. The variables specified in the configuration file and %{linkStart}CI/CD settings%{linkEnd} are used by default.',
)
"
>
<template #link="{ content }">
<gl-link :href="$options.variableSettings" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</div>
<div class="gl-mt-3 gl-text-center">
<gl-sprintf
:message="
s__(
'CiVariables|Variables specified here are %{boldStart}expanded%{boldEnd} and not %{boldStart}masked.%{boldEnd}',
)
"
>
<template #bold="{ content }">
<strong>
{{ content }}
</strong>
</template>
</gl-sprintf>
</div>
</div>
</template>

View File

@ -1,28 +1,16 @@
<script>
import {
GlFormInputGroup,
GlInputGroupText,
GlFormInput,
GlButton,
GlLink,
GlLoadingIcon,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import { cloneDeep, uniqueId } from 'lodash';
import { fetchPolicies } from '~/lib/graphql';
import { GlButton } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { TYPENAME_CI_BUILD, TYPENAME_COMMIT_STATUS } from '~/graphql_shared/constants';
import { TYPENAME_CI_BUILD } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { JOB_GRAPHQL_ERRORS } from '~/ci/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { reportToSentry } from '~/ci/utils';
import { confirmJobConfirmationMessage } from '~/ci/pipeline_details/graph/utils';
import GetJob from '../graphql/queries/get_job.query.graphql';
import playJobWithVariablesMutation from '../graphql/mutations/job_play_with_variables.mutation.graphql';
import retryJobWithVariablesMutation from '../graphql/mutations/job_retry_with_variables.mutation.graphql';
import JobVariablesForm from './job_variables_form.vue';
// This component is a port of ~/ci/job_details/components/legacy_manual_variables_form.vue
// It is meant to fetch/update the job information via GraphQL instead of REST API.
@ -30,42 +18,8 @@ import retryJobWithVariablesMutation from '../graphql/mutations/job_retry_with_v
export default {
name: 'ManualVariablesForm',
components: {
GlFormInputGroup,
GlInputGroupText,
GlFormInput,
GlButton,
GlLink,
GlLoadingIcon,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['projectPath'],
apollo: {
variables: {
query: GetJob,
variables() {
return {
fullPath: this.projectPath,
id: convertToGraphQLId(TYPENAME_COMMIT_STATUS, this.jobId),
};
},
skip() {
// variables list always contains one empty variable
// skip refetch if form already has non-empty variables
return this.variables.length > 1;
},
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
update(data) {
const jobVariables = cloneDeep(data?.project?.job?.manualVariables?.nodes);
return [...jobVariables.reverse(), ...this.variables];
},
error(error) {
createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
reportToSentry(this.$options.name, error);
},
},
JobVariablesForm,
},
props: {
isRetryable: {
@ -86,40 +40,16 @@ export default {
default: null,
},
},
clearBtnSharedClasses: ['gl-flex-grow-0 gl-basis-0 !gl-m-0 !gl-ml-3'],
inputTypes: {
key: 'key',
value: 'value',
data() {
return {
runBtnDisabled: false,
preparedVariables: [],
};
},
i18n: {
cancel: s__('CiVariables|Cancel'),
removeInputs: s__('CiVariables|Remove inputs'),
formHelpText: s__(
'CiVariables|Specify variable values to be used in this run. The variables specified in the configuration file and %{linkStart}CI/CD settings%{linkEnd} are used by default.',
),
overrideNoteText: s__(
'CiVariables|Variables specified here are %{boldStart}expanded%{boldEnd} and not %{boldStart}masked.%{boldEnd}',
),
header: s__('CiVariables|Variables'),
keyLabel: s__('CiVariables|Key'),
keyPlaceholder: s__('CiVariables|Input variable key'),
runAgainButtonText: s__('CiVariables|Run job again'),
runButtonText: s__('CiVariables|Run job'),
valueLabel: s__('CiVariables|Value'),
valuePlaceholder: s__('CiVariables|Input variable value'),
},
data() {
return {
job: {},
variables: [
{
id: uniqueId(),
key: '',
value: '',
},
],
runBtnDisabled: false,
};
},
computed: {
mutationVariables() {
@ -128,21 +58,18 @@ export default {
variables: this.preparedVariables,
};
},
preparedVariables() {
return this.variables
.filter((variable) => variable.key !== '')
.map(({ key, value }) => ({ key, value }));
},
runBtnText() {
return this.isRetryable
? this.$options.i18n.runAgainButtonText
: this.$options.i18n.runButtonText;
},
variableSettings() {
return helpPagePath('ci/variables/_index', { anchor: 'for-a-project' });
},
},
methods: {
onVariablesUpdate(variables) {
this.preparedVariables = variables
.filter((variable) => variable.key !== '')
.map(({ key, value }) => ({ key, value }));
},
async playJob() {
try {
const { data } = await this.$apollo.mutate({
@ -175,31 +102,6 @@ export default {
reportToSentry(this.$options.name, error);
}
},
addEmptyVariable() {
const lastVar = this.variables[this.variables.length - 1];
if (lastVar.key === '') {
return;
}
this.variables.push({
id: uniqueId(),
key: '',
value: '',
});
},
canRemove(index) {
return index < this.variables.length - 1;
},
deleteVariable(id) {
this.variables.splice(
this.variables.findIndex((el) => el.id === id),
1,
);
},
inputRef(type, id) {
return `${this.$options.inputTypes[type]}-${id}`;
},
navigateToJob(path) {
visitUrl(path);
},
@ -227,103 +129,25 @@ export default {
};
</script>
<template>
<gl-loading-icon v-if="$apollo.queries.variables.loading" class="gl-mt-9" size="lg" />
<div v-else class="row gl-justify-center">
<div class="col-10">
<label>{{ $options.i18n.header }}</label>
<div>
<job-variables-form :job-id="jobId" @update-variables="onVariablesUpdate" />
<div
v-for="(variable, index) in variables"
:key="variable.id"
class="gl-mb-5 gl-flex gl-items-center"
data-testid="ci-variable-row"
<div class="gl-mt-5 gl-flex gl-justify-center gl-gap-x-2">
<gl-button
v-if="isRetryable"
data-testid="cancel-btn"
@click="$emit('hideManualVariablesForm')"
>{{ $options.i18n.cancel }}
</gl-button>
<gl-button
variant="confirm"
category="primary"
:disabled="runBtnDisabled"
data-testid="run-manual-job-btn"
@click="runJob"
>
<gl-form-input-group class="gl-mr-4 gl-grow">
<template #prepend>
<gl-input-group-text>
{{ $options.i18n.keyLabel }}
</gl-input-group-text>
</template>
<gl-form-input
:ref="inputRef('key', variable.id)"
v-model="variable.key"
:placeholder="$options.i18n.keyPlaceholder"
data-testid="ci-variable-key"
@change="addEmptyVariable"
/>
</gl-form-input-group>
<gl-form-input-group class="gl-grow-2">
<template #prepend>
<gl-input-group-text>
{{ $options.i18n.valueLabel }}
</gl-input-group-text>
</template>
<gl-form-input
:ref="inputRef('value', variable.id)"
v-model="variable.value"
:placeholder="$options.i18n.valuePlaceholder"
data-testid="ci-variable-value"
/>
</gl-form-input-group>
<gl-button
v-if="canRemove(index)"
v-gl-tooltip
:aria-label="$options.i18n.removeInputs"
:title="$options.i18n.removeInputs"
:class="$options.clearBtnSharedClasses"
category="tertiary"
icon="remove"
data-testid="delete-variable-btn"
@click="deleteVariable(variable.id)"
/>
<!-- Placeholder button to keep the layout fixed -->
<gl-button
v-else
class="gl-pointer-events-none gl-opacity-0"
:class="$options.clearBtnSharedClasses"
data-testid="delete-variable-btn-placeholder"
category="tertiary"
icon="remove"
/>
</div>
<div class="gl-mt-5 gl-text-center">
<gl-sprintf :message="$options.i18n.formHelpText">
<template #link="{ content }">
<gl-link :href="variableSettings" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</div>
<div class="gl-mt-3 gl-text-center">
<gl-sprintf :message="$options.i18n.overrideNoteText">
<template #bold="{ content }">
<strong>
{{ content }}
</strong>
</template>
</gl-sprintf>
</div>
<div class="gl-mt-5 gl-flex gl-justify-center">
<gl-button
v-if="isRetryable"
data-testid="cancel-btn"
@click="$emit('hideManualVariablesForm')"
>{{ $options.i18n.cancel }}
</gl-button>
<gl-button
variant="confirm"
category="primary"
:disabled="runBtnDisabled"
data-testid="run-manual-job-btn"
@click="runJob"
>
{{ runBtnText }}
</gl-button>
</div>
{{ runBtnText }}
</gl-button>
</div>
</div>
</template>

View File

@ -432,7 +432,7 @@ module SearchHelper
return []
end
search_using_search_service(current_user, 'users', term, limit).map do |user|
search_using_search_service(current_user, 'users', term, limit, { autocomplete: true }).map do |user|
{
category: "Users",
id: user.id,

View File

@ -8,7 +8,8 @@ module Search
{
state: params[:state],
confidential: params[:confidential],
include_archived: params[:include_archived]
include_archived: params[:include_archived],
autocomplete: params[:autocomplete]
}
end
end

View File

@ -2,6 +2,8 @@
module Snippets
class RepositoryValidationService
INVALID_REPOSITORY = :invalid_snippet_repository
attr_reader :current_user, :snippet, :repository
RepositoryValidationError = Class.new(StandardError)
@ -25,7 +27,7 @@ module Snippets
ServiceResponse.success(message: 'Valid snippet repository.')
rescue RepositoryValidationError => e
ServiceResponse.error(message: "Error: #{e.message}", http_status: 400)
ServiceResponse.error(message: "Error: #{e.message}", reason: INVALID_REPOSITORY)
end
private

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
class AddUsableStorageBytesToZoektNodes < Gitlab::Database::Migration[2.2]
milestone '17.10'
def change
add_column :zoekt_nodes, :usable_storage_bytes, :bigint, null: false, default: 0, if_not_exists: true
add_column :zoekt_nodes, :usable_storage_bytes_locked_until, :timestamptz, if_not_exists: true
end
end

View File

@ -0,0 +1 @@
7ee4d9d63499f305e7dbe6107de736b578f898d323d295a7372dc34c29da4df4

View File

@ -24553,6 +24553,8 @@ CREATE TABLE zoekt_nodes (
search_base_url text NOT NULL,
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
indexed_bytes bigint DEFAULT 0 NOT NULL,
usable_storage_bytes bigint DEFAULT 0 NOT NULL,
usable_storage_bytes_locked_until timestamp with time zone,
CONSTRAINT check_32f39efba3 CHECK ((char_length(search_base_url) <= 1024)),
CONSTRAINT check_38c354a3c2 CHECK ((char_length(index_base_url) <= 1024))
);

View File

@ -418,7 +418,7 @@ To disable the banner:
By default, a banner shows in merge requests in projects with the [Jenkins integration enabled](../../integration/jenkins.md) to prompt migration to GitLab CI/CD.
![A banner prompting migration from Jenkins to GitLab CI](img/suggest_migrate_from_jenkins_v_17_7.png)
![A banner prompting migration from Jenkins to GitLab CI](img/suggest_migrate_from_jenkins_v17_7.png)
To disable the banner:

View File

@ -22330,7 +22330,7 @@ Represents the Geo replication and verification state of a ci_secure_file.
| <a id="cisecurefileregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the CiSecureFileRegistry do not match on the primary and secondary. |
| <a id="cisecurefileregistrycisecurefileid"></a>`ciSecureFileId` | [`ID!`](#id) | ID of the Ci Secure File. |
| <a id="cisecurefileregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the CiSecureFileRegistry was created. |
| <a id="cisecurefileregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="cisecurefileregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="cisecurefileregistryid"></a>`id` | [`ID!`](#id) | ID of the CiSecureFileRegistry. |
| <a id="cisecurefileregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the CiSecureFileRegistry. |
| <a id="cisecurefileregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the CiSecureFileRegistry. |
@ -23161,7 +23161,7 @@ Represents the Geo replication and verification state of an Container Repository
| <a id="containerrepositoryregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the ContainerRepositoryRegistry do not match on the primary and secondary. |
| <a id="containerrepositoryregistrycontainerrepositoryid"></a>`containerRepositoryId` | [`ID!`](#id) | ID of the ContainerRepository. |
| <a id="containerrepositoryregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the ContainerRepositoryRegistry was created. |
| <a id="containerrepositoryregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="containerrepositoryregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="containerrepositoryregistryid"></a>`id` | [`ID!`](#id) | ID of the ContainerRepositoryRegistry. |
| <a id="containerrepositoryregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the ContainerRepositoryRegistry. |
| <a id="containerrepositoryregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the ContainerRepositoryRegistry. |
@ -24267,7 +24267,7 @@ Represents the Geo replication and verification state of a dependency_proxy_blob
| <a id="dependencyproxyblobregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the DependencyProxyBlobRegistry do not match on the primary and secondary. |
| <a id="dependencyproxyblobregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the DependencyProxyBlobRegistry was created. |
| <a id="dependencyproxyblobregistrydependencyproxyblobid"></a>`dependencyProxyBlobId` | [`ID!`](#id) | ID of the Dependency Proxy Blob. |
| <a id="dependencyproxyblobregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="dependencyproxyblobregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="dependencyproxyblobregistryid"></a>`id` | [`ID!`](#id) | ID of the DependencyProxyBlobRegistry. |
| <a id="dependencyproxyblobregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the DependencyProxyBlobRegistry. |
| <a id="dependencyproxyblobregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the DependencyProxyBlobRegistry. |
@ -24326,7 +24326,7 @@ Represents the Geo replication and verification state of a dependency_proxy_mani
| <a id="dependencyproxymanifestregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the DependencyProxyManifestRegistry do not match on the primary and secondary. |
| <a id="dependencyproxymanifestregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the DependencyProxyManifestRegistry was created. |
| <a id="dependencyproxymanifestregistrydependencyproxymanifestid"></a>`dependencyProxyManifestId` | [`ID!`](#id) | ID of the Dependency Proxy Manifest. |
| <a id="dependencyproxymanifestregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="dependencyproxymanifestregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="dependencyproxymanifestregistryid"></a>`id` | [`ID!`](#id) | ID of the DependencyProxyManifestRegistry. |
| <a id="dependencyproxymanifestregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the DependencyProxyManifestRegistry. |
| <a id="dependencyproxymanifestregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the DependencyProxyManifestRegistry. |
@ -24695,7 +24695,7 @@ Represents the Geo replication and verification state of a Design Management Rep
| <a id="designmanagementrepositoryregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the DesignManagementRepositoryRegistry do not match on the primary and secondary. |
| <a id="designmanagementrepositoryregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the DesignManagementRepositoryRegistry was created. |
| <a id="designmanagementrepositoryregistrydesignmanagementrepositoryid"></a>`designManagementRepositoryId` | [`ID!`](#id) | ID of the Design Management Repository. |
| <a id="designmanagementrepositoryregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="designmanagementrepositoryregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="designmanagementrepositoryregistryid"></a>`id` | [`ID!`](#id) | ID of the DesignManagementRepositoryRegistry. |
| <a id="designmanagementrepositoryregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the DesignManagementRepositoryRegistry. |
| <a id="designmanagementrepositoryregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the DesignManagementRepositoryRegistry. |
@ -28350,7 +28350,7 @@ Represents the Geo sync and verification state of a group wiki repository.
| ---- | ---- | ----------- |
| <a id="groupwikirepositoryregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the GroupWikiRepositoryRegistry do not match on the primary and secondary. |
| <a id="groupwikirepositoryregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the GroupWikiRepositoryRegistry was created. |
| <a id="groupwikirepositoryregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="groupwikirepositoryregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="groupwikirepositoryregistrygroupwikirepositoryid"></a>`groupWikiRepositoryId` | [`ID!`](#id) | ID of the Group Wiki Repository. |
| <a id="groupwikirepositoryregistryid"></a>`id` | [`ID!`](#id) | ID of the GroupWikiRepositoryRegistry. |
| <a id="groupwikirepositoryregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the GroupWikiRepositoryRegistry. |
@ -29068,7 +29068,7 @@ Represents the Geo replication and verification state of a job_artifact.
| <a id="jobartifactregistryartifactid"></a>`artifactId` | [`ID!`](#id) | ID of the Job Artifact. |
| <a id="jobartifactregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the JobArtifactRegistry do not match on the primary and secondary. |
| <a id="jobartifactregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the JobArtifactRegistry was created. |
| <a id="jobartifactregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="jobartifactregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="jobartifactregistryid"></a>`id` | [`ID!`](#id) | ID of the JobArtifactRegistry. |
| <a id="jobartifactregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the JobArtifactRegistry. |
| <a id="jobartifactregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the JobArtifactRegistry. |
@ -29172,7 +29172,7 @@ Represents the Geo sync and verification state of an LFS object.
| ---- | ---- | ----------- |
| <a id="lfsobjectregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the LfsObjectRegistry do not match on the primary and secondary. |
| <a id="lfsobjectregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the LfsObjectRegistry was created. |
| <a id="lfsobjectregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="lfsobjectregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="lfsobjectregistryid"></a>`id` | [`ID!`](#id) | ID of the LfsObjectRegistry. |
| <a id="lfsobjectregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the LfsObjectRegistry. |
| <a id="lfsobjectregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the LfsObjectRegistry. |
@ -30422,7 +30422,7 @@ Represents the Geo sync and verification state of a Merge Request diff.
| ---- | ---- | ----------- |
| <a id="mergerequestdiffregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the MergeRequestDiffRegistry do not match on the primary and secondary. |
| <a id="mergerequestdiffregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the MergeRequestDiffRegistry was created. |
| <a id="mergerequestdiffregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="mergerequestdiffregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="mergerequestdiffregistryid"></a>`id` | [`ID!`](#id) | ID of the MergeRequestDiffRegistry. |
| <a id="mergerequestdiffregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the MergeRequestDiffRegistry. |
| <a id="mergerequestdiffregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the MergeRequestDiffRegistry. |
@ -32503,7 +32503,7 @@ Represents the Geo sync and verification state of a package file.
| ---- | ---- | ----------- |
| <a id="packagefileregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the PackageFileRegistry do not match on the primary and secondary. |
| <a id="packagefileregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the PackageFileRegistry was created. |
| <a id="packagefileregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="packagefileregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="packagefileregistryid"></a>`id` | [`ID!`](#id) | ID of the PackageFileRegistry. |
| <a id="packagefileregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the PackageFileRegistry. |
| <a id="packagefileregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the PackageFileRegistry. |
@ -32704,7 +32704,7 @@ Represents the Geo replication and verification state of a pages_deployment.
| ---- | ---- | ----------- |
| <a id="pagesdeploymentregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the PagesDeploymentRegistry do not match on the primary and secondary. |
| <a id="pagesdeploymentregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the PagesDeploymentRegistry was created. |
| <a id="pagesdeploymentregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="pagesdeploymentregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="pagesdeploymentregistryid"></a>`id` | [`ID!`](#id) | ID of the PagesDeploymentRegistry. |
| <a id="pagesdeploymentregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the PagesDeploymentRegistry. |
| <a id="pagesdeploymentregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the PagesDeploymentRegistry. |
@ -33041,7 +33041,7 @@ Represents the Geo sync and verification state of a pipeline artifact.
| ---- | ---- | ----------- |
| <a id="pipelineartifactregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the PipelineArtifactRegistry do not match on the primary and secondary. |
| <a id="pipelineartifactregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the PipelineArtifactRegistry was created. |
| <a id="pipelineartifactregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="pipelineartifactregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="pipelineartifactregistryid"></a>`id` | [`ID!`](#id) | ID of the PipelineArtifactRegistry. |
| <a id="pipelineartifactregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the PipelineArtifactRegistry. |
| <a id="pipelineartifactregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the PipelineArtifactRegistry. |
@ -35818,7 +35818,7 @@ Represents the Geo replication and verification state of a project repository.
| ---- | ---- | ----------- |
| <a id="projectrepositoryregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the ProjectRepositoryRegistry do not match on the primary and secondary. |
| <a id="projectrepositoryregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the ProjectRepositoryRegistry was created. |
| <a id="projectrepositoryregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="projectrepositoryregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="projectrepositoryregistryid"></a>`id` | [`ID!`](#id) | ID of the ProjectRepositoryRegistry. |
| <a id="projectrepositoryregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the ProjectRepositoryRegistry. |
| <a id="projectrepositoryregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the ProjectRepositoryRegistry. |
@ -36106,7 +36106,7 @@ Represents the Geo replication and verification state of a project_wiki_reposito
| ---- | ---- | ----------- |
| <a id="projectwikirepositoryregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the ProjectWikiRepositoryRegistry do not match on the primary and secondary. |
| <a id="projectwikirepositoryregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the ProjectWikiRepositoryRegistry was created. |
| <a id="projectwikirepositoryregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="projectwikirepositoryregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="projectwikirepositoryregistryid"></a>`id` | [`ID!`](#id) | ID of the ProjectWikiRepositoryRegistry. |
| <a id="projectwikirepositoryregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the ProjectWikiRepositoryRegistry. |
| <a id="projectwikirepositoryregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the ProjectWikiRepositoryRegistry. |
@ -37318,7 +37318,7 @@ Represents the Geo sync and verification state of a snippet repository.
| ---- | ---- | ----------- |
| <a id="snippetrepositoryregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the SnippetRepositoryRegistry do not match on the primary and secondary. |
| <a id="snippetrepositoryregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the SnippetRepositoryRegistry was created. |
| <a id="snippetrepositoryregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="snippetrepositoryregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="snippetrepositoryregistryid"></a>`id` | [`ID!`](#id) | ID of the SnippetRepositoryRegistry. |
| <a id="snippetrepositoryregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the SnippetRepositoryRegistry. |
| <a id="snippetrepositoryregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the SnippetRepositoryRegistry. |
@ -37649,7 +37649,7 @@ Represents the Geo sync and verification state of a terraform state version.
| ---- | ---- | ----------- |
| <a id="terraformstateversionregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the TerraformStateVersionRegistry do not match on the primary and secondary. |
| <a id="terraformstateversionregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the TerraformStateVersionRegistry was created. |
| <a id="terraformstateversionregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="terraformstateversionregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="terraformstateversionregistryid"></a>`id` | [`ID!`](#id) | ID of the TerraformStateVersionRegistry. |
| <a id="terraformstateversionregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the TerraformStateVersionRegistry. |
| <a id="terraformstateversionregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the TerraformStateVersionRegistry. |
@ -37971,7 +37971,7 @@ Represents the Geo replication and verification state of an upload.
| <a id="uploadregistrychecksummismatch"></a>`checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the UploadRegistry do not match on the primary and secondary. |
| <a id="uploadregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the UploadRegistry was created. |
| <a id="uploadregistryfileid"></a>`fileId` | [`ID!`](#id) | ID of the Upload. |
| <a id="uploadregistryforcetoredownload"></a>`forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
| <a id="uploadregistryforcetoredownload"></a>`forceToRedownload` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated** in GitLab 17.10. Removed from registry tables in the database in favor of the newer reusable framework. |
| <a id="uploadregistryid"></a>`id` | [`ID!`](#id) | ID of the UploadRegistry. |
| <a id="uploadregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the UploadRegistry. |
| <a id="uploadregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the UploadRegistry. |

View File

@ -380,6 +380,10 @@ cannot push to the repository in your project.
You can also control this setting with the [`ci_push_repository_for_job_token_allowed`](../../api/projects.md#edit-a-project)
parameter in the `projects` REST API endpoint.
## Fine-grained permissions for job tokens
Fine-grained permissions for job tokens are an [experiment](../../policy/development_stages_support.md#experiment). For information on this feature and the available resources, see [Fine-grained permissions for CI/CD job tokens](fine_grained_permissions.md). Feedback is welcome on this [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/519575).
## Use a job token
### To `git clone` a private project's repository

View File

@ -1,6 +1,6 @@
---
stage: Software Supply Chain Security
group: Pipeline Security
group: Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Fine-grained permissions for CI/CD job tokens
---
@ -23,10 +23,43 @@ Status: Experiment
{{< /details >}}
{{< history >}}
- [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/15234) in GitLab 17.10. This feature is an [experiment](../../policy/development_stages_support.md#experiment).
{{< /history >}}
{{< alert type="flag" >}}
The availability of this feature is controlled by a feature flag.
For more information, see the history.
This feature is available for testing, but not ready for production use.
{{< /alert >}}
You can use fine-grained permissions to explicitly allow access to a limited set of API endpoints.
These permissions are applied to the CI/CD job tokens in a specified project.
This feature is an [experiment](../../policy/development_stages_support.md#experiment).
## Enable fine-grained permissions
### On GitLab Self-Managed
1. Start the GitLab Rails console. For information, see [Enable and disable GitLab features deployed behind feature flags](../../administration/feature_flags.md#enable-or-disable-the-feature)
1. Turn on the [feature flag](../../administration/feature_flags.md):
```ruby
# You must include a specific project ID with this command.
Feature.enable(:add_policies_to_ci_job_token, <project_id>)
```
### On GitLab.com
Add a comment on this [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/519575) with your project ID.
## Available API endpoints
The following endpoints are available for CI/CD job tokens.
You can use fine-grained permissions to explicitly allow access to a limited set of the following API endpoints.
`None` means fine-grained permissions cannot control access to this endpoint.

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -758,7 +758,7 @@ To determine the IP address of an instance runner:
1. Select **CI/CD > Runners**.
1. Find the runner in the table and view the **IP Address** column.
![Instance runner IP address](img/shared_runner_ip_address_14_5.png)
![Instance runner IP address](img/shared_runner_ip_address_v14_5.png)
### Determine the IP address of a project runner

View File

@ -350,7 +350,7 @@ For each selected vulnerability:
- A badge is added to its severity, indicating that the severity has been overridden.
- Manual severity adjustments are recorded in the vulnerability's **history**.
![Vulnerability Severity Override](img/vulnerability_severity_change_17.10.png)
![Vulnerability Severity Override](img/vulnerability_severity_change_v17_10.png)
## Sort vulnerabilities by date detected

View File

@ -32,19 +32,19 @@ short-lived (results from hyperparameter tuning triggered by a merge request),
but usually hold model runs that have a similar set of parameters measured
by the same metrics.
![List of experiments](img/experiments.png)
![List of experiments](img/experiments_v17_9.png)
## Model run
A model run is a variation of the training of a machine learning model, that can be eventually promoted to a version
of the model.
![Experiment runs](img/runs.png)
![Experiment runs](img/runs_v17_9.png)
The goal of a data scientist is to find the model run whose parameter values lead to the best model
performance, as indicated by the given metrics.
![Run Detail](img/run.png)
![Run Detail](img/run_v17_9.png)
Some example parameters:
@ -76,7 +76,7 @@ Trial artifacts are saved as packages. After an artifact is logged for a run, al
You can associate runs to the CI job that created them, allowing quick links to the merge request, pipeline, and user that triggered the pipeline:
![CI information in run detail](img/run_detail_ci.png)
![CI information in run detail](img/run_detail_ci_v17_9.png)
## View logged metrics
@ -89,4 +89,4 @@ To view logged metrics:
1. Select the experiment you want to view.
1. Select the **Performance** tab.
![A graph of an experiment's performance](img/metrics.png)
![A graph of an experiment's performance](img/metrics_v17_10.png)

View File

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 209 KiB

View File

@ -0,0 +1,250 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import { createAlert } from '~/alert';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { JOB_GRAPHQL_ERRORS } from '~/ci/constants';
import waitForPromises from 'helpers/wait_for_promises';
import JobVariablesForm from '~/ci/job_details/components/job_variables_form.vue';
import getJobQuery from '~/ci/job_details/graphql/queries/get_job.query.graphql';
import { mockFullPath, mockId, mockJobResponse, mockJobWithVariablesResponse } from '../mock_data';
jest.mock('~/alert');
Vue.use(VueApollo);
const defaultProvide = {
projectPath: mockFullPath,
};
const defaultProps = {
jobId: mockId,
};
describe('Job Variables Form', () => {
let wrapper;
let mockApollo;
const getJobQueryResponseHandlerWithVariables = jest.fn().mockResolvedValue(mockJobResponse);
const defaultHandlers = {
getJobQueryResponseHandlerWithVariables,
};
const createComponent = ({ handlers = defaultHandlers } = {}) => {
mockApollo = createMockApollo([
[getJobQuery, handlers.getJobQueryResponseHandlerWithVariables],
]);
const options = {
apolloProvider: mockApollo,
};
wrapper = mountExtended(JobVariablesForm, {
propsData: {
...defaultProps,
},
provide: {
...defaultProvide,
},
...options,
});
return waitForPromises();
};
const findHelpText = () => wrapper.findComponent(GlSprintf);
const findHelpLink = () => wrapper.findComponent(GlLink);
const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key');
const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key');
const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value');
const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row');
const setCiVariableKey = () => {
findCiVariableKey().setValue('new key');
findCiVariableKey().vm.$emit('change');
nextTick();
};
const setCiVariableKeyByPosition = (position, value) => {
findAllCiVariableKeys().at(position).setValue(value);
findAllCiVariableKeys().at(position).vm.$emit('change');
nextTick();
};
afterEach(() => {
createAlert.mockClear();
});
describe('when page renders', () => {
beforeEach(async () => {
await createComponent();
});
it('renders help text with provided link', () => {
expect(findHelpText().exists()).toBe(true);
expect(findHelpLink().attributes('href')).toBe('/help/ci/variables/_index#for-a-project');
});
});
describe('when query is unsuccessful', () => {
beforeEach(async () => {
await createComponent({
handlers: {
getJobQueryResponseHandlerWithVariables: jest.fn().mockRejectedValue({}),
},
});
});
it('shows an alert with error', () => {
expect(createAlert).toHaveBeenCalledWith({
message: JOB_GRAPHQL_ERRORS.jobQueryErrorText,
});
});
});
describe('when job has variables', () => {
beforeEach(async () => {
await createComponent({
handlers: {
getJobQueryResponseHandlerWithVariables: jest
.fn()
.mockResolvedValue(mockJobWithVariablesResponse),
},
});
});
it('sets job variables', () => {
const queryKey = mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].key;
const queryValue =
mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].value;
expect(findCiVariableKey().element.value).toBe(queryKey);
expect(findCiVariableValue().element.value).toBe(queryValue);
});
});
describe('updating variables in UI', () => {
beforeEach(async () => {
await createComponent({
handlers: {
getJobQueryResponseHandlerWithVariables: jest.fn().mockResolvedValue(mockJobResponse),
},
});
});
it('creates a new variable when user enters a new key value', async () => {
expect(findAllVariables()).toHaveLength(1);
await setCiVariableKey();
expect(findAllVariables()).toHaveLength(2);
});
it('does not create extra empty variables', async () => {
expect(findAllVariables()).toHaveLength(1);
await setCiVariableKey();
expect(findAllVariables()).toHaveLength(2);
await setCiVariableKey();
expect(findAllVariables()).toHaveLength(2);
});
it('removes the correct variable row', async () => {
const variableKeyNameOne = 'key-one';
const variableKeyNameThree = 'key-three';
await setCiVariableKeyByPosition(0, variableKeyNameOne);
await setCiVariableKeyByPosition(1, 'key-two');
await setCiVariableKeyByPosition(2, variableKeyNameThree);
expect(findAllVariables()).toHaveLength(4);
await findAllDeleteVarBtns().at(1).trigger('click');
expect(findAllVariables()).toHaveLength(3);
expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
expect(findAllCiVariableKeys().at(2).element.value).toBe('');
});
it('delete variable button should only show when there is more than one variable', async () => {
expect(findDeleteVarBtn().exists()).toBe(false);
await setCiVariableKey();
expect(findDeleteVarBtn().exists()).toBe(true);
});
});
describe('variable delete button placeholder', () => {
beforeEach(async () => {
await createComponent({
handlers: {
getJobQueryResponseHandlerWithVariables: jest.fn().mockResolvedValue(mockJobResponse),
},
});
});
it('delete variable button placeholder should only exist when a user cannot remove', () => {
expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
});
it('does not show the placeholder button', () => {
expect(findDeleteVarBtnPlaceholder().classes('gl-opacity-0')).toBe(true);
});
it('placeholder button will not delete the row on click', async () => {
expect(findAllCiVariableKeys()).toHaveLength(1);
expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
await findDeleteVarBtnPlaceholder().trigger('click');
expect(findAllCiVariableKeys()).toHaveLength(1);
expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
});
});
describe('emitting events', () => {
beforeEach(async () => {
await createComponent({
handlers: {
getJobQueryResponseHandlerWithVariables: jest.fn().mockResolvedValue(mockJobResponse),
},
});
});
it('emits update-variables event when data changes', async () => {
const newVariable = { key: 'new key', value: 'test-value' };
const emptyVariable = { key: '', value: '' };
const initialEvent = wrapper.emitted('update-variables').at(0)[0];
expect(initialEvent).toHaveLength(1);
expect(initialEvent).toEqual(
expect.arrayContaining([expect.objectContaining({ ...emptyVariable })]),
);
await setCiVariableKey();
await findCiVariableValue().setValue(newVariable.value);
const lastEvent = wrapper.emitted('update-variables').at(-1)[0];
expect(lastEvent).toHaveLength(2);
expect(lastEvent).toEqual(
expect.arrayContaining([
expect.objectContaining({
...newVariable,
}),
expect.objectContaining({ ...emptyVariable }),
]),
);
});
});
});

View File

@ -1,8 +1,7 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import { createAlert } from '~/alert';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { TYPENAME_CI_BUILD } from '~/graphql_shared/constants';
import { JOB_GRAPHQL_ERRORS } from '~/ci/constants';
@ -10,16 +9,14 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import waitForPromises from 'helpers/wait_for_promises';
import { visitUrl } from '~/lib/utils/url_utility';
import ManualVariablesForm from '~/ci/job_details/components/manual_variables_form.vue';
import getJobQuery from '~/ci/job_details/graphql/queries/get_job.query.graphql';
import playJobMutation from '~/ci/job_details/graphql/mutations/job_play_with_variables.mutation.graphql';
import retryJobMutation from '~/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import JobVariablesForm from '~/ci/job_details/components/job_variables_form.vue';
import {
mockFullPath,
mockId,
mockJobResponse,
mockJobWithVariablesResponse,
mockJobPlayMutationData,
mockJobRetryMutationData,
} from '../mock_data';
@ -42,12 +39,10 @@ describe('Manual Variables Form', () => {
let mockApollo;
let requestHandlers;
const getJobQueryResponseHandlerWithVariables = jest.fn().mockResolvedValue(mockJobResponse);
const playJobMutationHandler = jest.fn().mockResolvedValue({});
const retryJobMutationHandler = jest.fn().mockResolvedValue({});
const defaultHandlers = {
getJobQueryResponseHandlerWithVariables,
playJobMutationHandler,
retryJobMutationHandler,
};
@ -56,7 +51,6 @@ describe('Manual Variables Form', () => {
requestHandlers = handlers;
mockApollo = createMockApollo([
[getJobQuery, handlers.getJobQueryResponseHandlerWithVariables],
[playJobMutation, handlers.playJobMutationHandler],
[retryJobMutation, handlers.retryJobMutationHandler],
]);
@ -65,7 +59,7 @@ describe('Manual Variables Form', () => {
apolloProvider: mockApollo,
};
wrapper = mountExtended(ManualVariablesForm, {
wrapper = shallowMountExtended(ManualVariablesForm, {
propsData: {
jobId: mockId,
jobName: 'job-name',
@ -77,74 +71,33 @@ describe('Manual Variables Form', () => {
},
...options,
});
return waitForPromises();
};
const findHelpText = () => wrapper.findComponent(GlSprintf);
const findHelpLink = () => wrapper.findComponent(GlLink);
const findCancelBtn = () => wrapper.findByTestId('cancel-btn');
const findRunBtn = () => wrapper.findByTestId('run-manual-job-btn');
const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key');
const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key');
const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value');
const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row');
const setCiVariableKey = () => {
findCiVariableKey().setValue('new key');
findCiVariableKey().vm.$emit('change');
nextTick();
};
const setCiVariableKeyByPosition = (position, value) => {
findAllCiVariableKeys().at(position).setValue(value);
findAllCiVariableKeys().at(position).vm.$emit('change');
nextTick();
};
const findVariablesForm = () => wrapper.findComponent(JobVariablesForm);
afterEach(() => {
createAlert.mockClear();
});
describe('when page renders', () => {
beforeEach(async () => {
await createComponent();
beforeEach(() => {
createComponent();
});
it('renders help text with provided link', () => {
expect(findHelpText().exists()).toBe(true);
expect(findHelpLink().attributes('href')).toBe('/help/ci/variables/_index#for-a-project');
});
});
describe('when query is unsuccessful', () => {
beforeEach(async () => {
await createComponent({
handlers: {
getJobQueryResponseHandlerWithVariables: jest.fn().mockRejectedValue({}),
},
});
it('renders job id to variables form', () => {
expect(findVariablesForm().exists()).toBe(true);
});
it('shows an alert with error', () => {
expect(createAlert).toHaveBeenCalledWith({
message: JOB_GRAPHQL_ERRORS.jobQueryErrorText,
});
it('provides job variables form', () => {
expect(findVariablesForm().props('jobId')).toBe(mockId);
});
});
describe('when job has not been retried', () => {
beforeEach(async () => {
await createComponent({
handlers: {
getJobQueryResponseHandlerWithVariables: jest
.fn()
.mockResolvedValue(mockJobWithVariablesResponse),
},
});
beforeEach(() => {
createComponent();
});
it('does not render the cancel button', () => {
@ -153,45 +106,25 @@ describe('Manual Variables Form', () => {
});
});
describe('when job has variables', () => {
beforeEach(async () => {
await createComponent({
handlers: {
getJobQueryResponseHandlerWithVariables: jest
.fn()
.mockResolvedValue(mockJobWithVariablesResponse),
},
});
});
it('sets manual job variables', () => {
const queryKey = mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].key;
const queryValue =
mockJobWithVariablesResponse.data.project.job.manualVariables.nodes[0].value;
expect(findCiVariableKey().element.value).toBe(queryKey);
expect(findCiVariableValue().element.value).toBe(queryValue);
});
});
describe('when play mutation fires', () => {
beforeEach(async () => {
await createComponent({
beforeEach(() => {
createComponent({
handlers: {
getJobQueryResponseHandlerWithVariables: jest
.fn()
.mockResolvedValue(mockJobWithVariablesResponse),
playJobMutationHandler: jest.fn().mockResolvedValue(mockJobPlayMutationData),
},
});
});
it('passes variables in correct format', async () => {
await setCiVariableKey();
it('passes variables in correct format', () => {
findVariablesForm().vm.$emit('update-variables', [
{
id: 'gid://gitlab/Ci::JobVariable/6',
key: 'new key',
value: 'new value',
},
]);
await findCiVariableValue().setValue('new value');
await findRunBtn().vm.$emit('click');
findRunBtn().vm.$emit('click');
expect(requestHandlers.playJobMutationHandler).toHaveBeenCalledTimes(1);
expect(requestHandlers.playJobMutationHandler).toHaveBeenCalledWith({
@ -212,15 +145,6 @@ describe('Manual Variables Form', () => {
expect(requestHandlers.playJobMutationHandler).toHaveBeenCalledTimes(1);
expect(visitUrl).toHaveBeenCalledWith(mockJobPlayMutationData.data.jobPlay.job.webPath);
});
it('does not refetch variables after job is run', async () => {
expect(requestHandlers.getJobQueryResponseHandlerWithVariables).toHaveBeenCalledTimes(1);
findRunBtn().vm.$emit('click');
await waitForPromises();
expect(requestHandlers.getJobQueryResponseHandlerWithVariables).toHaveBeenCalledTimes(1);
});
});
describe('when play mutation is unsuccessful', () => {
@ -243,13 +167,10 @@ describe('Manual Variables Form', () => {
});
describe('when job is retryable', () => {
beforeEach(async () => {
await createComponent({
beforeEach(() => {
createComponent({
props: { isRetryable: true },
handlers: {
getJobQueryResponseHandlerWithVariables: jest
.fn()
.mockResolvedValue(mockJobWithVariablesResponse),
retryJobMutationHandler: jest.fn().mockResolvedValue(mockJobRetryMutationData),
},
});
@ -267,16 +188,13 @@ describe('Manual Variables Form', () => {
});
describe('with confirmation message', () => {
beforeEach(async () => {
await createComponent({
beforeEach(() => {
createComponent({
props: {
isRetryable: true,
confirmationMessage: 'Are you sure?',
},
handlers: {
getJobQueryResponseHandlerWithVariables: jest
.fn()
.mockResolvedValue(mockJobWithVariablesResponse),
retryJobMutationHandler: jest.fn().mockResolvedValue(mockJobRetryMutationData),
},
});
@ -314,20 +232,11 @@ describe('Manual Variables Form', () => {
expect(requestHandlers.retryJobMutationHandler).toHaveBeenCalledTimes(1);
expect(visitUrl).toHaveBeenCalledWith(mockJobRetryMutationData.data.jobRetry.job.webPath);
});
it('does not refetch variables after job is rerun', async () => {
expect(requestHandlers.getJobQueryResponseHandlerWithVariables).toHaveBeenCalledTimes(1);
findRunBtn().vm.$emit('click');
await waitForPromises();
expect(requestHandlers.getJobQueryResponseHandlerWithVariables).toHaveBeenCalledTimes(1);
});
});
describe('when retry mutation is unsuccessful', () => {
beforeEach(async () => {
await createComponent({
beforeEach(() => {
createComponent({
props: { isRetryable: true },
handlers: {
retryJobMutationHandler: jest.fn().mockRejectedValue({}),
@ -344,91 +253,4 @@ describe('Manual Variables Form', () => {
});
});
});
describe('updating variables in UI', () => {
beforeEach(async () => {
await createComponent({
handlers: {
getJobQueryResponseHandlerWithVariables: jest.fn().mockResolvedValue(mockJobResponse),
},
});
});
it('creates a new variable when user enters a new key value', async () => {
expect(findAllVariables()).toHaveLength(1);
await setCiVariableKey();
expect(findAllVariables()).toHaveLength(2);
});
it('does not create extra empty variables', async () => {
expect(findAllVariables()).toHaveLength(1);
await setCiVariableKey();
expect(findAllVariables()).toHaveLength(2);
await setCiVariableKey();
expect(findAllVariables()).toHaveLength(2);
});
it('removes the correct variable row', async () => {
const variableKeyNameOne = 'key-one';
const variableKeyNameThree = 'key-three';
await setCiVariableKeyByPosition(0, variableKeyNameOne);
await setCiVariableKeyByPosition(1, 'key-two');
await setCiVariableKeyByPosition(2, variableKeyNameThree);
expect(findAllVariables()).toHaveLength(4);
await findAllDeleteVarBtns().at(1).trigger('click');
expect(findAllVariables()).toHaveLength(3);
expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne);
expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree);
expect(findAllCiVariableKeys().at(2).element.value).toBe('');
});
it('delete variable button should only show when there is more than one variable', async () => {
expect(findDeleteVarBtn().exists()).toBe(false);
await setCiVariableKey();
expect(findDeleteVarBtn().exists()).toBe(true);
});
});
describe('variable delete button placeholder', () => {
beforeEach(async () => {
await createComponent({
handlers: {
getJobQueryResponseHandlerWithVariables: jest.fn().mockResolvedValue(mockJobResponse),
},
});
});
it('delete variable button placeholder should only exist when a user cannot remove', () => {
expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
});
it('does not show the placeholder button', () => {
expect(findDeleteVarBtnPlaceholder().classes('gl-opacity-0')).toBe(true);
});
it('placeholder button will not delete the row on click', async () => {
expect(findAllCiVariableKeys()).toHaveLength(1);
expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
await findDeleteVarBtnPlaceholder().trigger('click');
expect(findAllCiVariableKeys()).toHaveLength(1);
expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
});
});
});

View File

@ -22,6 +22,7 @@ RSpec.describe Snippets::RepositoryValidationService, feature_category: :source_
allow(repository).to receive(:branch_count).and_return(2)
expect(subject).to be_error
expect(subject.reason).to eq(described_class::INVALID_REPOSITORY)
expect(subject.message).to match(/Repository has more than one branch/)
end
@ -29,6 +30,7 @@ RSpec.describe Snippets::RepositoryValidationService, feature_category: :source_
allow(repository).to receive(:branch_names).and_return(['foo'])
expect(subject).to be_error
expect(subject.reason).to eq(described_class::INVALID_REPOSITORY)
expect(subject.message).to match(/Repository has an invalid default branch name/)
end
@ -36,6 +38,7 @@ RSpec.describe Snippets::RepositoryValidationService, feature_category: :source_
allow(repository).to receive(:tag_count).and_return(1)
expect(subject).to be_error
expect(subject.reason).to eq(described_class::INVALID_REPOSITORY)
expect(subject.message).to match(/Repository has tags/)
end
@ -45,6 +48,7 @@ RSpec.describe Snippets::RepositoryValidationService, feature_category: :source_
allow(repository).to receive(:ls_files).and_return(files)
expect(subject).to be_error
expect(subject.reason).to eq(described_class::INVALID_REPOSITORY)
expect(subject.message).to match(/Repository files count over the limit/)
end
@ -52,6 +56,7 @@ RSpec.describe Snippets::RepositoryValidationService, feature_category: :source_
allow(repository).to receive(:ls_files).and_return([])
expect(subject).to be_error
expect(subject.reason).to eq(described_class::INVALID_REPOSITORY)
expect(subject.message).to match(/Repository must contain at least 1 file/)
end
@ -61,6 +66,7 @@ RSpec.describe Snippets::RepositoryValidationService, feature_category: :source_
end
expect(subject).to be_error
expect(subject.reason).to eq(described_class::INVALID_REPOSITORY)
expect(subject.message).to match(/Repository size is above the limit/)
end

View File

@ -1,6 +1,6 @@
---
stage: Software Supply Chain Security
group: Pipeline Security
group: Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: Fine-grained permissions for CI/CD job tokens
---
@ -23,10 +23,43 @@ Status: Experiment
{{< /details >}}
{{< history >}}
- [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/15234) in GitLab 17.10. This feature is an [experiment](../../policy/development_stages_support.md#experiment).
{{< /history >}}
{{< alert type="flag" >}}
The availability of this feature is controlled by a feature flag.
For more information, see the history.
This feature is available for testing, but not ready for production use.
{{< /alert >}}
You can use fine-grained permissions to explicitly allow access to a limited set of API endpoints.
These permissions are applied to the CI/CD job tokens in a specified project.
This feature is an [experiment](../../policy/development_stages_support.md#experiment).
## Enable fine-grained permissions
### On GitLab Self-Managed
1. Start the GitLab Rails console. For information, see [Enable and disable GitLab features deployed behind feature flags](../../administration/feature_flags.md#enable-or-disable-the-feature)
1. Turn on the [feature flag](../../administration/feature_flags.md):
```ruby
# You must include a specific project ID with this command.
Feature.enable(:add_policies_to_ci_job_token, <project_id>)
```
### On GitLab.com
Add a comment on this [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/519575) with your project ID.
## Available API endpoints
The following endpoints are available for CI/CD job tokens.
You can use fine-grained permissions to explicitly allow access to a limited set of the following API endpoints.
`None` means fine-grained permissions cannot control access to this endpoint.