Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b29b091f52
commit
d8aae64906
|
|
@ -5,8 +5,6 @@
|
|||
# If it turns out that a method you are attempting to remove is in fact in use,
|
||||
# remove it from this file and add it to `excluded_methods.yml`.
|
||||
#
|
||||
tag_pair_for_link:
|
||||
file: ee/app/helpers/admin/application_settings_helper.rb
|
||||
compliance_frameworks_list_data:
|
||||
file: ee/app/helpers/compliance_management/compliance_framework/group_settings_helper.rb
|
||||
project_vulnerability_path:
|
||||
|
|
|
|||
|
|
@ -131,7 +131,6 @@ RSpec/BeEq:
|
|||
- 'ee/spec/lib/gitlab/database/desired_sharding_key_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/duo/chat/react_executor_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/duo_workflow/client_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/elastic/document_reference_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/elastic/elasticsearch_enabled_cache_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/geo/event_gap_tracking_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/geo/oauth/login_state_spec.rb'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
RSpec/IdenticalEqualityAssertion:
|
||||
Exclude:
|
||||
- 'ee/spec/lib/gitlab/ci/reports/license_scanning/license_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/elastic/document_reference_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/geo/oauth/login_state_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/llm/ai_message_spec.rb'
|
||||
- 'ee/spec/lib/search/cluster_health_check/elastic_spec.rb'
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ Style/SymbolProc:
|
|||
- 'ee/spec/features/billings/qrtly_reconciliation_alert_spec.rb'
|
||||
- 'ee/spec/helpers/ee/registrations_helper_spec.rb'
|
||||
- 'ee/spec/lib/ee/gitlab/search_results_spec.rb'
|
||||
- 'ee/spec/lib/gitlab/elastic/document_reference_spec.rb'
|
||||
- 'ee/spec/services/groups/participants_service_spec.rb'
|
||||
- 'ee/spec/support/helpers/subscription_portal_helpers.rb'
|
||||
- 'ee/spec/support/shared_examples/lib/gitlab/graphql/issuables_lazy_links_aggregate_shared_examples.rb'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
02eb9160dd6a4825e892b22eb5543c5c1da5cc65
|
||||
398249273423b358d1c226d6379391f5bf0c927c
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { createAlert } from '~/alert';
|
|||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __, s__ } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { BULK_UPDATE_UNASSIGNED } from '../../constants';
|
||||
import workItemBulkUpdateMutation from '../../graphql/list/work_item_bulk_update.mutation.graphql';
|
||||
import workItemParent from '../../graphql/list/work_item_parent.query.graphql';
|
||||
|
|
@ -36,6 +37,7 @@ export default {
|
|||
WorkItemBulkEditDropdown,
|
||||
WorkItemBulkEditLabels,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
inject: ['hasIssuableHealthStatusFeature'],
|
||||
props: {
|
||||
checkedItems: {
|
||||
|
|
@ -74,14 +76,16 @@ export default {
|
|||
query: workItemParent,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
fullPath: this.isGroup
|
||||
? this.fullPath
|
||||
: this.fullPath.substring(0, this.fullPath.lastIndexOf('/')),
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data.namespace.id;
|
||||
},
|
||||
skip() {
|
||||
return !this.isEpicsList;
|
||||
return !this.shouldUseGraphQLBulkEdit;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -91,12 +95,17 @@ export default {
|
|||
const basePath = this.isGroup ? `groups/${this.fullPath}` : this.fullPath;
|
||||
return `${domain}/${basePath}/-/issues/bulk_update`;
|
||||
},
|
||||
shouldUseGraphQLBulkEdit() {
|
||||
return this.isEpicsList || this.glFeatures.workItemsBulkEdit;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async handleFormSubmitted() {
|
||||
this.$emit('start');
|
||||
|
||||
const executeBulkEdit = this.isEpicsList ? this.performBulkEdit : this.performLegacyBulkEdit;
|
||||
const executeBulkEdit = this.shouldUseGraphQLBulkEdit
|
||||
? this.performBulkEdit
|
||||
: this.performLegacyBulkEdit;
|
||||
|
||||
try {
|
||||
await executeBulkEdit();
|
||||
|
|
@ -154,7 +163,7 @@ export default {
|
|||
<template>
|
||||
<gl-form id="work-item-list-bulk-edit" class="gl-p-5" @submit.prevent="handleFormSubmitted">
|
||||
<work-item-bulk-edit-dropdown
|
||||
v-if="!isEpicsList"
|
||||
v-if="!shouldUseGraphQLBulkEdit"
|
||||
v-model="state"
|
||||
:header-text="__('Select state')"
|
||||
:items="$options.stateItems"
|
||||
|
|
@ -162,7 +171,7 @@ export default {
|
|||
data-testid="bulk-edit-state"
|
||||
/>
|
||||
<work-item-bulk-edit-assignee
|
||||
v-if="!isEpicsList"
|
||||
v-if="!shouldUseGraphQLBulkEdit"
|
||||
v-model="assigneeId"
|
||||
:full-path="fullPath"
|
||||
:is-group="isGroup"
|
||||
|
|
@ -183,7 +192,7 @@ export default {
|
|||
@select="removeLabelIds = $event"
|
||||
/>
|
||||
<work-item-bulk-edit-dropdown
|
||||
v-if="!isEpicsList && hasIssuableHealthStatusFeature"
|
||||
v-if="!shouldUseGraphQLBulkEdit && hasIssuableHealthStatusFeature"
|
||||
v-model="healthStatus"
|
||||
:header-text="__('Select health status')"
|
||||
:items="$options.healthStatusItems"
|
||||
|
|
@ -191,7 +200,7 @@ export default {
|
|||
data-testid="bulk-edit-health-status"
|
||||
/>
|
||||
<work-item-bulk-edit-dropdown
|
||||
v-if="!isEpicsList"
|
||||
v-if="!shouldUseGraphQLBulkEdit"
|
||||
v-model="subscription"
|
||||
:header-text="__('Select subscription')"
|
||||
:items="$options.subscriptionItems"
|
||||
|
|
@ -199,7 +208,7 @@ export default {
|
|||
data-testid="bulk-edit-subscription"
|
||||
/>
|
||||
<work-item-bulk-edit-dropdown
|
||||
v-if="!isEpicsList"
|
||||
v-if="!shouldUseGraphQLBulkEdit"
|
||||
v-model="confidentiality"
|
||||
:header-text="__('Select confidentiality')"
|
||||
:items="$options.confidentialityItems"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ module Groups
|
|||
push_frontend_feature_flag(:issues_list_drawer, group)
|
||||
push_frontend_feature_flag(:work_item_status_feature_flag, group&.root_ancestor)
|
||||
push_frontend_feature_flag(:work_item_planning_view, group)
|
||||
push_frontend_feature_flag(:work_items_bulk_edit, group&.root_ancestor)
|
||||
end
|
||||
before_action :handle_new_work_item_path, only: [:show]
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
push_force_frontend_feature_flag(:work_items_alpha, !!project&.work_items_alpha_feature_flag_enabled?)
|
||||
push_frontend_feature_flag(:work_item_view_for_issues, project&.group)
|
||||
push_frontend_feature_flag(:work_item_status_feature_flag, project&.root_ancestor)
|
||||
push_frontend_feature_flag(:work_items_bulk_edit, project&.root_ancestor)
|
||||
push_frontend_feature_flag(:hide_incident_management_features, project)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class Projects::WorkItemsController < Projects::ApplicationController
|
|||
push_force_frontend_feature_flag(:continue_indented_text, !!project&.continue_indented_text_feature_flag_enabled?)
|
||||
push_frontend_feature_flag(:work_item_status_feature_flag, project&.root_ancestor)
|
||||
push_frontend_feature_flag(:work_item_planning_view, project&.group)
|
||||
push_frontend_feature_flag(:work_items_bulk_edit, project&.root_ancestor)
|
||||
end
|
||||
|
||||
feature_category :team_planning
|
||||
|
|
|
|||
|
|
@ -91,12 +91,14 @@ module Namespaces
|
|||
items
|
||||
end
|
||||
|
||||
def sort(items)
|
||||
return items.projects_order_id_desc unless params[:sort]
|
||||
def should_sort_by_similarity?
|
||||
params[:search].present? && (params[:sort].nil? || params[:sort].to_s == 'similarity')
|
||||
end
|
||||
|
||||
if params[:sort] == :similarity && params[:search].present?
|
||||
return items.sorted_by_similarity_desc(params[:search])
|
||||
end
|
||||
def sort(items)
|
||||
return items.sorted_by_similarity_desc(params[:search]) if should_sort_by_similarity?
|
||||
|
||||
return items.projects_order_id_desc unless params[:sort]
|
||||
|
||||
items.sort_by_attribute(params[:sort])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -279,10 +279,14 @@ class ProjectsFinder < UnionFinder
|
|||
items
|
||||
end
|
||||
|
||||
def sort(items)
|
||||
return items.projects_order_id_desc unless params[:sort]
|
||||
def should_sort_by_similarity?
|
||||
params[:search].present? && (params[:sort].nil? || params[:sort].to_s == 'similarity')
|
||||
end
|
||||
|
||||
return items.sorted_by_similarity_desc(params[:search]) if params[:sort] == 'similarity' && params[:search].present?
|
||||
def sort(items)
|
||||
return items.sorted_by_similarity_desc(params[:search]) if should_sort_by_similarity?
|
||||
|
||||
return items.projects_order_id_desc unless params[:sort]
|
||||
|
||||
items.sort_by_attribute(params[:sort])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module WorkItems
|
||||
module CSV
|
||||
class Import < BaseMutation
|
||||
graphql_name 'WorkItemsCsvImport'
|
||||
|
||||
include FindsProject
|
||||
|
||||
EXTENSION_ALLOWLIST = %w[csv].map(&:downcase).freeze
|
||||
|
||||
authorize :import_work_items
|
||||
|
||||
argument :project_path, GraphQL::Types::ID,
|
||||
required: true,
|
||||
description: 'Full project path.'
|
||||
|
||||
argument :file, ApolloUploadServer::Upload,
|
||||
required: true,
|
||||
description: 'CSV file to import work items from.'
|
||||
|
||||
field :message, GraphQL::Types::String,
|
||||
null: true,
|
||||
description: 'Import request result message.'
|
||||
|
||||
def resolve(args)
|
||||
project_path = args.delete(:project_path)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
validate_import_available_for!(project)
|
||||
|
||||
file = args[:file]
|
||||
|
||||
unless file_is_valid?(file)
|
||||
return {
|
||||
message: nil,
|
||||
errors: [invalid_file_message]
|
||||
}
|
||||
end
|
||||
|
||||
result = ::WorkItems::PrepareImportCsvService.new(project, current_user, file:).execute
|
||||
|
||||
return { message: result.message, errors: [] } if result.success?
|
||||
|
||||
{ message: nil, errors: [result.message] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_import_available_for!(project)
|
||||
return if Feature.enabled?(:import_export_work_items_csv, project)
|
||||
|
||||
raise_resource_not_available_error! '`import_export_work_items_csv` feature flag is disabled.'
|
||||
end
|
||||
|
||||
def file_is_valid?(file)
|
||||
return false unless file.respond_to?(:original_filename)
|
||||
|
||||
file_extension = File.extname(file.original_filename).downcase.delete('.')
|
||||
EXTENSION_ALLOWLIST.include?(file_extension)
|
||||
end
|
||||
|
||||
def invalid_file_message
|
||||
supported_file_extensions = ".#{EXTENSION_ALLOWLIST.join(', .')}"
|
||||
format(_("The uploaded file was invalid. Supported file extensions are %{extensions}."),
|
||||
{ extensions: supported_file_extensions })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -26,9 +26,9 @@ module ProjectSearchArguments
|
|||
|
||||
argument :sort, GraphQL::Types::String,
|
||||
required: false,
|
||||
default_value: 'id_desc',
|
||||
default_value: nil,
|
||||
description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \
|
||||
"for example: `id_desc` or `name_asc`"
|
||||
"for example: `id_desc` or `name_asc`. Defaults to `id_desc`, or `similarity` if search used."
|
||||
|
||||
argument :namespace_path, GraphQL::Types::ID,
|
||||
required: false,
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ module Types
|
|||
mount_mutation Mutations::WorkItems::Delete, experiment: { milestone: '15.1' }
|
||||
mount_mutation Mutations::WorkItems::Update, experiment: { milestone: '15.1' }
|
||||
mount_mutation Mutations::WorkItems::CSV::Export, experiment: { milestone: '15.10' }
|
||||
mount_mutation Mutations::WorkItems::CSV::Import, experiment: { milestone: '18.2' }
|
||||
mount_mutation Mutations::WorkItems::Convert, experiment: { milestone: '15.11' }
|
||||
mount_mutation Mutations::WorkItems::LinkedItems::Add, experiment: { milestone: '16.3' }
|
||||
mount_mutation Mutations::WorkItems::LinkedItems::Remove, experiment: { milestone: '16.3' }
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ module Ci
|
|||
query.select(:last_used)
|
||||
end
|
||||
|
||||
scope :with_token, ->(tokens) { where(token: Array.wrap(tokens).compact.reject(&:blank?)) }
|
||||
|
||||
def set_default_values
|
||||
self.token = "#{TRIGGER_TOKEN_PREFIX}#{SecureRandom.hex(20)}" if self.token.blank?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,24 +19,14 @@ module ContainerRegistry
|
|||
|
||||
validate :validate_access_levels
|
||||
|
||||
scope :immutable, -> { where(immutable_where_conditions) }
|
||||
scope :mutable, -> { where.not(immutable_where_conditions) }
|
||||
scope :mutable, -> { where.not(minimum_access_level_for_push: nil, minimum_access_level_for_delete: nil) }
|
||||
|
||||
scope :for_actions_and_access, ->(actions, access_level, include_immutable: true) do
|
||||
conditions = []
|
||||
|
||||
conditions << arel_table[:minimum_access_level_for_push].gt(access_level) if actions.include?('push')
|
||||
conditions << arel_table[:minimum_access_level_for_delete].gt(access_level) if actions.include?('delete')
|
||||
|
||||
if include_immutable && (actions & %w[push delete]).any?
|
||||
immutable_where_conditions.each { |column, value| conditions << arel_table[column].eq(value) }
|
||||
end
|
||||
|
||||
where(conditions.reduce(:or))
|
||||
scope :for_actions_and_access, ->(actions, access_level) do
|
||||
where(base_conditions_for_actions_and_access(actions, access_level).reduce(:or))
|
||||
end
|
||||
|
||||
scope :for_delete_and_access, ->(access_level, include_immutable: true) do
|
||||
for_actions_and_access(DELETE_ACTIONS, access_level, include_immutable:)
|
||||
scope :for_delete_and_access, ->(access_level) do
|
||||
for_actions_and_access(DELETE_ACTIONS, access_level)
|
||||
end
|
||||
|
||||
scope :tag_name_patterns_for_project, ->(project_id) do
|
||||
|
|
@ -45,26 +35,21 @@ module ContainerRegistry
|
|||
|
||||
scope :pluck_tag_name_patterns, ->(limit = MAX_TAG_RULES_PER_PROJECT) { limit(limit).pluck(:tag_name_pattern) }
|
||||
|
||||
def self.immutable_where_conditions
|
||||
{ minimum_access_level_for_push: nil, minimum_access_level_for_delete: nil }
|
||||
def self.base_conditions_for_actions_and_access(actions, access_level)
|
||||
conditions = []
|
||||
conditions << arel_table[:minimum_access_level_for_push].gt(access_level) if actions.include?('push')
|
||||
conditions << arel_table[:minimum_access_level_for_delete].gt(access_level) if actions.include?('delete')
|
||||
conditions
|
||||
end
|
||||
|
||||
def push_restricted?(access_level)
|
||||
return Feature.enabled?(:container_registry_immutable_tags, project) if immutable?
|
||||
|
||||
Gitlab::Access.sym_options_with_admin[minimum_access_level_for_push.to_sym] > access_level
|
||||
end
|
||||
|
||||
def delete_restricted?(access_level)
|
||||
return Feature.enabled?(:container_registry_immutable_tags, project) if immutable?
|
||||
|
||||
Gitlab::Access.sym_options_with_admin[minimum_access_level_for_delete.to_sym] > access_level
|
||||
end
|
||||
|
||||
def immutable?
|
||||
minimum_access_level_for_push.nil? && minimum_access_level_for_delete.nil?
|
||||
end
|
||||
|
||||
def mutable?
|
||||
[minimum_access_level_for_push, minimum_access_level_for_delete].all?(&:present?)
|
||||
end
|
||||
|
|
@ -73,10 +58,7 @@ module ContainerRegistry
|
|||
return false if user.nil?
|
||||
return true if user.can_admin_all_resources?
|
||||
|
||||
user_access_level = project.team.max_member_access(user.id)
|
||||
minimum_level_to_delete_rule = immutable? ? Gitlab::Access::OWNER : Gitlab::Access::MAINTAINER
|
||||
|
||||
minimum_level_to_delete_rule <= user_access_level
|
||||
minimum_level_to_delete_rule <= project.team.max_member_access(user.id)
|
||||
end
|
||||
|
||||
def matches_tag_name?(name)
|
||||
|
|
@ -86,10 +68,16 @@ module ContainerRegistry
|
|||
private
|
||||
|
||||
def validate_access_levels
|
||||
return unless minimum_access_level_for_delete.present? ^ minimum_access_level_for_push.present?
|
||||
return if [minimum_access_level_for_delete, minimum_access_level_for_push].all?(&:present?)
|
||||
|
||||
errors.add(:base, _('Access levels should either both be present or both be nil'))
|
||||
errors.add(:base, _('Access levels should both be present'))
|
||||
end
|
||||
|
||||
def minimum_level_to_delete_rule
|
||||
Gitlab::Access::MAINTAINER
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ContainerRegistry::Protection::TagRule.prepend_mod
|
||||
|
|
|
|||
|
|
@ -318,8 +318,7 @@ class ContainerRepository < ApplicationRecord
|
|||
# Check for mutable tag protection rules
|
||||
return false unless project.has_container_registry_protected_tag_rules?(
|
||||
action: 'delete',
|
||||
access_level: project.team.max_member_access(user.id),
|
||||
include_immutable: false
|
||||
access_level: project.team.max_member_access(user.id)
|
||||
)
|
||||
|
||||
return false unless has_tags?
|
||||
|
|
|
|||
|
|
@ -3615,9 +3615,9 @@ class Project < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def has_container_registry_protected_tag_rules?(action:, access_level:, include_immutable: true)
|
||||
strong_memoize_with(:has_container_registry_protected_tag_rules, action, access_level, include_immutable) do
|
||||
container_registry_protection_tag_rules.for_actions_and_access([action], access_level, include_immutable:).exists?
|
||||
def has_container_registry_protected_tag_rules?(action:, access_level:)
|
||||
strong_memoize_with(:has_container_registry_protected_tag_rules, action, access_level) do
|
||||
container_registry_protection_tag_rules.for_actions_and_access([action], access_level).exists?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ module Auth
|
|||
return patterns if user.can_admin_all_resources?
|
||||
|
||||
user_access_level = project.team.max_member_access(user.id)
|
||||
applicable_rules = rules.for_actions_and_access(actions_to_check, user_access_level, include_immutable: false)
|
||||
applicable_rules = rules.for_actions_and_access(actions_to_check, user_access_level)
|
||||
|
||||
applicable_rules.each do |rule|
|
||||
if actions_to_check.include?('push') && rule.push_restricted?(user_access_level)
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ module ContainerRegistry
|
|||
|
||||
return false unless project.has_container_registry_protected_tag_rules?(
|
||||
action: 'delete',
|
||||
access_level: project.team.max_member_access(current_user.id),
|
||||
include_immutable: false
|
||||
access_level: project.team.max_member_access(current_user.id)
|
||||
)
|
||||
|
||||
project.has_container_registry_tags?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: work_items_bulk_edit
|
||||
description: Enables GraphQL-based bulk editing for work items instead of the legacy REST API
|
||||
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11901
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195770
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/551187
|
||||
milestone: '18.2'
|
||||
group: group::product planning
|
||||
type: wip
|
||||
default_enabled: false
|
||||
|
|
@ -1305,7 +1305,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="queryprojectsprogramminglanguagename"></a>`programmingLanguageName` | [`String`](#string) | Filter projects by programming language name (case insensitive). For example: "css" or "ruby". |
|
||||
| <a id="queryprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. |
|
||||
| <a id="queryprojectssearchnamespaces"></a>`searchNamespaces` | [`Boolean`](#boolean) | Include namespace in project search. |
|
||||
| <a id="queryprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: `<field_name>_<sort_direction>`, for example: `id_desc` or `name_asc`. |
|
||||
| <a id="queryprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: `<field_name>_<sort_direction>`, for example: `id_desc` or `name_asc`. Defaults to `id_desc`, or `similarity` if search used. |
|
||||
| <a id="queryprojectstopics"></a>`topics` | [`[String!]`](#string) | Filter projects by topics. |
|
||||
| <a id="queryprojectstrending"></a>`trending` | [`Boolean`](#boolean) | Return only projects that are trending. |
|
||||
| <a id="queryprojectsvisibilitylevel"></a>`visibilityLevel` | [`VisibilityLevelsEnum`](#visibilitylevelsenum) | Filter projects by visibility level. |
|
||||
|
|
@ -1700,6 +1700,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="queryvulnerabilitiesowasptopten"></a>`owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 2017 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 values. |
|
||||
| <a id="queryvulnerabilitiesowasptopten2021"></a>`owaspTopTen2021` {{< icon name="warning-solid" >}} | [`[VulnerabilityOwasp2021Top10!]`](#vulnerabilityowasp2021top10) | **Introduced** in GitLab 18.1. **Status**: Experiment. Filter vulnerabilities by OWASP Top 10 2021 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 2021 values. To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. |
|
||||
| <a id="queryvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
|
||||
| <a id="queryvulnerabilitiesreachability"></a>`reachability` {{< icon name="warning-solid" >}} | [`ReachabilityType`](#reachabilitytype) | **Introduced** in GitLab 18.2. **Status**: Experiment. Filter vulnerabilities by reachability. |
|
||||
| <a id="queryvulnerabilitiesreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. |
|
||||
| <a id="queryvulnerabilitiesscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. |
|
||||
| <a id="queryvulnerabilitiesscannerid"></a>`scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. |
|
||||
|
|
@ -13386,6 +13387,31 @@ Input type: `WorkItemsCsvExportInput`
|
|||
| <a id="mutationworkitemscsvexporterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
|
||||
| <a id="mutationworkitemscsvexportmessage"></a>`message` | [`String`](#string) | Export request result message. |
|
||||
|
||||
### `Mutation.workItemsCsvImport`
|
||||
|
||||
{{< details >}}
|
||||
**Introduced** in GitLab 18.2.
|
||||
**Status**: Experiment.
|
||||
{{< /details >}}
|
||||
|
||||
Input type: `WorkItemsCsvImportInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationworkitemscsvimportclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationworkitemscsvimportfile"></a>`file` | [`Upload!`](#upload) | CSV file to import work items from. |
|
||||
| <a id="mutationworkitemscsvimportprojectpath"></a>`projectPath` | [`ID!`](#id) | Full project path. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationworkitemscsvimportclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationworkitemscsvimporterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during the mutation. |
|
||||
| <a id="mutationworkitemscsvimportmessage"></a>`message` | [`String`](#string) | Import request result message. |
|
||||
|
||||
### `Mutation.workItemsHierarchyReorder`
|
||||
|
||||
Reorder a work item in the hierarchy tree.
|
||||
|
|
@ -24049,7 +24075,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="cirunnerprojectspersonal"></a>`personal` | [`Boolean`](#boolean) | Return only personal projects. |
|
||||
| <a id="cirunnerprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. |
|
||||
| <a id="cirunnerprojectssearchnamespaces"></a>`searchNamespaces` | [`Boolean`](#boolean) | Include namespace in project search. |
|
||||
| <a id="cirunnerprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: `<field_name>_<sort_direction>`, for example: `id_desc` or `name_asc`. |
|
||||
| <a id="cirunnerprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: `<field_name>_<sort_direction>`, for example: `id_desc` or `name_asc`. Defaults to `id_desc`, or `similarity` if search used. |
|
||||
| <a id="cirunnerprojectstopics"></a>`topics` | [`[String!]`](#string) | Filter projects by topics. |
|
||||
|
||||
### `CiRunnerCloudProvisioningStep`
|
||||
|
|
@ -30070,6 +30096,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="groupvulnerabilitiesowasptopten"></a>`owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 2017 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 values. |
|
||||
| <a id="groupvulnerabilitiesowasptopten2021"></a>`owaspTopTen2021` {{< icon name="warning-solid" >}} | [`[VulnerabilityOwasp2021Top10!]`](#vulnerabilityowasp2021top10) | **Introduced** in GitLab 18.1. **Status**: Experiment. Filter vulnerabilities by OWASP Top 10 2021 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 2021 values. To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. |
|
||||
| <a id="groupvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
|
||||
| <a id="groupvulnerabilitiesreachability"></a>`reachability` {{< icon name="warning-solid" >}} | [`ReachabilityType`](#reachabilitytype) | **Introduced** in GitLab 18.2. **Status**: Experiment. Filter vulnerabilities by reachability. |
|
||||
| <a id="groupvulnerabilitiesreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. |
|
||||
| <a id="groupvulnerabilitiesscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. |
|
||||
| <a id="groupvulnerabilitiesscannerid"></a>`scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. |
|
||||
|
|
@ -30164,6 +30191,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount).
|
|||
| <a id="groupvulnerabilityseveritiescountowasptopten"></a>`owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 2017 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 values. |
|
||||
| <a id="groupvulnerabilityseveritiescountowasptopten2021"></a>`owaspTopTen2021` {{< icon name="warning-solid" >}} | [`[VulnerabilityOwasp2021Top10!]`](#vulnerabilityowasp2021top10) | **Introduced** in GitLab 18.1. **Status**: Experiment. Filter vulnerabilities by OWASP Top 10 2021 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 2021 values. To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. |
|
||||
| <a id="groupvulnerabilityseveritiescountprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
|
||||
| <a id="groupvulnerabilityseveritiescountreachability"></a>`reachability` {{< icon name="warning-solid" >}} | [`ReachabilityType`](#reachabilitytype) | **Introduced** in GitLab 18.2. **Status**: Experiment. Filter vulnerabilities by reachability. |
|
||||
| <a id="groupvulnerabilityseveritiescountreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. |
|
||||
| <a id="groupvulnerabilityseveritiescountscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by scanner. |
|
||||
| <a id="groupvulnerabilityseveritiescountscannerid"></a>`scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. |
|
||||
|
|
@ -31106,6 +31134,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount).
|
|||
| <a id="instancesecuritydashboardvulnerabilityseveritiescountowasptopten"></a>`owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 2017 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 values. |
|
||||
| <a id="instancesecuritydashboardvulnerabilityseveritiescountowasptopten2021"></a>`owaspTopTen2021` {{< icon name="warning-solid" >}} | [`[VulnerabilityOwasp2021Top10!]`](#vulnerabilityowasp2021top10) | **Introduced** in GitLab 18.1. **Status**: Experiment. Filter vulnerabilities by OWASP Top 10 2021 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 2021 values. To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. |
|
||||
| <a id="instancesecuritydashboardvulnerabilityseveritiescountprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
|
||||
| <a id="instancesecuritydashboardvulnerabilityseveritiescountreachability"></a>`reachability` {{< icon name="warning-solid" >}} | [`ReachabilityType`](#reachabilitytype) | **Introduced** in GitLab 18.2. **Status**: Experiment. Filter vulnerabilities by reachability. |
|
||||
| <a id="instancesecuritydashboardvulnerabilityseveritiescountreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. |
|
||||
| <a id="instancesecuritydashboardvulnerabilityseveritiescountscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by scanner. |
|
||||
| <a id="instancesecuritydashboardvulnerabilityseveritiescountscannerid"></a>`scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. |
|
||||
|
|
@ -35006,7 +35035,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="organizationprojectsprogramminglanguagename"></a>`programmingLanguageName` | [`String`](#string) | Filter projects by programming language name (case insensitive). For example: "css" or "ruby". |
|
||||
| <a id="organizationprojectssearch"></a>`search` | [`String`](#string) | Search query, which can be for the project name, a path, or a description. |
|
||||
| <a id="organizationprojectssearchnamespaces"></a>`searchNamespaces` | [`Boolean`](#boolean) | Include namespace in project search. |
|
||||
| <a id="organizationprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: `<field_name>_<sort_direction>`, for example: `id_desc` or `name_asc`. |
|
||||
| <a id="organizationprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: `<field_name>_<sort_direction>`, for example: `id_desc` or `name_asc`. Defaults to `id_desc`, or `similarity` if search used. |
|
||||
| <a id="organizationprojectstopics"></a>`topics` | [`[String!]`](#string) | Filter projects by topics. |
|
||||
| <a id="organizationprojectstrending"></a>`trending` | [`Boolean`](#boolean) | Return only projects that are trending. |
|
||||
| <a id="organizationprojectsvisibilitylevel"></a>`visibilityLevel` | [`VisibilityLevelsEnum`](#visibilitylevelsenum) | Filter projects by visibility level. |
|
||||
|
|
@ -38454,6 +38483,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="projectvulnerabilitiesowasptopten"></a>`owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 2017 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 values. |
|
||||
| <a id="projectvulnerabilitiesowasptopten2021"></a>`owaspTopTen2021` {{< icon name="warning-solid" >}} | [`[VulnerabilityOwasp2021Top10!]`](#vulnerabilityowasp2021top10) | **Introduced** in GitLab 18.1. **Status**: Experiment. Filter vulnerabilities by OWASP Top 10 2021 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 2021 values. To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. |
|
||||
| <a id="projectvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
|
||||
| <a id="projectvulnerabilitiesreachability"></a>`reachability` {{< icon name="warning-solid" >}} | [`ReachabilityType`](#reachabilitytype) | **Introduced** in GitLab 18.2. **Status**: Experiment. Filter vulnerabilities by reachability. |
|
||||
| <a id="projectvulnerabilitiesreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. |
|
||||
| <a id="projectvulnerabilitiesscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. |
|
||||
| <a id="projectvulnerabilitiesscannerid"></a>`scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. |
|
||||
|
|
@ -38535,6 +38565,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount).
|
|||
| <a id="projectvulnerabilityseveritiescountowasptopten"></a>`owaspTopTen` | [`[VulnerabilityOwaspTop10!]`](#vulnerabilityowasptop10) | Filter vulnerabilities by OWASP Top 10 2017 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 values. |
|
||||
| <a id="projectvulnerabilityseveritiescountowasptopten2021"></a>`owaspTopTen2021` {{< icon name="warning-solid" >}} | [`[VulnerabilityOwasp2021Top10!]`](#vulnerabilityowasp2021top10) | **Introduced** in GitLab 18.1. **Status**: Experiment. Filter vulnerabilities by OWASP Top 10 2021 category. Wildcard value `NONE` is also supported but it cannot be combined with other OWASP top 10 2021 values. To use this argument, you must have Elasticsearch configured and the `advanced_vulnerability_management` feature flag enabled. Not supported on Instance Security Dashboard queries. |
|
||||
| <a id="projectvulnerabilityseveritiescountprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
|
||||
| <a id="projectvulnerabilityseveritiescountreachability"></a>`reachability` {{< icon name="warning-solid" >}} | [`ReachabilityType`](#reachabilitytype) | **Introduced** in GitLab 18.2. **Status**: Experiment. Filter vulnerabilities by reachability. |
|
||||
| <a id="projectvulnerabilityseveritiescountreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. |
|
||||
| <a id="projectvulnerabilityseveritiescountscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by scanner. |
|
||||
| <a id="projectvulnerabilityseveritiescountscannerid"></a>`scannerId` | [`[VulnerabilitiesScannerID!]`](#vulnerabilitiesscannerid) | Filter vulnerabilities by scanner ID. |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
---
|
||||
stage: AI-powered
|
||||
group: AI Framework
|
||||
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review.
|
||||
title: AI Evaluation Guidelines
|
||||
---
|
||||
|
||||
Unlike traditional software systems that behave more-or-less predictably, minor input changes can cause AI-powered systems to produce significantly different outputs. This unpredictability stems from the non-deterministic nature of AI-generated responses. Traditional software testing methods are not designed to handle such variability, which is why AI evaluation has become essential. AI evaluation is a data-driven, quantitative process that analyzes AI outputs to assess system performance, quality, and reliability.
|
||||
|
||||
The [Centralized Evaluation Framework (CEF)](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/prompt-library) provides a streamlined, unified approach to evaluating AI features at GitLab.
|
||||
It is essential to [our strategy for ensuring the quality of our AI-powered features (internal)](https://internal.gitlab.com/handbook/product/ai-strategy/ai-integration-effort/ai_testing_and_evaluation).
|
||||
|
||||
Conceptually, there are three parts to an evaluation:
|
||||
|
||||
1. **Dataset**: A collection of test inputs (and, optionally, expected outputs).
|
||||
1. **Target**: The target of the evaluation. For example, a prompt, an agent, a tool, a feature, a system component, or the application end-to-end.
|
||||
1. **Metrics**: Measurable criteria used to assess the AI-generated output.
|
||||
|
||||
Each part plays a role in the evaluation process, as described below:
|
||||
|
||||
1. **Establish acceptance criteria**: Define metrics to indicate correct target behavior.
|
||||
1. **Design evaluations**: Design evaluators and scenarios to score the metrics to assess the criteria.
|
||||
1. **Create a dataset**: Collect representative examples covering typical usage patterns, edge cases, and error conditions.
|
||||
1. **Execute**: Run evaluations of the target against the dataset.
|
||||
1. **Analyze results**: Compare results with acceptance criteria and identify areas for improvement.
|
||||
1. **Iterate and refine**: Make necessary adjustments based on evaluation findings.
|
||||
|
||||
## Establish acceptance criteria
|
||||
|
||||
Define metrics to determine when the target AI feature or component is working correctly.
|
||||
The chosen metrics should align with success metrics that determine when desired business outcomes have been met.
|
||||
|
||||
### Types of metrics
|
||||
|
||||
The following are examples of metrics that might be relevant:
|
||||
|
||||
- **Accuracy**: Measures how often AI predictions are correct.
|
||||
- **Precision and Recall**: Evaluate the balance between correctly identified positive results and the number of actual positives.
|
||||
- **F1 score**: Combines precision and recall into a single metric.
|
||||
- **Latency**: Measures the time taken to produce a response.
|
||||
- **Token usage**: Evaluates the efficiency of the model in terms of token consumption.
|
||||
- **Conciseness and Coherence**: Assess the clarity and logical consistency of the AI output.
|
||||
|
||||
Please note that for some targets, domain-specific metrics are essential, perhaps even more important than the general metrics listed here.
|
||||
In some cases, choosing the right metric is a gradual, iterative process of discovery and experimentation involving multiple teams as well as feedback from users.
|
||||
|
||||
### Define thresholds
|
||||
|
||||
Establish clear thresholds for each metric if possible, such as minimum acceptable performance. For example:
|
||||
|
||||
- Accuracy: ≥85% of explanations are technically correct
|
||||
- Latency: ≤3 seconds for 95th percentile response time
|
||||
|
||||
Note that it might not be feasible to define a threshold for novel metrics. This particularly applies to domain-specific metrics.
|
||||
In general, we rely on user expectations to define thresholds for acceptable performance.
|
||||
In some cases we'll know what users will expect before releasing a feature and can define thresholds accordingly.
|
||||
In other cases we'll need to wait until we get feedback before we know what threshold to set.
|
||||
|
||||
## Design evaluations
|
||||
|
||||
When designing an evaluation, you define how you'll measure the target AI feature or component performance against acceptance criteria.
|
||||
This involves choosing the right evaluators.
|
||||
[Evaluators](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/prompt-library/-/tree/main/doc/evaluators) are functions that score the target AI performance on specific metrics.
|
||||
Designing evaluations can also involve creating scenarios that test the target AI feature or component under realistic conditions.
|
||||
You can implement different scenarios as distinct categories of dataset examples, or as variations in how the evaluation invokes the target AI feature or component.
|
||||
|
||||
Scenarios to consider include:
|
||||
|
||||
- **Baseline comparisons**: Compare new models or prompts against a baseline to determine improvements.
|
||||
- **Side-by-side evaluations**: Compare different models, prompts, or configurations directly against each other.
|
||||
- **Custom evaluators**: Implement custom evaluation functions to test specific aspects of AI performance relevant to your application's needs.
|
||||
- **Dataset sampling**: Sample different subsets of the dataset that focus on different aspects of the target.
|
||||
|
||||
## Create a dataset
|
||||
|
||||
A well-structured dataset enables consistent testing and validation of an AI system or component across different scenarios and use cases.
|
||||
|
||||
For an overview of working with datasets in the CEF and LangSmith, see the [dataset management](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/datasets/-/blob/main/doc/dataset_management.md) documentation.
|
||||
|
||||
For more detailed information on creating and preparing datasets for evaluation, see our [dataset creation guidelines](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/prompt-library/-/tree/main/doc/datasets#dataset-creation-guidelines-for-gitlab-ai-features) and [instructions for uploading datasets to LangSmith](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/datasets/-/blob/main/doc/guidelines/create_dataset.md).
|
||||
|
||||
### Synthetic prompt evaluation dataset generator
|
||||
|
||||
If you are evaluating a prompt, a quick way to get started is to use our [dataset generator](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/main/docs/evaluation/dataset_generation.md).
|
||||
It generates a synthetic evaluation dataset directly from an AI Gateway prompt definition.
|
||||
You can watch a quick [demonstration](https://www.youtube.com/watch?v=qZEnC4PN3Co).
|
||||
|
||||
## Execute evaluations
|
||||
|
||||
When an evaluation is executed, the CEF invokes the target AI feature or component at least once for each input example in the evaluation dataset.
|
||||
The framework then invokes evaluators to score the AI output, and provides you with the results of the evaluation.
|
||||
|
||||
### In merge requests
|
||||
|
||||
[Evaluation Runner](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/evaluation-runner) can be used to run an evaluation in a CI pipeline in a merge request. It spins up a new [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/) instance on a remote environment, runs an evaluation using the CEF, and reports the results in the CI job log. See the guide for [how to use evaluation runner](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/evaluation-runner#how-to-use).
|
||||
|
||||
### On your local machine
|
||||
|
||||
See the [step-by-step guide for conducting evaluations using the CEF](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/prompt-library/-/blob/main/doc/index.md?ref_type=heads).
|
||||
|
||||
## Analyze results
|
||||
|
||||
The CEF uses LangSmith to store and analyze evaluation results. See the [LangSmith guide for how to analyze an experiment](https://docs.smith.langchain.com/evaluation/how_to_guides/analyze_single_experiment).
|
||||
|
||||
For guidance regarding specific features, see the Analyze Results section of the feature-specific documentation for [running evaluations locally](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/prompt-library/-/tree/main/doc/running_evaluation_locally). You can also find some information about interpreting evaluation metrics in the [Duo Chat evaluation documentation](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/prompt-library/-/tree/main/doc/duo_chat).
|
||||
|
||||
Please note that we're updating the documentation on executing and interpreting the results of existing evaluation pipelines (see [#671](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/prompt-library/-/issues/671)).
|
||||
|
||||
## Iterate and refine
|
||||
|
||||
Similar to the [AI feature development process](ai_feature_development_playbook.md), iterating on evaluation means returning to previous steps as indicated by the evaluation results. [Prompt engineering](prompt_engineering.md) is key to this step. However, it might also involve adding examples to the dataset, editing existing examples, adjusting the design of the evaluations, or reviewing and revising the metrics and success criteria.
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [AI evaluation tooling](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation): The group containing AI evaluation tooling used at GitLab.
|
||||
- [AI evaluation and testing strategy (internal)](https://internal.gitlab.com/handbook/product/ai-strategy/ai-integration-effort/ai_testing_and_evaluation/): An overview of how testing and evaluation combine in our strategy for ensuring the quality of AI-powered features.
|
||||
- [LangSmith Evaluations YouTube playlist](https://www.youtube.com/playlist?list=PLfaIDFEXuae0um8Fj0V4dHG37fGFU8Q5S):
|
||||
Deep dive on evaluation with LangSmith.
|
||||
- [LangSmith Evaluation Cookbook](https://github.com/langchain-ai/langsmith-cookbook/blob/main/README.md#testing--evaluation):
|
||||
Contains various evaluation scenarios and examples.
|
||||
- [LangSmith How To Guides](https://docs.smith.langchain.com/evaluation/how_to_guides): Contains various how to
|
||||
walkthroughs.
|
||||
- [GitLab Duo Chat Documentation](duo_chat.md):
|
||||
Comprehensive guide on setting up and using LangSmith for chat evaluations.
|
||||
- [Prompt and AI Feature Evaluation Setup and Workflow](https://gitlab.com/groups/gitlab-org/-/epics/13952):
|
||||
Details on the overall workflow and setup for evaluations.
|
||||
|
|
@ -78,12 +78,7 @@ Teams implement evaluation strategies covering multiple aspects of the quality o
|
|||
- Performance testing
|
||||
- Security and safety validation
|
||||
- [Dataset creation](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/datasets/-/blob/main/doc/guidelines/create_dataset.md) and [management](https://gitlab.com/gitlab-org/modelops/ai-model-validation-and-research/ai-evaluation/datasets/-/blob/main/doc/dataset_management.md)
|
||||
- [Evaluation (internal)](https://internal.gitlab.com/handbook/product/ai-strategy/ai-integration-effort/ai_testing_and_evaluation/#test-2-ai-response-evaluation)
|
||||
- Feature evaluation
|
||||
- Prompt evaluation
|
||||
- Tool evaluation
|
||||
- Model evaluation
|
||||
- Latency evaluation
|
||||
- [Evaluation](ai_evaluation_guidelines.md)
|
||||
|
||||
#### Resources
|
||||
|
||||
|
|
|
|||
|
|
@ -181,37 +181,6 @@ For an overview, see [this video](https://youtu.be/-DK-XFFllwg).
|
|||
1. Adoption
|
||||
- Already in use by some teams, including code suggestions and create teams
|
||||
|
||||
## Evaluation & Monitoring
|
||||
|
||||
### Building Datasets for Eval
|
||||
|
||||
#### Why Do I Need A Dataset?
|
||||
|
||||
A dataset in its most simple form as a bunch of inputs with roughly expected outputs. Now there are cases (such as chat applications) where having a defined expected output is impossible, in which case a dataset is still very useful but the evaluation technique would change. For now as we are all more or less comfortable with the idea of testing code, let's keep it simple and work with datasets that have expected outputs.
|
||||
|
||||
Once we decide to start making an application, thinking about ways in which it could break and ways in which it should succeed is paramount. Having those potential input and their expected outputs collected in a dataset that we can run through our application is highly useful in both early and late development.
|
||||
|
||||
Once we have developed our application, being able to assure that it behaves as expected across a broad range of inputs is paramount. It is preferable to have as broad a range of prompts as we can achieve within reason. When we want to make a change to our prompt, tool selection, or choose a new model being able to compare how successful our changes are requires a dataset to evaluate against that possesses a large number of inputs paired with expected outputs.
|
||||
|
||||
#### Key Considerations When Making a Dataset
|
||||
|
||||
When first starting out building applications on top of LLMs, we will want to do evaluation. A common first question to ask is how many records should we have in our dataset. This question is a little premature for reasons that should be clear soon.
|
||||
|
||||
First things first, before thinking about how much data, lets think about how representative our data needs to be of the problem your app needs to solve and where we can get that.
|
||||
|
||||
As an example where we are constantly iterating on this at GitLab, let's consider the evaluation of our code completion offering. What we use in practice for evaluation is a dataset made up of functions taken from GitLab codebase that have been split in half. The first half is the prompt (input) the second half is the part we will compare to what the model produces (expected output).
|
||||
|
||||
As said we evaluate our code completion application with a dataset that was created from GitLab code, this means a lot of Ruby, Go, Python, and several other languages. Let's remember many of our customers write their code in Java. At this point a worthwhile question to ask is, would you characterise our dataset as representative? Honestly, in the beginning probably not. There are times where we must accept in the beginning that the best we can do is the best we can do, but keeping this in mind and trying to improve the alignment between what we evaluate against and what our application is the best thing to focus on when creating / improving a dataset. As part of this dataset creation / improvement effort we also want to keep a diverse spread of types of prompts. Following our code
|
||||
completion example, as mentioned before we probably want to have more Java prompts but we don't just want leet-code style interview questions. We also want examples that would be in enterprise backend applications, android applications, gradle plugins, yes even some basic interview questions, and any more diverse places where Java would be used.
|
||||
|
||||
Now that we have thought about having representative data, let's think about how many datapoints we need. In evaluation, we are trying to make an assessment of how well our application is doing. If you could imagine flipping a coin that may be unfair, flipping it 10 times wouldn't give you a lot of confidence but flipping it 10,000 times probably would. That said, similarly to how flipping a coin 10,000 times would take a while to do, running 10,000 prompts would take longer than running about a 100 or so. In the early stages of development, we would want to balance iteration speed and accuracy, and to that end we recommend 70 to 120 prompts, but if you can add more without compromising your iteration time, this is strongly encouraged. As you move toward an internal beta and definitely as you
|
||||
move toward general availability, we recommend running evaluation with several thousand prompts.
|
||||
|
||||
#### What is an output, ground truth, or expected answer?
|
||||
|
||||
- **Output**: The result of sending a message to your chosen LLM. For example, if I ask "In The Hitchhiker's Guide to the Galaxy, what is the number that was the meaning of life?", the output could be something like "In The Hitchhiker's Guide to the Galaxy, the number that represents the meaning of life is **42**".
|
||||
- **Ground Truth** or **Expected Answer**: The examples from our real would situation that we know to be true. For example, let's imagine we are trying to predict housing prices and have a bunch of validation data that is pulled from a realtor listing site. This data contains information about the house, and how much the house costs. That data could be called our ground truth.
|
||||
|
||||
## Further resources
|
||||
|
||||
For more comprehensive prompt engineering guides, see:
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ they have the following actions in common:
|
|||
|
||||
- [Add a to-do item](../../user/todos.md#create-a-to-do-item)
|
||||
- [Add labels](../../user/project/labels.md#assign-and-unassign-labels)
|
||||
- [Assign a milestone](../../user/project/milestones/_index.md#assign-a-milestone-to-an-issue-or-merge-request)
|
||||
- [Assign a milestone](../../user/project/milestones/_index.md#assign-a-milestone-to-an-item)
|
||||
- [Make an incident confidential](../../user/project/issues/confidential_issues.md)
|
||||
- [Set a due date](../../user/project/issues/due_dates.md)
|
||||
- [Toggle notifications](../../user/profile/notifications.md#edit-notification-settings-for-issues-merge-requests-and-epics)
|
||||
|
|
|
|||
|
|
@ -32,13 +32,6 @@ data. Only run API security testing against a test server.
|
|||
|
||||
{{< /alert >}}
|
||||
|
||||
API security testing can test the following web API types:
|
||||
|
||||
- REST API
|
||||
- SOAP
|
||||
- GraphQL
|
||||
- Form bodies, JSON, or XML
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
DAST API has been re-branded to API Security Testing. As part of this re-branding the template
|
||||
|
|
@ -46,7 +39,94 @@ name and variable prefixes have also been updated. The old template and variable
|
|||
|
||||
{{< /alert >}}
|
||||
|
||||
## When API security testing scans run
|
||||
## Getting started
|
||||
|
||||
Get started with API security testing by editing your CI/CD configuration.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You have a web API using one of the supported API types:
|
||||
- REST API
|
||||
- SOAP
|
||||
- GraphQL
|
||||
- Form bodies, JSON, or XML
|
||||
- You have an API specification in one of the following formats:
|
||||
- [OpenAPI v2 or v3 Specification](configuration/enabling_the_analyzer.md#openapi-specification)
|
||||
- [GraphQL Schema](configuration/enabling_the_analyzer.md#graphql-schema)
|
||||
- [HTTP Archive (HAR)](configuration/enabling_the_analyzer.md#http-archive-har)
|
||||
- [Postman Collection v2.0 or v2.1](configuration/enabling_the_analyzer.md#postman-collection)
|
||||
|
||||
Each scan supports exactly one specification. To scan more than one specification, use multiple scans.
|
||||
- You have a [GitLab Runner](../../../ci/runners/_index.md) available, with the
|
||||
[`docker` executor](https://docs.gitlab.com/runner/executors/docker.html) on Linux/amd64.
|
||||
- You have a deployed target application. For more details, see the [deployment options](#application-deployment-options).
|
||||
- The `dast` stage is added to your CI/CD pipeline definition, after the `deploy` stage. For example:
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
- dast
|
||||
```
|
||||
|
||||
To enable API security testing, you must alter your GitLab CI/CD configuration YAML based on the unique needs of your environment. You can specify the API you want to scan using:
|
||||
|
||||
- [OpenAPI v2 or v3 Specification](configuration/enabling_the_analyzer.md#openapi-specification)
|
||||
- [GraphQL Schema](configuration/enabling_the_analyzer.md#graphql-schema)
|
||||
- [HTTP Archive (HAR)](configuration/enabling_the_analyzer.md#http-archive-har)
|
||||
- [Postman Collection v2.0 or v2.1](configuration/enabling_the_analyzer.md#postman-collection)
|
||||
|
||||
## Understanding the results
|
||||
|
||||
To view the output of a security scan:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. On the left sidebar, select **Build > Pipelines**.
|
||||
1. Select the pipeline.
|
||||
1. Select the **Security** tab.
|
||||
1. Select a vulnerability to view its details, including:
|
||||
- Status: Indicates whether the vulnerability has been triaged or resolved.
|
||||
- Description: Explains the cause of the vulnerability, its potential impact, and recommended remediation steps.
|
||||
- Severity: Categorized into six levels based on impact.
|
||||
[Learn more about severity levels](../vulnerabilities/severities.md).
|
||||
- Scanner: Identifies which analyzer detected the vulnerability.
|
||||
- Method: Establishes the vulnerable server interaction type.
|
||||
- URL: Shows the location of the vulnerability.
|
||||
- Evidence: Describes test case to prove the presence of a given vulnerability
|
||||
- Identifiers: A list of references used to classify the vulnerability, such as CWE identifiers.
|
||||
|
||||
You can also download the security scan results:
|
||||
|
||||
- In the pipeline's **Security** tab, select **Download results**.
|
||||
|
||||
For more details, see the [pipeline security report](../vulnerability_report/pipeline.md).
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
Findings are generated on feature branches. When they are merged into the default branch, they become vulnerabilities. This distinction is important when evaluating your security posture.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
## Optimization
|
||||
|
||||
To get the most out of API security testing, follow these recommendations:
|
||||
|
||||
- Configure runners to use the [always pull policy](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy) to run the latest versions of the analyzers.
|
||||
- By default, API security testing downloads all artifacts defined by previous jobs in the pipeline.
|
||||
If your DAST job does not rely on `environment_url.txt` to define the URL under test or any other
|
||||
files created in previous jobs, you should not download artifacts. To avoid downloading artifacts,
|
||||
extend the analyzer CI/CD job to specify no dependencies. For example, for the API security
|
||||
testing analyzer, add the following to your `.gitlab-ci.yml` file:
|
||||
|
||||
```yaml
|
||||
api_security:
|
||||
dependencies: []
|
||||
```
|
||||
|
||||
To configure API security testing for your particular application or environment, see the full list of [configuration options](configuration/_index.md).
|
||||
|
||||
## Roll out
|
||||
|
||||
When run in your CI/CD pipeline, API security testing scanning runs in the `dast` stage by default. To ensure
|
||||
API security testing scanning examines the latest code, ensure your CI/CD pipeline deploys changes to a test
|
||||
|
|
@ -59,7 +139,7 @@ API security testing scan. The only changes to the API should be from the API se
|
|||
API (for example, by users, scheduled tasks, database changes, code changes, other pipelines, or
|
||||
other scanners) during a scan could cause inaccurate results.
|
||||
|
||||
## Example API security testing scanning configurations
|
||||
### Example API security testing scanning configurations
|
||||
|
||||
The following projects demonstrate API security testing scanning:
|
||||
|
||||
|
|
@ -71,6 +151,75 @@ The following projects demonstrate API security testing scanning:
|
|||
- [Example SOAP project](https://gitlab.com/gitlab-org/security-products/demos/api-dast/soap-example)
|
||||
- [Authentication Token using Selenium](https://gitlab.com/gitlab-org/security-products/demos/api-dast/auth-token-selenium)
|
||||
|
||||
### Application deployment options
|
||||
|
||||
API security testing requires a deployed application to be available to scan.
|
||||
|
||||
Depending on the complexity of the target application, there are a few options as to how to deploy and configure
|
||||
the API security testing template.
|
||||
|
||||
#### Review apps
|
||||
|
||||
Review apps are the most involved method of deploying your DAST target application. To assist in the process,
|
||||
GitLab created a review app deployment using Google Kubernetes Engine (GKE). This example can be found in the
|
||||
[Review Apps - GKE](https://gitlab.com/gitlab-org/security-products/demos/dast/review-app-gke) project, plus detailed
|
||||
instructions to configure review apps for DAST in the [README.md](https://gitlab.com/gitlab-org/security-products/demos/dast/review-app-gke/-/blob/master/README.md).
|
||||
|
||||
#### Docker Services
|
||||
|
||||
If your application uses Docker containers you have another option for deploying and scanning with DAST.
|
||||
After your Docker build job completes and your image is added to your container registry, you can use the image as a
|
||||
[service](../../../ci/services/_index.md).
|
||||
|
||||
By using service definitions in your `.gitlab-ci.yml`, you can scan services with the DAST analyzer.
|
||||
|
||||
When adding a `services` section to the job, the `alias` is used to define the hostname that can be used to access the service. In the following example, the `alias: yourapp` portion of the `dast` job definition means that the URL to the deployed application uses `yourapp` as the hostname (`https://yourapp/`).
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- build
|
||||
- dast
|
||||
|
||||
include:
|
||||
- template: API-Security.gitlab-ci.yml
|
||||
|
||||
# Deploys the container to the GitLab container registry
|
||||
deploy:
|
||||
services:
|
||||
- name: docker:dind
|
||||
alias: dind
|
||||
image: docker:20.10.16
|
||||
stage: build
|
||||
script:
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker pull $CI_REGISTRY_IMAGE:latest || true
|
||||
- docker build --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest .
|
||||
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
|
||||
- docker push $CI_REGISTRY_IMAGE:latest
|
||||
|
||||
api_security:
|
||||
services: # use services to link your app container to the dast job
|
||||
- name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
|
||||
alias: yourapp
|
||||
|
||||
variables:
|
||||
APISEC_TARGET_URL: https://yourapp
|
||||
```
|
||||
|
||||
Most applications depend on multiple services such as databases or caching services. By default, services defined in the services fields cannot communicate
|
||||
with each another. To allow communication between services, enable the `FF_NETWORK_PER_BUILD` [feature flag](https://docs.gitlab.com/runner/configuration/feature-flags.html#available-feature-flags).
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
FF_NETWORK_PER_BUILD: "true" # enable network per build so all services can communicate on the same network
|
||||
|
||||
services: # use services to link the container to the dast job
|
||||
- name: mongo:latest
|
||||
alias: mongo
|
||||
- name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
|
||||
alias: yourapp
|
||||
```
|
||||
|
||||
## Get support or request an improvement
|
||||
|
||||
To get support for your particular problem, use the [getting help channels](https://about.gitlab.com/get-help/).
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
title: Configuration
|
||||
---
|
||||
|
||||
- [Requirements](requirements.md)
|
||||
- [Requirements](../_index.md)
|
||||
- [Enabling the analyzer](enabling_the_analyzer.md)
|
||||
- [Customize analyzer settings](customizing_analyzer_settings.md)
|
||||
- [Overriding analyzer jobs](overriding_analyzer_jobs.md)
|
||||
|
|
|
|||
|
|
@ -1,116 +1,13 @@
|
|||
---
|
||||
type: reference, howto
|
||||
stage: Application Security Testing
|
||||
group: Dynamic Analysis
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
title: Requirements
|
||||
redirect_to: '../_index.md'
|
||||
remove_date: '2025-09-17'
|
||||
---
|
||||
|
||||
- A web API using one of the supported API types:
|
||||
- REST API
|
||||
- SOAP
|
||||
- GraphQL
|
||||
- Form bodies, JSON, or XML
|
||||
- An API specification in one of the following formats:
|
||||
- [OpenAPI v2 or v3 Specification](enabling_the_analyzer.md#openapi-specification)
|
||||
- [GraphQL Schema](enabling_the_analyzer.md#graphql-schema)
|
||||
- [HTTP Archive (HAR)](enabling_the_analyzer.md#http-archive-har)
|
||||
- [Postman Collection v2.0 or v2.1](enabling_the_analyzer.md#postman-collection)
|
||||
<!-- markdownlint-disable -->
|
||||
|
||||
Each scan supports exactly one specification. To scan more than one specification, use multiple scans.
|
||||
- [GitLab Runner](../../../../ci/runners/_index.md) available, with the
|
||||
[`docker` executor](https://docs.gitlab.com/runner/executors/docker.html) on Linux/amd64.
|
||||
- Target application deployed. For more details, read [Deployment options](#application-deployment-options).
|
||||
- `dast` stage added to the CI/CD pipeline definition. This should be added after the deploy step, for example:
|
||||
This document was moved to [another location](../_index.md).
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
- dast
|
||||
```
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Configure runners to use the [always pull policy](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy) to run the latest versions of the analyzers.
|
||||
- By default, API security testing downloads all artifacts defined by previous jobs in the pipeline.
|
||||
If your DAST job does not rely on `environment_url.txt` to define the URL under test or any other
|
||||
files created in previous jobs, you should not download artifacts. To avoid downloading artifacts,
|
||||
extend the analyzer CI/CD job to specify no dependencies. For example, for the API security
|
||||
testing analyzer, add the following to your `.gitlab-ci.yml` file:
|
||||
|
||||
```yaml
|
||||
api_security:
|
||||
dependencies: []
|
||||
```
|
||||
|
||||
## Application deployment options
|
||||
|
||||
API security testing requires a deployed application to be available to scan.
|
||||
|
||||
Depending on the complexity of the target application, there are a few options as to how to deploy and configure
|
||||
the API security testing template.
|
||||
|
||||
### Review apps
|
||||
|
||||
Review apps are the most involved method of deploying your DAST target application. To assist in the process,
|
||||
we created a Review App deployment using Google Kubernetes Engine (GKE). This example can be found in our
|
||||
[Review apps - GKE](https://gitlab.com/gitlab-org/security-products/demos/dast/review-app-gke) project, along with detailed
|
||||
instructions in the [README.md](https://gitlab.com/gitlab-org/security-products/demos/dast/review-app-gke/-/blob/master/README.md)
|
||||
on how to configure review apps for DAST.
|
||||
|
||||
### Docker Services
|
||||
|
||||
If your application uses Docker containers you have another option for deploying and scanning with DAST.
|
||||
After your Docker build job completes and your image is added to your container registry, you can use the image as a
|
||||
[service](../../../../ci/services/_index.md).
|
||||
|
||||
By using service definitions in your `.gitlab-ci.yml`, you can scan services with the DAST analyzer.
|
||||
|
||||
When adding a `services` section to the job, the `alias` is used to define the hostname that can be used to access the service. In the following example, the `alias: yourapp` portion of the `dast` job definition means that the URL to the deployed application uses `yourapp` as the hostname (`https://yourapp/`).
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- build
|
||||
- dast
|
||||
|
||||
include:
|
||||
- template: API-Security.gitlab-ci.yml
|
||||
|
||||
# Deploys the container to the GitLab container registry
|
||||
deploy:
|
||||
services:
|
||||
- name: docker:dind
|
||||
alias: dind
|
||||
image: docker:20.10.16
|
||||
stage: build
|
||||
script:
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker pull $CI_REGISTRY_IMAGE:latest || true
|
||||
- docker build --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest .
|
||||
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
|
||||
- docker push $CI_REGISTRY_IMAGE:latest
|
||||
|
||||
api_security:
|
||||
services: # use services to link your app container to the dast job
|
||||
- name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
|
||||
alias: yourapp
|
||||
|
||||
variables:
|
||||
APISEC_TARGET_URL: https://yourapp
|
||||
```
|
||||
|
||||
Most applications depend on multiple services such as databases or caching services. By default, services defined in the services fields cannot communicate
|
||||
with each another. To allow communication between services, enable the `FF_NETWORK_PER_BUILD` [feature flag](https://docs.gitlab.com/runner/configuration/feature-flags.html#available-feature-flags).
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
FF_NETWORK_PER_BUILD: "true" # enable network per build so all services can communicate on the same network
|
||||
|
||||
services: # use services to link the container to the dast job
|
||||
- name: mongo:latest
|
||||
alias: mongo
|
||||
- name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
|
||||
alias: yourapp
|
||||
```
|
||||
<!-- This redirect file can be deleted after <2025-09-17>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/development/documentation/redirects -->
|
||||
|
|
@ -20,6 +20,7 @@ to them.
|
|||
{{< history >}}
|
||||
|
||||
- [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169256) the minimum user role from Reporter to Planner in GitLab 17.7.
|
||||
- Ability to assign milestones to epics [introduced](https://gitlab.com/groups/gitlab-org/-/epics/329) in GitLab 18.2.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
|
|
@ -52,10 +53,10 @@ If you select **Inherited**:
|
|||
|
||||
- For the **start date**: GitLab scans all child epics and issues assigned to the epic,
|
||||
and sets the start date to match the earliest start date found in the child epics or the milestone
|
||||
assigned to the issues.
|
||||
assigned to the child items.
|
||||
- For the **due date**: GitLab scans all child epics and issues assigned to the epic,
|
||||
and sets the due date to match the latest due date found in the child epics or the milestone
|
||||
assigned to the issues.
|
||||
assigned to the child items.
|
||||
|
||||
These dates are dynamic and recalculated if any of the following occur:
|
||||
|
||||
|
|
@ -83,6 +84,7 @@ After you create an epic, you can edit the following details:
|
|||
- Start date
|
||||
- Due date
|
||||
- Labels
|
||||
- Milestone
|
||||
- [Color](#epic-color)
|
||||
|
||||
Prerequisites:
|
||||
|
|
@ -95,10 +97,10 @@ To edit an epic's title or description:
|
|||
1. Make your changes.
|
||||
1. Select **Save changes**.
|
||||
|
||||
To edit an epic's start date, due date, or labels:
|
||||
To edit an epic's start date, due date, milestone, or labels:
|
||||
|
||||
1. Next to each section in the right sidebar, select **Edit**.
|
||||
1. Select the dates or labels for your epic.
|
||||
1. Select the dates, milestone, or labels for your epic.
|
||||
|
||||
### Reorder list items in the epic description
|
||||
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ Pipeline badges can be rendered in different styles by adding the `style=style_n
|
|||
https://gitlab.example.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
- Flat square:
|
||||
|
||||
|
|
@ -244,7 +244,7 @@ Pipeline badges can be rendered in different styles by adding the `style=style_n
|
|||
https://gitlab.example.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat-square
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
### Customize badge text
|
||||
|
||||
|
|
@ -255,7 +255,7 @@ Customize the badge text and width by adding the `key_text=custom_text` and `key
|
|||
https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=karma&key_text=Frontend+Coverage&key_width=130
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
### Customize badge image
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="20">
|
||||
<linearGradient id="b" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
|
||||
<mask id="a">
|
||||
<rect width="120" height="20" rx="3" fill="#fff"/>
|
||||
</mask>
|
||||
|
||||
<g mask="url(#a)">
|
||||
<path fill="#555"
|
||||
d="M0 0 h62 v20 H0 z"/>
|
||||
<path fill="#9f9f9f"
|
||||
d="M62 0 h58 v20 H62 z"/>
|
||||
<path fill="url(#b)"
|
||||
d="M0 0 h120 v20 H0 z"/>
|
||||
</g>
|
||||
|
||||
<g fill="#fff" text-anchor="middle">
|
||||
<g font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
||||
<text x="31" y="15" fill="#010101" fill-opacity=".3">
|
||||
coverage
|
||||
</text>
|
||||
<text x="31" y="14">
|
||||
coverage
|
||||
</text>
|
||||
<text x="91" y="15" fill="#010101" fill-opacity=".3">
|
||||
unknown
|
||||
</text>
|
||||
<text x="91" y="14">
|
||||
unknown
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 950 B |
|
|
@ -0,0 +1,36 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="20">
|
||||
<linearGradient id="b" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
|
||||
<mask id="a">
|
||||
<rect width="120" height="20" rx="3" fill="#fff"/>
|
||||
</mask>
|
||||
|
||||
<g mask="url(#a)">
|
||||
<path fill="#555"
|
||||
d="M0 0 h62 v20 H0 z"/>
|
||||
<path fill="#9f9f9f"
|
||||
d="M62 0 h58 v20 H62 z"/>
|
||||
<path fill="url(#b)"
|
||||
d="M0 0 h120 v20 H0 z"/>
|
||||
</g>
|
||||
|
||||
<g fill="#fff" text-anchor="middle">
|
||||
<g font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
||||
<text x="31" y="15" fill="#010101" fill-opacity=".3">
|
||||
coverage
|
||||
</text>
|
||||
<text x="31" y="14">
|
||||
coverage
|
||||
</text>
|
||||
<text x="91" y="15" fill="#010101" fill-opacity=".3">
|
||||
unknown
|
||||
</text>
|
||||
<text x="91" y="14">
|
||||
unknown
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 950 B |
|
|
@ -0,0 +1,17 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="20">
|
||||
<g shape-rendering="crispEdges">
|
||||
<path fill="#555" d="M0 0 h62 v20 H0 z"/>
|
||||
<path fill="#9f9f9f" d="M62 0 h58 v20 H62 z"/>
|
||||
</g>
|
||||
|
||||
<g fill="#fff" text-anchor="middle">
|
||||
<g font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
||||
<text x="31" y="14">
|
||||
coverage
|
||||
</text>
|
||||
<text x="91" y="14">
|
||||
unknown
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 457 B |
|
|
@ -16,7 +16,7 @@ description: Burndown charts, goals, progress tracking, and releases.
|
|||
Milestones help track and organize work in GitLab.
|
||||
Milestones:
|
||||
|
||||
- Group related issues and merge requests to track progress toward a goal.
|
||||
- Group related issues, epics, and merge requests to track progress toward a goal.
|
||||
- Support time-based planning with optional start and due dates.
|
||||
- Work alongside iterations to track concurrent timeboxes.
|
||||
- Track releases and generate release evidence.
|
||||
|
|
@ -24,7 +24,7 @@ Milestones:
|
|||
|
||||
Milestones can belong to a [project](../_index.md) or [group](../../group/_index.md).
|
||||
Project milestones apply to issues and merge requests in that project only.
|
||||
Group milestones apply to any issue or merge request in that group's projects.
|
||||
Group milestones apply to any issue, epic or merge request in that group's projects.
|
||||
|
||||
For information about project and group milestones API, see:
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ milestones with the [Releases feature](../releases/_index.md#associate-milestone
|
|||
A milestone can belong to [project](../_index.md) or [group](../../group/_index.md).
|
||||
|
||||
You can assign **project milestones** to issues or merge requests in that project only.
|
||||
You can assign **group milestones** to any issue or merge request of any project in that group.
|
||||
You can assign **group milestones** to any issue, epic, or merge request of any project in that group.
|
||||
|
||||
For information about project and group milestones API, see:
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ in the **Milestones** page, select the title of the milestone you want to view.
|
|||
The milestone view shows the title and description.
|
||||
The tabs below the title and description show the following:
|
||||
|
||||
- **Issues**: Shows all issues assigned to the milestone. Issues are displayed in three columns named:
|
||||
- **Work Items**: Shows all work items assigned to the milestone. Work items are displayed in three columns named:
|
||||
- Unstarted Issues (open and unassigned)
|
||||
- Ongoing Issues (open and assigned)
|
||||
- Completed Issues (closed)
|
||||
|
|
@ -124,10 +124,10 @@ showing the progress of completing a milestone.
|
|||
|
||||
The sidebar on the milestone view shows the following:
|
||||
|
||||
- Percentage complete, which is calculated as number of closed issues divided by total number of issues.
|
||||
- Percentage complete, which is calculated as number of closed work items divided by total number of work items.
|
||||
- The start date and due date.
|
||||
- The total time spent on all issues and merge requests assigned to the milestone.
|
||||
- The total issue weight of all issues assigned to the milestone.
|
||||
- The total time spent on all work items and merge requests assigned to the milestone.
|
||||
- The total issue weight of all work items assigned to the milestone.
|
||||
- The count of total, open, closed, and merged merge requests.
|
||||
- Links to associated releases.
|
||||
- The milestone's reference you can copy to your clipboard.
|
||||
|
|
@ -140,6 +140,7 @@ The sidebar on the milestone view shows the following:
|
|||
|
||||
- [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/343889) the minimum user role from Developer to Reporter in GitLab 15.0.
|
||||
- [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169256) the minimum user role from Reporter to Planner in GitLab 17.7.
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195530) milestones to Epic work items in GitLab 18.2.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
|
|
@ -266,15 +267,21 @@ To promote a project milestone:
|
|||
- Select the milestone title, and then select **Milestone actions** ({{< icon name="ellipsis_v" >}}) > **Promote**.
|
||||
1. Select **Promote Milestone**.
|
||||
|
||||
## Assign a milestone to an issue or merge request
|
||||
## Assign a milestone to an item
|
||||
|
||||
Every issue and merge request can be assigned one milestone.
|
||||
{{< history >}}
|
||||
|
||||
- Ability to assign milestones to epics [introduced](https://gitlab.com/groups/gitlab-org/-/epics/329) in GitLab 18.2.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
Every issue, epic, or merge request can be assigned one milestone.
|
||||
The milestones are visible on every issue and merge request page, on the right sidebar.
|
||||
They are also visible in the issue board.
|
||||
They are also visible in the work item board.
|
||||
|
||||
To assign or unassign a milestone:
|
||||
|
||||
1. View an issue or a merge request.
|
||||
1. View an issue, an epic, or a merge request.
|
||||
1. On the right sidebar, next to **Milestones**, select **Edit**.
|
||||
1. In the **Assign milestone** list, search for a milestone by typing its name.
|
||||
You can select from both project and group milestones.
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ module API
|
|||
|
||||
latest_build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name])
|
||||
authorize_read_job_artifacts!(latest_build)
|
||||
|
||||
not_found! unless latest_build.artifacts_file&.exists?
|
||||
|
||||
audit_download(latest_build, latest_build.artifacts_file.filename)
|
||||
present_artifacts_file!(latest_build.artifacts_file)
|
||||
end
|
||||
|
|
@ -110,7 +113,7 @@ module API
|
|||
|
||||
build = find_build!(params[:job_id])
|
||||
authorize_read_job_artifacts!(build)
|
||||
audit_download(build, build.artifacts_file&.filename) if build.artifacts_file
|
||||
audit_download(build, build.artifacts_file.filename) if build.artifacts_file
|
||||
present_artifacts_file!(build.artifacts_file)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -395,7 +395,9 @@ module API
|
|||
authenticate_job_via_dependent_job!
|
||||
authorize_job_token_policies!(current_job.project)
|
||||
|
||||
audit_download(current_job, current_job.artifacts_file&.filename) if current_job.artifacts_file
|
||||
not_found! unless current_job.artifacts_file&.exists?
|
||||
|
||||
audit_download(current_job, current_job.artifacts_file.filename)
|
||||
present_artifacts_file!(current_job.artifacts_file, supports_direct_download: params[:direct_download])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ module API
|
|||
end
|
||||
|
||||
feature_category :mobile_devops
|
||||
urgency :low
|
||||
|
||||
default_format :json
|
||||
|
||||
|
|
|
|||
|
|
@ -3039,6 +3039,9 @@ msgstr ""
|
|||
msgid "Access granted"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access levels should both be present"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access levels should either both be present or both be nil"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -28382,6 +28385,9 @@ msgstr ""
|
|||
msgid "Geo|Verification status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Verification: %{status}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Verified"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -69097,9 +69103,6 @@ msgstr ""
|
|||
msgid "Vulnerability|Possibly active secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Project:"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@
|
|||
"crypto": "^1.0.1",
|
||||
"custom-jquery-matchers": "^2.1.0",
|
||||
"dependency-cruiser": "^16.9.0",
|
||||
"eslint": "9.30.0",
|
||||
"eslint": "9.30.1",
|
||||
"eslint-formatter-gitlab": "^6.0.1",
|
||||
"eslint-import-resolver-jest": "3.0.2",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'gitlab-qa', '~> 15', '>= 15.5.0', require: 'gitlab/qa'
|
||||
gem 'gitlab_quality-test_tooling', '~> 2.15.1', require: false
|
||||
gem 'gitlab_quality-test_tooling', '~> 2.15.2', require: false
|
||||
gem 'gitlab-utils', path: '../gems/gitlab-utils'
|
||||
gem 'activesupport', '~> 7.1.5.1' # This should stay in sync with the root's Gemfile
|
||||
gem 'allure-rspec', '~> 2.27.0'
|
||||
|
|
@ -14,7 +14,7 @@ gem 'rspec', '~> 3.13', '>= 3.13.1'
|
|||
gem 'selenium-webdriver', '= 4.33.0'
|
||||
gem 'rest-client', '~> 2.1.0'
|
||||
gem 'rspec_junit_formatter', '~> 0.6.0'
|
||||
gem 'faker', '~> 3.5', '>= 3.5.1'
|
||||
gem 'faker', '~> 3.5', '>= 3.5.2'
|
||||
gem 'knapsack', '~> 4.0'
|
||||
gem 'parallel_tests', '~> 5.1'
|
||||
gem 'rotp', '~> 6.3.0'
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ GEM
|
|||
logger
|
||||
factory_bot (6.5.1)
|
||||
activesupport (>= 6.1.0)
|
||||
faker (3.5.1)
|
||||
faker (3.5.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.9.2)
|
||||
faraday-net_http (>= 2.0, < 3.2)
|
||||
|
|
@ -140,7 +140,7 @@ GEM
|
|||
rainbow (>= 3, < 4)
|
||||
table_print (= 1.5.7)
|
||||
zeitwerk (>= 2, < 3)
|
||||
gitlab_quality-test_tooling (2.15.1)
|
||||
gitlab_quality-test_tooling (2.15.2)
|
||||
activesupport (>= 7.0, < 7.3)
|
||||
amatch (~> 0.4.1)
|
||||
fog-google (~> 1.24, >= 1.24.1)
|
||||
|
|
@ -379,13 +379,13 @@ DEPENDENCIES
|
|||
capybara-screenshot (~> 1.0.26)
|
||||
deprecation_toolkit (~> 2.2.3)
|
||||
factory_bot (~> 6.5.1)
|
||||
faker (~> 3.5, >= 3.5.1)
|
||||
faker (~> 3.5, >= 3.5.2)
|
||||
faraday-retry (~> 2.3, >= 2.3.2)
|
||||
fog-google (~> 1.25)
|
||||
gitlab-orchestrator!
|
||||
gitlab-qa (~> 15, >= 15.5.0)
|
||||
gitlab-utils!
|
||||
gitlab_quality-test_tooling (~> 2.15.1)
|
||||
gitlab_quality-test_tooling (~> 2.15.2)
|
||||
googleauth (~> 1.9.0)
|
||||
influxdb-client (~> 3.2)
|
||||
junit_merge (~> 0.1.2)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
ARG GDK_SHA=75d073fbd5e137ecbfe5832c491b4a9a258deefe
|
||||
ARG GDK_SHA=13206c818cb4d960511156d1895ede14a2969112
|
||||
# Use tag prefix when running on 'stable' branch to make sure 'protected' image is used which is not deleted by registry cleanup
|
||||
ARG GDK_BASE_TAG_PREFIX
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,25 @@ class ReleaseEnvironmentsModel
|
|||
@image_tag ||= "#{environment_base}-#{ENV['CI_COMMIT_SHORT_SHA']}"
|
||||
end
|
||||
|
||||
def omnibus_package_version
|
||||
# Omnibus package name has the syntax <branch>-<pipeline-id>-<short-commit-id>
|
||||
# e.g. 16.2+stable.12345.abc123
|
||||
converted_branch_name = security_omnibus_stable_branch.gsub(/(\d+)-(\d+)-stable/, '\1.\2+stable')
|
||||
"#{converted_branch_name}.#{ENV['CI_PIPELINE_ID']}.#{ENV['CI_COMMIT_SHORT_SHA']}"
|
||||
end
|
||||
|
||||
def write_deploy_env_file
|
||||
raise "Missing required environment variable." unless set_required_env_vars?
|
||||
|
||||
File.open(ENV['DEPLOY_ENV'], 'w') do |file|
|
||||
file.puts "ENVIRONMENT=#{environment}"
|
||||
file.puts "VERSIONS=#{generate_json}"
|
||||
file.puts "OMNIBUS_PACKAGE_VERSION=#{omnibus_package_version}"
|
||||
end
|
||||
|
||||
puts File.read(ENV['DEPLOY_ENV'])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This is to generate the environment name without "-security". It is used by the image tag
|
||||
|
|
@ -64,19 +83,18 @@ class ReleaseEnvironmentsModel
|
|||
def security_project?
|
||||
ENV['CI_PROJECT_PATH'] == "gitlab-org/security/gitlab"
|
||||
end
|
||||
|
||||
# Omnibus security stable branch has no -ee suffix
|
||||
def security_omnibus_stable_branch
|
||||
ENV['CI_COMMIT_BRANCH'].gsub("-ee", "")
|
||||
end
|
||||
end
|
||||
|
||||
# Outputs in `dotenv` format the ENVIRONMENT and VERSIONS to pass to release environments e.g.
|
||||
# ENVIRONMENT=15-10-stable(-security)
|
||||
# VERSIONS={"gitaly":"15-10-stable-c7c5131c","registry":"15-10-stable-c7c5131c","kas":"15-10-stable-c7c5131c", ...
|
||||
# OMNIBUS_PACKAGE_VERSION=15.10+stable.12345.c7c5131c
|
||||
if $PROGRAM_NAME == __FILE__
|
||||
model = ReleaseEnvironmentsModel.new
|
||||
raise "Missing required environment variable." unless model.set_required_env_vars?
|
||||
|
||||
File.open(ENV['DEPLOY_ENV'], 'w') do |file|
|
||||
file.puts "ENVIRONMENT=#{model.environment}"
|
||||
file.puts "VERSIONS=#{model.generate_json}"
|
||||
end
|
||||
|
||||
puts File.read(ENV['DEPLOY_ENV'])
|
||||
model.write_deploy_env_file
|
||||
end
|
||||
|
|
|
|||
|
|
@ -101,8 +101,9 @@ RSpec.describe Resolvers::NamespaceProjectsResolver, feature_category: :groups_a
|
|||
context 'when only search term is given' do
|
||||
let(:args) { default_args.merge(sort: nil, search: 'test') }
|
||||
|
||||
it 'filters out result that do not match the search input, but does not sort them' do
|
||||
expect(project_names).to contain_exactly('Test', 'Test Project')
|
||||
it 'filters out result that do not match the search input, and applies default similarity sort' do
|
||||
expect(project_names.first).to eq('Test')
|
||||
expect(project_names.second).to eq('Test Project')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1576,11 +1576,6 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
|
|||
end
|
||||
|
||||
before_all do
|
||||
create(:container_registry_protection_tag_rule, :immutable,
|
||||
project: project,
|
||||
tag_name_pattern: 'immutable-1'
|
||||
)
|
||||
|
||||
create(:container_registry_protection_tag_rule,
|
||||
project: project,
|
||||
minimum_access_level_for_push: Gitlab::Access::MAINTAINER,
|
||||
|
|
|
|||
|
|
@ -87,35 +87,31 @@ RSpec.describe ContainerRegistry::Protection::TagRule, type: :model, feature_cat
|
|||
end
|
||||
|
||||
context 'when both access levels are present' do
|
||||
it 'is valid' do
|
||||
expect(tag_rule).to be_valid
|
||||
end
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'when both access levels are nil' do
|
||||
context 'when both access levels are nil', unless: Gitlab.ee? do
|
||||
let(:minimum_access_level_for_delete) { nil }
|
||||
let(:minimum_access_level_for_push) { nil }
|
||||
|
||||
it 'is valid' do
|
||||
expect(tag_rule).to be_valid
|
||||
it 'is not valid' do
|
||||
is_expected.to be_invalid.and have_attributes(errors: match_array(['Access levels should both be present']))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when minimum_access_level_for_push is nil' do
|
||||
context 'when minimum_access_level_for_push is nil', unless: Gitlab.ee? do
|
||||
let(:minimum_access_level_for_push) { nil }
|
||||
|
||||
it 'is not valid' do
|
||||
expect(tag_rule).not_to be_valid
|
||||
expect(tag_rule.errors[:base]).to include('Access levels should either both be present or both be nil')
|
||||
is_expected.to be_invalid.and have_attributes(errors: match_array(['Access levels should both be present']))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when minimum_access_level_for_delete is nil' do
|
||||
context 'when minimum_access_level_for_delete is nil', unless: Gitlab.ee? do
|
||||
let(:minimum_access_level_for_delete) { nil }
|
||||
|
||||
it 'is not valid' do
|
||||
expect(tag_rule).not_to be_valid
|
||||
expect(tag_rule.errors[:base]).to include('Access levels should either both be present or both be nil')
|
||||
is_expected.to be_invalid.and have_attributes(errors: match_array(['Access levels should both be present']))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -150,28 +146,21 @@ RSpec.describe ContainerRegistry::Protection::TagRule, type: :model, feature_cat
|
|||
minimum_access_level_for_delete: :admin)
|
||||
end
|
||||
|
||||
let_it_be(:rule_five) do
|
||||
create(:container_registry_protection_tag_rule,
|
||||
tag_name_pattern: 'five',
|
||||
minimum_access_level_for_push: nil,
|
||||
minimum_access_level_for_delete: nil)
|
||||
end
|
||||
|
||||
where(:user_access_level, :actions, :expected_rules) do
|
||||
Gitlab::Access::DEVELOPER | ['push'] | lazy { [rule_one, rule_two, rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::DEVELOPER | ['delete'] | lazy { [rule_one, rule_two, rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::DEVELOPER | %w[push delete] | lazy { [rule_one, rule_two, rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::DEVELOPER | ['unknown'] | lazy { [rule_one, rule_two, rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::DEVELOPER | %w[push unknown] | lazy { [rule_one, rule_two, rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::MAINTAINER | ['push'] | lazy { [rule_two, rule_four, rule_five] }
|
||||
Gitlab::Access::MAINTAINER | ['delete'] | lazy { [rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::MAINTAINER | %w[push delete] | lazy { [rule_two, rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::OWNER | ['push'] | lazy { [rule_four, rule_five] }
|
||||
Gitlab::Access::OWNER | ['delete'] | lazy { [rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::OWNER | %w[push delete] | lazy { [rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::ADMIN | ['push'] | lazy { [rule_five] }
|
||||
Gitlab::Access::ADMIN | ['delete'] | lazy { [rule_five] }
|
||||
Gitlab::Access::ADMIN | %w[push delete] | lazy { [rule_five] }
|
||||
Gitlab::Access::DEVELOPER | ['push'] | lazy { [rule_one, rule_two, rule_three, rule_four] }
|
||||
Gitlab::Access::DEVELOPER | ['delete'] | lazy { [rule_one, rule_two, rule_three, rule_four] }
|
||||
Gitlab::Access::DEVELOPER | %w[push delete] | lazy { [rule_one, rule_two, rule_three, rule_four] }
|
||||
Gitlab::Access::DEVELOPER | ['unknown'] | lazy { [rule_one, rule_two, rule_three, rule_four] }
|
||||
Gitlab::Access::DEVELOPER | %w[push unknown] | lazy { [rule_one, rule_two, rule_three, rule_four] }
|
||||
Gitlab::Access::MAINTAINER | ['push'] | lazy { [rule_two, rule_four] }
|
||||
Gitlab::Access::MAINTAINER | ['delete'] | lazy { [rule_three, rule_four] }
|
||||
Gitlab::Access::MAINTAINER | %w[push delete] | lazy { [rule_two, rule_three, rule_four] }
|
||||
Gitlab::Access::OWNER | ['push'] | lazy { [rule_four] }
|
||||
Gitlab::Access::OWNER | ['delete'] | lazy { [rule_three, rule_four] }
|
||||
Gitlab::Access::OWNER | %w[push delete] | lazy { [rule_three, rule_four] }
|
||||
Gitlab::Access::ADMIN | ['push'] | lazy { [] }
|
||||
Gitlab::Access::ADMIN | ['delete'] | lazy { [] }
|
||||
Gitlab::Access::ADMIN | %w[push delete] | lazy { [] }
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
@ -212,18 +201,11 @@ RSpec.describe ContainerRegistry::Protection::TagRule, type: :model, feature_cat
|
|||
minimum_access_level_for_delete: :admin)
|
||||
end
|
||||
|
||||
let_it_be(:rule_five) do
|
||||
create(:container_registry_protection_tag_rule,
|
||||
tag_name_pattern: 'five',
|
||||
minimum_access_level_for_push: nil,
|
||||
minimum_access_level_for_delete: nil)
|
||||
end
|
||||
|
||||
where(:user_access_level, :expected_rules) do
|
||||
Gitlab::Access::DEVELOPER | lazy { [rule_one, rule_two, rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::MAINTAINER | lazy { [rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::OWNER | lazy { [rule_three, rule_four, rule_five] }
|
||||
Gitlab::Access::ADMIN | lazy { [rule_five] }
|
||||
Gitlab::Access::DEVELOPER | lazy { [rule_one, rule_two, rule_three, rule_four] }
|
||||
Gitlab::Access::MAINTAINER | lazy { [rule_three, rule_four] }
|
||||
Gitlab::Access::OWNER | lazy { [rule_three, rule_four] }
|
||||
Gitlab::Access::ADMIN | lazy { [] }
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
@ -270,22 +252,6 @@ RSpec.describe ContainerRegistry::Protection::TagRule, type: :model, feature_cat
|
|||
with_them do
|
||||
it { is_expected.to be(expected_result) }
|
||||
end
|
||||
|
||||
context 'for an immutable tag rule' do
|
||||
let_it_be(:rule) do
|
||||
build(:container_registry_protection_tag_rule, :immutable)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning same result for different access levels', true
|
||||
|
||||
context 'when the feature container_registry_immutable_tags is disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_immutable_tags: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning same result for different access levels', false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete_restricted?' do
|
||||
|
|
@ -308,22 +274,6 @@ RSpec.describe ContainerRegistry::Protection::TagRule, type: :model, feature_cat
|
|||
with_them do
|
||||
it { is_expected.to be(expected_result) }
|
||||
end
|
||||
|
||||
context 'for an immutable tag rule' do
|
||||
let_it_be(:rule) do
|
||||
build(:container_registry_protection_tag_rule, :immutable)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning same result for different access levels', true
|
||||
|
||||
context 'when the feature container_registry_immutable_tags is disabled' do
|
||||
before do
|
||||
stub_feature_flags(container_registry_immutable_tags: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning same result for different access levels', false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mutable?' do
|
||||
|
|
@ -354,34 +304,6 @@ RSpec.describe ContainerRegistry::Protection::TagRule, type: :model, feature_cat
|
|||
end
|
||||
end
|
||||
|
||||
describe '#immutable?' do
|
||||
subject { rule.immutable? }
|
||||
|
||||
context 'when access levels are nil' do
|
||||
let(:rule) do
|
||||
build(
|
||||
:container_registry_protection_tag_rule,
|
||||
minimum_access_level_for_push: nil,
|
||||
minimum_access_level_for_delete: nil
|
||||
)
|
||||
end
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
end
|
||||
|
||||
context 'when access levels are not nil' do
|
||||
let(:rule) do
|
||||
build(
|
||||
:container_registry_protection_tag_rule,
|
||||
minimum_access_level_for_push: :owner,
|
||||
minimum_access_level_for_delete: :owner
|
||||
)
|
||||
end
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#can_be_deleted?' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
|
@ -403,24 +325,6 @@ RSpec.describe ContainerRegistry::Protection::TagRule, type: :model, feature_cat
|
|||
it { is_expected.to be(true) }
|
||||
end
|
||||
|
||||
context 'when the rule is immutable' do
|
||||
let_it_be(:rule) { build(:container_registry_protection_tag_rule, :immutable, project:) }
|
||||
|
||||
where(:user_role, :expected_result) do
|
||||
:developer | false
|
||||
:maintainer | false
|
||||
:owner | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
project.send(:"add_#{user_role}", user)
|
||||
end
|
||||
|
||||
it { is_expected.to be(expected_result) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the rule is mutable' do
|
||||
let_it_be(:rule) { build(:container_registry_protection_tag_rule, project:) }
|
||||
|
||||
|
|
|
|||
|
|
@ -1265,19 +1265,6 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont
|
|||
|
||||
it_behaves_like 'checking mutable tag rules on a container repository'
|
||||
|
||||
context 'when the project has immutable tag protection rules' do
|
||||
before_all do
|
||||
create(
|
||||
:container_registry_protection_tag_rule,
|
||||
:immutable,
|
||||
tag_name_pattern: 'i',
|
||||
project: project
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'checking mutable tag rules on a container repository'
|
||||
end
|
||||
|
||||
context 'when the user is nil' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
|
|
|
|||
|
|
@ -10169,9 +10169,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
|
||||
describe '#has_container_registry_protected_tag_rules?' do
|
||||
let_it_be_with_refind(:project) { create(:project) }
|
||||
let(:include_immutable) { true }
|
||||
|
||||
subject { project.has_container_registry_protected_tag_rules?(action: 'delete', access_level: Gitlab::Access::OWNER, include_immutable: include_immutable) }
|
||||
subject { project.has_container_registry_protected_tag_rules?(action: 'delete', access_level: Gitlab::Access::OWNER) }
|
||||
|
||||
it 'returns false when there is no matching tag protection rule' do
|
||||
create(:container_registry_protection_tag_rule,
|
||||
|
|
@ -10194,57 +10193,14 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
expect(subject).to eq(true)
|
||||
end
|
||||
|
||||
context 'with immutable tag rules only' do
|
||||
before_all do
|
||||
create(:container_registry_protection_tag_rule, :immutable, project: project)
|
||||
end
|
||||
|
||||
context 'when include_immutable is true' do
|
||||
let(:include_immutable) { true }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when include_immutable is false' do
|
||||
let(:include_immutable) { false }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with both mutable and immutable tag rules' do
|
||||
before_all do
|
||||
create(:container_registry_protection_tag_rule, :immutable, project: project)
|
||||
create(
|
||||
:container_registry_protection_tag_rule,
|
||||
project: project,
|
||||
tag_name_pattern: 'mutable',
|
||||
minimum_access_level_for_push: Gitlab::Access::MAINTAINER,
|
||||
minimum_access_level_for_delete: Gitlab::Access::ADMIN
|
||||
)
|
||||
end
|
||||
|
||||
context 'when include_immutable is true' do
|
||||
let(:include_immutable) { true }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when include_immutable is false' do
|
||||
let(:include_immutable) { false }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
|
||||
it 'memoizes calls with the same parameters' do
|
||||
allow(project.container_registry_protection_tag_rules).to receive(:for_actions_and_access).and_call_original
|
||||
|
||||
2.times do
|
||||
project.has_container_registry_protected_tag_rules?(action: 'push', access_level: :maintainer, include_immutable: true)
|
||||
project.has_container_registry_protected_tag_rules?(action: 'push', access_level: :maintainer)
|
||||
end
|
||||
|
||||
expect(project.container_registry_protection_tag_rules).to have_received(:for_actions_and_access).with(%w[push], :maintainer, include_immutable: true).once
|
||||
expect(project.container_registry_protection_tag_rules).to have_received(:for_actions_and_access).with(%w[push], :maintainer).once
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -117,16 +117,16 @@ RSpec.describe 'Updating the container registry tag protection rule', :aggregate
|
|||
it_behaves_like 'returning a GraphQL error', /tagNamePattern can't be blank/
|
||||
end
|
||||
|
||||
context 'with only `minimumAccessLevelForDelete` blank' do
|
||||
context 'with only `minimumAccessLevelForDelete` blank', unless: Gitlab.ee? do
|
||||
let(:input) { super().merge(minimum_access_level_for_delete: nil) }
|
||||
|
||||
it_behaves_like 'returning a mutation error', 'Access levels should either both be present or both be nil'
|
||||
it_behaves_like 'returning a mutation error', 'Access levels should both be present'
|
||||
end
|
||||
|
||||
context 'with only `minimumAccessLevelForPush` blank' do
|
||||
context 'with only `minimumAccessLevelForPush` blank', unless: Gitlab.ee? do
|
||||
let(:input) { super().merge(minimum_access_level_for_push: nil) }
|
||||
|
||||
it_behaves_like 'returning a mutation error', 'Access levels should either both be present or both be nil'
|
||||
it_behaves_like 'returning a mutation error', 'Access levels should both be present'
|
||||
end
|
||||
|
||||
include_examples 'when user does not have permission'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Import work items', feature_category: :team_planning do
|
||||
include GraphqlHelpers
|
||||
include WorkhorseHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:reporter) { create(:user, reporter_of: project) }
|
||||
let_it_be(:guest) { create(:user, guest_of: project) }
|
||||
|
||||
let(:file) { fixture_file_upload('spec/fixtures/work_items_valid.csv') }
|
||||
let(:input) { { 'projectPath' => project.full_path, 'file' => file } }
|
||||
let(:mutation) { graphql_mutation(:workItemsCsvImport, input) }
|
||||
let(:mutation_response) { graphql_mutation_response(:work_items_csv_import) }
|
||||
|
||||
context 'when user is not allowed to import work items' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it 'returns access denied error' do
|
||||
post_graphql_mutation_with_uploads(mutation, current_user: current_user)
|
||||
|
||||
expect(graphql_errors).to be_present
|
||||
error_messages = graphql_errors.pluck('message')
|
||||
expect(error_messages).to include(
|
||||
match(/The resource that you are attempting to access does not exist or you don't have permission/)
|
||||
)
|
||||
end
|
||||
|
||||
context 'when import_export_work_items_csv feature flag is disabled' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
before do
|
||||
stub_feature_flags(import_export_work_items_csv: false)
|
||||
end
|
||||
|
||||
it 'returns feature flag disabled error' do
|
||||
post_graphql_mutation_with_uploads(mutation, current_user: current_user)
|
||||
|
||||
expect(graphql_errors).to be_present
|
||||
error_messages = graphql_errors.pluck('message')
|
||||
expect(error_messages).to include(
|
||||
match(/import_export_work_items_csv.*feature flag is disabled/)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has permissions to import work items' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
context 'with valid CSV file' do
|
||||
it 'schedules import job with success message and correct parameters', :aggregate_failures do
|
||||
expect(WorkItems::PrepareImportCsvService).to receive(:new) do |received_project, received_user, options|
|
||||
expect(received_project).to eq(project)
|
||||
expect(received_user).to eq(reporter)
|
||||
|
||||
uploaded_file = options[:file]
|
||||
expect(uploaded_file).to be_present
|
||||
expect(uploaded_file).to respond_to(:original_filename)
|
||||
expect(uploaded_file.original_filename).to eq('work_items_valid.csv')
|
||||
|
||||
instance_double(WorkItems::PrepareImportCsvService).tap do |service_double|
|
||||
allow(service_double).to receive(:execute).and_return(ServiceResponse.success(message: 'Import started'))
|
||||
end
|
||||
end
|
||||
|
||||
post_graphql_mutation_with_uploads(mutation, current_user: current_user)
|
||||
|
||||
expect(mutation_response['message']).to eq('Import started')
|
||||
expect(mutation_response['errors']).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'with file validation' do
|
||||
shared_examples 'rejects invalid file with error message' do
|
||||
it 'rejects file with proper error message' do
|
||||
post_graphql_mutation_with_uploads(mutation, current_user: current_user)
|
||||
|
||||
expect(mutation_response['message']).to be_nil
|
||||
expect(mutation_response['errors']).to eq(
|
||||
['The uploaded file was invalid. Supported file extensions are .csv.']
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not call the import service' do
|
||||
expect(WorkItems::PrepareImportCsvService).not_to receive(:new)
|
||||
post_graphql_mutation_with_uploads(mutation, current_user: current_user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with image file' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
|
||||
|
||||
it_behaves_like 'rejects invalid file with error message'
|
||||
end
|
||||
|
||||
context 'with PDF file' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/sample.pdf') }
|
||||
|
||||
it_behaves_like 'rejects invalid file with error message'
|
||||
end
|
||||
|
||||
context 'with JSON file' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/service_account.json') }
|
||||
|
||||
it_behaves_like 'rejects invalid file with error message'
|
||||
end
|
||||
|
||||
context 'with XML file' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/unsafe_javascript.xml') }
|
||||
|
||||
it_behaves_like 'rejects invalid file with error message'
|
||||
end
|
||||
|
||||
context 'with markdown file' do
|
||||
let(:file) { fixture_file_upload('spec/fixtures/sample_doc.md') }
|
||||
|
||||
it_behaves_like 'rejects invalid file with error message'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when import service returns error' do
|
||||
it 'returns error message and verifies service parameters', :aggregate_failures do
|
||||
expect(WorkItems::PrepareImportCsvService).to receive(:new) do |received_project, received_user, options|
|
||||
expect(received_project).to eq(project)
|
||||
expect(received_user).to eq(reporter)
|
||||
|
||||
# Verify the file is passed and has the correct properties
|
||||
uploaded_file = options[:file]
|
||||
expect(uploaded_file).to be_present
|
||||
expect(uploaded_file).to respond_to(:original_filename)
|
||||
expect(uploaded_file.original_filename).to eq('work_items_valid.csv')
|
||||
|
||||
# Create a double that returns an error response
|
||||
instance_double(WorkItems::PrepareImportCsvService).tap do |service_double|
|
||||
allow(service_double).to receive(:execute).and_return(ServiceResponse.error(message: 'Invalid file format'))
|
||||
end
|
||||
end
|
||||
|
||||
post_graphql_mutation_with_uploads(mutation, current_user: current_user)
|
||||
|
||||
expect(mutation_response['message']).to be_nil
|
||||
expect(mutation_response['errors']).to eq(['Invalid file format'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when verifying file content is processed' do
|
||||
it 'ensures the uploaded file is accessible to the service' do
|
||||
expect(File.exist?('spec/fixtures/work_items_valid.csv')).to be true
|
||||
file_content = File.read('spec/fixtures/work_items_valid.csv')
|
||||
expect(file_content).to include('title,type')
|
||||
expect(file_content).to include('Issue')
|
||||
|
||||
expect(WorkItems::PrepareImportCsvService).to receive(:new) do |_received_project, _received_user, options|
|
||||
uploaded_file = options[:file]
|
||||
expect(uploaded_file).to be_present
|
||||
expect(uploaded_file.original_filename).to eq('work_items_valid.csv')
|
||||
|
||||
file_content = if uploaded_file.respond_to?(:tempfile)
|
||||
uploaded_file.tempfile.rewind
|
||||
uploaded_file.tempfile.read
|
||||
else
|
||||
uploaded_file.read
|
||||
end
|
||||
|
||||
expect(file_content).to include('title,type')
|
||||
expect(file_content).to include('Issue')
|
||||
|
||||
instance_double(WorkItems::PrepareImportCsvService).tap do |service_double|
|
||||
allow(service_double).to receive(:execute).and_return(ServiceResponse.success(message: 'Import started'))
|
||||
end
|
||||
end
|
||||
|
||||
post_graphql_mutation_with_uploads(mutation, current_user: current_user)
|
||||
|
||||
expect(mutation_response['message']).to eq('Import started')
|
||||
expect(mutation_response['errors']).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
require 'tempfile'
|
||||
require_relative '../../../scripts/release_environment/construct-release-environments-versions'
|
||||
|
||||
RSpec.describe ReleaseEnvironmentsModel, feature_category: :delivery do
|
||||
|
|
@ -35,7 +36,7 @@ RSpec.describe ReleaseEnvironmentsModel, feature_category: :delivery do
|
|||
|
||||
context 'when required env vars are missing' do
|
||||
it 'returns false' do
|
||||
ENV.delete('DEPLOY_ENV')
|
||||
stub_env('DEPLOY_ENV', nil)
|
||||
expect(model.set_required_env_vars?).to be false
|
||||
end
|
||||
end
|
||||
|
|
@ -80,4 +81,52 @@ RSpec.describe ReleaseEnvironmentsModel, feature_category: :delivery do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#omnibus_package_version' do
|
||||
it 'generates the correct omnibus package name' do
|
||||
stub_env('CI_COMMIT_BRANCH', '15-10-stable-ee')
|
||||
stub_env('CI_PIPELINE_ID', '12345')
|
||||
stub_env('CI_COMMIT_SHORT_SHA', 'abcdef')
|
||||
|
||||
expected_package = '15.10+stable.12345.abcdef'
|
||||
expect(model.omnibus_package_version).to eq(expected_package)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#write_deploy_env_file' do
|
||||
let(:temp_file) { Tempfile.new('deploy_env_test') }
|
||||
|
||||
before do
|
||||
stub_env('DEPLOY_ENV', temp_file.path)
|
||||
stub_env('CI_COMMIT_SHORT_SHA', 'abc123')
|
||||
stub_env('CI_COMMIT_REF_NAME', '16-0-stable-ee')
|
||||
stub_env('CI_PROJECT_PATH', 'gitlab-org/gitlab')
|
||||
stub_env('CI_PIPELINE_ID', '98765')
|
||||
stub_env('CI_COMMIT_BRANCH', '16-0-stable-ee')
|
||||
end
|
||||
|
||||
after do
|
||||
temp_file.close
|
||||
temp_file.unlink
|
||||
end
|
||||
|
||||
context 'when all env vars are present' do
|
||||
it 'writes the correct content to the DEPLOY_ENV file' do
|
||||
expect { model.write_deploy_env_file }.to output(/ENVIRONMENT=16-0-stable/).to_stdout
|
||||
|
||||
content = File.read(temp_file.path)
|
||||
expect(content).to include('ENVIRONMENT=16-0-stable')
|
||||
expect(content).to include('VERSIONS={"gitaly":"16-0-stable-abc123"')
|
||||
expect(content).to include('OMNIBUS_PACKAGE_VERSION=16.0+stable.98765.abc123')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when required env vars are missing' do
|
||||
it 'raises an error' do
|
||||
stub_env('DEPLOY_ENV', nil)
|
||||
|
||||
expect { model.write_deploy_env_file }.to raise_error(RuntimeError, "Missing required environment variable.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -87,12 +87,6 @@ RSpec.describe ContainerRegistry::Protection::Concerns::TagRule, feature_categor
|
|||
|
||||
subject(:protected_by_rules) { service.protected_for_delete?(project:, current_user:) }
|
||||
|
||||
context 'when project has immutable tag rules' do
|
||||
before_all do
|
||||
create(:container_registry_protection_tag_rule, :immutable, tag_name_pattern: 'a', project: project)
|
||||
end
|
||||
|
||||
it_behaves_like 'checking for mutable tag rules' # immutable rules are ignored
|
||||
end
|
||||
it_behaves_like 'checking for mutable tag rules'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -75,14 +75,14 @@ RSpec.describe ContainerRegistry::Protection::CreateTagRuleService, '#execute',
|
|||
|
||||
it_behaves_like 'a successful service response'
|
||||
|
||||
context 'with invalid params' do
|
||||
context 'with invalid params', unless: Gitlab.ee? do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:params_invalid, :message_expected) do
|
||||
{ tag_name_pattern: '' } | ["Tag name pattern can't be blank"]
|
||||
{ tag_name_pattern: '*' } | ["Tag name pattern not valid RE2 syntax: no argument for repetition operator: *"]
|
||||
{ minimum_access_level_for_delete: nil } | ['Access levels should either both be present or both be nil']
|
||||
{ minimum_access_level_for_push: nil } | ['Access levels should either both be present or both be nil']
|
||||
{ minimum_access_level_for_delete: nil } | ['Access levels should both be present']
|
||||
{ minimum_access_level_for_push: nil } | ['Access levels should both be present']
|
||||
{ minimum_access_level_for_delete: 1000 } | "'1000' is not a valid minimum_access_level_for_delete"
|
||||
{ minimum_access_level_for_push: 1000 } | "'1000' is not a valid minimum_access_level_for_push"
|
||||
end
|
||||
|
|
@ -91,8 +91,6 @@ RSpec.describe ContainerRegistry::Protection::CreateTagRuleService, '#execute',
|
|||
let(:params) { super().merge(params_invalid) }
|
||||
|
||||
it_behaves_like 'an erroneous service response', message: params[:message_expected]
|
||||
|
||||
it { is_expected.to have_attributes message: message_expected }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -73,26 +73,22 @@ RSpec.describe ContainerRegistry::Protection::UpdateTagRuleService, '#execute',
|
|||
end
|
||||
end
|
||||
|
||||
context 'with invalid params' do
|
||||
context 'with invalid params', unless: Gitlab.ee? do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:params_invalid, :message_expected) do
|
||||
{ tag_name_pattern: '' } | ["Tag name pattern can't be blank"]
|
||||
{ tag_name_pattern: '*' } | ['Tag name pattern not valid RE2 syntax: no argument for repetition operator: *']
|
||||
{ minimum_access_level_for_delete: nil } | ['Access levels should either both be present or both be nil']
|
||||
{ minimum_access_level_for_push: nil } | ['Access levels should either both be present or both be nil']
|
||||
{ minimum_access_level_for_delete: nil } | ['Access levels should both be present']
|
||||
{ minimum_access_level_for_push: nil } | ['Access levels should both be present']
|
||||
{ minimum_access_level_for_delete: 1000 } | "'1000' is not a valid minimum_access_level_for_delete"
|
||||
{ minimum_access_level_for_push: 1000 } | "'1000' is not a valid minimum_access_level_for_push"
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:params) do
|
||||
super().merge(params_invalid)
|
||||
end
|
||||
let(:params) { super().merge(params_invalid) }
|
||||
|
||||
it_behaves_like 'an erroneous service response', message: params[:message_expected]
|
||||
|
||||
it { is_expected.to have_attributes message: message_expected }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1570,11 +1570,7 @@ RSpec.shared_examples 'a container registry auth service' do
|
|||
project: current_project,
|
||||
tag_name_pattern: 'admin-only',
|
||||
minimum_access_level_for_push: :admin,
|
||||
minimum_access_level_for_delete: :owner),
|
||||
create(:container_registry_protection_tag_rule,
|
||||
:immutable,
|
||||
project: current_project,
|
||||
tag_name_pattern: 'immutable-pattern-not-included')
|
||||
minimum_access_level_for_delete: :owner)
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
26
yarn.lock
26
yarn.lock
|
|
@ -1315,10 +1315,10 @@
|
|||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.30.0", "@eslint/js@^9.15.0":
|
||||
version "9.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.30.0.tgz#c396fa450d5505dd9b7b8846b33f0491aebd9a2d"
|
||||
integrity sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==
|
||||
"@eslint/js@9.30.1", "@eslint/js@^9.15.0":
|
||||
version "9.30.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.30.1.tgz#ebe9dd52a38345784c486300175a28c6013c088d"
|
||||
integrity sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==
|
||||
|
||||
"@eslint/object-schema@^2.1.6":
|
||||
version "2.1.6"
|
||||
|
|
@ -1440,8 +1440,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/fonts/-/fonts-1.3.0.tgz#df89c1bb6714e4a8a5d3272568aa4de7fb337267"
|
||||
integrity sha512-DoMUIN3DqjEn7wvcxBg/b7Ite5fTdF5EmuOZoBRo2j0UBGweDXmNBi+9HrTZs4cBU660dOxcf1hATFcG3npbPg==
|
||||
|
||||
"@gitlab/noop@^1.0.1", jackspeak@^3.1.2, "jackspeak@npm:@gitlab/noop@1.0.1":
|
||||
name jackspeak
|
||||
"@gitlab/noop@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454"
|
||||
integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q==
|
||||
|
|
@ -7519,10 +7518,10 @@ eslint-visitor-keys@^4.2.1:
|
|||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
eslint@9.30.0:
|
||||
version "9.30.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.30.0.tgz#fb0c655f5e28fc1b2f4050c28efa1876d78034fc"
|
||||
integrity sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==
|
||||
eslint@9.30.1:
|
||||
version "9.30.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.30.1.tgz#d4107b39964412acd9b5c0744f1c6df514fa1211"
|
||||
integrity sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@eslint-community/regexpp" "^4.12.1"
|
||||
|
|
@ -7530,7 +7529,7 @@ eslint@9.30.0:
|
|||
"@eslint/config-helpers" "^0.3.0"
|
||||
"@eslint/core" "^0.14.0"
|
||||
"@eslint/eslintrc" "^3.3.1"
|
||||
"@eslint/js" "9.30.0"
|
||||
"@eslint/js" "9.30.1"
|
||||
"@eslint/plugin-kit" "^0.3.1"
|
||||
"@humanfs/node" "^0.16.6"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
|
|
@ -9510,6 +9509,11 @@ istanbul-reports@^3.1.3:
|
|||
html-escaper "^2.0.0"
|
||||
istanbul-lib-report "^3.0.0"
|
||||
|
||||
jackspeak@^3.1.2, "jackspeak@npm:@gitlab/noop@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454"
|
||||
integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q==
|
||||
|
||||
jed@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"
|
||||
|
|
|
|||
Loading…
Reference in New Issue