From 2113bb8ffef32a75b1d020e218a45c5a4eb5eb05 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 28 Feb 2025 21:13:37 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo/gitlab/service_response.yml | 1 - .../components/job_variables_form.vue | 215 +++++++++++++++ .../components/manual_variables_form.vue | 238 +++-------------- app/helpers/search_helper.rb | 2 +- app/services/concerns/search/filter.rb | 3 +- .../snippets/repository_validation_service.rb | 4 +- ...add_usable_storage_bytes_to_zoekt_nodes.rb | 10 + db/schema_migrations/20250221160238 | 1 + db/structure.sql | 2 + .../settings/continuous_integration.md | 2 +- ...=> suggest_migrate_from_jenkins_v17_7.png} | Bin doc/api/graphql/reference/_index.md | 34 +-- doc/ci/jobs/ci_job_token.md | 4 + doc/ci/jobs/fine_grained_permissions.md | 37 ++- ...png => shared_runner_ip_address_v14_5.png} | Bin doc/ci/runners/runners_scope.md | 2 +- .../vulnerability_report/_index.md | 2 +- ... vulnerability_severity_change_v17_10.png} | Bin .../project/ml/experiment_tracking/_index.md | 10 +- ...{experiments.png => experiments_v17_9.png} | Bin .../img/{metrics.png => metrics_v17_10.png} | Bin ..._detail_ci.png => run_detail_ci_v17_9.png} | Bin .../img/{run.png => run_v17_9.png} | Bin .../img/{runs.png => runs_v17_9.png} | Bin .../components/job_variables_form_spec.js | 250 ++++++++++++++++++ .../components/manual_variables_form_spec.js | 236 ++--------------- .../repository_validation_service_spec.rb | 6 + .../templates/fine_grained_permissions.md.erb | 37 ++- 28 files changed, 649 insertions(+), 447 deletions(-) create mode 100644 app/assets/javascripts/ci/job_details/components/job_variables_form.vue create mode 100644 db/migrate/20250221160238_add_usable_storage_bytes_to_zoekt_nodes.rb create mode 100644 db/schema_migrations/20250221160238 rename doc/administration/settings/img/{suggest_migrate_from_jenkins_v_17_7.png => suggest_migrate_from_jenkins_v17_7.png} (100%) rename doc/ci/runners/img/{shared_runner_ip_address_14_5.png => shared_runner_ip_address_v14_5.png} (100%) rename doc/user/application_security/vulnerability_report/img/{vulnerability_severity_change_17.10.png => vulnerability_severity_change_v17_10.png} (100%) rename doc/user/project/ml/experiment_tracking/img/{experiments.png => experiments_v17_9.png} (100%) rename doc/user/project/ml/experiment_tracking/img/{metrics.png => metrics_v17_10.png} (100%) rename doc/user/project/ml/experiment_tracking/img/{run_detail_ci.png => run_detail_ci_v17_9.png} (100%) rename doc/user/project/ml/experiment_tracking/img/{run.png => run_v17_9.png} (100%) rename doc/user/project/ml/experiment_tracking/img/{runs.png => runs_v17_9.png} (100%) create mode 100644 spec/frontend/ci/job_details/components/job_variables_form_spec.js diff --git a/.rubocop_todo/gitlab/service_response.yml b/.rubocop_todo/gitlab/service_response.yml index ac568723676..86b1d75f824 100644 --- a/.rubocop_todo/gitlab/service_response.yml +++ b/.rubocop_todo/gitlab/service_response.yml @@ -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' diff --git a/app/assets/javascripts/ci/job_details/components/job_variables_form.vue b/app/assets/javascripts/ci/job_details/components/job_variables_form.vue new file mode 100644 index 00000000000..d728f4fa3b6 --- /dev/null +++ b/app/assets/javascripts/ci/job_details/components/job_variables_form.vue @@ -0,0 +1,215 @@ + + diff --git a/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue index 5ad40e8c4c7..5734882c44f 100644 --- a/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue +++ b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue @@ -1,28 +1,16 @@ 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. -![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: 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. -![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 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**. -![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 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. -![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) 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.