Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5376a0c41d
commit
83e25d1437
|
|
@ -0,0 +1,86 @@
|
|||
<script>
|
||||
import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButtonGroup,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: {
|
||||
forksCount: {
|
||||
default: 0,
|
||||
},
|
||||
projectFullPath: {
|
||||
default: '',
|
||||
},
|
||||
projectForksUrl: {
|
||||
default: '',
|
||||
},
|
||||
userForkUrl: {
|
||||
default: '',
|
||||
},
|
||||
newForkUrl: {
|
||||
default: '',
|
||||
},
|
||||
canReadCode: {
|
||||
default: false,
|
||||
},
|
||||
canCreateFork: {
|
||||
default: false,
|
||||
},
|
||||
canForkProject: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
forkButtonUrl() {
|
||||
return this.userForkUrl || this.newForkUrl;
|
||||
},
|
||||
userHasForkAccess() {
|
||||
return Boolean(this.userForkUrl) && this.canReadCode;
|
||||
},
|
||||
userCanFork() {
|
||||
return this.canReadCode && this.canCreateFork && this.canForkProject;
|
||||
},
|
||||
forkButtonEnabled() {
|
||||
return this.userHasForkAccess || this.userCanFork;
|
||||
},
|
||||
forkButtonTooltip() {
|
||||
if (!this.canForkProject) {
|
||||
return s__("ProjectOverview|You don't have permission to fork this project");
|
||||
}
|
||||
|
||||
if (!this.canCreateFork) {
|
||||
return s__('ProjectOverview|You have reached your project limit');
|
||||
}
|
||||
|
||||
if (this.userHasForkAccess) {
|
||||
return s__('ProjectOverview|Go to your fork');
|
||||
}
|
||||
|
||||
return s__('ProjectOverview|Create new fork');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-button-group :vertical="false">
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
data-testid="fork-button"
|
||||
:disabled="!forkButtonEnabled"
|
||||
:href="forkButtonUrl"
|
||||
icon="fork"
|
||||
:title="forkButtonTooltip"
|
||||
>{{ s__('ProjectOverview|Forks') }}</gl-button
|
||||
>
|
||||
<gl-button data-testid="forks-count" :disabled="!canReadCode" :href="projectForksUrl">{{
|
||||
forksCount
|
||||
}}</gl-button>
|
||||
</gl-button-group>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import ForksButton from './components/forks_button.vue';
|
||||
|
||||
const initForksButton = () => {
|
||||
const el = document.getElementById('js-forks-button');
|
||||
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
forksCount,
|
||||
projectFullPath,
|
||||
projectForksUrl,
|
||||
userForkUrl,
|
||||
newForkUrl,
|
||||
canReadCode,
|
||||
canCreateFork,
|
||||
canForkProject,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
forksCount,
|
||||
projectFullPath,
|
||||
projectForksUrl,
|
||||
userForkUrl,
|
||||
newForkUrl,
|
||||
canReadCode: parseBoolean(canReadCode),
|
||||
canCreateFork: parseBoolean(canCreateFork),
|
||||
canForkProject: parseBoolean(canForkProject),
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(ForksButton);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default initForksButton;
|
||||
|
|
@ -8,6 +8,8 @@ import initTerraformNotification from '~/projects/terraform_notification';
|
|||
import { initUploadFileTrigger } from '~/projects/upload_file';
|
||||
import initReadMore from '~/read_more';
|
||||
|
||||
import initForksButton from '~/forks/init_forks_button';
|
||||
|
||||
// Project show page loads different overview content based on user preferences
|
||||
if (document.getElementById('js-tree-list')) {
|
||||
import(/* webpackChunkName: 'treeList' */ 'ee_else_ce/repository')
|
||||
|
|
@ -57,3 +59,5 @@ if (document.querySelector('.js-autodevops-banner')) {
|
|||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
initForksButton();
|
||||
|
|
|
|||
|
|
@ -464,14 +464,23 @@ module ProjectsHelper
|
|||
project.forking_enabled? && can?(user, :read_code, project)
|
||||
end
|
||||
|
||||
def fork_button_disabled_tooltip(project)
|
||||
def fork_button_data_attributes(project)
|
||||
return unless current_user
|
||||
|
||||
if !current_user.can?(:fork_project, project)
|
||||
s_("ProjectOverview|You don't have permission to fork this project")
|
||||
elsif !current_user.can?(:create_fork)
|
||||
s_('ProjectOverview|You have reached your project limit')
|
||||
if current_user.already_forked?(project) && current_user.forkable_namespaces.size < 2
|
||||
user_fork_url = namespace_project_path(current_user, current_user.fork_of(project))
|
||||
end
|
||||
|
||||
{
|
||||
forks_count: project.forks_count,
|
||||
project_full_path: project.full_path,
|
||||
project_forks_url: project_forks_path(project),
|
||||
user_fork_url: user_fork_url,
|
||||
new_fork_url: new_project_fork_path(project),
|
||||
can_read_code: can?(current_user, :read_code, project).to_s,
|
||||
can_fork_project: can?(current_user, :fork_project, project).to_s,
|
||||
can_create_fork: can?(current_user, :create_fork).to_s
|
||||
}
|
||||
end
|
||||
|
||||
def import_from_bitbucket_message
|
||||
|
|
|
|||
|
|
@ -163,6 +163,14 @@ class Packages::Package < ApplicationRecord
|
|||
scope :preload_pypi_metadatum, -> { preload(:pypi_metadatum) }
|
||||
scope :preload_conan_metadatum, -> { preload(:conan_metadatum) }
|
||||
|
||||
scope :with_npm_scope, ->(scope) do
|
||||
if Feature.enabled?(:npm_package_registry_fix_group_path_validation)
|
||||
npm.where("position('/' in packages_packages.name) > 0 AND split_part(packages_packages.name, '/', 1) = :package_scope", package_scope: "@#{sanitize_sql_like(scope)}")
|
||||
else
|
||||
npm.where("name ILIKE :package_name", package_name: "@#{sanitize_sql_like(scope)}/%")
|
||||
end
|
||||
end
|
||||
|
||||
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
|
||||
|
||||
scope :has_version, -> { where.not(version: nil) }
|
||||
|
|
@ -184,7 +192,6 @@ class Packages::Package < ApplicationRecord
|
|||
scope :order_project_name, -> { joins(:project).reorder('projects.name ASC') }
|
||||
scope :order_project_name_desc, -> { joins(:project).reorder('projects.name DESC') }
|
||||
scope :order_by_package_file, -> { joins(:package_files).order('packages_package_files.created_at ASC') }
|
||||
scope :with_npm_scope, ->(scope) { npm.where("name ILIKE :package_name", package_name: "@#{sanitize_sql_like(scope)}/%") }
|
||||
|
||||
scope :order_project_path, -> do
|
||||
keyset_order = keyset_pagination_order(join_class: Project, column_name: :path, direction: :asc)
|
||||
|
|
|
|||
|
|
@ -5,32 +5,32 @@ module Deployments
|
|||
class CreateForJobService
|
||||
DeploymentCreationError = Class.new(StandardError)
|
||||
|
||||
def execute(build)
|
||||
return unless build.is_a?(::Ci::Processable) && build.persisted_environment.present?
|
||||
def execute(job)
|
||||
return unless job.is_a?(::Ci::Processable) && job.persisted_environment.present?
|
||||
|
||||
environment = build.actual_persisted_environment
|
||||
environment = job.actual_persisted_environment
|
||||
|
||||
deployment = to_resource(build, environment)
|
||||
deployment = to_resource(job, environment)
|
||||
|
||||
return unless deployment
|
||||
|
||||
deployment.save!
|
||||
build.association(:deployment).target = deployment
|
||||
build.association(:deployment).loaded!
|
||||
job.association(:deployment).target = deployment
|
||||
job.association(:deployment).loaded!
|
||||
|
||||
deployment
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
|
||||
DeploymentCreationError.new(e.message), build_id: build.id)
|
||||
DeploymentCreationError.new(e.message), job_id: job.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_resource(build, environment)
|
||||
return build.deployment if build.deployment
|
||||
return unless build.deployment_job?
|
||||
def to_resource(job, environment)
|
||||
return job.deployment if job.deployment
|
||||
return unless job.deployment_job?
|
||||
|
||||
deployment = ::Deployment.new(attributes(build, environment))
|
||||
deployment = ::Deployment.new(attributes(job, environment))
|
||||
|
||||
# If there is a validation error on environment creation, such as
|
||||
# the name contains invalid character, the job will fall back to a
|
||||
|
|
@ -42,7 +42,7 @@ module Deployments
|
|||
deployment.cluster_id = cluster.id
|
||||
deployment.deployment_cluster = ::DeploymentCluster.new(
|
||||
cluster_id: cluster.id,
|
||||
kubernetes_namespace: cluster.kubernetes_namespace_for(deployment.environment, deployable: build)
|
||||
kubernetes_namespace: cluster.kubernetes_namespace_for(deployment.environment, deployable: job)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -53,16 +53,16 @@ module Deployments
|
|||
deployment
|
||||
end
|
||||
|
||||
def attributes(build, environment)
|
||||
def attributes(job, environment)
|
||||
{
|
||||
project: build.project,
|
||||
project: job.project,
|
||||
environment: environment,
|
||||
deployable: build,
|
||||
user: build.user,
|
||||
ref: build.ref,
|
||||
tag: build.tag,
|
||||
sha: build.sha,
|
||||
on_stop: build.on_stop
|
||||
deployable: job,
|
||||
user: job.user,
|
||||
ref: job.ref,
|
||||
tag: job.tag,
|
||||
sha: job.sha,
|
||||
on_stop: job.on_stop
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@
|
|||
module Environments
|
||||
# This class creates an environment record for a pipeline job.
|
||||
class CreateForJobService
|
||||
def execute(build)
|
||||
return unless build.is_a?(::Ci::Processable) && build.has_environment_keyword?
|
||||
def execute(job)
|
||||
return unless job.is_a?(::Ci::Processable) && job.has_environment_keyword?
|
||||
|
||||
environment = to_resource(build)
|
||||
environment = to_resource(job)
|
||||
|
||||
if environment.persisted?
|
||||
build.persisted_environment = environment
|
||||
build.assign_attributes(metadata_attributes: { expanded_environment_name: environment.name })
|
||||
job.persisted_environment = environment
|
||||
job.assign_attributes(metadata_attributes: { expanded_environment_name: environment.name })
|
||||
else
|
||||
build.assign_attributes(status: :failed, failure_reason: :environment_creation_failure)
|
||||
job.assign_attributes(status: :failed, failure_reason: :environment_creation_failure)
|
||||
end
|
||||
|
||||
environment
|
||||
|
|
@ -21,20 +21,20 @@ module Environments
|
|||
private
|
||||
|
||||
# rubocop: disable Performance/ActiveRecordSubtransactionMethods
|
||||
def to_resource(build)
|
||||
build.project.environments.safe_find_or_create_by(name: build.expanded_environment_name) do |environment|
|
||||
def to_resource(job)
|
||||
job.project.environments.safe_find_or_create_by(name: job.expanded_environment_name) do |environment|
|
||||
# Initialize the attributes at creation
|
||||
environment.auto_stop_in = expanded_auto_stop_in(build)
|
||||
environment.tier = build.environment_tier_from_options
|
||||
environment.merge_request = build.pipeline.merge_request
|
||||
environment.auto_stop_in = expanded_auto_stop_in(job)
|
||||
environment.tier = job.environment_tier_from_options
|
||||
environment.merge_request = job.pipeline.merge_request
|
||||
end
|
||||
end
|
||||
# rubocop: enable Performance/ActiveRecordSubtransactionMethods
|
||||
|
||||
def expanded_auto_stop_in(build)
|
||||
return unless build.environment_auto_stop_in
|
||||
def expanded_auto_stop_in(job)
|
||||
return unless job.environment_auto_stop_in
|
||||
|
||||
ExpandVariables.expand(build.environment_auto_stop_in, -> { build.simple_variables.sort_and_expand_all })
|
||||
ExpandVariables.expand(job.environment_auto_stop_in, -> { job.simple_variables.sort_and_expand_all })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ module Groups
|
|||
|
||||
return false unless valid_share_with_group_lock_change?
|
||||
|
||||
return false unless valid_path_change_with_npm_packages?
|
||||
return false unless valid_path_change?
|
||||
|
||||
return false unless update_shared_runners
|
||||
|
||||
|
|
@ -46,6 +46,29 @@ module Groups
|
|||
|
||||
private
|
||||
|
||||
def valid_path_change?
|
||||
unless Feature.enabled?(:npm_package_registry_fix_group_path_validation)
|
||||
return valid_path_change_with_npm_packages?
|
||||
end
|
||||
|
||||
return true unless group.packages_feature_enabled?
|
||||
return true if params[:path].blank?
|
||||
return true if group.has_parent?
|
||||
return true if !group.has_parent? && group.path == params[:path]
|
||||
|
||||
# we have a path change on a root group:
|
||||
# check that we don't have any npm package with a scope set to the group path
|
||||
npm_packages = ::Packages::GroupPackagesFinder.new(current_user, group, package_type: :npm, preload_pipelines: false)
|
||||
.execute
|
||||
.with_npm_scope(group.path)
|
||||
|
||||
return true unless npm_packages.exists?
|
||||
|
||||
group.errors.add(:path, s_('GroupSettings|cannot change when group contains projects with NPM packages'))
|
||||
false
|
||||
end
|
||||
|
||||
# TODO: delete this function along with npm_package_registry_fix_group_path_validation
|
||||
def valid_path_change_with_npm_packages?
|
||||
return true unless group.packages_feature_enabled?
|
||||
return true if params[:path].blank?
|
||||
|
|
|
|||
|
|
@ -1,16 +1,3 @@
|
|||
- unless @project.empty_repo?
|
||||
- if current_user
|
||||
.count-badge.btn-group
|
||||
- if current_user.already_forked?(@project) && current_user.forkable_namespaces.size < 2
|
||||
= link_button_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'has-tooltip fork-btn', icon: 'fork' do
|
||||
= s_('ProjectOverview|Fork')
|
||||
- else
|
||||
- disabled_tooltip = fork_button_disabled_tooltip(@project)
|
||||
- count_class = 'disabled' unless can?(current_user, :read_code, @project)
|
||||
- button_class = 'disabled' if disabled_tooltip
|
||||
|
||||
%span.btn-group{ class: ('has-tooltip' if disabled_tooltip), title: disabled_tooltip }
|
||||
= link_button_to new_project_fork_path(@project), class: "fork-btn #{button_class}", data: { qa_selector: 'fork_button' }, icon: 'fork' do
|
||||
= s_('ProjectOverview|Fork')
|
||||
= link_button_to project_forks_path(@project), title: n_(s_('ProjectOverview|Forks'), s_('ProjectOverview|Forks'), @project.forks_count), class: "count has-tooltip fork-count #{count_class}" do
|
||||
= @project.forks_count
|
||||
#js-forks-button{ data: fork_button_data_attributes(@project) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: npm_package_registry_fix_group_path_validation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127164
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/420160
|
||||
milestone: '16.3'
|
||||
type: development
|
||||
group: group::package registry
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddNpmScopeAndProjectIndexToPackages < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'idx_packages_packages_on_npm_scope_and_project_id'
|
||||
|
||||
def up
|
||||
add_concurrent_index :packages_packages,
|
||||
"split_part(name, '/', 1), project_id",
|
||||
where: "package_type = 2 AND position('/' in name) > 0 AND status IN (0, 3) AND version IS NOT NULL",
|
||||
name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :packages_packages, INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
8e3a03a08085c8cc882ce9e87846402ef49dc0a48e8f0c8980e8462337536674
|
||||
|
|
@ -30130,6 +30130,8 @@ CREATE UNIQUE INDEX idx_packages_on_project_id_name_version_unique_when_helm ON
|
|||
|
||||
CREATE UNIQUE INDEX idx_packages_on_project_id_name_version_unique_when_npm ON packages_packages USING btree (project_id, name, version) WHERE ((package_type = 2) AND (status <> 4));
|
||||
|
||||
CREATE INDEX idx_packages_packages_on_npm_scope_and_project_id ON packages_packages USING btree (split_part((name)::text, '/'::text, 1), project_id) WHERE ((package_type = 2) AND ("position"((name)::text, '/'::text) > 0) AND (status = ANY (ARRAY[0, 3])) AND (version IS NOT NULL));
|
||||
|
||||
CREATE INDEX idx_packages_packages_on_project_id_name_version_package_type ON packages_packages USING btree (project_id, name, version, package_type);
|
||||
|
||||
CREATE INDEX idx_personal_access_tokens_on_previous_personal_access_token_id ON personal_access_tokens USING btree (previous_personal_access_token_id);
|
||||
|
|
|
|||
|
|
@ -43,9 +43,10 @@ POST /bulk_imports
|
|||
| `entities` | Array | yes | List of entities to import. |
|
||||
| `entities[source_type]` | String | yes | Source entity type. Valid values are `group_entity` (GitLab 14.2 and later) and `project_entity` (GitLab 15.11 and later). |
|
||||
| `entities[source_full_path]` | String | yes | Source full path of the entity to import. |
|
||||
| `entities[destination_name]` | String | yes | Deprecated: Use :destination_slug instead. Destination slug for the entity. |
|
||||
| `entities[destination_slug]` | String | yes | Destination slug for the entity. |
|
||||
| `entities[destination_name]` | String | no | Deprecated: Use `destination_slug` instead. Destination slug for the entity. |
|
||||
| `entities[destination_namespace]` | String | yes | Destination namespace for the entity. |
|
||||
| `entities[migrate_projects]` | Boolean | no | Also import all nested projects of the group (if `source_type` is `group_entity`). Defaults to `true`. |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/bulk_imports" \
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ group: unassigned
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# AI Architecture (Experiment)
|
||||
# AI Architecture
|
||||
|
||||
GitLab has created a common set of tools to support our product groups and their utilization of AI. Our goals with this common architecture are:
|
||||
|
||||
|
|
@ -13,79 +13,20 @@ GitLab has created a common set of tools to support our product groups and their
|
|||
|
||||
AI is moving very quickly, and we need to be able to keep pace with changes in the area. We have built an [abstraction layer](../../ee/development/ai_features.md) to do this, allowing us to take a more "pluggable" approach to the underlying models, data stores, and other technologies.
|
||||
|
||||
The following diagram shows a simplified view of how the different components in GitLab interact. The abstraction layer helps avoid code duplication within the REST APIs within the `AI API` block.
|
||||
The following diagram from the [architecture blueprint](../architecture/blueprints/ai_gateway/index.md) shows a simplified view of how the different components in GitLab interact. The abstraction layer helps avoid code duplication within the REST APIs within the `AI API` block.
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
skin rose
|
||||
|
||||
package "Code Suggestions" {
|
||||
node "Model Gateway"
|
||||
node "Triton Inference Server" as Triton
|
||||
}
|
||||
|
||||
package "Code Suggestions Models" as CSM {
|
||||
node "codegen"
|
||||
node "PaLM"
|
||||
}
|
||||
|
||||
package "Suggested Reviewers" {
|
||||
node "Model Gateway (SR)"
|
||||
node "Extractor"
|
||||
node "Serving Model"
|
||||
}
|
||||
|
||||
package "AI API" as AIF {
|
||||
node "OpenAI"
|
||||
node "Vertex AI"
|
||||
}
|
||||
|
||||
package GitLab {
|
||||
node "Web IDE"
|
||||
|
||||
package "Web" {
|
||||
node "REST API"
|
||||
node "GraphQL"
|
||||
}
|
||||
|
||||
package "Jobs" {
|
||||
node "Sidekiq"
|
||||
}
|
||||
}
|
||||
|
||||
package Databases {
|
||||
node "Vector Database"
|
||||
node "PostgreSQL"
|
||||
}
|
||||
|
||||
node "VSCode"
|
||||
|
||||
"Model Gateway" --> Triton
|
||||
Triton --> CSM
|
||||
GitLab --> Databases
|
||||
VSCode --> "Model Gateway"
|
||||
"Web IDE" --> "Model Gateway"
|
||||
"Web IDE" --> "GraphQL"
|
||||
"Web IDE" --> "REST API"
|
||||
"Model Gateway" -[#blue]--> "REST API": user authorized?
|
||||
|
||||
"Sidekiq" --> AIF
|
||||
Web --> AIF
|
||||
|
||||
"Model Gateway (SR)" --> "REST API"
|
||||
"Model Gateway (SR)" --> "Serving Model"
|
||||
"Extractor" --> "GraphQL"
|
||||
"Sidekiq" --> "Model Gateway (SR)"
|
||||
|
||||
@enduml
|
||||
```
|
||||

|
||||
|
||||
## SaaS-based AI abstraction layer
|
||||
|
||||
GitLab currently operates a cloud-hosted AI architecture. We are exploring how self-managed instances integrate with it.
|
||||
GitLab currently operates a cloud-hosted AI architecture. We will allow access to it for licensed self managed instances using the AI-gateway. Please see [the blueprint](../architecture/blueprints/ai_gateway) for details
|
||||
|
||||
There are two primary reasons for this: the best AI models are cloud-based as they often depend on specialized hardware designed for this purpose, and operating self-managed infrastructure capable of AI at-scale and with appropriate performance is a significant undertaking. We are actively [tracking self-managed customers interested in AI](https://gitlab.com/gitlab-org/gitlab/-/issues/409183).
|
||||
|
||||
## AI Gateway
|
||||
|
||||
The AI Gateway (formerly the [model gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist)) is a standalone-service that will give access to AI features to all users of GitLab, no matter which instance they are using: self-managed, dedicated or GitLab.com. The SaaS-based AI abstraction layer will transition to connecting to this gateway, rather than accessing cloud-based providers directly.
|
||||
|
||||
## Supported technologies
|
||||
|
||||
As part of the AI working group, we have been investigating various technologies and vetting them. Below is a list of the tools which have been reviewed and already approved for use within the GitLab application.
|
||||
|
|
@ -127,3 +68,38 @@ For optimal `probes` and `lists` values:
|
|||
|
||||
- Use `lists` equal to `rows / 1000` for tables with up to 1 million rows and `sqrt(rows)` for larger datasets.
|
||||
- For `probes` start with `lists / 10` for tables up to 1 million rows and `sqrt(lists)` for larger datasets.
|
||||
|
||||
### Code Suggestions
|
||||
|
||||
Code Suggestions is being integrated as part of the GitLab-Rails repository which will unify the architectures between Code Suggestions and AI features that use the abstraction layer, along with offering self-managed support for the other AI features.
|
||||
|
||||
The following table documents functionality that Code Suggestions offers today, and what those changes will look like as part of the unification:
|
||||
|
||||
| Topic | Details | Where this happens today | Where this will happen going forward |
|
||||
| ----- | ------ | -------------- | ------------------ |
|
||||
| Request processing | | | |
|
||||
| | Receives requests from IDEs (VSCode, GitLab WebIDE, MS Visual Studio, IntelliJ, JetBrains, VIM, Emacs, Sublime), including code before and after the cursor | AI Gateway | Abstraction Layer |
|
||||
| | Authentication the current user, verifies they are authorized to use Code Suggestions for this project | AI Gateway | Abstraction layer |
|
||||
| | Preprocesses the request to add context, such as including imports via TreeSitter | AI Gateway | Undecided |
|
||||
| | Routes the request to the AI Provider | AI Gateway | AI Gateway |
|
||||
| | Returns the response to the IDE | AI Gateway | Abstraction Layer |
|
||||
| | Logs the request, including timestamp, response time, model, etc | AI Gateway | Both |
|
||||
| Telemetry | | | |
|
||||
| | User acceptance or rejection in the IDE | AI Gateway | [Both](https://gitlab.com/gitlab-org/gitlab/-/issues/418282) |
|
||||
| | Number of unique users per day | [Abstraction Layer](https://app.periscopedata.com/app/gitlab/1143612/Code-Suggestions-Usage) | Undecided |
|
||||
| | Error rate, model usage, response time, IDE usage | [AI Gateway](https://log.gprd.gitlab.net/app/dashboards#/view/6c947f80-7c07-11ed-9f43-e3784d7fe3ca?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-6h,to:now))) | Both |
|
||||
| | Suggestions per language | AI Gateway |[Both](https://gitlab.com/groups/gitlab-org/-/epics/11017) |
|
||||
| Monitoring | | Both | Both |
|
||||
| | | | |
|
||||
| Model Routing | | | |
|
||||
| | Currently we are not using this functionality, but Code Suggestions is able to support routing to multiple models based on a percentage of traffic | AI Gateway | Both |
|
||||
| Internal Models | | | |
|
||||
| | Currently unmaintained, the ability to run models in our own instance, running them inside Triton, and routing requests to our own models | AI Gateway | AI Gateway |
|
||||
|
||||
#### Code Suggestions Latency
|
||||
|
||||
Code Suggestions acceptance rates are _highly_ sensitive to latency. While writing code with an AI assistant, a user will pause only for a short duration before continuing on with manually typing out a block of code. As soon as the user has pressed a subsequent keypress, the existing suggestion will be invalidated and a new request will need to be issued to the code suggestions endpoint. In turn, this request will also be highly sensitive to latency.
|
||||
|
||||
In a worst case with sufficient latency, the IDE could be issuing a string of requests, each of which is then ignored as the user proceeds without waiting for the response. This adds no value for the user, while still putting load on our services.
|
||||
|
||||
See our discussions [here](https://gitlab.com/gitlab-org/gitlab/-/issues/418955) around how we plan to iterate on latency for this feature.
|
||||
|
|
|
|||
|
|
@ -36154,7 +36154,7 @@ msgstr ""
|
|||
msgid "ProjectList|Yours"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectOverview|Fork"
|
||||
msgid "ProjectOverview|Create new fork"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectOverview|Forks"
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ module QA
|
|||
element :forked_from_link
|
||||
end
|
||||
|
||||
view 'app/views/projects/buttons/_fork.html.haml' do
|
||||
view 'app/assets/javascripts/forks/components/forks_button.vue' do
|
||||
element :fork_button
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ RSpec.describe 'Project fork', feature_category: :groups_and_projects do
|
|||
end
|
||||
|
||||
shared_examples 'fork button on project page' do
|
||||
context 'when the user has access to only one namespace and has already forked the project' do
|
||||
context 'when the user has access to only one namespace and has already forked the project', :js do
|
||||
before do
|
||||
fork_project(project, user, repository: true, namespace: user.namespace)
|
||||
end
|
||||
|
|
@ -25,7 +25,7 @@ RSpec.describe 'Project fork', feature_category: :groups_and_projects do
|
|||
path = namespace_project_path(user, user.fork_of(project))
|
||||
|
||||
fork_button = find_link 'Fork'
|
||||
expect(fork_button['href']).to eq(path)
|
||||
expect(fork_button['href']).to include(path)
|
||||
expect(fork_button['class']).not_to include('disabled')
|
||||
end
|
||||
end
|
||||
|
|
@ -37,7 +37,7 @@ RSpec.describe 'Project fork', feature_category: :groups_and_projects do
|
|||
path = new_project_fork_path(project)
|
||||
|
||||
fork_button = find_link 'Fork'
|
||||
expect(fork_button['href']).to eq(path)
|
||||
expect(fork_button['href']).to include(path)
|
||||
expect(fork_button['class']).not_to include('disabled')
|
||||
end
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ RSpec.describe 'Project fork', feature_category: :groups_and_projects do
|
|||
path = new_project_fork_path(project)
|
||||
|
||||
fork_button = find_link 'Fork'
|
||||
expect(fork_button['href']).to eq(path)
|
||||
expect(fork_button['href']).to include(path)
|
||||
expect(fork_button['class']).to include('disabled')
|
||||
end
|
||||
end
|
||||
|
|
@ -69,17 +69,17 @@ RSpec.describe 'Project fork', feature_category: :groups_and_projects do
|
|||
path = new_project_fork_path(project)
|
||||
|
||||
fork_button = find_link 'Fork'
|
||||
expect(fork_button['href']).to eq(path)
|
||||
expect(fork_button['href']).to include(path)
|
||||
expect(fork_button['class']).to include('disabled')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user has not already forked the project' do
|
||||
context 'when the user has not already forked the project', :js do
|
||||
it_behaves_like 'fork button creates new fork'
|
||||
end
|
||||
|
||||
context 'when the user has access to more than one namespace' do
|
||||
context 'when the user has access to more than one namespace', :js do
|
||||
let(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
|
|
@ -99,11 +99,11 @@ RSpec.describe 'Project fork', feature_category: :groups_and_projects do
|
|||
context 'forking is enabled' do
|
||||
let(:forking_access_level) { ProjectFeature::ENABLED }
|
||||
|
||||
it 'enables fork button' do
|
||||
it 'enables fork button', :js do
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).to have_css('a', text: 'Fork')
|
||||
expect(page).not_to have_css('a.disabled', text: 'Select')
|
||||
fork_button = find_link 'Fork'
|
||||
expect(fork_button['class']).not_to include('disabled')
|
||||
end
|
||||
|
||||
it 'renders new project fork page' do
|
||||
|
|
@ -117,11 +117,13 @@ RSpec.describe 'Project fork', feature_category: :groups_and_projects do
|
|||
context 'forking is disabled' do
|
||||
let(:forking_access_level) { ProjectFeature::DISABLED }
|
||||
|
||||
it 'render a disabled fork button' do
|
||||
it 'render a disabled fork button', :js do
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).to have_css('a.disabled', text: 'Fork')
|
||||
expect(page).to have_css('a.count', text: '0')
|
||||
fork_button = find_link 'Fork'
|
||||
|
||||
expect(fork_button['class']).to include('disabled')
|
||||
expect(page).to have_selector('[data-testid="forks-count"]')
|
||||
end
|
||||
|
||||
it 'does not render new project fork page' do
|
||||
|
|
@ -139,11 +141,13 @@ RSpec.describe 'Project fork', feature_category: :groups_and_projects do
|
|||
end
|
||||
|
||||
context 'user is not a team member' do
|
||||
it 'render a disabled fork button' do
|
||||
it 'render a disabled fork button', :js do
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).to have_css('a.disabled', text: 'Fork')
|
||||
expect(page).to have_css('a.count', text: '0')
|
||||
fork_button = find_link 'Fork'
|
||||
|
||||
expect(fork_button['class']).to include('disabled')
|
||||
expect(page).to have_selector('[data-testid="forks-count"]')
|
||||
end
|
||||
|
||||
it 'does not render new project fork page' do
|
||||
|
|
@ -158,12 +162,13 @@ RSpec.describe 'Project fork', feature_category: :groups_and_projects do
|
|||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'enables fork button' do
|
||||
it 'enables fork button', :js do
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).to have_css('a', text: 'Fork')
|
||||
expect(page).to have_css('a.count', text: '0')
|
||||
expect(page).not_to have_css('a.disabled', text: 'Fork')
|
||||
fork_button = find_link 'Fork'
|
||||
|
||||
expect(fork_button['class']).not_to include('disabled')
|
||||
expect(page).to have_selector('[data-testid="forks-count"]')
|
||||
end
|
||||
|
||||
it 'renders new project fork page' do
|
||||
|
|
@ -242,7 +247,8 @@ RSpec.describe 'Project fork', feature_category: :groups_and_projects do
|
|||
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).to have_css('.fork-count', text: 2)
|
||||
forks_count_button = find('[data-testid="forks-count"]')
|
||||
expect(forks_count_button).to have_content("2")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ForksButton from '~/forks/components/forks_button.vue';
|
||||
|
||||
describe('ForksButton', () => {
|
||||
let wrapper;
|
||||
|
||||
const findForkButton = () => wrapper.findByTestId('fork-button');
|
||||
const findForksCountButton = () => wrapper.findByTestId('forks-count');
|
||||
|
||||
const mountComponent = ({ injections } = {}) => {
|
||||
wrapper = mountExtended(ForksButton, {
|
||||
provide: {
|
||||
forksCount: 10,
|
||||
projectForksUrl: '/project/forks',
|
||||
userForkUrl: '/user/fork',
|
||||
newForkUrl: '/new/fork',
|
||||
canReadCode: true,
|
||||
canCreateFork: true,
|
||||
canForkProject: true,
|
||||
...injections,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('forks count button', () => {
|
||||
it('renders the correct number of forks', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findForksCountButton().text()).toBe('10');
|
||||
});
|
||||
|
||||
it('is disabled when the user cannot read code', () => {
|
||||
mountComponent({ injections: { canReadCode: false } });
|
||||
|
||||
expect(findForksCountButton().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('is enabled when the user can read code and has the correct link', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findForksCountButton().props('disabled')).toBe(false);
|
||||
expect(findForksCountButton().attributes('href')).toBe('/project/forks');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fork button', () => {
|
||||
const userForkUrlPath = '/user/fork';
|
||||
const newForkPath = '/new/fork';
|
||||
|
||||
const goToYourForkTitle = 'Go to your fork';
|
||||
const createNewForkTitle = 'Create new fork';
|
||||
const reachedLimitTitle = 'You have reached your project limit';
|
||||
const noPermissionsTitle = "You don't have permission to fork this project";
|
||||
|
||||
it.each`
|
||||
userForkUrl | canReadCode | canCreateFork | canForkProject | isDisabled | title | href
|
||||
${userForkUrlPath} | ${true} | ${true} | ${true} | ${false} | ${goToYourForkTitle} | ${userForkUrlPath}
|
||||
${userForkUrlPath} | ${false} | ${true} | ${true} | ${true} | ${createNewForkTitle} | ${userForkUrlPath}
|
||||
${null} | ${true} | ${true} | ${true} | ${false} | ${createNewForkTitle} | ${newForkPath}
|
||||
${null} | ${false} | ${true} | ${true} | ${true} | ${createNewForkTitle} | ${newForkPath}
|
||||
${null} | ${true} | ${false} | ${true} | ${true} | ${reachedLimitTitle} | ${newForkPath}
|
||||
${null} | ${true} | ${true} | ${false} | ${true} | ${noPermissionsTitle} | ${newForkPath}
|
||||
`(
|
||||
'has the right enabled state, title, and link',
|
||||
({ userForkUrl, canReadCode, canCreateFork, canForkProject, isDisabled, title, href }) => {
|
||||
mountComponent({ injections: { userForkUrl, canReadCode, canCreateFork, canForkProject } });
|
||||
|
||||
expect(findForkButton().props('disabled')).toBe(isDisabled);
|
||||
expect(findForkButton().attributes('title')).toBe(title);
|
||||
expect(findForkButton().attributes('href')).toBe(href);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1130,16 +1130,39 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#fork_button_disabled_tooltip' do
|
||||
describe '#fork_button_data_attributes' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
subject { helper.fork_button_disabled_tooltip(project) }
|
||||
let_it_be(:project) { create(:project, :repository, :public) }
|
||||
|
||||
where(:has_user, :can_fork_project, :can_create_fork, :expected) do
|
||||
false | false | false | nil
|
||||
true | true | true | nil
|
||||
true | false | true | 'You don\'t have permission to fork this project'
|
||||
true | true | false | 'You have reached your project limit'
|
||||
project_path = '/project/path'
|
||||
project_forks_path = '/project/forks'
|
||||
project_new_fork_path = '/project/new/fork'
|
||||
user_fork_url = '/user/fork'
|
||||
|
||||
common_data_attributes = {
|
||||
forks_count: 4,
|
||||
project_full_path: project_path,
|
||||
project_forks_url: project_forks_path,
|
||||
can_create_fork: "true",
|
||||
can_fork_project: "true",
|
||||
can_read_code: "true",
|
||||
new_fork_url: project_new_fork_path
|
||||
}
|
||||
|
||||
data_attributes_with_user_fork_url = common_data_attributes.merge({ user_fork_url: user_fork_url })
|
||||
data_attributes_without_user_fork_url = common_data_attributes.merge({ user_fork_url: nil })
|
||||
|
||||
subject { helper.fork_button_data_attributes(project) }
|
||||
|
||||
# The stubs for the forkable namespaces seem not to make sense (they're just numbers),
|
||||
# but they're set up that way because we don't really care about what the array contains, only about its length
|
||||
where(:has_user, :project_already_forked, :forkable_namespaces, :expected) do
|
||||
false | false | [] | nil
|
||||
true | false | [0] | data_attributes_without_user_fork_url
|
||||
true | false | [0, 1] | data_attributes_without_user_fork_url
|
||||
true | true | [0] | data_attributes_with_user_fork_url
|
||||
true | true | [0, 1] | data_attributes_without_user_fork_url
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
@ -1147,13 +1170,22 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
|
|||
current_user = user if has_user
|
||||
|
||||
allow(helper).to receive(:current_user).and_return(current_user)
|
||||
allow(user).to receive(:can?).with(:fork_project, project).and_return(can_fork_project)
|
||||
allow(user).to receive(:can?).with(:create_fork).and_return(can_create_fork)
|
||||
allow(user).to receive(:can?).with(:fork_project, project).and_return(true)
|
||||
allow(user).to receive(:can?).with(:create_fork).and_return(true)
|
||||
allow(user).to receive(:can?).with(:create_projects, anything).and_return(true)
|
||||
allow(user).to receive(:already_forked?).with(project).and_return(project_already_forked)
|
||||
allow(user).to receive(:forkable_namespaces).and_return(forkable_namespaces)
|
||||
|
||||
allow(project).to receive(:forks_count).and_return(4)
|
||||
allow(project).to receive(:full_path).and_return(project_path)
|
||||
|
||||
user_fork_path = user_fork_url if project_already_forked
|
||||
allow(helper).to receive(:namespace_project_path).with(user, anything).and_return(user_fork_path)
|
||||
allow(helper).to receive(:new_project_fork_path).with(project).and_return(project_new_fork_path)
|
||||
allow(helper).to receive(:project_forks_path).with(project).and_return(project_forks_path)
|
||||
end
|
||||
|
||||
it 'returns tooltip text when user lacks privilege' do
|
||||
expect(subject).to eq(expected)
|
||||
end
|
||||
it { is_expected.to eq(expected) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -867,6 +867,24 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
|
|||
end
|
||||
end
|
||||
|
||||
describe '.with_npm_scope' do
|
||||
let_it_be(:package1) { create(:npm_package, name: '@test/foobar') }
|
||||
let_it_be(:package2) { create(:npm_package, name: '@test2/foobar') }
|
||||
let_it_be(:package3) { create(:npm_package, name: 'foobar') }
|
||||
|
||||
subject { described_class.with_npm_scope('test') }
|
||||
|
||||
it { is_expected.to contain_exactly(package1) }
|
||||
|
||||
context 'when npm_package_registry_fix_group_path_validation is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_package_registry_fix_group_path_validation: false)
|
||||
end
|
||||
|
||||
it { is_expected.to contain_exactly(package1) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.without_nuget_temporary_name' do
|
||||
let!(:package1) { create(:nuget_package) }
|
||||
let!(:package2) { create(:nuget_package, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
|
||||
|
|
|
|||
|
|
@ -9,33 +9,10 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
|
|||
let!(:public_group) { create(:group, :public) }
|
||||
|
||||
describe "#execute" do
|
||||
shared_examples 'with packages' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
context 'with npm packages' do
|
||||
let!(:package) { create(:npm_package, project: project) }
|
||||
|
||||
it 'does not allow a path update' do
|
||||
expect(update_group(group, user, path: 'updated')).to be false
|
||||
expect(group.errors[:path]).to include('cannot change when group contains projects with NPM packages')
|
||||
end
|
||||
|
||||
it 'allows name update' do
|
||||
expect(update_group(group, user, name: 'Updated')).to be true
|
||||
expect(group.errors).to be_empty
|
||||
expect(group.name).to eq('Updated')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with project' do
|
||||
let!(:group) { create(:group, :public) }
|
||||
let(:project) { create(:project, namespace: group) }
|
||||
|
||||
it_behaves_like 'with packages'
|
||||
|
||||
context 'located in a subgroup' do
|
||||
let(:subgroup) { create(:group, parent: group) }
|
||||
let!(:project) { create(:project, namespace: subgroup) }
|
||||
|
|
@ -44,8 +21,6 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
|
|||
subgroup.add_owner(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'with packages'
|
||||
|
||||
it 'does allow a path update if there is not a root namespace change' do
|
||||
expect(update_group(subgroup, user, path: 'updated')).to be true
|
||||
expect(subgroup.errors[:path]).to be_empty
|
||||
|
|
@ -251,6 +226,163 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
|
|||
end
|
||||
end
|
||||
|
||||
context "path change validation" do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:project) { create(:project, namespace: subgroup) }
|
||||
|
||||
subject(:execute_update) { update_group(target_group, user, update_params) }
|
||||
|
||||
shared_examples 'not allowing a path update' do
|
||||
let(:update_params) { { path: 'updated' } }
|
||||
|
||||
it 'does not allow a path update' do
|
||||
target_group.add_maintainer(user)
|
||||
|
||||
expect(execute_update).to be false
|
||||
expect(target_group.errors[:path]).to include('cannot change when group contains projects with NPM packages')
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'allowing an update' do |on:|
|
||||
let(:update_params) { { on => 'updated' } }
|
||||
|
||||
it "allows an update on #{on}" do
|
||||
target_group.reload.add_maintainer(user)
|
||||
|
||||
expect(execute_update).to be true
|
||||
expect(target_group.errors).to be_empty
|
||||
expect(target_group[on]).to eq('updated')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with namespaced npm packages' do
|
||||
let_it_be(:package) { create(:npm_package, project: project, name: "@#{group.path}/test") }
|
||||
|
||||
context 'updating the root group' do
|
||||
let_it_be_with_refind(:target_group) { group }
|
||||
|
||||
it_behaves_like 'not allowing a path update'
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_package_registry_fix_group_path_validation is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_package_registry_fix_group_path_validation: false)
|
||||
expect_next_instance_of(::Groups::UpdateService) do |service|
|
||||
expect(service).to receive(:valid_path_change_with_npm_packages?).and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'not allowing a path update'
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
|
||||
context 'updating the subgroup' do
|
||||
let_it_be_with_refind(:target_group) { subgroup }
|
||||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_package_registry_fix_group_path_validation is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_package_registry_fix_group_path_validation: false)
|
||||
expect_next_instance_of(::Groups::UpdateService) do |service|
|
||||
expect(service).to receive(:valid_path_change_with_npm_packages?).and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'not allowing a path update'
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with scoped npm packages' do
|
||||
let_it_be(:package) { create(:npm_package, project: project, name: '@any_scope/test') }
|
||||
|
||||
context 'updating the root group' do
|
||||
let_it_be_with_refind(:target_group) { group }
|
||||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_package_registry_fix_group_path_validation is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_package_registry_fix_group_path_validation: false)
|
||||
expect_next_instance_of(::Groups::UpdateService) do |service|
|
||||
expect(service).to receive(:valid_path_change_with_npm_packages?).and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'not allowing a path update'
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
|
||||
context 'updating the subgroup' do
|
||||
let_it_be_with_refind(:target_group) { subgroup }
|
||||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_package_registry_fix_group_path_validation is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_package_registry_fix_group_path_validation: false)
|
||||
expect_next_instance_of(::Groups::UpdateService) do |service|
|
||||
expect(service).to receive(:valid_path_change_with_npm_packages?).and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'not allowing a path update'
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unscoped npm packages' do
|
||||
let_it_be(:package) { create(:npm_package, project: project, name: 'test') }
|
||||
|
||||
context 'updating the root group' do
|
||||
let_it_be_with_refind(:target_group) { group }
|
||||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_package_registry_fix_group_path_validation is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_package_registry_fix_group_path_validation: false)
|
||||
expect_next_instance_of(::Groups::UpdateService) do |service|
|
||||
expect(service).to receive(:valid_path_change_with_npm_packages?).and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'not allowing a path update'
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
|
||||
context 'updating the subgroup' do
|
||||
let_it_be_with_refind(:target_group) { subgroup }
|
||||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_package_registry_fix_group_path_validation is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_package_registry_fix_group_path_validation: false)
|
||||
expect_next_instance_of(::Groups::UpdateService) do |service|
|
||||
expect(service).to receive(:valid_path_change_with_npm_packages?).and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'not allowing a path update'
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not group owner' do
|
||||
context 'when group is private' do
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -2,23 +2,23 @@
|
|||
|
||||
RSpec.shared_examples 'create deployment for job' do
|
||||
describe '#execute' do
|
||||
subject { service.execute(build) }
|
||||
subject { service.execute(job) }
|
||||
|
||||
context 'with a deployment job' do
|
||||
let!(:build) { create(factory_type, :start_review_app, project: project) }
|
||||
let!(:environment) { create(:environment, project: project, name: build.expanded_environment_name) }
|
||||
let!(:job) { create(factory_type, :start_review_app, project: project) }
|
||||
let!(:environment) { create(:environment, project: project, name: job.expanded_environment_name) }
|
||||
|
||||
it 'creates a deployment record' do
|
||||
expect { subject }.to change { Deployment.count }.by(1)
|
||||
|
||||
build.reset
|
||||
expect(build.deployment.project).to eq(build.project)
|
||||
expect(build.deployment.ref).to eq(build.ref)
|
||||
expect(build.deployment.sha).to eq(build.sha)
|
||||
expect(build.deployment.deployable).to eq(build)
|
||||
expect(build.deployment.deployable_type).to eq('CommitStatus')
|
||||
expect(build.deployment.environment).to eq(build.persisted_environment)
|
||||
expect(build.deployment.valid?).to be_truthy
|
||||
job.reset
|
||||
expect(job.deployment.project).to eq(job.project)
|
||||
expect(job.deployment.ref).to eq(job.ref)
|
||||
expect(job.deployment.sha).to eq(job.sha)
|
||||
expect(job.deployment.deployable).to eq(job)
|
||||
expect(job.deployment.deployable_type).to eq('CommitStatus')
|
||||
expect(job.deployment.environment).to eq(job.persisted_environment)
|
||||
expect(job.deployment.valid?).to be_truthy
|
||||
end
|
||||
|
||||
context 'when creation failure occures' do
|
||||
|
|
@ -41,39 +41,39 @@ RSpec.shared_examples 'create deployment for job' do
|
|||
it 'does not create a deployment record' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
|
||||
expect(build.deployment).to be_nil
|
||||
expect(job.deployment).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a teardown job' do
|
||||
let!(:build) { create(factory_type, :stop_review_app, project: project) }
|
||||
let!(:environment) { create(:environment, name: build.expanded_environment_name) }
|
||||
let!(:job) { create(factory_type, :stop_review_app, project: project) }
|
||||
let!(:environment) { create(:environment, name: job.expanded_environment_name) }
|
||||
|
||||
it 'does not create a deployment record' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
|
||||
expect(build.deployment).to be_nil
|
||||
expect(job.deployment).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a normal job' do
|
||||
let!(:build) { create(factory_type, project: project) }
|
||||
let!(:job) { create(factory_type, project: project) }
|
||||
|
||||
it 'does not create a deployment record' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
|
||||
expect(build.deployment).to be_nil
|
||||
expect(job.deployment).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build has environment attribute' do
|
||||
let!(:build) do
|
||||
context 'when job has environment attribute' do
|
||||
let!(:job) do
|
||||
create(factory_type, environment: 'production', project: project,
|
||||
options: { environment: { name: 'production', **kubernetes_options } }) # rubocop:disable Layout/ArgumentAlignment
|
||||
end
|
||||
|
||||
let!(:environment) { create(:environment, project: project, name: build.expanded_environment_name) }
|
||||
let!(:environment) { create(:environment, project: project, name: job.expanded_environment_name) }
|
||||
|
||||
let(:kubernetes_options) { {} }
|
||||
|
||||
|
|
@ -113,25 +113,25 @@ RSpec.shared_examples 'create deployment for job' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when build already has deployment' do
|
||||
let!(:build) { create(factory_type, :with_deployment, project: project, environment: 'production') }
|
||||
context 'when job already has deployment' do
|
||||
let!(:job) { create(factory_type, :with_deployment, project: project, environment: 'production') }
|
||||
let!(:environment) {} # rubocop:disable Lint/EmptyBlock
|
||||
|
||||
it 'returns the persisted deployment' do
|
||||
expect { subject }.not_to change { Deployment.count }
|
||||
|
||||
is_expected.to eq(build.deployment)
|
||||
is_expected.to eq(job.deployment)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build does not start environment' do
|
||||
context 'when job does not start environment' do
|
||||
where(:action) do
|
||||
%w[stop prepare verify access]
|
||||
end
|
||||
|
||||
with_them do
|
||||
let!(:build) do
|
||||
let!(:job) do
|
||||
create(factory_type, environment: 'production', project: project,
|
||||
options: { environment: { name: 'production', action: action } }) # rubocop:disable Layout/ArgumentAlignment
|
||||
end
|
||||
|
|
@ -142,8 +142,8 @@ RSpec.shared_examples 'create deployment for job' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when build does not have environment attribute' do
|
||||
let!(:build) { create(factory_type, project: project) }
|
||||
context 'when job does not have environment attribute' do
|
||||
let!(:job) { create(factory_type, project: project) }
|
||||
|
||||
it 'returns nothing' do
|
||||
is_expected.to be_nil
|
||||
|
|
|
|||
Loading…
Reference in New Issue