Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-06-27 12:18:41 +00:00
parent d83c5c5d4e
commit b9f4411b1b
96 changed files with 1214 additions and 530 deletions

View File

@ -4267,7 +4267,6 @@ Gitlab/BoundedContexts:
- 'lib/quality/seeders/issues.rb'
- 'lib/release_highlights/validator.rb'
- 'lib/release_highlights/validator/entry.rb'
- 'lib/result.rb'
- 'lib/rouge/formatters/html_gitlab.rb'
- 'lib/rouge/plugins/common_mark.rb'
- 'lib/safe_zip/entry.rb'

View File

@ -479,7 +479,6 @@ Layout/ClassStructure:
- 'lib/peek/views/external_http.rb'
- 'lib/peek/views/zoekt.rb'
- 'lib/release_highlights/validator.rb'
- 'lib/result.rb'
- 'lib/sbom/package_url.rb'
- 'lib/system_check/base_check.rb'
- 'lib/uploaded_file.rb'

View File

@ -366,6 +366,7 @@ RSpec/FeatureCategory:
- 'ee/spec/helpers/analytics/code_review_helper_spec.rb'
- 'ee/spec/helpers/application_helper_spec.rb'
- 'ee/spec/helpers/boards_helper_spec.rb'
- 'ee/spec/helpers/container_registry/container_registry_helper_spec.rb'
- 'ee/spec/helpers/credentials_inventory_helper_spec.rb'
- 'ee/spec/helpers/ee/access_tokens_helper_spec.rb'
- 'ee/spec/helpers/ee/admin/identities_helper_spec.rb'
@ -1945,7 +1946,7 @@ RSpec/FeatureCategory:
- 'spec/helpers/commits_helper_spec.rb'
- 'spec/helpers/components_helper_spec.rb'
- 'spec/helpers/container_expiration_policies_helper_spec.rb'
- 'spec/helpers/container_registry_helper_spec.rb'
- 'spec/helpers/container_registry/container_registry_helper_spec.rb'
- 'spec/helpers/cookies_helper_spec.rb'
- 'spec/helpers/dashboard_helper_spec.rb'
- 'spec/helpers/dev_ops_report_helper_spec.rb'

View File

@ -2447,7 +2447,6 @@ Style/InlineDisableAnnotation:
- 'lib/kramdown/parser/atlassian_document_format.rb'
- 'lib/object_storage/pending_direct_upload.rb'
- 'lib/quality/seeders/issues.rb'
- 'lib/result.rb'
- 'lib/safe_zip/extract.rb'
- 'lib/sidebars/context.rb'
- 'lib/sidebars/menu_item.rb'
@ -2727,7 +2726,6 @@ Style/InlineDisableAnnotation:
- 'spec/lib/mattermost/team_spec.rb'
- 'spec/lib/object_storage/pending_direct_upload_spec.rb'
- 'spec/lib/omni_auth/strategies/jwt_spec.rb'
- 'spec/lib/result_spec.rb'
- 'spec/mailers/notify_spec.rb'
- 'spec/migrations/20230726144458_swap_notes_id_to_bigint_for_self_managed_spec.rb'
- 'spec/migrations/20230803125434_add_has_merge_request_on_vulnerability_reads_trigger_spec.rb'

View File

@ -510,7 +510,7 @@ group :development, :test do
end
group :development, :test, :danger do
gem 'gitlab-dangerfiles', '~> 4.7.0', require: false, feature_category: :tooling
gem 'gitlab-dangerfiles', '~> 4.8.0', require: false, feature_category: :tooling
end
group :development, :test, :coverage do

View File

@ -212,7 +212,7 @@
{"name":"gitaly","version":"17.0.2","platform":"ruby","checksum":"ecd1cdf097318652e0df7eb661f7c1689f8734b0fa36dade52fb0799fe4fba82"},
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
{"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
{"name":"gitlab-dangerfiles","version":"4.7.0","platform":"ruby","checksum":"2576876a8dcb7290853fc3aef8048001cfe593b87318dd0016959d42e0e145ca"},
{"name":"gitlab-dangerfiles","version":"4.8.0","platform":"ruby","checksum":"b327d079552ec974a63bf34d749a0308425af6ebf51d01064f1a6ff216a523db"},
{"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},
{"name":"gitlab-fog-azure-rm","version":"1.9.1","platform":"ruby","checksum":"026b8e188ac4183c1bf1b1909b0489da0ffad453996a6e744e0eba67dc284f37"},
{"name":"gitlab-glfm-markdown","version":"0.0.17","platform":"aarch64-linux","checksum":"81ccfd91c7a1da4b165e700f1a6fbb15cf20ffd283ec8c6e05d5e2078a569717"},

View File

@ -691,7 +691,7 @@ GEM
terminal-table (>= 1.5.1)
gitlab-chronic (0.10.5)
numerizer (~> 0.2)
gitlab-dangerfiles (4.7.0)
gitlab-dangerfiles (4.8.0)
danger (>= 9.3.0)
danger-gitlab (>= 8.0.0)
rake (~> 13.0)
@ -2003,7 +2003,7 @@ DEPENDENCIES
gitaly (~> 17.0.1)
gitlab-backup-cli!
gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 4.7.0)
gitlab-dangerfiles (~> 4.8.0)
gitlab-experiment (~> 0.9.1)
gitlab-fog-azure-rm (~> 1.9.1)
gitlab-glfm-markdown (~> 0.0.17)

View File

@ -1,17 +1,17 @@
<script>
import {
GlCard,
GlTable,
GlButton,
GlPagination,
GlIcon,
GlLoadingIcon,
GlEmptyState,
GlModal,
GlTooltipDirective,
} from '@gitlab/ui';
import { __ } from '~/locale';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { createAlert } from '~/alert';
@ -86,16 +86,18 @@ export default {
csrf,
DEFAULT_PER_PAGE,
components: {
GlCard,
CrudComponent,
GlTable,
GlButton,
GlPagination,
TimeAgoTooltip,
GlIcon,
GlLoadingIcon,
GlEmptyState,
GlModal,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['editPath', 'deletePath', 'createPath', 'emptyStateSvgPath'],
data() {
return {
@ -190,19 +192,13 @@ export default {
</script>
<template>
<gl-card
class="gl-new-card"
header-class="gl-new-card-header"
body-class="gl-new-card-body gl-px-0"
<crud-component
:title="$options.i18n.pageTitle"
:count="totalItems.toString()"
icon="key"
class="gl-mt-5"
>
<template #header>
<div class="gl-new-card-title-wrapper">
<h3 class="gl-new-card-title">{{ $options.i18n.pageTitle }}</h3>
<span class="gl-new-card-count">
<gl-icon name="key" class="gl-mr-2" />
{{ totalItems }}
</span>
</div>
<template #actions>
<div class="gl-new-card-actions">
<gl-button size="small" :href="createPath" data-testid="new-deploy-key-button">{{
$options.i18n.newDeployKeyButtonText
@ -257,21 +253,24 @@ export default {
</template>
<template #cell(actions)="{ item: { id } }">
<gl-button
icon="pencil"
size="small"
:aria-label="$options.i18n.edit"
:href="editHref(id)"
class="gl-mr-2"
/>
<gl-button
variant="danger"
category="secondary"
icon="remove"
size="small"
:aria-label="$options.i18n.delete"
@click="handleDeleteClick(id)"
/>
<div class="gl-flex gl-gap-2 -gl-my-3">
<gl-button
v-gl-tooltip
:title="$options.i18n.edit"
category="tertiary"
icon="pencil"
:aria-label="$options.i18n.edit"
:href="editHref(id)"
/>
<gl-button
v-gl-tooltip
:title="$options.i18n.delete"
category="tertiary"
icon="remove"
:aria-label="$options.i18n.delete"
@click="handleDeleteClick(id)"
/>
</div>
</template>
</gl-table>
<gl-empty-state
@ -307,5 +306,5 @@ export default {
</form>
{{ $options.i18n.modal.body }}
</gl-modal>
</gl-card>
</crud-component>
</template>

View File

@ -257,7 +257,6 @@ export default {
stacked="md"
fixed
show-empty
no-sort-reset
no-local-sorting
@sort-changed="(val) => $emit('sort-changed', val)"
>

View File

@ -238,7 +238,6 @@ export default {
stacked="md"
table-class="text-secondary"
show-empty
no-sort-reset
:empty-text="$options.i18n.noFilesMessage"
>
<template #table-busy>

View File

@ -18,7 +18,12 @@ export default {
TitleArea,
MetadataItem,
GlLink,
MetadataContainerScanning: () =>
import(
'ee_component/packages_and_registries/container_registry/explorer/components/list_page/metadata_container_scanning.vue'
),
},
inject: ['config'],
props: {
expirationPolicy: {
type: Object,
@ -115,5 +120,8 @@ export default {
$options.i18n.SET_UP_CLEANUP
}}</gl-link>
</template>
<template v-if="!config.isGroupPage" #metadata-container-scanning>
<metadata-container-scanning />
</template>
</title-area>
</template>

View File

@ -9,23 +9,21 @@ export const mergeVariables = (existing, incoming) => {
return incoming;
};
export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
batchMax: 1,
cacheConfig: {
typePolicies: {
ContainerRepositoryDetails: {
fields: {
tags: {
keyArgs: ['id', 'name', 'sort'],
merge: mergeVariables,
},
},
export const config = {
cacheConfig: {
typePolicies: {
ContainerRepositoryDetails: {
fields: {
tags: {
keyArgs: ['id', 'name', 'sort'],
merge: mergeVariables,
},
},
},
},
),
},
};
export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({}, config),
});

View File

@ -5,7 +5,7 @@ import PerformancePlugin from '~/performance/vue_performance_plugin';
import Translate from '~/vue_shared/translate';
import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
import { apolloProvider } from './graphql/index';
import { apolloProvider } from 'ee_else_ce/packages_and_registries/container_registry/explorer/graphql';
import RegistryExplorer from './pages/index.vue';
import createRouter from './router';
@ -41,6 +41,8 @@ export default () => {
showUnfinishedTagCleanupCallout,
connectionError,
invalidPathError,
securityConfigurationPath,
containerScanningForRegistryDocsPath,
...config
} = el.dataset;
@ -76,6 +78,8 @@ export default () => {
connectionError: parseBoolean(connectionError),
invalidPathError: parseBoolean(invalidPathError),
isMetadataDatabaseEnabled: parseBoolean(isMetadataDatabaseEnabled),
securityConfigurationPath,
containerScanningForRegistryDocsPath,
},
/* eslint-disable @gitlab/require-i18n-strings */
dockerBuildCommand: `docker build -t ${config.repositoryUrl} .`,

View File

@ -112,7 +112,6 @@ export default {
:sort-by="sortBy"
sort-desc
no-local-sorting
no-sort-reset
@sort-changed="$emit('sortChanged', $event)"
>
<template v-for="field in fields" #[getHeaderSlotName(field.key)]>

View File

@ -141,7 +141,9 @@ export default {
return url.split(value).join(`{${key}}`);
},
onItemInput({ index, key, value }) {
this.$set(this.items, index, { key, value });
const copy = [...this.items];
copy[index] = { key, value };
this.items = copy;
},
addItem() {
this.items.push({});

View File

@ -17,16 +17,8 @@ module Groups
end
def show
@work_item = group.work_items.find_by_iid(show_params[:iid])
not_found unless Feature.enabled?(:namespace_level_work_items, group)
end
private
def show_params
params.permit(:iid)
end
end
end

View File

@ -15,12 +15,13 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
path = project_runners_path(project)
if ::Ci::Runners::AssignRunnerService.new(@runner, @project, current_user).execute.success?
response = ::Ci::Runners::AssignRunnerService.new(@runner, @project, current_user).execute
if response.success?
flash[:success] = s_('Runners|Runner assigned to project.')
redirect_to path
else
assign_to_messages = @runner.errors.messages[:assign_to]
alert = assign_to_messages&.join(',') || 'Failed adding runner to project'
assign_to_messages = [response.message] + (@runner.errors.messages[:assign_to] || [])
alert = assign_to_messages.join(', ').presence || 'Failed adding runner to project'
redirect_to path, alert: alert
end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
class Projects::RunnersController < Projects::ApplicationController
before_action :authorize_admin_runner!
before_action :authorize_read_runner!
before_action :authorize_admin_runner!, except: [:index, :show]
before_action :authorize_create_runner!, only: [:new, :register]
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show, :register]

View File

@ -32,6 +32,11 @@ module Mutations
description: 'Indicates CI/CD job tokens generated in other projects ' \
'have restricted access to this project.'
argument :push_repository_for_job_token_allowed, GraphQL::Types::Boolean,
required: false,
description: 'Indicates the ability to push to the original project ' \
'repository using a job token'
field :ci_cd_settings,
Types::Ci::CiCdSettingType,
null: false,

View File

@ -37,6 +37,13 @@ module Types
null: true,
description: 'Project the CI/CD settings belong to.',
authorize: :admin_project
field :push_repository_for_job_token_allowed,
GraphQL::Types::Boolean,
null: true,
description: 'Indicates the ability to push to the original project ' \
'repository using a job token',
method: :push_repository_for_job_token_allowed?,
authorize: :admin_project
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
module ContainerRegistry
module ContainerRegistryHelper
def container_repository_gid_prefix
"gid://#{GlobalID.app}/#{ContainerRepository.name}/"
end
def project_container_registry_template_data(project, connection_error, invalid_path_error)
{
endpoint: project_container_registry_index_path(project),
expiration_policy: project.container_expiration_policy.to_json,
help_page_path: help_page_path('user/packages/container_registry/index'),
two_factor_auth_help_link: help_page_path('user/profile/account/two_factor_authentication'),
personal_access_tokens_help_link: help_page_path('user/profile/personal_access_tokens'),
no_containers_image: image_path('illustrations/docker-empty-state.svg'),
containers_error_image: image_path('illustrations/docker-error-state.svg'),
repository_url: escape_once(project.container_registry_url),
registry_host_url_with_port: escape_once(registry_config.host_port),
expiration_policy_help_page_path:
help_page_path('user/packages/container_registry/reduce_container_registry_storage',
anchor: 'cleanup-policy'),
garbage_collection_help_page_path: help_page_path('administration/packages/container_registry',
anchor: 'container-registry-garbage-collection'),
run_cleanup_policies_help_page_path: help_page_path('administration/packages/container_registry',
anchor: 'run-the-cleanup-policy-now'),
project_path: project.full_path,
gid_prefix: container_repository_gid_prefix,
is_admin: current_user&.admin.to_s,
show_cleanup_policy_link: show_cleanup_policy_link(project).to_s,
show_container_registry_settings: show_container_registry_settings(project).to_s,
cleanup_policies_settings_path: project_settings_packages_and_registries_path(project),
connection_error: (!!connection_error).to_s,
invalid_path_error: (!!invalid_path_error).to_s,
user_callouts_path: callouts_path,
user_callout_id: Users::CalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
is_metadata_database_enabled: ContainerRegistry::GitlabApiClient.supports_gitlab_api?.to_s,
show_unfinished_tag_cleanup_callout: show_unfinished_tag_cleanup_callout?.to_s
}
end
end
end

View File

@ -1,7 +0,0 @@
# frozen_string_literal: true
module ContainerRegistryHelper
def container_repository_gid_prefix
"gid://#{GlobalID.app}/#{ContainerRepository.name}/"
end
end

View File

@ -247,7 +247,11 @@ class ProjectPolicy < BasePolicy
end
condition(:push_repository_for_job_token_allowed) do
@user&.from_ci_job_token? && project.ci_push_repository_for_job_token_allowed? && @user.ci_job_token_scope.self_referential?(project)
if ::Feature.enabled?(:allow_push_repository_for_job_token, @subject)
@user&.from_ci_job_token? && project.ci_push_repository_for_job_token_allowed? && @user.ci_job_token_scope.self_referential?(project)
else
false
end
end
condition(:packages_disabled, scope: :subject) { !@subject.packages_enabled }
@ -622,6 +626,7 @@ class ProjectPolicy < BasePolicy
end
rule { can?(:admin_build) }.enable :manage_trigger
rule { can?(:admin_runner) }.enable :read_runner
rule { public_project & metrics_dashboard_allowed }.policy do
enable :metrics_dashboard

View File

@ -2,35 +2,56 @@
module Ci
module Runners
# Service used to assign a runner to a project.
# This class can be reused by SetRunnerAssociatedProjectsService in the context of a bulk assignment.
class AssignRunnerService
# @param [Ci::Runner] runner: the runner to assign to a project
# @param [Project] project: the new project to assign the runner to
# @param [User] user: the user performing the operation
def initialize(runner, project, user)
# @param [Boolean] quiet: true if service should avoid side-effects, such as logging
# (e.g. when used by another service)
def initialize(runner, project, user, quiet: false)
@runner = runner
@project = project
@user = user
@quiet = quiet
end
def execute
unless @user.present? && @user.can?(:assign_runner, @runner)
return ServiceResponse.error(message: 'user not allowed to assign runner', http_status: :forbidden)
end
unless @user.can?(:create_runner, @project)
return ServiceResponse.error(message: 'user not allowed to add runners to project', http_status: :forbidden)
end
response = validate
return response if response.error?
if @runner.assign_to(@project, @user)
ServiceResponse.success
else
ServiceResponse.error(message: 'failed to assign runner')
ServiceResponse.error(
message: @runner.errors.full_messages_for(:assign_to).presence || _('failed to assign runner to project'),
reason: :runner_error)
end
end
private
attr_reader :runner, :project, :user
attr_reader :runner, :project, :user, :quiet
def validate
unless @user.present? && @user.can?(:assign_runner, @runner)
return ServiceResponse.error(message: _('user not allowed to assign runner'),
reason: :not_authorized_to_assign_runner)
end
unless @user.can?(:create_runner, @project)
return ServiceResponse.error(message: _('user is not authorized to add runners to project'),
reason: :not_authorized_to_add_runner_in_project)
end
if project.organization_id != runner.owner_project.organization_id
return ServiceResponse.error(message: _('runner can only be assigned to projects in the same organization'),
reason: :project_not_in_same_organization)
end
ServiceResponse.success
end
end
end
end

View File

@ -14,7 +14,8 @@ module Ci
def execute
unless current_user&.can?(:assign_runner, runner)
return ServiceResponse.error(message: 'user not allowed to assign runner', http_status: :forbidden)
return ServiceResponse.error(message: _('user not allowed to assign runner'),
reason: :not_authorized_to_assign_runner)
end
return ServiceResponse.success if project_ids.nil?
@ -44,12 +45,17 @@ module Ci
def associate_new_projects(new_project_ids, current_project_ids)
missing_projects = Project.id_in(new_project_ids - current_project_ids)
unless missing_projects.all? { |project| current_user.can?(:create_runner, project) }
return ServiceResponse.error(message: 'user is not authorized to add runners to project')
end
error_responses = missing_projects.map do |project|
Ci::Runners::AssignRunnerService.new(runner, project, current_user, quiet: true)
end.map(&:execute).select(&:error?)
unless missing_projects.all? { |project| runner.assign_to(project, current_user) }
return ServiceResponse.error(message: 'failed to assign projects to runner')
if error_responses.any?
return error_responses.first if error_responses.count == 1
return ServiceResponse.error(
message: error_responses.map(&:message).uniq,
reason: :multiple_errors
)
end
ServiceResponse.success
@ -65,7 +71,7 @@ module Ci
.all?(&:destroyed?)
return ServiceResponse.success if all_destroyed
ServiceResponse.error(message: 'failed to destroy runner project')
ServiceResponse.error(message: _('failed to destroy runner project'), reason: :failed_runner_project_destroy)
end
attr_reader :runner, :current_user, :project_ids

View File

@ -69,6 +69,9 @@
},
"remove_project": {
"type": "boolean"
},
"read_runners": {
"type": "boolean"
}
}
}

View File

@ -1,6 +1,7 @@
- page_title _('Health Check')
- no_errors = @errors.blank?
#div{ data: { event_tracking_load: 'true', event_tracking: 'view_admin_health_check_pageload' } }
%h1.page-title.gl-font-size-h-display= page_title
= render Pajamas::AlertComponent.new(variant: :tip, dismissible: false, alert_options: { class: 'gl-mb-5' }) do |c|
- c.with_body do

View File

@ -1,7 +1,7 @@
- page_title _('System information')
= render ::Layouts::PageHeadingComponent.new(_('System information'))
.gl-grid.sm:gl-grid-cols-2.md:gl-grid-cols-3.gl-gap-5
.gl-grid.sm:gl-grid-cols-2.md:gl-grid-cols-3.gl-gap-5{ data: { event_tracking_load: 'true', event_tracking: 'view_admin_system_info_pageload' } }
= render Pajamas::CardComponent.new do |c|
- c.with_body do
- if @cpus

View File

@ -1,7 +1,5 @@
- page_title "#{@work_item.title} (#{@work_item.to_reference})", _("Epics")
- page_description @work_item.description_html
- page_card_attributes @work_item.card_attributes
- add_work_item_show_breadcrumb(@group, @work_item.iid)
- page_title "##{request.params['iid']}"
- add_work_item_show_breadcrumb(@group, request.params['iid'])
- add_page_specific_style 'page_bundles/work_items'
- add_page_specific_style 'page_bundles/design_management'
- @gfm_form = true

View File

@ -2,27 +2,4 @@
- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false, sort: nil})
%section
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
expiration_policy: @project.container_expiration_policy.to_json,
"help_page_path" => help_page_path('user/packages/container_registry/index'),
"two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'),
"personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'),
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"repository_url" => escape_once(@project.container_registry_url),
"registry_host_url_with_port" => escape_once(registry_config.host_port),
"expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/reduce_container_registry_storage', anchor: 'cleanup-policy'),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"project_path": @project.full_path,
"gid_prefix": container_repository_gid_prefix,
"is_admin": current_user&.admin.to_s,
"show_cleanup_policy_link": show_cleanup_policy_link(@project).to_s,
"show_container_registry_settings": show_container_registry_settings(@project).to_s,
"cleanup_policies_settings_path": project_settings_packages_and_registries_path(@project),
connection_error: (!!@connection_error).to_s,
invalid_path_error: (!!@invalid_path_error).to_s,
user_callouts_path: callouts_path,
user_callout_id: Users::CalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
is_metadata_database_enabled: ContainerRegistry::GitlabApiClient.supports_gitlab_api?.to_s,
show_unfinished_tag_cleanup_callout: show_unfinished_tag_cleanup_callout?.to_s, } }
#js-container-registry{ data: project_container_registry_template_data(@project, @connection_error, @invalid_path_error) }

View File

@ -0,0 +1,3 @@
- return unless can?(current_user, :read_tracing, @project) || can?(current_user, :read_observability_metrics, @project) || can?(current_user, :read_observability_logs, @project)
.js-observability-settings

View File

@ -2,6 +2,7 @@
- breadcrumb_title _('Monitor Settings')
- @force_desktop_expanded_sidebar = true
= render 'projects/settings/operations/observability'
= render 'projects/settings/operations/error_tracking'
= render 'projects/settings/operations/alert_management'
= render 'projects/settings/operations/incidents'

View File

@ -0,0 +1,16 @@
---
description: Tracks pageviews for the admin health check page
internal_events: true
action: view_admin_health_check_pageload
identifiers:
- user
product_group: personal_productivity
milestone: '17.2'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156829
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate

View File

@ -0,0 +1,16 @@
---
description: Tracks pageviews for the admin system information page
internal_events: true
action: view_admin_system_info_pageload
identifiers:
- user
product_group: personal_productivity
milestone: '17.2'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156825
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate

View File

@ -0,0 +1,8 @@
---
name: allow_push_repository_for_job_token
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154111
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/468320
milestone: "17.2"
type: development
group: group::pipeline security
default_enabled: false

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_view_admin_health_check_pageload_monthly
description: Monthly count of unique users who visited the admin health check page
product_group: personal_productivity
performance_indicator_type: []
value_type: number
status: active
milestone: '17.2'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156829
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: view_admin_health_check_pageload
unique: user.id

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_view_admin_system_info_pageload_monthly
description: Monthly count of unique users who visited the admin system info page
product_group: personal_productivity
performance_indicator_type: []
value_type: number
status: active
milestone: '17.2'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156825
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: view_admin_system_info_pageload
unique: user.id

View File

@ -0,0 +1,21 @@
---
key_path: counts.count_total_view_admin_health_check_pageload_monthly
description: Monthly count of total users who visited the admin health check page
product_group: personal_productivity
performance_indicator_type: []
value_type: number
status: active
milestone: '17.2'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156829
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: view_admin_health_check_pageload

View File

@ -0,0 +1,21 @@
---
key_path: counts.count_total_view_admin_system_info_pageload_monthly
description: Monthly count of total users who visited the admin system info page
product_group: personal_productivity
performance_indicator_type: []
value_type: number
status: active
milestone: '17.2'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156825
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: view_admin_system_info_pageload

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_view_admin_health_check_pageload_weekly
description: Weekly count of unique users who visited the admin health check page
product_group: personal_productivity
performance_indicator_type: []
value_type: number
status: active
milestone: '17.2'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156829
time_frame: 7d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: view_admin_health_check_pageload
unique: user.id

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_view_admin_system_info_pageload_weekly
description: Weekly count of unique users who visited the admin system info page
product_group: personal_productivity
performance_indicator_type: []
value_type: number
status: active
milestone: '17.2'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156825
time_frame: 7d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: view_admin_system_info_pageload
unique: user.id

View File

@ -0,0 +1,21 @@
---
key_path: counts.count_total_view_admin_health_check_pageload_weekly
description: Weekly count of total users who visited the admin health check page
product_group: personal_productivity
performance_indicator_type: []
value_type: number
status: active
milestone: '17.2'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156829
time_frame: 7d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: view_admin_health_check_pageload

View File

@ -0,0 +1,21 @@
---
key_path: counts.count_total_view_admin_system_info_pageload_weekly
description: Weekly count of total users who visited the admin system info page
product_group: personal_productivity
performance_indicator_type: []
value_type: number
status: active
milestone: '17.2'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156825
time_frame: 7d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: view_admin_system_info_pageload

View File

@ -12,10 +12,6 @@ MARKDOWN
POST_TABLE_MESSAGE = <<MARKDOWN
**Please check reviewer's status!**
- ![available](https://gitlab-org.gitlab.io/gitlab-roulette/status-png/_success.png){width=12} Reviewer is available!
- ![unavailable](https://gitlab-org.gitlab.io/gitlab-roulette/status-png/_alert.png){width=12} Reviewer is unavailable!
Please refer to [documentation page](https://docs.gitlab.com/ee/development/code_review.html#reviewer-roulette)
for guidance on how you can benefit from the Reviewer Roulette, or use the
[GitLab Review Workload Dashboard](https://gitlab-org.gitlab.io/gitlab-roulette/)

View File

@ -125,6 +125,7 @@ We do not yet support multi-arch image, only `linux/amd64`. If you try to run th
```shell
AI_GATEWAY_URL=https://<your_ai_gitlab_domain>
CLOUD_CONNECTOR_SELF_SIGN_TOKENS=1
```
1. After you've set up the environment variables, run the image. For example:

View File

@ -7411,6 +7411,7 @@ Input type: `ProjectCiCdSettingsUpdateInput`
| <a id="mutationprojectcicdsettingsupdatemergepipelinesenabled"></a>`mergePipelinesEnabled` | [`Boolean`](#boolean) | Indicates if merged results pipelines are enabled for the project. |
| <a id="mutationprojectcicdsettingsupdatemergetrainsenabled"></a>`mergeTrainsEnabled` | [`Boolean`](#boolean) | Indicates if merge trains are enabled for the project. |
| <a id="mutationprojectcicdsettingsupdatemergetrainsskiptrainallowed"></a>`mergeTrainsSkipTrainAllowed` | [`Boolean`](#boolean) | Indicates whether an option is allowed to merge without refreshing the merge train. Ignored unless the `merge_trains_skip_train` feature flag is also enabled. |
| <a id="mutationprojectcicdsettingsupdatepushrepositoryforjobtokenallowed"></a>`pushRepositoryForJobTokenAllowed` | [`Boolean`](#boolean) | Indicates the ability to push to the original project repository using a job token. |
#### Fields
@ -29348,6 +29349,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="projectcicdsettingmergetrainsenabled"></a>`mergeTrainsEnabled` | [`Boolean`](#boolean) | Whether merge trains are enabled. |
| <a id="projectcicdsettingmergetrainsskiptrainallowed"></a>`mergeTrainsSkipTrainAllowed` | [`Boolean!`](#boolean) | Whether merge immediately is allowed for merge trains. |
| <a id="projectcicdsettingproject"></a>`project` | [`Project`](#project) | Project the CI/CD settings belong to. |
| <a id="projectcicdsettingpushrepositoryforjobtokenallowed"></a>`pushRepositoryForJobTokenAllowed` | [`Boolean`](#boolean) | Indicates the ability to push to the original project repository using a job token. |
### `ProjectDataTransfer`
@ -34732,6 +34734,7 @@ Member role permission.
| <a id="memberrolepermissionread_code"></a>`READ_CODE` | Allows read-only access to the source code in the user interface. Does not allow users to edit or download repository archives, clone or pull repositories, view source code in an IDE, or view merge requests for private projects. You can download individual files because read-only access inherently grants the ability to make a local copy of the file. |
| <a id="memberrolepermissionread_crm_contact"></a>`READ_CRM_CONTACT` | Read CRM contact. |
| <a id="memberrolepermissionread_dependency"></a>`READ_DEPENDENCY` | Allows read-only access to the dependencies and licenses. |
| <a id="memberrolepermissionread_runners"></a>`READ_RUNNERS` | Allows read-only access to group or project runners, including the runner fleet dashboard. |
| <a id="memberrolepermissionread_vulnerability"></a>`READ_VULNERABILITY` | Read vulnerability reports and security dashboards. |
| <a id="memberrolepermissionremove_group"></a>`REMOVE_GROUP` | Ability to delete or restore a group. This ability does not allow deleting top level groups. Review the Retention period settings to prevent accidental deletion. |
| <a id="memberrolepermissionremove_project"></a>`REMOVE_PROJECT` | Allows deletion of projects. |

View File

@ -77,6 +77,7 @@ Example response:
"manage_project_access_tokens": false,
"manage_security_policy_link": false,
"read_code": true,
"read_runners": false,
"read_dependency": false,
"read_vulnerability": false,
"remove_group": false,
@ -115,6 +116,7 @@ Supported attributes:
| `manage_project_access_tokens` | boolean | no | Permission to manage project access tokens. |
| `manage_security_policy_link` | boolean | no | Permission to link security policy projects. |
| `read_code` | boolean | no | Permission to read project code. |
| `read_runners` | boolean | no | Permission to view project runners. |
| `read_dependency` | boolean | no | Permission to read project dependencies. |
| `read_vulnerability` | boolean | no | Permission to read project vulnerabilities. |
| `remove_group` | boolean | no | Permission to delete or restore a group. |
@ -152,6 +154,7 @@ Example response:
"manage_project_access_tokens": false,
"manage_security_policy_link": false,
"read_code": true,
"read_runners": false,
"read_dependency": false,
"read_vulnerability": false,
"remove_group": false,
@ -236,6 +239,7 @@ Example response:
"manage_project_access_tokens": false,
"manage_security_policy_link": false,
"read_code": true,
"read_runners": false,
"read_dependency": false,
"read_vulnerability": false,
"remove_group": false,
@ -262,6 +266,7 @@ Example response:
"manage_project_access_tokens": false,
"manage_security_policy_link": false,
"read_code": true,
"read_runners": false,
"read_dependency": true,
"read_vulnerability": true,
"remove_group": false,
@ -300,6 +305,7 @@ Parameters:
| `manage_project_access_tokens` | boolean | no | Permission to manage project access tokens. |
| `manage_security_policy_link` | boolean | no | Permission to link security policy projects. |
| `read_code` | boolean | no | Permission to read project code. |
| `read_runners` | boolean | no | Permission to view project runners. |
| `read_dependency` | boolean | no | Permission to read project dependencies. |
| `read_vulnerability` | boolean | no | Permission to read project vulnerabilities. |
| `remove_group` | boolean | no | Permission to delete or restore a group. |
@ -335,6 +341,7 @@ Example response:
"manage_project_access_tokens": false,
"manage_security_policy_link": false,
"read_code": true,
"read_runners": false,
"read_dependency": false,
"read_vulnerability": false,
"remove_group": false,

View File

@ -249,6 +249,8 @@ When the user is authenticated and `simple` is not set this returns something li
"ci_job_token_scope_enabled": false,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"ci_pipeline_variables_minimum_override_role": "maintainer",
"ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"build_timeout": 3600,
"auto_cancel_pending_pipelines": "enabled",
@ -425,6 +427,8 @@ GET /users/:user_id/projects
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"ci_pipeline_variables_minimum_override_role": "maintainer",
"ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
@ -546,6 +550,8 @@ GET /users/:user_id/projects
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"ci_pipeline_variables_minimum_override_role": "maintainer",
"ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
@ -1218,6 +1224,8 @@ GET /projects/:id
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"ci_pipeline_variables_minimum_override_role": "maintainer",
"ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"shared_with_groups": [
{
@ -1760,6 +1768,8 @@ General project attributes:
| `ci_allow_fork_pipelines_to_run_in_parent_project` | boolean | No | Enable or disable [running pipelines in the parent project for merge requests from forks](../ci/pipelines/merge_request_pipelines.md#run-pipelines-in-the-parent-project). _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325189) in GitLab 15.3.)_ |
| `ci_separated_caches` | boolean | No | Set whether or not caches should be [separated](../ci/caching/index.md#cache-key-names) by branch protection status. |
| `ci_restrict_pipeline_cancellation_role` | string | No | Set the [role required to cancel a pipeline or job](../ci/pipelines/settings.md#restrict-roles-that-can-cancel-pipelines-or-jobs). One of `developer`, `maintainer`, or `no_one`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/429921) in GitLab 16.8. Premium and Ultimate only. |
| `ci_pipeline_variables_minimum_override_role` | string | No | When `restrict_user_defined_variables` is enabled, you can specify which role can override variables. One of `owner`, `maintainer`, `developer` or `no_one_allowed`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/440338) in GitLab 17.1. |
| `ci_push_repository_for_job_token_allowed` | boolean | No | Enable or disable the ability to push to the project repository using job token. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389060) in GitLab 17.2. |
| `container_expiration_policy_attributes` | hash | No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (integer), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
| `container_registry_enabled` | boolean | No | _(Deprecated)_ Enable container registry for this project. Use `container_registry_access_level` instead. |
| `default_branch` | string | No | The [default branch](../user/project/repository/branches/default.md) name. |
@ -2379,6 +2389,8 @@ Example response:
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"ci_pipeline_variables_minimum_override_role": "maintainer",
"ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
@ -2511,6 +2523,8 @@ Example response:
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
"ci_pipeline_variables_minimum_override_role": "maintainer",
"ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,

View File

@ -276,3 +276,21 @@ While troubleshooting CI/CD job token authentication issues, be aware that:
- To remove project access.
- The CI job token becomes invalid if the job is no longer running, has been erased,
or if the project is in the process of being deleted.
### Push to a project repository using a job token
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389060) in GitLab 17.2. [with a flag](../../administration/feature_flags.md) named `allow_push_repository_for_job_token`. Disabled by default.
WARNING:
Pushing via job token is still in development and is not yet optimized for performance.
If you enable this feature for testing, you must thoroughly test and implement validation measures
to prevent infinite loops of "push" pipelines triggering more pipelines.
By default, pushing to a project repository by authenticating with a job token is disabled.
To enable this ability, you can:
- Feature flag named `allow_push_repository_for_job_token` should be enabled.
- Enable the [`pushRepositoryForJobTokenAllowed`](../../api/graphql/reference/index.md#mutationprojectcicdsettingsupdate) GraphQL endpoint.
- Enable the [`ci_push_repository_for_job_token_allowed`](../../api/projects.md#edit-project) REST API endpoint.
You are only permitted to push to the repository of the project where the job is running.

View File

@ -151,6 +151,7 @@ If you are currently tracking a metric in `RedisHLL` like this:
To start using Internal Events Tracking, follow these steps:
1. If event is not being sent to Snowplow, consider renaming if to meet [our naming convention](quick_start.md#defining-event-and-metrics).
1. Create an event definition that describes `git_write_action` ([guide](event_definition_guide.md)).
1. Find metric definitions that list `git_write_action` in the events section (`20210216182041_action_monthly_active_users_git_write.yml` and `20210216184045_git_write_action_weekly.yml`).
1. Change the `data_source` from `redis_hll` to `internal_events` in the metric definition files.

View File

@ -452,6 +452,55 @@ PersonalAccessToken.project_access_token.where(expires_at: Date.today .. Date.to
end
```
#### tokens_with_no_expiry.rb
This script finds tokens that do not have an expiry date, that is, `expires_at` is set to `NULL`. For users who have not yet upgraded to GitLab version 16.0 or later, the token `expires_at` value will be `NULL` and can be used to identify tokens that will be set with an expiration date.
```ruby
# This script finds tokens which do not have an expires_at value set.
# Check for expiring personal access tokens
PersonalAccessToken.owner_is_human.where(expires_at: nil).find_each do |token|
puts "Expires_at is nil for Personal Access Token ID: #{token.id}, User Email: #{token.user.email}, Name: #{token.name}, Scopes: #{token.scopes}, Last used: #{token.last_used_at}"
end
# Check for expiring project and group access tokens
PersonalAccessToken.project_access_token.where(expires_at: nil).find_each do |token|
token.user.members.each do |member|
type = member.is_a?(GroupMember) ? 'Group' : 'Project'
puts "Expires_at is nil for #{type} access token in #{type} ID #{member.source_id}, Token ID: #{token.id}, Name: #{token.name}, Scopes: #{token.scopes}, Last used: #{token.last_used_at}"
end
end
```
You can use this script in either the [Rails console](../administration/operations/rails_console.md) or the [Rails Runner](../administration/operations/rails_console.md#using-the-rails-runner):
::Tabs
:::TabTitle Rails console session
1. In your terminal window, connect to your instance.
1. Start a Rails console session with `sudo gitlab-rails console`.
1. Paste in the entire script.
1. Press <kbd>Enter</kbd>.
:::TabTitle Rails Runner
1. In your terminal window, connect to your instance.
1. Copy this entire script, and save it as a file on your instance:
- Name it `tokens_with_no_expiry.rb`.
- The file must be accessible to `git:git`.
1. Run this command, changing the path to the _full_ path to your `tokens_with_no_expiry.rb` file:
```shell
sudo gitlab-rails runner /path/to/tokens_with_no_expiry.rb
```
For more information, see the [Rails Runner troubleshooting section](../administration/operations/rails_console.md#troubleshooting).
::EndTabs
### Extend token lifetime
Delay the expiration of certain tokens with this script.

View File

@ -67,6 +67,7 @@ These requirements are documented in the `Required permission` column in the fol
| Name | Required permission | Description | Introduced in | Feature flag | Enabled in |
|:-----|:------------|:------------------|:---------|:--------------|:---------|
| [`admin_runners`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151825) | | Create, view, edit, and delete group or project Runners. Includes configuring Runner settings. | GitLab [17.1](https://gitlab.com/gitlab-org/gitlab/-/issues/442851) | `custom_ability_admin_runners` | |
| [`read_runners`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156798) | | Allows read-only access to group or project runners, including the runner fleet dashboard. | GitLab [17.2](https://gitlab.com/gitlab-org/gitlab/-/issues/468202) | | |
## Secrets management

View File

@ -320,10 +320,11 @@ module API
runner = get_runner(params[:runner_id])
authenticate_enable_runner!(runner)
if ::Ci::Runners::AssignRunnerService.new(runner, user_project, current_user).execute.success?
result = ::Ci::Runners::AssignRunnerService.new(runner, user_project, current_user).execute
if result.success?
present runner, with: Entities::Ci::Runner
else
render_validation_error!(runner)
render_api_error_with_reason!(:bad_request, result.message, result.reason)
end
end

View File

@ -131,6 +131,7 @@ module API
expose :auto_devops_deploy_strategy, documentation: { type: 'string', example: 'continuous' } do |project, options|
project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy
end
expose :ci_push_repository_for_job_token_allowed, documentation: { type: 'boolean' }
end
expose :ci_config_path, documentation: { type: 'string', example: '' }, if: ->(project, options) { Ability.allowed?(options[:current_user], :read_code, project) }

View File

@ -117,6 +117,7 @@ module API
optional :ci_separated_caches, type: Boolean, desc: 'Enable or disable separated caches based on branch protection.'
optional :restrict_user_defined_variables, type: Boolean, desc: 'Restrict use of user-defined variables when triggering a pipeline'
optional :ci_pipeline_variables_minimum_override_role, values: %w[no_one_allowed developer maintainer owner], type: String, desc: 'Limit ability to override CI/CD variables when triggering a pipeline to only users with at least the set minimum role'
optional :ci_push_repository_for_job_token_allowed, type: Boolean, desc: "Allow pushing to this project's repository by authenticating with a CI/CD job token generated in this project."
end
params :optional_update_params_ee do
@ -210,6 +211,7 @@ module API
:model_registry_access_level,
:warn_about_potentially_unwanted_characters,
:ci_pipeline_variables_minimum_override_role,
:ci_push_repository_for_job_token_allowed,
# TODO: remove in API v5, replaced by *_access_level
:issues_enabled,

233
lib/gitlab/fp/result.rb Normal file
View File

@ -0,0 +1,233 @@
# frozen_string_literal: true
# A (partial) implementation of the functional Result type, with naming conventions based on the
# Rust implementation (https://doc.rust-lang.org/std/result/index.html)
#
# Modern Ruby 3+ destructuring and pattern matching are supported.
#
# - See "Railway Oriented Programming and the Result Class" in `ee/lib/remote_development/README.md` for details
# and example usage.
# - See `spec/lib/gitlab/fp/result_spec.rb` for detailed executable example usage.
# - See https://en.wikipedia.org/wiki/Result_type for a general description of the Result pattern.
# - See https://fsharpforfunandprofit.com/rop/ for how this can be used with Railway Oriented Programming (ROP)
# to improve design and architecture
# - See https://doc.rust-lang.org/std/result/ for the Rust implementation.
module Gitlab
# noinspection RubyClassModuleNamingConvention -- JetBrains is changing this to allow shorter names
module Fp
class Result
# The .ok and .err factory class methods are the only way to create a Result
#
# "self.ok" corresponds to Ok(T) in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#variant.Ok
#
# @param [Object, #new] ok_value
# @return [Result]
# noinspection MissingYardParamTag -- RubyMine does not recognize "duck type" Types
# (https://rubydoc.info/gems/yard/file/docs/Tags.md#duck-types). This has been
# reported to JetBrains - issue link pending
def self.ok(ok_value)
new(ok_value: ok_value)
end
# "self.err" corresponds to Err(E) in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#variant.Err
#
# @param [Object, #new] err_value
# @return [Result]
# noinspection MissingYardParamTag -- RubyMine does not recognize "duck type" Types
# (https://rubydoc.info/gems/yard/file/docs/Tags.md#duck-types). This has been
# reported to JetBrains - issue link pending
def self.err(err_value)
new(err_value: err_value)
end
# @param [Object, nil] ok_value
# @param [Object, nil] err_value
# @return [Object]
def initialize(ok_value: nil, err_value: nil)
if (!ok_value.nil? && !err_value.nil?) || (ok_value.nil? && err_value.nil?)
raise(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err')
end
@ok = err_value.nil?
@value = ok? ? ok_value : err_value
end
private :initialize
# "#unwrap" corresponds to "unwrap" in Rust.
#
# @return [Object]
# @raise [RuntimeError] if called on an "err" Result
def unwrap
ok? ? value : raise("Called Result#unwrap on an 'err' Result")
end
# "#unwrap" corresponds to "unwrap" in Rust.
#
# @return [Object]
# @raise [RuntimeError] if called on an "ok" Result
def unwrap_err
err? ? value : raise("Called Result#unwrap_err on an 'ok' Result")
end
# The `ok?` attribute is true if the Result was constructed with .ok, and false if it was constructed with .err
#
# "#ok?" corresponds to "is_ok" in Rust.
# @return [Boolean]
def ok?
# We don't make `@ok` an attr_reader, because we don't want to confusingly shadow the class method `.ok`
@ok
end
# The `err?` attribute is false if the Result was constructed with .ok, and true if it was constructed with .err
# "#err?" corresponds to "is_err" in Rust.
#
# @return [Boolean]
def err?
!ok?
end
# `and_then` is a functional way to chain together operations which may succeed or have errors. It is passed
# a lambda or class (singleton) method object, and must return a Result object representing "ok"
# or "err".
#
# If the Result object it is called on is "ok", then the passed lambda or singleton method
# is called with the value contained in the Result.
#
# If the Result object it is called on is "err", then it is returned without calling the passed
# lambda or method.
#
# It only supports being passed a lambda, or a class (singleton) method object
# which responds to `call` with a single argument (arity of 1). If multiple values are needed,
# pass a hash or array. Note that passing `Proc` objects is NOT supported, even though the YARD
# annotation contains `Proc` (because the type of a lambda is also `Proc`).
#
# Passing instance methods to `and_then` is not supported, because the methods in the chain should be
# stateless "pure functions", and should not be persisting or referencing any instance state anyway.
#
# "#and_then" corresponds to "and_then" in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#method.and_then
#
# @param [Proc, Method] lambda_or_singleton_method
# @return [Result]
# @raise [TypeError]
def and_then(lambda_or_singleton_method)
validate_lambda_or_singleton_method(lambda_or_singleton_method)
# Return/passthough the Result itself if it is an err
return self if err?
# If the Result is ok, call the lambda or singleton method with the contained value
result = lambda_or_singleton_method.call(value)
unless result.is_a?(Result)
err_msg = "'Result##{__method__}' expects a lambda or singleton method object which returns a 'Result' " \
"type, but instead received '#{lambda_or_singleton_method.inspect}' which returned '#{result.class}'. " \
"Check that the previous method calls in the '#and_then' chain are correct."
raise(TypeError, err_msg)
end
result
end
# `map` is similar to `and_then`, but it is used for "single track" methods which always succeed,
# and have no possibility of returning an error (but they may still raise exceptions,
# which is unrelated to the Result handling). The passed lambda or singleton method must return
# a value, not a Result.
#
# If the Result object it is called on is "ok", then the passed lambda or singleton method
# is called with the value contained in the Result.
#
# If the Result object it is called on is "err", then it is returned without calling the passed
# lambda or method.
#
# "#map" corresponds to "map" in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#method.map
#
# @param [Proc, Method] lambda_or_singleton_method
# @return [Result]
# @raise [TypeError]
def map(lambda_or_singleton_method)
validate_lambda_or_singleton_method(lambda_or_singleton_method)
# Return/passthrough the Result itself if it is an err
return self if err?
# If the Result is ok, call the lambda or singleton method with the contained value
mapped_value = lambda_or_singleton_method.call(value)
if mapped_value.is_a?(Result)
err_msg = "'Result##{__method__}' expects a lambda or singleton method object which returns an unwrapped " \
"value, not a 'Result', but instead received '#{lambda_or_singleton_method.inspect}' which returned " \
"a 'Result'."
raise(TypeError, err_msg)
end
# wrap the returned mapped_value in an "ok" Result.
Result.ok(mapped_value)
end
# `to_h` supports destructuring of a result object, for example: `result => { ok: }; puts ok`
#
# @return [Hash]
def to_h
ok? ? { ok: value } : { err: value }
end
# `deconstruct_keys` supports pattern matching on a Result object with a `case` statement. See specs for examples.
#
# @param [Array] keys
# @return [Hash]
# @raise [ArgumentError]
def deconstruct_keys(keys)
raise(ArgumentError, 'Use either :ok or :err for pattern matching') unless [[:ok], [:err]].include?(keys)
to_h
end
# @param [Result] other
# @return [Boolean]
def ==(other)
# NOTE: The underlying `@ok` instance variable is a boolean, so we only need to check `ok?`, not `err?` too
self.class == other.class && other.ok? == ok? && other.instance_variable_get(:@value) == value
end
private
# The `value` attribute will contain either the ok_value or the err_value
#
# @return [Object]
# noinspection RubyMismatchedReturnType
def value # rubocop:disable Style/TrivialAccessors -- We are not using attr_reader here, so we can avoid nilability type errors in RubyMine
# TODO: We are not using attr_reader here, so we can avoid nilability type errors in RubyMine.
# This will be reported to JetBrains and tracked on
# https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/,
# this comment should then be updated with the issue link on that page.
# Note that we don't use `noinspection` to suppress this error because there's several instances where
# this error leaks to other classes through the call stack.
@value
end
# @param [Proc, Method] lambda_or_singleton_method
# @return [void]
# @raise [TypeError]
def validate_lambda_or_singleton_method(lambda_or_singleton_method)
is_lambda = lambda_or_singleton_method.is_a?(Proc) && lambda_or_singleton_method.lambda?
is_singleton_method =
lambda_or_singleton_method.is_a?(Method) && lambda_or_singleton_method.owner.singleton_class?
unless is_lambda || is_singleton_method
err_msg = "'Result##{__method__}' expects a lambda or singleton method object, " \
"but instead received '#{lambda_or_singleton_method.inspect}'."
raise(TypeError, err_msg)
end
arity = lambda_or_singleton_method.arity
return if arity == 1
return if arity == -1 && lambda_or_singleton_method.source_location[0].include?('rspec')
err_msg = "'Result##{__method__}' expects a lambda or singleton method object with a single argument " \
"(arity of 1), but instead received '#{lambda_or_singleton_method.inspect}' with an arity of #{arity}."
raise(ArgumentError, err_msg)
end
end
end
end

View File

@ -19,7 +19,11 @@ module Gitlab
fp_class_singleton_methods = fp_module_or_class.singleton_methods(false)
public_singleton_methods = fp_class_singleton_methods - public_singleton_methods_to_ignore
return public_singleton_methods.first if public_singleton_methods.size == 1
# Note: Intentionally using Array#[] instead of Array#first here, because there appears to be a bug
# in the type declaration, that doesn't indicate that #first should have `implicitly-returns-nil`
# behavior. See https://github.com/ruby/rbs/pull/1226, this probably needs to be fixed for #first too.
# Until then, we use #[] to avoid type inspection warnings in RubyMine.
return public_singleton_methods[0] if public_singleton_methods.size == 1
fp_doc_link =
"https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/remote_development/README.md#functional-patterns"

View File

@ -304,7 +304,8 @@ module Gitlab
_('One or more contacts were successfully added.')
end
command :add_contacts do |contact_emails|
@updates[:add_contacts] = contact_emails.split(' ')
@updates[:add_contacts] ||= []
@updates[:add_contacts] += contact_emails.split(' ')
end
desc { _('Remove customer relation contacts') }
@ -319,7 +320,8 @@ module Gitlab
_('One or more contacts were successfully removed.')
end
command :remove_contacts do |contact_emails|
@updates[:remove_contacts] = contact_emails.split(' ')
@updates[:remove_contacts] ||= []
@updates[:remove_contacts] += contact_emails.split(' ')
end
desc { _('Add a timeline event to incident') }

View File

@ -6,7 +6,7 @@ module RemoteDevelopment
include Messages
# @param [Hash] context
# @return [Result]
# @return [Gitlab::Fp::Result]
def self.read(context)
err_result = nil
context[:settings].each_key do |setting_name|
@ -19,8 +19,8 @@ module RemoteDevelopment
setting_type = context[:setting_types][setting_name]
unless current_setting_value.is_a?(setting_type)
# err_result will be set to a non-nil Result.err if type check fails
err_result = Result.err(SettingsCurrentSettingsReadFailed.new(
# err_result will be set to a non-nil Gitlab::Fp::Result.err if type check fails
err_result = Gitlab::Fp::Result.err(SettingsCurrentSettingsReadFailed.new(
details: "Gitlab::CurrentSettings.#{setting_name} type of '#{current_setting_value.class}' " \
"did not match initialized Remote Development Settings type of '#{setting_type}'."
))
@ -32,7 +32,7 @@ module RemoteDevelopment
return err_result if err_result
Result.ok(context)
Gitlab::Fp::Result.ok(context)
end
end
end

View File

@ -10,7 +10,7 @@ module RemoteDevelopment
REQUIRED_ENV_VAR_PREFIX = "GITLAB_REMOTE_DEVELOPMENT"
# @param [Hash] context
# @return [Result]
# @return [Gitlab::Fp::Result]
def self.read(context)
err_result = nil
context[:settings].each_key do |setting_name|
@ -27,8 +27,8 @@ module RemoteDevelopment
setting_type: context[:setting_types][setting_name]
)
rescue RuntimeError => e
# err_result will be set to a non-nil Result.err if casting fails
err_result = Result.err(SettingsEnvironmentVariableReadFailed.new(details: e.message))
# err_result will be set to a non-nil Gitlab::Fp::Result.err if casting fails
err_result = Gitlab::Fp::Result.err(SettingsEnvironmentVariableReadFailed.new(details: e.message))
end
# ENV var matches an existing setting and is of the correct type, use its value to override the default value
@ -37,7 +37,7 @@ module RemoteDevelopment
return err_result if err_result
Result.ok(context)
Gitlab::Fp::Result.ok(context)
end
# @param [String] env_var_name

View File

@ -6,7 +6,7 @@ module RemoteDevelopment
include Messages
# @param [Hash] context
# @return [Result]
# @return [Gitlab::Fp::Result]
def self.validate(context)
context => { settings: Hash => settings }
settings => { vscode_extensions_gallery_metadata: Hash => extensions_gallery_metadata }
@ -15,9 +15,11 @@ module RemoteDevelopment
errors = validate_against_schema(validatable_hash)
if errors.none?
Result.ok(context)
Gitlab::Fp::Result.ok(context)
else
Result.err(SettingsVscodeExtensionsGalleryMetadataValidationFailed.new(details: errors.join(". ")))
Gitlab::Fp::Result.err(
SettingsVscodeExtensionsGalleryMetadataValidationFailed.new(details: errors.join(". "))
)
end
end

View File

@ -6,7 +6,7 @@ module RemoteDevelopment
include Messages
# @param [Hash] context
# @return [Result]
# @return [Gitlab::Fp::Result]
def self.validate(context)
context => { settings: Hash => settings }
settings => { vscode_extensions_gallery: Hash => vscode_extensions_gallery }
@ -17,9 +17,9 @@ module RemoteDevelopment
errors = validate_against_schema(vscode_extensions_gallery.deep_stringify_keys)
if errors.none?
Result.ok(context)
Gitlab::Fp::Result.ok(context)
else
Result.err(SettingsVscodeExtensionsGalleryValidationFailed.new(details: errors.join(". ")))
Gitlab::Fp::Result.err(SettingsVscodeExtensionsGalleryValidationFailed.new(details: errors.join(". ")))
end
end

View File

@ -10,7 +10,7 @@ module RemoteDevelopment
# @return [Hash]
# @raise [UnmatchedResultError]
def self.get_settings(context)
initial_result = Result.ok(context)
initial_result = Gitlab::Fp::Result.ok(context)
# The order of the chain determines the precedence of settings. I.e., defaults are
# overridden by env vars, and any subsequent steps override env vars.

View File

@ -6,7 +6,7 @@ module RemoteDevelopment
include Messages
# @param [Hash] context
# @return [Result]
# @return [Gitlab::Fp::Result]
def self.validate(context)
context => {
settings: {
@ -16,24 +16,24 @@ module RemoteDevelopment
}
unless partial_reconciliation_interval_seconds > 0
return Result.err(SettingsPartialReconciliationIntervalSecondsValidationFailed.new(
details: "Partial reconciliation interval must be greater than zero")
)
return Gitlab::Fp::Result.err(SettingsPartialReconciliationIntervalSecondsValidationFailed.new(
details: "Partial reconciliation interval must be greater than zero"
))
end
unless full_reconciliation_interval_seconds > 0
return Result.err(SettingsFullReconciliationIntervalSecondsValidationFailed.new(
details: "Full reconciliation interval must be greater than zero")
)
details = "Full reconciliation interval must be greater than zero"
return Gitlab::Fp::Result.err(SettingsFullReconciliationIntervalSecondsValidationFailed.new(details: details))
end
if full_reconciliation_interval_seconds <= partial_reconciliation_interval_seconds
return Result.err(SettingsPartialReconciliationIntervalSecondsValidationFailed.new(
details: "Partial reconciliation interval must be less than full reconciliation interval")
)
details = "Partial reconciliation interval must be less than full reconciliation interval"
return Gitlab::Fp::Result.err(
SettingsPartialReconciliationIntervalSecondsValidationFailed.new(details: details)
)
end
Result.ok(context)
Gitlab::Fp::Result.ok(context)
end
end
end

View File

@ -2,7 +2,7 @@
module RemoteDevelopment
class UnmatchedResultError < RuntimeError
# @param [Result] result
# @param [Gitlab::Fp::Result] result
# @return [void]
def initialize(result:)
msg = "Failed to pattern match #{result.ok? ? "'ok'" : "'err'"} Result " \

View File

@ -1,219 +0,0 @@
# frozen_string_literal: true
# A (partial) implementation of the functional Result type, with naming conventions based on the
# Rust implementation (https://doc.rust-lang.org/std/result/index.html)
#
# Modern Ruby 3+ destructuring and pattern matching are supported.
#
# - See "Railway Oriented Programming and the Result Class" in `ee/lib/remote_development/README.md` for details
# and example usage.
# - See `spec/lib/result_spec.rb` for detailed executable example usage.
# - See https://en.wikipedia.org/wiki/Result_type for a general description of the Result pattern.
# - See https://fsharpforfunandprofit.com/rop/ for how this can be used with Railway Oriented Programming (ROP)
# to improve design and architecture
# - See https://doc.rust-lang.org/std/result/ for the Rust implementation.
# NOTE: This class is intentionally not namespaced to allow for more concise, readable, and explicit usage.
# It it a generic reusable implementation of the Result type, and is not specific to any domain
# rubocop:disable Gitlab/NamespacedClass -- TODO: We should move this to the GitLab namespace, as it is part of the platform layer for bounded contexts - https://gitlab.com/gitlab-org/gitlab/-/issues/467293
# rubocop:disable Gitlab/BoundedContexts -- TODO: We should move this to the GitLab namespace, as it is part of the platform layer for bounded contexts - https://gitlab.com/gitlab-org/gitlab/-/issues/467293
class Result
# The .ok and .err factory class methods are the only way to create a Result
#
# "self.ok" corresponds to Ok(T) in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#variant.Ok
#
# @param [Object, #new] ok_value
# @return [Result]
def self.ok(ok_value)
new(ok_value: ok_value)
end
# "self.err" corresponds to Err(E) in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#variant.Err
#
# @param [Object, #new] ok_value
# @return [Result]
def self.err(err_value)
new(err_value: err_value)
end
# "#unwrap" corresponds to "unwrap" in Rust.
#
# @return [Object]
# @raise [RuntimeError] if called on an "err" Result
def unwrap
ok? ? value : raise("Called Result#unwrap on an 'err' Result")
end
# "#unwrap" corresponds to "unwrap" in Rust.
#
# @return [Object]
# @raise [RuntimeError] if called on an "ok" Result
def unwrap_err
err? ? value : raise("Called Result#unwrap_err on an 'ok' Result")
end
# The `ok?` attribute will be true if the Result was constructed with .ok, and false if it was constructed with .err
#
# "#ok?" corresponds to "is_ok" in Rust.
# @return [Boolean]
def ok?
# We don't make `@ok` an attr_reader, because we don't want to confusingly shadow the class method `.ok`
@ok
end
# The `err?` attribute will be false if the Result was constructed with .ok, and true if it was constructed with .err
# "#err?" corresponds to "is_err" in Rust.
#
# @return [Boolean]
def err?
!ok?
end
# `and_then` is a functional way to chain together operations which may succeed or have errors. It is passed
# a lambda or class (singleton) method object, and must return a Result object representing "ok"
# or "err".
#
# If the Result object it is called on is "ok", then the passed lambda or singleton method
# is called with the value contained in the Result.
#
# If the Result object it is called on is "err", then it is returned without calling the passed
# lambda or method.
#
# It only supports being passed a lambda, or a class (singleton) method object
# which responds to `call` with a single argument (arity of 1). If multiple values are needed,
# pass a hash or array. Note that passing `Proc` objects is NOT supported, even though the YARD
# annotation contains `Proc` (because the type of a lambda is also `Proc`).
#
# Passing instance methods to `and_then` is not supported, because the methods in the chain should be
# stateless "pure functions", and should not be persisting or referencing any instance state anyway.
#
# "#and_then" corresponds to "and_then" in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#method.and_then
#
# @param [Proc, Method] lambda_or_singleton_method
# @return [Result]
# @raise [TypeError]
def and_then(lambda_or_singleton_method)
validate_lambda_or_singleton_method(lambda_or_singleton_method)
# Return/passthough the Result itself if it is an err
return self if err?
# If the Result is ok, call the lambda or singleton method with the contained value
result = lambda_or_singleton_method.call(value)
unless result.is_a?(Result)
err_msg = "'Result##{__method__}' expects a lambda or singleton method object which returns a 'Result' type " \
", but instead received '#{lambda_or_singleton_method.inspect}' which returned '#{result.class}'. " \
"Check that the previous method calls in the '#and_then' chain are correct."
raise(TypeError, err_msg)
end
result
end
# `map` is similar to `and_then`, but it is used for "single track" methods which always succeed,
# and have no possibility of returning an error (but they may still raise exceptions,
# which is unrelated to the Result handling). The passed lambda or singleton method must return
# a value, not a Result.
#
# If the Result object it is called on is "ok", then the passed lambda or singleton method
# is called with the value contained in the Result.
#
# If the Result object it is called on is "err", then it is returned without calling the passed
# lambda or method.
#
# "#map" corresponds to "map" in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#method.map
#
# @param [Proc, Method] lambda_or_singleton_method
# @return [Result]
# @raise [TypeError]
def map(lambda_or_singleton_method)
validate_lambda_or_singleton_method(lambda_or_singleton_method)
# Return/passthrough the Result itself if it is an err
return self if err?
# If the Result is ok, call the lambda or singleton method with the contained value
mapped_value = lambda_or_singleton_method.call(value)
if mapped_value.is_a?(Result)
err_msg = "'Result##{__method__}' expects a lambda or singleton method object which returns an unwrapped " \
"value, not a 'Result', but instead received '#{lambda_or_singleton_method.inspect}' which returned " \
"a 'Result'."
raise(TypeError, err_msg)
end
# wrap the returned mapped_value in an "ok" Result.
Result.ok(mapped_value)
end
# `to_h` supports destructuring of a result object, for example: `result => { ok: }; puts ok`
#
# @return [Hash]
def to_h
ok? ? { ok: value } : { err: value }
end
# `deconstruct_keys` supports pattern matching on a Result object with a `case` statement. See specs for examples.
#
# @param [Array] keys
# @return [Hash]
# @raise [ArgumentError]
def deconstruct_keys(keys)
raise(ArgumentError, 'Use either :ok or :err for pattern matching') unless [[:ok], [:err]].include?(keys)
to_h
end
# @return [Boolean]
def ==(other)
# NOTE: The underlying `@ok` instance variable is a boolean, so we only need to check `ok?`, not `err?` too
self.class == other.class && other.ok? == ok? && other.instance_variable_get(:@value) == value
end
private
# The `value` attribute will contain either the ok_value or the err_value
def value # rubocop:disable Style/TrivialAccessors -- We are not using attr_reader here, so we can avoid nilability type errors in RubyMine
# TODO: We are not using attr_reader here, so we can avoid nilability type errors in RubyMine. This will be reported
# to JetBrains and tracked on
# https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/,
# this comment should then be updated with the issue link on that page.
# Note that we don't use `noinspection` to suppress this error because there's several instances where this error
# leaks to other classes through the call stack.
@value
end
def initialize(ok_value: nil, err_value: nil) # rubocop:disable Layout/ClassStructure -- We want this constructor to be private
if (!ok_value.nil? && !err_value.nil?) || (ok_value.nil? && err_value.nil?)
raise(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err')
end
@ok = err_value.nil?
@value = ok? ? ok_value : err_value
end
# @param [Proc, Method] lambda_or_singleton_method
# @return [void]
# @raise [TypeError]
def validate_lambda_or_singleton_method(lambda_or_singleton_method)
is_lambda = lambda_or_singleton_method.is_a?(Proc) && lambda_or_singleton_method.lambda?
is_singleton_method = lambda_or_singleton_method.is_a?(Method) && lambda_or_singleton_method.owner.singleton_class?
unless is_lambda || is_singleton_method
err_msg = "'Result##{__method__}' expects a lambda or singleton method object, " \
"but instead received '#{lambda_or_singleton_method.inspect}'."
raise(TypeError, err_msg)
end
arity = lambda_or_singleton_method.arity
return if arity == 1
return if arity == -1 && lambda_or_singleton_method.source_location[0].include?('rspec')
err_msg = "'Result##{__method__}' expects a lambda or singleton method object with a single argument " \
"(arity of 1), but instead received '#{lambda_or_singleton_method.inspect}' with an arity of #{arity}."
raise(ArgumentError, err_msg)
end
end
# rubocop:enable Gitlab/NamespacedClass
# rubocop:enable Gitlab/BoundedContexts

View File

@ -14277,12 +14277,21 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
msgid "ContainerRegistry|Container Scanning for Registry: Off"
msgstr ""
msgid "ContainerRegistry|Container Scanning for Registry: On"
msgstr ""
msgid "ContainerRegistry|Container protection rule deleted."
msgstr ""
msgid "ContainerRegistry|Container protection rule updated."
msgstr ""
msgid "ContainerRegistry|Continuous container scanning runs in the registry when any image or database is updated."
msgstr ""
msgid "ContainerRegistry|Copy build command"
msgstr ""
@ -36018,6 +36027,9 @@ msgstr ""
msgid "Observability|Enable"
msgstr ""
msgid "Observability|Enable tracing, metrics, or logs on your project."
msgstr ""
msgid "Observability|Error: Failed to enable GitLab Observability. Please retry later."
msgstr ""
@ -36066,6 +36078,15 @@ msgstr ""
msgid "Observability|To widen your search, change or remove filters above"
msgstr ""
msgid "Observability|Tracing, Metrics & Logs"
msgstr ""
msgid "Observability|View our %{docsLink} for further instructions on how to use these features."
msgstr ""
msgid "Observability|documentation"
msgstr ""
msgid "Oct"
msgstr ""
@ -59539,6 +59560,9 @@ msgstr ""
msgid "What is a compute quota?"
msgstr ""
msgid "What is continuous container scanning?"
msgstr ""
msgid "What is listed here?"
msgstr ""
@ -62748,6 +62772,12 @@ msgstr ""
msgid "failed"
msgstr ""
msgid "failed to assign runner to project"
msgstr ""
msgid "failed to destroy runner project"
msgstr ""
msgid "failed to dismiss associated finding(id=%{finding_id}): %{message}"
msgstr ""
@ -63854,6 +63884,9 @@ msgid_plural "rules"
msgstr[0] ""
msgstr[1] ""
msgid "runner can only be assigned to projects in the same organization"
msgstr ""
msgid "running"
msgstr ""
@ -64095,9 +64128,15 @@ msgstr[1] ""
msgid "user avatar"
msgstr ""
msgid "user is not authorized to add runners to project"
msgstr ""
msgid "user namespace cannot be the parent of another namespace"
msgstr ""
msgid "user not allowed to assign runner"
msgstr ""
msgid "username"
msgstr ""

View File

@ -132,7 +132,7 @@ QA_GITLAB_URL="http://{GDK IP ADDRESS}:{GDK PORT}" \
bundle exec rspec <path/to/spec.rb>
```
For an explanation of the variables see the [additional examples below](#overriding-gitlab-address) and the [list of supported environment variables](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-environment-variables).
For an explanation of the variables see the [additional examples below](#overriding-gitlab-address) and the [list of supported environment variables](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-gitlab-environment-variables).
#### Run the end-to-end tests on GitLab in Docker

View File

@ -1,10 +1,7 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Package', :object_storage, product_group: :package_registry, quarantine: {
type: :waiting_on,
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/469067'
} do
RSpec.describe 'Package', :object_storage, product_group: :package_registry do
describe 'PyPI Repository', :external_api_calls do
include Runtime::Fixtures
include Support::Helpers::MaskToken

View File

@ -32,8 +32,9 @@ function run_rubocop {
done < <(find . -path './**/remote_development/*.rb' -print0)
files_for_rubocop+=(
"lib/gitlab/fp/rop_helpers.rb"
"lib/result.rb"
"spec/lib/result_spec.rb"
"spec/lib/gitlab/fp/rop_helpers_spec.rb"
"lib/gitlab/fp/result.rb"
"spec/lib/gitlab/fp/result_spec.rb"
"spec/support/matchers/invoke_rop_steps.rb"
"spec/support/railway_oriented_programming.rb"
"spec/support_specs/matchers/result_matchers_spec.rb"
@ -73,7 +74,7 @@ function run_rspec_rails {
"ee/spec/requests/api/internal/kubernetes_spec.rb"
"spec/graphql/types/subscription_type_spec.rb"
"spec/lib/gitlab/fp/rop_helpers_spec.rb"
"spec/lib/result_spec.rb"
"spec/lib/gitlab/fp/result_spec.rb"
"spec/support_specs/matchers/result_matchers_spec.rb"
)

View File

@ -1,8 +1,8 @@
import { merge } from 'lodash';
import { GlCard, GlLoadingIcon, GlEmptyState, GlPagination, GlModal } from '@gitlab/ui';
import { GlLoadingIcon, GlEmptyState, GlPagination, GlModal } from '@gitlab/ui';
import { nextTick } from 'vue';
import responseBody from 'test_fixtures/api/deploy_keys/index.json';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { stubComponent } from 'helpers/stub_component';
@ -45,8 +45,8 @@ describe('DeployKeysTable', () => {
});
};
const findCard = () => wrapper.findComponent(GlCard);
const findCardTitle = () => findCard().find('.gl-new-card-title-wrapper');
const findCrud = () => wrapper.findComponent(CrudComponent);
const findCrudTitle = () => wrapper.findByTestId('crud-title');
const findEditButton = (index) =>
wrapper.findAllByLabelText(DeployKeysTable.i18n.edit, { selector: 'a' }).at(index);
const findRemoveButton = (index) =>
@ -132,11 +132,11 @@ describe('DeployKeysTable', () => {
});
it('renders card with the deploy keys', () => {
expect(findCard().exists()).toBe(true);
expect(findCrud().exists()).toBe(true);
});
it('shows the correct number of deploy keys', () => {
expect(findCardTitle().text()).toMatchInterpolatedText(
expect(findCrudTitle().text()).toMatchInterpolatedText(
`Public deploy keys ${responseBody.length}`,
);
});

View File

@ -28,9 +28,17 @@ describe('registry_header', () => {
stubs: {
GlSprintf,
TitleArea,
MetadataContainerScanning: true,
},
propsData,
slots,
provide() {
return {
config: {
isGroupPage: false,
},
};
},
});
await nextTick();
};

View File

@ -103,6 +103,7 @@ describe('List Page', () => {
RegistryHeader,
TitleArea,
DeleteImage,
MetadataContainerScanning: true,
},
mocks: {
$toast,

View File

@ -9,6 +9,7 @@ RSpec.describe Types::Ci::CiCdSettingType, feature_category: :continuous_integra
expected_fields = %w[
inbound_job_token_scope_enabled job_token_scope_enabled
keep_latest_artifact merge_pipelines_enabled project
push_repository_for_job_token_allowed
]
if Gitlab.ee?

View File

@ -0,0 +1,95 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ContainerRegistry::ContainerRegistryHelper, feature_category: :container_registry, type: :helper do
include Devise::Test::ControllerHelpers
let_it_be(:project) { build_stubbed(:project, :repository) }
let_it_be(:user) { build_stubbed(:user) }
let_it_be(:admin) { build_stubbed(:admin) }
let_it_be(:container_expiration_policy) { build_stubbed(:container_expiration_policy, project: project) }
before_all do
project.add_maintainer(user)
project.add_maintainer(admin)
end
describe '#project_container_registry_template_data' do
subject(:project_container_registry_template_data) do
helper.project_container_registry_template_data(project, connection_error, invalid_path_error)
end
let(:connection_error) { nil }
let(:invalid_path_error) { nil }
it 'returns the correct template data' do
allow(helper).to receive(:current_user).and_return(user)
expect(project_container_registry_template_data).to include(
endpoint: helper.project_container_registry_index_path(project),
expiration_policy: container_expiration_policy.to_json,
help_page_path: help_page_path('user/packages/container_registry/index'),
two_factor_auth_help_link: help_page_path('user/profile/account/two_factor_authentication'),
personal_access_tokens_help_link: help_page_path('user/profile/personal_access_tokens'),
no_containers_image: match_asset_path('illustrations/docker-empty-state.svg'),
containers_error_image: match_asset_path('illustrations/docker-error-state.svg'),
repository_url: escape_once(project.container_registry_url),
registry_host_url_with_port: escape_once(Gitlab.config.registry.host_port),
expiration_policy_help_page_path:
help_page_path('user/packages/container_registry/reduce_container_registry_storage',
anchor: 'cleanup-policy'),
garbage_collection_help_page_path: help_page_path('administration/packages/container_registry',
anchor: 'container-registry-garbage-collection'),
run_cleanup_policies_help_page_path: help_page_path('administration/packages/container_registry',
anchor: 'run-the-cleanup-policy-now'),
project_path: project.full_path,
gid_prefix: helper.container_repository_gid_prefix,
is_admin: user.admin.to_s,
show_cleanup_policy_link: helper.show_cleanup_policy_link(project).to_s,
show_container_registry_settings: helper.show_container_registry_settings(project).to_s,
cleanup_policies_settings_path: helper.project_settings_packages_and_registries_path(project),
connection_error: (!!connection_error).to_s,
invalid_path_error: (!!invalid_path_error).to_s,
user_callouts_path: callouts_path,
user_callout_id: Users::CalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
is_metadata_database_enabled: ContainerRegistry::GitlabApiClient.supports_gitlab_api?.to_s,
show_unfinished_tag_cleanup_callout: helper.show_unfinished_tag_cleanup_callout?.to_s
)
end
context 'when there is a connection error' do
let(:connection_error) { true }
it 'sets connection_error to true' do
allow(helper).to receive(:current_user).and_return(user)
expect(project_container_registry_template_data[:connection_error]).to eq('true')
end
end
context 'when there is an invalid path error' do
let(:invalid_path_error) { true }
it 'sets invalid_path_error to true' do
allow(helper).to receive(:current_user).and_return(user)
expect(project_container_registry_template_data[:invalid_path_error]).to eq('true')
end
end
context 'when current user is admin' do
before do
allow(helper).to receive(:current_user).and_return(admin)
end
it 'sets is_admin to true' do
expect(project_container_registry_template_data[:is_admin]).to eq('true')
end
end
end
describe '#container_repository_gid_prefix' do
subject { helper.container_repository_gid_prefix }
it { is_expected.to eq('gid://gitlab/ContainerRepository/') }
end
end

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ContainerRegistryHelper do
describe '#container_repository_gid_prefix' do
subject { helper.container_repository_gid_prefix }
it { is_expected.to eq('gid://gitlab/ContainerRepository/') }
end
end

View File

@ -9,18 +9,18 @@ require 'fast_spec_helper'
# To support this, we have intentionally used some `rubocop:disable` comments to allow for more
# explicit and readable examples.
# rubocop:disable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration -- intentionally disabled per comment above
RSpec.describe Result, feature_category: :remote_development do
describe 'usage of Result.ok and Result.err' do
RSpec.describe Gitlab::Fp::Result, feature_category: :shared do
describe 'usage of Gitlab::Fp::Result.ok and Gitlab::Fp::Result.err' do
context 'when checked with .ok? and .err?' do
it 'works with ok result' do
result = Result.ok(:success)
result = Gitlab::Fp::Result.ok(:success)
expect(result.ok?).to eq(true)
expect(result.err?).to eq(false)
expect(result.unwrap).to eq(:success)
end
it 'works with error result' do
result = Result.err(:failure)
result = Gitlab::Fp::Result.err(:failure)
expect(result.err?).to eq(true)
expect(result.ok?).to eq(false)
expect(result.unwrap_err).to eq(:failure)
@ -29,18 +29,18 @@ RSpec.describe Result, feature_category: :remote_development do
context 'when checked with destructuring' do
it 'works with ok result' do
Result.ok(:success) => { ok: } # example of rightward assignment
Gitlab::Fp::Result.ok(:success) => { ok: } # example of rightward assignment
expect(ok).to eq(:success)
Result.ok(:success) => { ok: success_value } # rightward assignment destructuring to different var
Gitlab::Fp::Result.ok(:success) => { ok: success_value } # rightward assignment destructuring to different var
expect(success_value).to eq(:success)
end
it 'works with error result' do
Result.err(:failure) => { err: }
Gitlab::Fp::Result.err(:failure) => { err: }
expect(err).to eq(:failure)
Result.err(:failure) => { err: error_value }
Gitlab::Fp::Result.err(:failure) => { err: error_value }
expect(error_value).to eq(:failure)
end
end
@ -58,24 +58,24 @@ RSpec.describe Result, feature_category: :remote_development do
end
it 'works with ok result' do
ok_result = Result.ok(:success_symbol)
ok_result = Gitlab::Fp::Result.ok(:success_symbol)
expect(check_result_with_pattern_matching(ok_result)).to eq({ success: :success_symbol })
end
it 'works with error result' do
error_result = Result.err('failure string')
error_result = Gitlab::Fp::Result.err('failure string')
expect(check_result_with_pattern_matching(error_result)).to eq({ failure: 'failure string' })
end
it 'raises error with unmatched type in pattern match' do
unmatched_type_result = Result.ok([])
unmatched_type_result = Gitlab::Fp::Result.ok([])
expect do
check_result_with_pattern_matching(unmatched_type_result)
end.to raise_error(RuntimeError, 'Unmatched result type: Array')
end
it 'raises error with invalid pattern matching key' do
result = Result.ok(:success)
result = Gitlab::Fp::Result.ok(:success)
expect do
case result
in { invalid_pattern_match_because_it_is_not_ok_or_err: :value }
@ -91,22 +91,22 @@ RSpec.describe Result, feature_category: :remote_development do
describe 'usage of #and_then' do
context 'when passed a proc' do
it 'returns last ok value in successful chain' do
initial_result = Result.ok(1)
initial_result = Gitlab::Fp::Result.ok(1)
final_result =
initial_result
.and_then(->(value) { Result.ok(value + 1) })
.and_then(->(value) { Result.ok(value + 1) })
.and_then(->(value) { Gitlab::Fp::Result.ok(value + 1) })
.and_then(->(value) { Gitlab::Fp::Result.ok(value + 1) })
expect(final_result.ok?).to eq(true)
expect(final_result.unwrap).to eq(3)
end
it 'short-circuits the rest of the chain on the first err value encountered' do
initial_result = Result.ok(1)
initial_result = Gitlab::Fp::Result.ok(1)
final_result =
initial_result
.and_then(->(value) { Result.err("invalid: #{value}") })
.and_then(->(value) { Result.ok(value + 1) })
.and_then(->(value) { Gitlab::Fp::Result.err("invalid: #{value}") })
.and_then(->(value) { Gitlab::Fp::Result.ok(value + 1) })
expect(final_result.err?).to eq(true)
expect(final_result.unwrap_err).to eq('invalid: 1')
@ -116,22 +116,22 @@ RSpec.describe Result, feature_category: :remote_development do
context 'when passed a module or class (singleton) method object' do
module MyModuleUsingResult
def self.double(value)
Result.ok(value * 2)
Gitlab::Fp::Result.ok(value * 2)
end
def self.return_err(value)
Result.err("invalid: #{value}")
Gitlab::Fp::Result.err("invalid: #{value}")
end
class MyClassUsingResult
def self.triple(value)
Result.ok(value * 3)
Gitlab::Fp::Result.ok(value * 3)
end
end
end
it 'returns last ok value in successful chain' do
initial_result = Result.ok(1)
initial_result = Gitlab::Fp::Result.ok(1)
final_result =
initial_result
.and_then(::MyModuleUsingResult.method(:double))
@ -142,7 +142,7 @@ RSpec.describe Result, feature_category: :remote_development do
end
it 'returns first err value in failed chain' do
initial_result = Result.ok(1)
initial_result = Gitlab::Fp::Result.ok(1)
final_result =
initial_result
.and_then(::MyModuleUsingResult.method(:double))
@ -161,25 +161,25 @@ RSpec.describe Result, feature_category: :remote_development do
ex = TypeError
msg = /expects a lambda or singleton method object/
# noinspection RubyMismatchedArgumentType -- intentionally passing invalid types
expect { Result.ok(1).and_then('string') }.to raise_error(ex, msg)
expect { Result.ok(1).and_then(proc { Result.ok(1) }) }.to raise_error(ex, msg)
expect { Result.ok(1).and_then(1.method(:to_s)) }.to raise_error(ex, msg)
expect { Result.ok(1).and_then(Integer.method(:to_s)) }.to raise_error(ex, msg)
expect { Gitlab::Fp::Result.ok(1).and_then('string') }.to raise_error(ex, msg)
expect { Gitlab::Fp::Result.ok(1).and_then(proc { Gitlab::Fp::Result.ok(1) }) }.to raise_error(ex, msg)
expect { Gitlab::Fp::Result.ok(1).and_then(1.method(:to_s)) }.to raise_error(ex, msg)
expect { Gitlab::Fp::Result.ok(1).and_then(Integer.method(:to_s)) }.to raise_error(ex, msg)
end
end
describe 'enforcement of argument arity' do
it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do
expect do
Result.ok(1).and_then(->(a, b) { Result.ok(a + b) })
Gitlab::Fp::Result.ok(1).and_then(->(a, b) { Gitlab::Fp::Result.ok(a + b) })
end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/)
end
end
describe 'enforcement that passed lambda or method returns a Result type' do
describe 'enforcement that passed lambda or method returns a Gitlab::Fp::Result type' do
it 'raises ArgumentError if passed lambda or singleton method object which returns non-Result type' do
expect do
Result.ok(1).and_then(->(a) { a + 1 })
Gitlab::Fp::Result.ok(1).and_then(->(a) { a + 1 })
end.to raise_error(TypeError, /expects .* which returns a 'Result' type/)
end
end
@ -189,7 +189,7 @@ RSpec.describe Result, feature_category: :remote_development do
describe 'usage of #map' do
context 'when passed a proc' do
it 'returns last ok value in successful chain' do
initial_result = Result.ok(1)
initial_result = Gitlab::Fp::Result.ok(1)
final_result =
initial_result
.map(->(value) { value + 1 })
@ -200,10 +200,10 @@ RSpec.describe Result, feature_category: :remote_development do
end
it 'returns first err value in failed chain' do
initial_result = Result.ok(1)
initial_result = Gitlab::Fp::Result.ok(1)
final_result =
initial_result
.and_then(->(value) { Result.err("invalid: #{value}") })
.and_then(->(value) { Gitlab::Fp::Result.err("invalid: #{value}") })
.map(->(value) { value + 1 })
expect(final_result.err?).to eq(true)
@ -225,7 +225,7 @@ RSpec.describe Result, feature_category: :remote_development do
end
it 'returns last ok value in successful chain' do
initial_result = Result.ok(1)
initial_result = Gitlab::Fp::Result.ok(1)
final_result =
initial_result
.map(::MyModuleNotUsingResult.method(:double))
@ -236,11 +236,11 @@ RSpec.describe Result, feature_category: :remote_development do
end
it 'returns first err value in failed chain' do
initial_result = Result.ok(1)
initial_result = Gitlab::Fp::Result.ok(1)
final_result =
initial_result
.map(::MyModuleNotUsingResult.method(:double))
.and_then(->(value) { Result.err("invalid: #{value}") })
.and_then(->(value) { Gitlab::Fp::Result.err("invalid: #{value}") })
.map(::MyModuleUsingResult.method(:double))
expect(final_result.err?).to eq(true)
@ -253,18 +253,18 @@ RSpec.describe Result, feature_category: :remote_development do
it 'raises TypeError if passed anything other than a lambda or singleton method object' do
ex = TypeError
msg = /expects a lambda or singleton method object/
# noinspection RubyMismatchedArgumentType
expect { Result.ok(1).map('string') }.to raise_error(ex, msg)
expect { Result.ok(1).map(proc { 1 }) }.to raise_error(ex, msg)
expect { Result.ok(1).map(1.method(:to_s)) }.to raise_error(ex, msg)
expect { Result.ok(1).map(Integer.method(:to_s)) }.to raise_error(ex, msg)
# noinspection RubyMismatchedArgumentType -- intentionally passing invalid types
expect { Gitlab::Fp::Result.ok(1).map('string') }.to raise_error(ex, msg)
expect { Gitlab::Fp::Result.ok(1).map(proc { 1 }) }.to raise_error(ex, msg)
expect { Gitlab::Fp::Result.ok(1).map(1.method(:to_s)) }.to raise_error(ex, msg)
expect { Gitlab::Fp::Result.ok(1).map(Integer.method(:to_s)) }.to raise_error(ex, msg)
end
end
describe 'enforcement of argument arity' do
it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do
expect do
Result.ok(1).map(->(a, b) { a + b })
Gitlab::Fp::Result.ok(1).map(->(a, b) { a + b })
end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/)
end
end
@ -272,7 +272,7 @@ RSpec.describe Result, feature_category: :remote_development do
describe 'enforcement that passed lambda or method does not return a Result type' do
it 'raises TypeError if passed lambda or singleton method object which returns non-Result type' do
expect do
Result.ok(1).map(->(a) { Result.ok(a + 1) })
Gitlab::Fp::Result.ok(1).map(->(a) { Gitlab::Fp::Result.ok(a + 1) })
end.to raise_error(TypeError, /expects .* which returns an unwrapped value, not a 'Result'/)
end
end
@ -281,31 +281,33 @@ RSpec.describe Result, feature_category: :remote_development do
describe '#unwrap' do
it 'returns wrapped value if ok' do
expect(Result.ok(1).unwrap).to eq(1)
expect(Gitlab::Fp::Result.ok(1).unwrap).to eq(1)
end
it 'raises error if err' do
expect { Result.err('error').unwrap }.to raise_error(RuntimeError, /called.*unwrap.*on an 'err' Result/i)
expect { Gitlab::Fp::Result.err('error').unwrap }
.to raise_error(RuntimeError, /called.*unwrap.*on an 'err' Result/i)
end
end
describe '#unwrap_err' do
it 'returns wrapped value if err' do
expect(Result.err('error').unwrap_err).to eq('error')
expect(Gitlab::Fp::Result.err('error').unwrap_err).to eq('error')
end
it 'raises error if ok' do
expect { Result.ok(1).unwrap_err }.to raise_error(RuntimeError, /called.*unwrap_err.*on an 'ok' Result/i)
expect { Gitlab::Fp::Result.ok(1).unwrap_err }
.to raise_error(RuntimeError, /called.*unwrap_err.*on an 'ok' Result/i)
end
end
describe '#==' do
it 'implements equality' do
expect(Result.ok(1)).to eq(Result.ok(1))
expect(Result.err('error')).to eq(Result.err('error'))
expect(Result.ok(1)).not_to eq(Result.ok(2))
expect(Result.err('error')).not_to eq(Result.err('other error'))
expect(Result.ok(1)).not_to eq(Result.err(1))
expect(Gitlab::Fp::Result.ok(1)).to eq(Gitlab::Fp::Result.ok(1))
expect(Gitlab::Fp::Result.err('error')).to eq(Gitlab::Fp::Result.err('error'))
expect(Gitlab::Fp::Result.ok(1)).not_to eq(Gitlab::Fp::Result.ok(2))
expect(Gitlab::Fp::Result.err('error')).not_to eq(Gitlab::Fp::Result.err('other error'))
expect(Gitlab::Fp::Result.ok(1)).not_to eq(Gitlab::Fp::Result.err(1))
end
end
@ -313,12 +315,12 @@ RSpec.describe Result, feature_category: :remote_development do
context 'for enforcing usage of only public interface' do
context 'when private constructor is called with invalid params' do
it 'raises ArgumentError if both ok_value and err_value are passed' do
expect { Result.new(ok_value: :ignored, err_value: :ignored) }
expect { Gitlab::Fp::Result.new(ok_value: :ignored, err_value: :ignored) }
.to raise_error(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err')
end
it 'raises ArgumentError if neither ok_value nor err_value are passed' do
expect { Result.new }
expect { Gitlab::Fp::Result.new }
.to raise_error(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err')
end
end

View File

@ -32,7 +32,7 @@ RSpec.describe ::RemoteDevelopment::Settings::CurrentSettingsReader, feature_cat
context "when there are no errors" do
it "returns ::Gitlab::CurrentSettings overridden settings and non-overridden settings" do
expect(result).to eq(Result.ok(
expect(result).to eq(Gitlab::Fp::Result.ok(
{
settings: {
non_overridden_setting: "not_overridden",

View File

@ -35,7 +35,7 @@ RSpec.describe RemoteDevelopment::Settings::EnvVarReader, :rd_fast, feature_cate
let(:setting_type) { String }
it "uses the string value of the overridden ENV var value" do
expect(result).to eq(Result.ok(
expect(result).to eq(Gitlab::Fp::Result.ok(
{
settings: { the_setting: env_var_value },
setting_types: { the_setting: setting_type }
@ -49,7 +49,7 @@ RSpec.describe RemoteDevelopment::Settings::EnvVarReader, :rd_fast, feature_cate
let(:setting_type) { Integer }
it "uses the casted type of the overridden ENV var value" do
expect(result).to eq(Result.ok(
expect(result).to eq(Gitlab::Fp::Result.ok(
{
settings: { the_setting: env_var_value.to_i },
setting_types: { the_setting: setting_type }
@ -63,7 +63,7 @@ RSpec.describe RemoteDevelopment::Settings::EnvVarReader, :rd_fast, feature_cate
let(:setting_type) { Hash }
it "uses the casted type of the overridden ENV var value" do
expect(result).to eq(Result.ok(
expect(result).to eq(Gitlab::Fp::Result.ok(
{
settings: { the_setting: { a: 1 } },
setting_types: { the_setting: setting_type }
@ -77,7 +77,7 @@ RSpec.describe RemoteDevelopment::Settings::EnvVarReader, :rd_fast, feature_cate
let(:setting_type) { Array }
it "uses the casted type of the overridden ENV var value" do
expect(result).to eq(Result.ok(
expect(result).to eq(Gitlab::Fp::Result.ok(
{
settings: { the_setting: ["a", 1] },
setting_types: { the_setting: setting_type }
@ -92,7 +92,7 @@ RSpec.describe RemoteDevelopment::Settings::EnvVarReader, :rd_fast, feature_cate
let(:env_var_value) { "maybe some old deprecated setting, doesn't matter, it's ignored" }
it "ignores the ENV var" do
expect(result).to eq(Result.ok(
expect(result).to eq(Gitlab::Fp::Result.ok(
{
settings: { the_setting: default_setting_value },
setting_types: { the_setting: setting_type }

View File

@ -20,7 +20,7 @@ RSpec.describe RemoteDevelopment::Settings::ExtensionsGalleryMetadataValidator,
context "when vscode_extensions_gallery_metadata is valid" do
shared_examples "success result" do
it "return an ok Result containing the original context which was passed" do
expect(result).to eq(Result.ok(context))
expect(result).to eq(Gitlab::Fp::Result.ok(context))
end
end

View File

@ -31,7 +31,7 @@ RSpec.describe RemoteDevelopment::Settings::ExtensionsGalleryValidator, :rd_fast
context "when vscode_extensions_gallery is valid" do
shared_examples "success result" do
it "return an ok Result containing the original context which was passed" do
expect(result).to eq(Result.ok(context))
expect(result).to eq(Gitlab::Fp::Result.ok(context))
end
end

View File

@ -23,7 +23,7 @@ RSpec.describe RemoteDevelopment::Settings::ReconciliationIntervalSecondsValidat
context "when partial_reconciliation_interval_seconds and full_reconciliation_interval_seconds is valid" do
it "return an ok Result containing the original context which was passed" do
expect(result).to eq(Result.ok(context))
expect(result).to eq(Gitlab::Fp::Result.ok(context))
end
end

View File

@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
RSpec.describe AddTicketWorkItemType, :migration, feature_category: :service_desk do
RSpec.describe AddTicketWorkItemType, :migration, feature_category: :service_desk, allowed_to_be_slow: true do
include MigrationHelpers::WorkItemTypesHelper
let(:work_item_types) { table(:work_item_types) }

View File

@ -27,6 +27,12 @@ RSpec.describe ProjectCiCdSetting, feature_category: :continuous_integration do
end
end
describe '#push_repository_for_job_token_allowed' do
it 'is false by default' do
expect(described_class.new.push_repository_for_job_token_allowed).to be_falsey
end
end
describe '#separated_caches' do
it 'is true by default' do
expect(described_class.new.separated_caches).to be_truthy

View File

@ -3704,19 +3704,24 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
let(:policy) { :build_push_code }
where(:user_role, :project_visibility, :push_repository_for_job_token_allowed, :self_referential_project, :allowed) do
:maintainer | :public | true | true | true
:owner | :public | true | true | true
:maintainer | :private | true | true | true
:developer | :public | true | true | true
:reporter | :public | true | true | false
:guest | :public | true | true | false
:guest | :private | true | true | false
:guest | :internal | true | true | false
:anonymous | :public | true | true | false
:maintainer | :public | false | true | false
:maintainer | :public | true | false | false
:maintainer | :public | false | false | false
where(:user_role, :project_visibility, :push_repository_for_job_token_allowed, :self_referential_project, :allowed, :ff_disabled) do
:maintainer | :public | true | true | true | false
:owner | :public | true | true | true | false
:maintainer | :private | true | true | true | false
:developer | :public | true | true | true | false
:reporter | :public | true | true | false | false
:guest | :public | true | true | false | false
:guest | :private | true | true | false | false
:guest | :internal | true | true | false | false
:anonymous | :public | true | true | false | false
:maintainer | :public | false | true | false | false
:maintainer | :public | true | false | false | false
:maintainer | :public | false | false | false | false
:maintainer | :public | true | true | false | true
:owner | :public | true | true | false | true
:maintainer | :private | true | true | false | true
:developer | :public | true | true | false | true
:reporter | :public | true | true | false | true
end
with_them do
@ -3730,6 +3735,8 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
let(:scope_project) { public_send(:private_project) }
before do
stub_feature_flags(allow_push_repository_for_job_token: false) if ff_disabled
project.add_guest(guest)
project.add_reporter(reporter)
project.add_developer(developer)

View File

@ -49,6 +49,8 @@ RSpec.describe 'Getting Ci Cd Setting', feature_category: :continuous_integratio
expect(settings_data['jobTokenScopeEnabled']).to eql project.ci_cd_settings.job_token_scope_enabled?
expect(settings_data['inboundJobTokenScopeEnabled']).to eql(
project.ci_cd_settings.inbound_job_token_scope_enabled?)
expect(settings_data['pushRepositoryForJobTokenAllowed']).to eql(
project.ci_cd_settings.push_repository_for_job_token_allowed?)
if Gitlab.ee?
expect(settings_data['mergeTrainsEnabled']).to eql project.ci_cd_settings.merge_trains_enabled?

View File

@ -18,7 +18,8 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
full_path: project.full_path,
keep_latest_artifact: false,
job_token_scope_enabled: false,
inbound_job_token_scope_enabled: false
inbound_job_token_scope_enabled: false,
push_repository_for_job_token_allowed: false
}
end
@ -69,6 +70,23 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
expect(project.ci_outbound_job_token_scope_enabled).to eq(false)
end
context 'when push_repository_for_job_token_allowed requested to be true' do
let(:variables) do
{
full_path: project.full_path,
push_repository_for_job_token_allowed: true
}
end
it 'updates push_repository_for_job_token_allowed' do
post_graphql_mutation(mutation, current_user: user)
project.reload
expect(response).to have_gitlab_http_status(:success)
expect(project.ci_cd_settings.push_repository_for_job_token_allowed).to eq(true)
end
end
context 'when job_token_scope_enabled: true' do
let(:variables) do
{

View File

@ -103,6 +103,7 @@ ci_cd_settings:
- push_repository_for_job_token_allowed
remapped_attributes:
pipeline_variables_minimum_override_role: ci_pipeline_variables_minimum_override_role
push_repository_for_job_token_allowed: ci_push_repository_for_job_token_allowed
default_git_depth: ci_default_git_depth
forward_deployment_enabled: ci_forward_deployment_enabled
forward_deployment_rollback_allowed: ci_forward_deployment_rollback_allowed

View File

@ -3323,7 +3323,8 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
'build_timeout',
'auto_devops_enabled',
'auto_devops_deploy_strategy',
'import_error'
'import_error',
'ci_push_repository_for_job_token_allowed'
)
end
end
@ -4074,6 +4075,20 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and
let(:failed_status_code) { :not_found }
end
describe 'updating ci_push_repository_for_job_token_allowed attribute' do
it 'is disabled by default' do
expect(project.ci_push_repository_for_job_token_allowed).to be_falsey
end
it 'enables push to repository using job token' do
put(api(path, user), params: { ci_push_repository_for_job_token_allowed: true })
expect(response).to have_gitlab_http_status(:ok)
expect(project.reload.ci_push_repository_for_job_token_allowed).to be_truthy
expect(json_response['ci_push_repository_for_job_token_allowed']).to eq(true)
end
end
describe 'updating packages_enabled attribute' do
it 'is enabled by default' do
expect(project.packages_enabled).to be true

View File

@ -3,21 +3,25 @@
require 'spec_helper'
RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category: :runner do
subject(:execute) { described_class.new(runner, new_project, user).execute }
let(:service) { described_class.new(runner, new_project, user) }
let_it_be(:owner_group) { create(:group) }
let_it_be(:owner_project) { create(:project, group: owner_group) }
let_it_be(:new_project) { create(:project) }
let_it_be(:organization1) { create(:organization) }
let_it_be(:owner_group) { create(:group, organization: organization1) }
let_it_be(:owner_project) { create(:project, group: owner_group, organization: organization1) }
let_it_be(:new_project) { create(:project, organization: organization1) }
let_it_be(:runner) { create(:ci_runner, :project, projects: [owner_project]) }
subject(:execute) { service.execute }
context 'without user' do
let(:user) { nil }
it 'does not call assign_to on runner and returns error response', :aggregate_failures do
expect(runner).not_to receive(:assign_to)
is_expected.to be_error
expect(execute.message).to eq('user not allowed to assign runner')
expect(execute).to be_error
expect(execute.reason).to eq(:not_authorized_to_assign_runner)
expect(execute.message).to eq(_('user not allowed to assign runner'))
end
end
@ -27,8 +31,9 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category:
it 'does not call assign_to on runner and returns error message' do
expect(runner).not_to receive(:assign_to)
is_expected.to be_error
expect(execute.message).to eq('user not allowed to assign runner')
expect(execute).to be_error
expect(execute.reason).to eq(:not_authorized_to_assign_runner)
expect(execute.message).to eq(_('user not allowed to assign runner'))
end
end
@ -44,7 +49,32 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category:
it 'calls assign_to on runner and returns success response' do
expect(runner).to receive(:assign_to).with(new_project, user).once.and_call_original
is_expected.to be_success
expect(execute).to be_success
end
context 'when runner returns error' do
let(:new_project) { owner_project }
it 'returns error response' do
expect(execute).to be_error
expect(execute.reason).to eq(:runner_error)
expect(execute.errors).to contain_exactly(
'Assign to Validation failed: Runner projects runner has already been taken')
end
end
context 'when new project is from a different organization' do
let_it_be(:organization2) { create(:organization) }
let_it_be(:new_project) { create(:project, organization: organization2) }
it 'returns error response and rolls back transaction' do
expect(execute).to be_error
expect(execute.reason).to eq(:project_not_in_same_organization)
expect(execute.errors).to contain_exactly(
_('runner can only be assigned to projects in the same organization')
)
expect(runner.reload.projects).to contain_exactly(owner_project)
end
end
end
@ -56,8 +86,9 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category:
it 'does not call assign_to on runner and returns error message', :aggregate_failures do
expect(runner).not_to receive(:assign_to)
is_expected.to be_error
expect(execute.message).to eq('user not allowed to add runners to project')
expect(execute).to be_error
expect(execute.reason).to eq(:not_authorized_to_add_runner_in_project)
expect(execute.message).to eq(_('user is not authorized to add runners to project'))
end
end
@ -69,7 +100,8 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category:
it 'does not call assign_to on runner and returns error message' do
expect(runner).not_to receive(:assign_to)
is_expected.to be_error
expect(execute).to be_error
expect(execute.reason).to eq(:not_authorized_to_assign_runner)
expect(execute.message).to eq('user not allowed to assign runner')
end
end
@ -81,7 +113,7 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category:
it 'calls assign_to on runner and returns success response' do
expect(runner).to receive(:assign_to).with(new_project, user).once.and_call_original
is_expected.to be_success
expect(execute).to be_success
end
end
end

View File

@ -7,8 +7,9 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
described_class.new(runner: runner, current_user: user, project_ids: new_projects.map(&:id)).execute
end
let_it_be(:owner_project) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:organization1) { create(:organization) }
let_it_be(:owner_project) { create(:project, organization: organization1) }
let_it_be(:project2) { create(:project, organization: organization1) }
let(:original_projects) { [owner_project, project2] }
let(:runner) { create(:ci_runner, :project, projects: original_projects) }
@ -21,7 +22,8 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
expect(runner).not_to receive(:assign_to)
expect(execute).to be_error
expect(execute.message).to eq('user not allowed to assign runner')
expect(execute.reason).to eq(:not_authorized_to_assign_runner)
expect(execute.message).to eq(_('user not allowed to assign runner'))
end
end
@ -33,13 +35,14 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
expect(runner).not_to receive(:assign_to)
expect(execute).to be_error
expect(execute.message).to eq('user not allowed to assign runner')
expect(execute.reason).to eq(:not_authorized_to_assign_runner)
expect(execute.message).to eq(_('user not allowed to assign runner'))
end
end
context 'with authorized user' do
let_it_be(:project3) { create(:project) }
let_it_be(:project4) { create(:project) }
let_it_be(:project3) { create(:project, organization: organization1) }
let_it_be(:project4) { create(:project, organization: organization1) }
let(:projects_with_maintainer_access) { original_projects }
@ -57,7 +60,7 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
runner.reload
expect(runner.owner_project).to eq(owner_project)
expect(runner.projects.ids).to match_array([owner_project.id] + new_projects.map(&:id))
expect(runner.projects.ids).to match_array([owner_project, *new_projects].map(&:id))
end
end
@ -70,7 +73,7 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
runner.reload
expect(runner.owner_project).to eq(owner_project)
expect(runner.projects.ids).to match_array([owner_project.id] + new_projects.map(&:id))
expect(runner.projects.ids).to eq([owner_project, *new_projects].map(&:id))
end
end
@ -97,6 +100,7 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
end
expect(execute).to be_error
expect(execute.reason).to eq(:failed_runner_project_destroy)
expect(runner.reload.projects.order(:id)).to eq(original_projects)
end
end
@ -116,10 +120,39 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
it 'returns error response and rolls back transaction' do
expect(execute).to be_error
expect(execute.errors).to contain_exactly('user is not authorized to add runners to project')
expect(execute.reason).to eq(:not_authorized_to_add_runner_in_project)
expect(execute.errors).to contain_exactly(_('user is not authorized to add runners to project'))
expect(runner.reload.projects.order(:id)).to eq(original_projects)
end
end
context 'when some of the new projects are from a different organization' do
let_it_be(:organization2) { create(:organization) }
let_it_be(:project4) { create(:project, organization: organization2) }
it 'returns error response and rolls back transaction' do
expect(execute).to be_error
expect(execute.reason).to eq(:project_not_in_same_organization)
expect(execute.errors).to contain_exactly(
_('runner can only be assigned to projects in the same organization')
)
expect(runner.reload.projects.order(:id)).to eq(original_projects)
end
context 'with multiple failures' do
let(:projects_with_maintainer_access) { original_projects + [project4] }
it 'returns error response and rolls back transaction' do
expect(execute).to be_error
expect(execute.reason).to eq(:multiple_errors)
expect(execute.errors).to contain_exactly(
_('runner can only be assigned to projects in the same organization'),
_('user is not authorized to add runners to project')
)
expect(runner.reload.projects.order(:id)).to eq(original_projects)
end
end
end
end
end

View File

@ -2916,6 +2916,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
context 'crm_contact commands' do
let_it_be(:new_contact) { create(:contact, group: group) }
let_it_be(:another_contact) { create(:contact, group: group) }
let_it_be(:existing_contact) { create(:contact, group: group) }
let(:add_command) { service.execute("/add_contacts #{new_contact.email}", issue) }
@ -2926,18 +2927,62 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
create(:issue_customer_relations_contact, issue: issue, contact: existing_contact)
end
it 'add_contacts command adds the contact' do
_, updates, message = add_command
describe 'add_contacts command' do
it 'adds a contact' do
_, updates, message = add_command
expect(updates).to eq(add_contacts: [new_contact.email])
expect(message).to eq(_('One or more contacts were successfully added.'))
expect(updates).to eq(add_contacts: [new_contact.email])
expect(message).to eq(_('One or more contacts were successfully added.'))
end
context 'with multiple contacts in the same command' do
it 'adds both contacts' do
_, updates, message = service.execute("/add_contacts #{new_contact.email} #{another_contact.email}", issue)
expect(updates).to eq(add_contacts: [new_contact.email, another_contact.email])
expect(message).to eq(_('One or more contacts were successfully added.'))
end
end
context 'with multiple commands' do
it 'adds both contacts' do
_, updates, message = service.execute("/add_contacts #{new_contact.email}\n/add_contacts #{another_contact.email}", issue)
expect(updates).to eq(add_contacts: [new_contact.email, another_contact.email])
expect(message).to eq(_('One or more contacts were successfully added. One or more contacts were successfully added.'))
end
end
end
it 'remove_contacts command removes the contact' do
_, updates, message = remove_command
describe 'remove_contacts command' do
before do
create(:issue_customer_relations_contact, issue: issue, contact: another_contact)
end
expect(updates).to eq(remove_contacts: [existing_contact.email])
expect(message).to eq(_('One or more contacts were successfully removed.'))
it 'removes the contact' do
_, updates, message = remove_command
expect(updates).to eq(remove_contacts: [existing_contact.email])
expect(message).to eq(_('One or more contacts were successfully removed.'))
end
context 'with multiple contacts in the same command' do
it 'removes the contact' do
_, updates, message = service.execute("/remove_contacts #{existing_contact.email} #{another_contact.email}", issue)
expect(updates).to eq(remove_contacts: [existing_contact.email, another_contact.email])
expect(message).to eq(_('One or more contacts were successfully removed.'))
end
end
context 'with multiple commands' do
it 'removes the contact' do
_, updates, message = service.execute("/remove_contacts #{existing_contact.email}\n/remove_contacts #{another_contact.email}", issue)
expect(updates).to eq(remove_contacts: [existing_contact.email, another_contact.email])
expect(message).to eq(_('One or more contacts were successfully removed. One or more contacts were successfully removed.'))
end
end
end
end

View File

@ -11,14 +11,14 @@ module InvokeRopSteps
result_type = :err
step_class, returned_message = parse_result_for_step(err_result_for_step, result_type)
err_results_for_steps[step_class] = Result.err(returned_message)
err_results_for_steps[step_class] = Gitlab::Fp::Result.err(returned_message)
end
def add_ok_result_for_step(ok_result_for_step, ok_results_for_steps)
result_type = :ok
step_class, returned_message = parse_result_for_step(ok_result_for_step, result_type)
ok_results_for_steps[step_class] = Result.ok(returned_message)
ok_results_for_steps[step_class] = Gitlab::Fp::Result.ok(returned_message)
end
def parse_result_for_step(result_for_step, result_type)
@ -88,9 +88,11 @@ module InvokeRopSteps
end
def validate_expected_return_value(expected_return_value)
if expected_return_value.is_a?(Hash) || expected_return_value.is_a?(Result) || expected_return_value < RuntimeError
return
end
return_value_is_valid = expected_return_value.is_a?(Hash) ||
expected_return_value.is_a?(Gitlab::Fp::Result) ||
expected_return_value < RuntimeError
return if return_value_is_valid
raise "'and_return_expected_value' argument must be a Hash,Result or a subclass of RuntimeError, " \
"but was a #{expected_return_value.class}"
@ -130,7 +132,7 @@ module InvokeRopSteps
elsif ok_results_for_steps.key?(step_class)
expected_rop_step[:returned_object] = ok_results_for_steps[step_class]
elsif step_action == :and_then
expected_rop_step[:returned_object] = Result.ok(context_passed_along_steps)
expected_rop_step[:returned_object] = Gitlab::Fp::Result.ok(context_passed_along_steps)
elsif step_action == :map
expected_rop_step[:returned_object] = context_passed_along_steps
else
@ -148,7 +150,7 @@ module InvokeRopSteps
step => {
step_class: Class => step_class,
step_class_method: Symbol => step_class_method,
returned_object: Result | Hash => returned_object
returned_object: Gitlab::Fp::Result | Hash => returned_object
}
set_up_step_class_expectation(
@ -240,7 +242,7 @@ RSpec::Matchers.define :invoke_rop_steps do |rop_steps|
validate_expected_return_value(value)
expected_return_value = value
# noinspection RubyUnusedLocalVariable -- TODO: open issue and add to https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues
expected_return_value_matcher = if value.is_a?(Hash) || value.is_a?(Result)
expected_return_value_matcher = if value.is_a?(Hash) || value.is_a?(Gitlab::Fp::Result)
->(main) { expect(main.call).to eq(value) }
else
->(main) { expect { main.call }.to raise_error(value) }

View File

@ -31,7 +31,7 @@ module ResultMatchers
def matches?(actual, &block)
@actual = actual
raise "#{actual} must be a #{::Result}, but it was a #{actual.class}" unless actual.is_a?(::Result)
raise "#{actual} must be a Result, but it was a #{actual.class}" unless actual.is_a?(Gitlab::Fp::Result)
@failure_message_suffix = "be an '#{ok_or_err}' type"
return false unless actual.ok? == ok?

View File

@ -849,6 +849,7 @@
- './ee/spec/helpers/boards_helper_spec.rb'
- './ee/spec/helpers/compliance_management/compliance_framework/group_settings_helper_spec.rb'
- './ee/spec/helpers/credentials_inventory_helper_spec.rb'
- './ee/spec/helpers/container_registry/container_registry_helper_spec.rb'
- './ee/spec/helpers/ee/access_tokens_helper_spec.rb'
- './ee/spec/helpers/ee/admin/identities_helper_spec.rb'
- './ee/spec/helpers/ee/application_settings_helper_spec.rb'
@ -4594,7 +4595,7 @@
- './spec/helpers/commits_helper_spec.rb'
- './spec/helpers/components_helper_spec.rb'
- './spec/helpers/container_expiration_policies_helper_spec.rb'
- './spec/helpers/container_registry_helper_spec.rb'
- './spec/helpers/container_registry/container_registry_helper_spec.rb'
- './spec/helpers/cookies_helper_spec.rb'
- './spec/helpers/dashboard_helper_spec.rb'
- './spec/helpers/deploy_tokens_helper_spec.rb'

View File

@ -8,13 +8,13 @@ RSpec.describe 'result matchers', feature_category: :remote_development do
include ResultMatchers
it 'works with value asserted via argument' do
expect(Result.ok(1)).to be_ok_result(1)
expect(Result.ok(1)).not_to be_ok_result(2)
expect(Result.ok(1)).not_to be_err_result(1)
expect(Gitlab::Fp::Result.ok(1)).to be_ok_result(1)
expect(Gitlab::Fp::Result.ok(1)).not_to be_ok_result(2)
expect(Gitlab::Fp::Result.ok(1)).not_to be_err_result(1)
end
it 'works with value asserted via block' do
expect(Result.err('hello')).to be_err_result do |result_value|
expect(Gitlab::Fp::Result.err('hello')).to be_err_result do |result_value|
expect(result_value).to match(/hello/i)
end
end