diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index ffee19ba056..3d1c87fc86f 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -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,
diff --git a/app/services/concerns/search/filter.rb b/app/services/concerns/search/filter.rb
index bf89f2bfe61..72752b3db87 100644
--- a/app/services/concerns/search/filter.rb
+++ b/app/services/concerns/search/filter.rb
@@ -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
diff --git a/app/services/snippets/repository_validation_service.rb b/app/services/snippets/repository_validation_service.rb
index 7e9b2eded16..fba19f9c922 100644
--- a/app/services/snippets/repository_validation_service.rb
+++ b/app/services/snippets/repository_validation_service.rb
@@ -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
diff --git a/db/migrate/20250221160238_add_usable_storage_bytes_to_zoekt_nodes.rb b/db/migrate/20250221160238_add_usable_storage_bytes_to_zoekt_nodes.rb
new file mode 100644
index 00000000000..14139d5c859
--- /dev/null
+++ b/db/migrate/20250221160238_add_usable_storage_bytes_to_zoekt_nodes.rb
@@ -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
diff --git a/db/schema_migrations/20250221160238 b/db/schema_migrations/20250221160238
new file mode 100644
index 00000000000..bd08f697d56
--- /dev/null
+++ b/db/schema_migrations/20250221160238
@@ -0,0 +1 @@
+7ee4d9d63499f305e7dbe6107de736b578f898d323d295a7372dc34c29da4df4
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 943169e456d..804ab25825b 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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))
);
diff --git a/doc/administration/settings/continuous_integration.md b/doc/administration/settings/continuous_integration.md
index c5dc1f50bd6..29d4ea305f1 100644
--- a/doc/administration/settings/continuous_integration.md
+++ b/doc/administration/settings/continuous_integration.md
@@ -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.
-
+
To disable the banner:
diff --git a/doc/administration/settings/img/suggest_migrate_from_jenkins_v_17_7.png b/doc/administration/settings/img/suggest_migrate_from_jenkins_v17_7.png
similarity index 100%
rename from doc/administration/settings/img/suggest_migrate_from_jenkins_v_17_7.png
rename to doc/administration/settings/img/suggest_migrate_from_jenkins_v17_7.png
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 80265cae7a4..b33088e712c 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -22330,7 +22330,7 @@ Represents the Geo replication and verification state of a ci_secure_file.
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the CiSecureFileRegistry do not match on the primary and secondary. |
| `ciSecureFileId` | [`ID!`](#id) | ID of the Ci Secure File. |
| `createdAt` | [`Time`](#time) | Timestamp when the CiSecureFileRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the CiSecureFileRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the CiSecureFileRegistry. |
| `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
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the ContainerRepositoryRegistry do not match on the primary and secondary. |
| `containerRepositoryId` | [`ID!`](#id) | ID of the ContainerRepository. |
| `createdAt` | [`Time`](#time) | Timestamp when the ContainerRepositoryRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the ContainerRepositoryRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the ContainerRepositoryRegistry. |
| `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
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the DependencyProxyBlobRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the DependencyProxyBlobRegistry was created. |
| `dependencyProxyBlobId` | [`ID!`](#id) | ID of the Dependency Proxy Blob. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the DependencyProxyBlobRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the DependencyProxyBlobRegistry. |
| `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
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the DependencyProxyManifestRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the DependencyProxyManifestRegistry was created. |
| `dependencyProxyManifestId` | [`ID!`](#id) | ID of the Dependency Proxy Manifest. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the DependencyProxyManifestRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the DependencyProxyManifestRegistry. |
| `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
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the DesignManagementRepositoryRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the DesignManagementRepositoryRegistry was created. |
| `designManagementRepositoryId` | [`ID!`](#id) | ID of the Design Management Repository. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the DesignManagementRepositoryRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the DesignManagementRepositoryRegistry. |
| `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.
| ---- | ---- | ----------- |
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the GroupWikiRepositoryRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the GroupWikiRepositoryRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `groupWikiRepositoryId` | [`ID!`](#id) | ID of the Group Wiki Repository. |
| `id` | [`ID!`](#id) | ID of the GroupWikiRepositoryRegistry. |
| `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.
| `artifactId` | [`ID!`](#id) | ID of the Job Artifact. |
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the JobArtifactRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the JobArtifactRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the JobArtifactRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the JobArtifactRegistry. |
| `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.
| ---- | ---- | ----------- |
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the LfsObjectRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the LfsObjectRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the LfsObjectRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the LfsObjectRegistry. |
| `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.
| ---- | ---- | ----------- |
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the MergeRequestDiffRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the MergeRequestDiffRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the MergeRequestDiffRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the MergeRequestDiffRegistry. |
| `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.
| ---- | ---- | ----------- |
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the PackageFileRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the PackageFileRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the PackageFileRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the PackageFileRegistry. |
| `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.
| ---- | ---- | ----------- |
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the PagesDeploymentRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the PagesDeploymentRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the PagesDeploymentRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the PagesDeploymentRegistry. |
| `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.
| ---- | ---- | ----------- |
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the PipelineArtifactRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the PipelineArtifactRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the PipelineArtifactRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the PipelineArtifactRegistry. |
| `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.
| ---- | ---- | ----------- |
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the ProjectRepositoryRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the ProjectRepositoryRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the ProjectRepositoryRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the ProjectRepositoryRegistry. |
| `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
| ---- | ---- | ----------- |
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the ProjectWikiRepositoryRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the ProjectWikiRepositoryRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the ProjectWikiRepositoryRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the ProjectWikiRepositoryRegistry. |
| `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.
| ---- | ---- | ----------- |
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the SnippetRepositoryRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the SnippetRepositoryRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the SnippetRepositoryRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the SnippetRepositoryRegistry. |
| `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.
| ---- | ---- | ----------- |
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the TerraformStateVersionRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the TerraformStateVersionRegistry was created. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the TerraformStateVersionRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the TerraformStateVersionRegistry. |
| `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.
| `checksumMismatch` | [`Boolean`](#boolean) | Indicate if the checksums of the UploadRegistry do not match on the primary and secondary. |
| `createdAt` | [`Time`](#time) | Timestamp when the UploadRegistry was created. |
| `fileId` | [`ID!`](#id) | ID of the Upload. |
-| `forceToRedownload` | [`Boolean`](#boolean) | Indicate if a forced redownload is to be performed. |
+| `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. |
| `id` | [`ID!`](#id) | ID of the UploadRegistry. |
| `lastSyncFailure` | [`String`](#string) | Error message during sync of the UploadRegistry. |
| `lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the UploadRegistry. |
diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md
index 081cf4a4cb8..8334faf055a 100644
--- a/doc/ci/jobs/ci_job_token.md
+++ b/doc/ci/jobs/ci_job_token.md
@@ -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
diff --git a/doc/ci/jobs/fine_grained_permissions.md b/doc/ci/jobs/fine_grained_permissions.md
index 3c053887978..92d858e2c6e 100644
--- a/doc/ci/jobs/fine_grained_permissions.md
+++ b/doc/ci/jobs/fine_grained_permissions.md
@@ -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, )
+```
+
+### 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.
diff --git a/doc/ci/runners/img/shared_runner_ip_address_14_5.png b/doc/ci/runners/img/shared_runner_ip_address_v14_5.png
similarity index 100%
rename from doc/ci/runners/img/shared_runner_ip_address_14_5.png
rename to doc/ci/runners/img/shared_runner_ip_address_v14_5.png
diff --git a/doc/ci/runners/runners_scope.md b/doc/ci/runners/runners_scope.md
index 835119bde12..acf2c6eb5f1 100644
--- a/doc/ci/runners/runners_scope.md
+++ b/doc/ci/runners/runners_scope.md
@@ -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.
-
+
### Determine the IP address of a project runner
diff --git a/doc/user/application_security/vulnerability_report/_index.md b/doc/user/application_security/vulnerability_report/_index.md
index 2643ff690f0..11287edf03d 100644
--- a/doc/user/application_security/vulnerability_report/_index.md
+++ b/doc/user/application_security/vulnerability_report/_index.md
@@ -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**.
-
+
## Sort vulnerabilities by date detected
diff --git a/doc/user/application_security/vulnerability_report/img/vulnerability_severity_change_17.10.png b/doc/user/application_security/vulnerability_report/img/vulnerability_severity_change_v17_10.png
similarity index 100%
rename from doc/user/application_security/vulnerability_report/img/vulnerability_severity_change_17.10.png
rename to doc/user/application_security/vulnerability_report/img/vulnerability_severity_change_v17_10.png
diff --git a/doc/user/project/ml/experiment_tracking/_index.md b/doc/user/project/ml/experiment_tracking/_index.md
index 07a345d1065..82ca4e4a285 100644
--- a/doc/user/project/ml/experiment_tracking/_index.md
+++ b/doc/user/project/ml/experiment_tracking/_index.md
@@ -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.
-
+
## 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.
-
+
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.
-
+
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:
-
+
## View logged metrics
@@ -89,4 +89,4 @@ To view logged metrics:
1. Select the experiment you want to view.
1. Select the **Performance** tab.
-
+
diff --git a/doc/user/project/ml/experiment_tracking/img/experiments.png b/doc/user/project/ml/experiment_tracking/img/experiments_v17_9.png
similarity index 100%
rename from doc/user/project/ml/experiment_tracking/img/experiments.png
rename to doc/user/project/ml/experiment_tracking/img/experiments_v17_9.png
diff --git a/doc/user/project/ml/experiment_tracking/img/metrics.png b/doc/user/project/ml/experiment_tracking/img/metrics_v17_10.png
similarity index 100%
rename from doc/user/project/ml/experiment_tracking/img/metrics.png
rename to doc/user/project/ml/experiment_tracking/img/metrics_v17_10.png
diff --git a/doc/user/project/ml/experiment_tracking/img/run_detail_ci.png b/doc/user/project/ml/experiment_tracking/img/run_detail_ci_v17_9.png
similarity index 100%
rename from doc/user/project/ml/experiment_tracking/img/run_detail_ci.png
rename to doc/user/project/ml/experiment_tracking/img/run_detail_ci_v17_9.png
diff --git a/doc/user/project/ml/experiment_tracking/img/run.png b/doc/user/project/ml/experiment_tracking/img/run_v17_9.png
similarity index 100%
rename from doc/user/project/ml/experiment_tracking/img/run.png
rename to doc/user/project/ml/experiment_tracking/img/run_v17_9.png
diff --git a/doc/user/project/ml/experiment_tracking/img/runs.png b/doc/user/project/ml/experiment_tracking/img/runs_v17_9.png
similarity index 100%
rename from doc/user/project/ml/experiment_tracking/img/runs.png
rename to doc/user/project/ml/experiment_tracking/img/runs_v17_9.png
diff --git a/spec/frontend/ci/job_details/components/job_variables_form_spec.js b/spec/frontend/ci/job_details/components/job_variables_form_spec.js
new file mode 100644
index 00000000000..d87d2a2cb6b
--- /dev/null
+++ b/spec/frontend/ci/job_details/components/job_variables_form_spec.js
@@ -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 }),
+ ]),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/ci/job_details/components/manual_variables_form_spec.js b/spec/frontend/ci/job_details/components/manual_variables_form_spec.js
index 46e2ca2d8dd..046ebd194a2 100644
--- a/spec/frontend/ci/job_details/components/manual_variables_form_spec.js
+++ b/spec/frontend/ci/job_details/components/manual_variables_form_spec.js
@@ -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);
- });
- });
});
diff --git a/spec/services/snippets/repository_validation_service_spec.rb b/spec/services/snippets/repository_validation_service_spec.rb
index 76fe4ef0033..d8acb09fdb2 100644
--- a/spec/services/snippets/repository_validation_service_spec.rb
+++ b/spec/services/snippets/repository_validation_service_spec.rb
@@ -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
diff --git a/tooling/ci/job_tokens/docs/templates/fine_grained_permissions.md.erb b/tooling/ci/job_tokens/docs/templates/fine_grained_permissions.md.erb
index d1bf8cd3c0b..bea36f2d883 100644
--- a/tooling/ci/job_tokens/docs/templates/fine_grained_permissions.md.erb
+++ b/tooling/ci/job_tokens/docs/templates/fine_grained_permissions.md.erb
@@ -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, )
+```
+
+### 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.