Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-07-12 06:10:10 +00:00
parent bb01b338bf
commit 09acddd7fd
62 changed files with 1122 additions and 152 deletions

View File

@ -5,9 +5,6 @@ include:
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "click_house-client"
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "gitlab-ipynbdiff"
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "gitlab-rspec"
@ -17,6 +14,9 @@ include:
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "gitlab-utils"
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "ipynbdiff"
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "rspec_flaky"

View File

@ -275,7 +275,7 @@ rspec:deprecations:
script:
- grep -h -R "keyword" deprecations/ | awk '{$1=$1};1' | sort | uniq -c | sort
- grep -R "keyword" deprecations/ | wc
- run_timed_command "fail_on_warnings bundle exec rubocop --only Lint/LastKeywordArgument --parallel"
- run_timed_command "fail_on_warnings bundle exec rubocop --config .rubocop.yml --only Lint/LastKeywordArgument --parallel"
artifacts:
expire_in: 31d
when: always

View File

@ -137,7 +137,7 @@ rubocop:
select_existing_files < "${RSPEC_CHANGED_FILES_PATH}" > "${RUBOCOP_TARGET_FILES}"
# Skip running RuboCop if there's no target files
if [ -s "${RUBOCOP_TARGET_FILES}" ]; then
run_timed_command "fail_on_warnings bundle exec rubocop --parallel --force-exclusion $(cat ${RUBOCOP_TARGET_FILES})"
run_timed_command "fail_on_warnings bundle exec rubocop --config .rubocop.yml --parallel --force-exclusion $(cat ${RUBOCOP_TARGET_FILES})"
else
echoinfo "Nothing interesting changed for RuboCop. Skipping."
fi
@ -177,7 +177,7 @@ feature-flags-usage:
script:
# We need to disable the cache for this cop since it creates files under tmp/feature_flags/*.used,
# the cache would prevent these files from being created.
- run_timed_command "fail_on_warnings bundle exec rubocop --only Gitlab/MarkUsedFeatureFlags --cache false"
- run_timed_command "fail_on_warnings bundle exec rubocop --config .rubocop.yml --only Gitlab/MarkUsedFeatureFlags --cache false"
artifacts:
expire_in: 31d
when: always

View File

@ -14,6 +14,7 @@ spec:
- if: '$CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached"'
changes:
- "$[[inputs.gem_path_prefix]]$[[inputs.gem_name]]/**/*"
- ".gitlab/ci/gitlab-gems.gitlab-ci.yml"
- ".gitlab/ci/templates/gem.gitlab-ci.yml"
- "gems/gem.gitlab-ci.yml"

View File

@ -184,7 +184,7 @@ gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentati
gem 'elasticsearch-api', '7.13.3'
gem 'aws-sdk-core', '~> 3.178.0'
gem 'aws-sdk-cloudformation', '~> 1'
gem 'aws-sdk-s3', '~> 1.128.0'
gem 'aws-sdk-s3', '~> 1.129.0'
gem 'faraday_middleware-aws-sigv4', '~>0.3.0'
gem 'typhoeus', '~> 1.4.0' # Used with Elasticsearch to support http keep-alive connections

View File

@ -39,7 +39,7 @@
{"name":"aws-sdk-cloudformation","version":"1.41.0","platform":"ruby","checksum":"31e47539719734413671edf9b1a31f8673fbf9688549f50c41affabbcb1c6b26"},
{"name":"aws-sdk-core","version":"3.178.0","platform":"ruby","checksum":"192485a032536ff8c8eb037f1204b432129a612f4de13e36c0d2cf0dec8165cb"},
{"name":"aws-sdk-kms","version":"1.64.0","platform":"ruby","checksum":"40de596c95047bfc6e1aacea24f3df6241aa716b6f7ce08ac4c5f7e3120395ad"},
{"name":"aws-sdk-s3","version":"1.128.0","platform":"ruby","checksum":"5b1420d5be9654a9b1b5c8309d75ce72592f3a1e29def15ea07a853b96999d85"},
{"name":"aws-sdk-s3","version":"1.129.0","platform":"ruby","checksum":"82b8eab53d22754e5855dbec3e7a9a53c348de2bbf202774b4483f9b06cb0f1a"},
{"name":"aws-sigv4","version":"1.6.0","platform":"ruby","checksum":"ca9e6a15cd424f1f32b524b9760995331459bc22e67d3daad4fcf0c0084b087d"},
{"name":"axe-core-api","version":"4.6.0","platform":"ruby","checksum":"1b0ddec3353f108dc10363baf2282f43a5ff7f13d4e25f99071294e78f8a6c62"},
{"name":"axe-core-rspec","version":"4.6.0","platform":"ruby","checksum":"11c25bc9dd388c137ba4e5e63d64d20092bf22c884d8ffc829a22acfbacd747f"},

View File

@ -262,7 +262,7 @@ GEM
aws-sdk-kms (1.64.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.128.0)
aws-sdk-s3 (1.129.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6)
@ -1740,7 +1740,7 @@ DEPENDENCIES
awesome_print
aws-sdk-cloudformation (~> 1)
aws-sdk-core (~> 3.178.0)
aws-sdk-s3 (~> 1.128.0)
aws-sdk-s3 (~> 1.129.0)
axe-core-rspec
babosa (~> 2.0)
base32 (~> 0.3.0)

View File

@ -1,10 +1,9 @@
import $ from 'jquery';
import Vue from 'vue';
import initDatePicker from '~/behaviors/date_picker';
import GLForm from '~/gl_form';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Milestone from '~/milestones/milestone';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { mountMarkdownEditor } from '~/vue_shared/components/markdown/mount_markdown_editor';
import Sidebar from '~/right_sidebar';
import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar';
import Translate from '~/vue_shared/translate';
@ -22,22 +21,10 @@ export const MILESTONE_DESCRIPTION_ELEMENT = '.milestone-detail .description';
export const MILESTONE_DESCRIPTION_TASK_LIST_CONTAINER_ELEMENT = `${MILESTONE_DESCRIPTION_ELEMENT}.js-task-list-container`;
export const MILESTONE_DETAIL_ELEMENT = '.milestone-detail';
export function initForm(initGFM = true) {
export function initForm() {
mountMarkdownEditor();
new ZenMode(); // eslint-disable-line no-new
initDatePicker();
// eslint-disable-next-line no-new
new GLForm($('.milestone-form'), {
emojis: true,
members: initGFM,
issues: initGFM,
mergeRequests: initGFM,
epics: initGFM,
milestones: initGFM,
labels: initGFM,
snippets: initGFM,
vulnerabilities: initGFM,
});
}
export function initShow() {

View File

@ -43,7 +43,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['isGroupPage'],
inject: ['isGroupPage', 'canDeletePackages'],
props: {
packageEntity: {
type: Object,
@ -122,7 +122,7 @@ export default {
<list-item data-testid="package-row" :selected="selected" v-bind="$attrs">
<template #left-action>
<gl-form-checkbox
v-if="packageEntity.canDestroy"
v-if="canDeletePackages"
class="gl-m-0"
:checked="selected"
@change="$emit('select')"

View File

@ -36,6 +36,7 @@ export default {
RegistryList,
},
mixins: [Tracking.mixin()],
inject: ['canDeletePackages'],
props: {
list: {
type: Array,
@ -175,6 +176,7 @@ export default {
>
<registry-list
data-testid="packages-table"
:hidden-delete="!canDeletePackages"
:is-loading="isLoading"
:items="list"
:pagination="pageInfo"

View File

@ -1,5 +1,6 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { parseBoolean } from '~/lib/utils/common_utils';
import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
import PackageRegistry from '~/packages_and_registries/package_registry/pages/index.vue';
import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
@ -20,6 +21,7 @@ export default () => {
projectListUrl,
groupListUrl,
settingsPath,
canDeletePackages,
} = el.dataset;
const isGroupPage = pageType === 'groups';
@ -50,6 +52,7 @@ export default () => {
groupListUrl,
breadCrumbState,
settingsPath,
canDeletePackages: parseBoolean(canDeletePackages),
},
render(createElement) {
return createElement(PackageRegistry);

View File

@ -78,6 +78,7 @@ export default {
</gl-alert>
<packages-settings
class="settings-section-no-bottom"
:package-settings="packageSettings"
:is-loading="isLoading"
@success="handleSuccess(2)"

View File

@ -147,7 +147,7 @@ export default {
<p v-if="value.nextRunAt" data-testid="next-run-at">
{{ nextCleanupMessage }}
</p>
<div class="gl-mt-7 gl-display-flex gl-align-items-center">
<div class="gl-mt-6 gl-display-flex gl-align-items-center">
<gl-button
data-testid="save-button"
type="submit"

View File

@ -1,17 +1,15 @@
<template>
<section class="settings gl-py-7">
<div class="row">
<div class="col-lg-4">
<h4>
<section class="settings-section">
<div class="settings-sticky-header">
<div class="settings-sticky-header-inner">
<h4 class="gl-my-0">
<slot name="title"></slot>
</h4>
<p>
<slot name="description"></slot>
</p>
</div>
<div class="col-lg-8 gl-pt-3">
<slot></slot>
</div>
</div>
<p class="gl-text-secondary">
<slot name="description"></slot>
</p>
<slot></slot>
</section>
</template>

View File

@ -79,6 +79,7 @@ export function mountMarkdownEditor(options = {}) {
const supportsQuickActions = parseBoolean(el.dataset.supportsQuickActions ?? true);
const enableAutocomplete = parseBoolean(el.dataset.enableAutocomplete ?? true);
const disableAttachments = parseBoolean(el.dataset.disableAttachments ?? false);
const autofocus = parseBoolean(el.dataset.autofocus ?? true);
const hiddenInput = el.querySelector('input[type="hidden"]');
const formFieldName = hiddenInput.getAttribute('name');
const formFieldId = hiddenInput.getAttribute('id');
@ -128,7 +129,7 @@ export function mountMarkdownEditor(options = {}) {
autocompleteDataSources: gl.GfmAutoComplete?.dataSources,
supportsQuickActions,
disableAttachments,
autofocus: true,
autofocus,
},
});
},

View File

@ -76,6 +76,33 @@
}
}
.settings-section {
@include gl-pt-6;
&::after {
content: '';
display: block;
@include gl-pb-7;
}
}
.settings-section:first-of-type,
.settings-section-no-bottom + .settings-section {
@include gl-pt-0;
}
.settings-section:not(.settings-section-no-bottom) + .settings-section {
@include gl-border-t;
}
.settings-section-no-bottom::after {
@include gl-pb-0;
@include media-breakpoint-up(sm) {
@include gl-pb-5;
}
}
$sticky-header-z-index: 98;
.settings-sticky-header,

View File

@ -9,6 +9,10 @@ class Groups::MilestonesController < Groups::ApplicationController
feature_category :team_planning
urgency :low
before_action do
push_frontend_feature_flag(:content_editor_on_issues, group)
end
def index
respond_to do |format|
format.html do

View File

@ -24,6 +24,10 @@ class Projects::MilestonesController < Projects::ApplicationController
feature_category :team_planning
urgency :low
before_action do
push_frontend_feature_flag(:content_editor_on_issues, @project)
end
def index
@sort = params[:sort] || 'due_date_asc'
@milestones = milestones.sort_by_attribute(@sort)

View File

@ -0,0 +1,84 @@
# frozen_string_literal: true
module Projects
module ServiceDesk
class CustomEmailController < Projects::ApplicationController
before_action :check_feature_flag_enabled
before_action :authorize_admin_project!
feature_category :service_desk
urgency :low
def create
response = ::ServiceDesk::CustomEmails::CreateService.new(
project: project,
current_user: current_user,
params: params
).execute
json_response(service_response: response)
end
def update
response = ServiceDeskSettings::UpdateService.new(project, current_user, update_setting_params).execute
if response.error?
json_response(
error_message: s_("ServiceDesk|Cannot update custom email"),
status: :unprocessable_entity
)
return
end
json_response
end
def destroy
response = ::ServiceDesk::CustomEmails::DestroyService.new(
project: project,
current_user: current_user
).execute
json_response(service_response: response)
end
def show
json_response
end
private
def update_setting_params
params.permit(:custom_email_enabled)
end
def json_response(error_message: nil, status: :ok, service_response: nil)
if service_response.present?
status = service_response.success? ? :ok : :unprocessable_entity
error_message = service_response.message
end
respond_to do |format|
format.json { render json: custom_email_attributes(error_message: error_message), status: status }
end
end
def custom_email_attributes(error_message:)
setting = project.service_desk_setting
{
custom_email: setting&.custom_email,
custom_email_enabled: setting&.custom_email_enabled || false,
custom_email_verification_state: setting&.custom_email_verification&.state,
custom_email_verification_error: setting&.custom_email_verification&.error,
custom_email_smtp_address: setting&.custom_email_credential&.smtp_address,
error_message: error_message
}
end
def check_feature_flag_enabled
render_404 unless Feature.enabled?(:service_desk_custom_email, @project)
end
end
end
end

View File

@ -74,6 +74,16 @@ module PackagesHelper
Ability.allowed?(current_user, :admin_group, group)
end
def can_delete_packages?(project)
Gitlab.config.packages.enabled &&
Ability.allowed?(current_user, :destroy_package, project)
end
def can_delete_group_packages?(group)
group.packages_feature_enabled? &&
Ability.allowed?(current_user, :destroy_package, group)
end
def cleanup_settings_data
{
project_id: @project.id,

View File

@ -11,6 +11,7 @@ module Ml
belongs_to :project
belongs_to :user
belongs_to :model, optional: true, inverse_of: :default_experiment
has_many :candidates, class_name: 'Ml::Candidate'
has_many :metadata, class_name: 'Ml::ExperimentMetadata'
@ -22,10 +23,21 @@ module Ml
has_internal_id :iid, scope: :project
before_destroy :stop_destroy
def package_name
"#{PACKAGE_PREFIX}#{iid}"
end
def stop_destroy
return unless model_id
errors[:base] << "Cannot delete an experiment associated to a model"
# According to docs, throw is the correct way to stop on a callback
# https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#module-ActiveRecord::Callbacks-label-Canceling+callbacks
throw :abort # rubocop:disable Cop/BanCatchThrow
end
class << self
def by_project_id_and_iid(project_id, iid)
find_by(project_id: project_id, iid: iid)

24
app/models/ml/model.rb Normal file
View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module Ml
class Model < ApplicationRecord
validates :project, :default_experiment, presence: true
validates :name,
format: Gitlab::Regex.ml_model_name_regex,
uniqueness: { scope: :project },
presence: true,
length: { maximum: 255 }
validate :valid_default_experiment?
has_one :default_experiment, class_name: 'Ml::Experiment'
belongs_to :project
def valid_default_experiment?
return unless default_experiment
errors.add(:default_experiment) unless default_experiment.name == name
errors.add(:default_experiment) unless default_experiment.project_id == project_id
end
end
end

View File

@ -10,12 +10,16 @@
= render "shared/milestones/form_dates", f: f
.form-group
= f.label :description, _("Description")
= render layout: 'shared/md_preview', locals: { url: group_preview_markdown_path } do
= render 'shared/zen', f: f, attr: :description,
classes: 'note-textarea',
qa_selector: 'milestone_description_field',
supports_autocomplete: true,
placeholder: _('Write milestone description...')
- @gfm_form = true
.js-markdown-editor{ data: { render_markdown_path: group_preview_markdown_path,
markdown_docs_path: help_page_path('user/markdown'),
qa_selector: 'milestone_description_field',
form_field_placeholder: _('Write milestone description...'),
supports_quick_actions: 'false',
enable_autocomplete: 'true',
autofocus: 'false',
form_field_classes: 'note-textarea js-gfm-input markdown-area' } }
= f.hidden_field :description
.clearfix
.error-alert

View File

@ -10,4 +10,5 @@
npm_instance_url: package_registry_instance_url(:npm),
project_list_url: '',
settings_path: show_group_package_registry_settings(@group) ? group_settings_packages_and_registries_path(@group) : '',
can_delete_packages: can_delete_group_packages?(@group).to_s,
group_list_url: group_packages_path(@group) } }

View File

@ -8,7 +8,7 @@
.js-user-profile
- else
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
.js-search-settings-section
.settings-section.js-search-settings-section
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0
@ -44,9 +44,8 @@
button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } },
method: :delete) do
= s_("Profiles|Remove avatar")
.gl-pb-8
.js-search-settings-section.gl-border-t.gl-pt-6
.settings-section.js-search-settings-section.gl-border-t.gl-pt-6
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0= s_("Profiles|Current status")
@ -60,18 +59,16 @@
= status_form.hidden_field :clear_status_after,
value: user_clear_status_at(@user),
data: { js_name: 'clearStatusAfter' }
.gl-pb-7
.user-time-preferences.js-search-settings-section.gl-border-t.gl-pt-6
.settings-section.user-time-preferences.js-search-settings-section.gl-border-t.gl-pt-6
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0= s_("Profiles|Time settings")
%p.gl-text-secondary= s_("Profiles|Set your local time zone.")
= f.label :user_timezone, _("Time zone")
.js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone, name: 'user[timezone]' } }
.gl-pb-7
.js-search-settings-section.gl-border-t.gl-pt-6
.settings-section.js-search-settings-section.gl-border-t.gl-pt-6
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0
@ -168,7 +165,6 @@
= s_("Profiles|Achievements")
= f.gitlab_ui_checkbox_component :achievements_enabled,
s_('Profiles|Display achievements on your profile')
.gl-pb-7
.js-hide-when-nothing-matches-search.settings-sticky-footer
= f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true

View File

@ -12,13 +12,16 @@
= render 'shared/milestones/form_dates', f: f
.form-group
= f.label :description, _('Description')
= render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project) } do
= render 'shared/zen', f: f, attr: :description,
classes: 'note-textarea',
qa_selector: 'milestone_description_field',
supports_autocomplete: true,
placeholder: _('Write milestone description...')
= render 'shared/notes/hints'
- @gfm_form = true
.js-markdown-editor{ data: { render_markdown_path: preview_markdown_path(@project),
markdown_docs_path: help_page_path('user/markdown'),
qa_selector: 'milestone_description_field',
form_field_placeholder: _('Write milestone description...'),
supports_quick_actions: 'false',
enable_autocomplete: 'true',
autofocus: 'false',
form_field_classes: 'note-textarea js-gfm-input markdown-area' } }
= f.hidden_field :description
.clearfix
.error-alert

View File

@ -10,4 +10,5 @@
npm_instance_url: package_registry_instance_url(:npm),
project_list_url: project_packages_path(@project),
settings_path: show_package_registry_settings(@project) ? project_settings_packages_and_registries_path(@project) : '',
can_delete_packages: can_delete_packages?(@project).to_s,
group_list_url: '' } }

View File

@ -1,8 +0,0 @@
---
name: load_merge_request_via_links
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117321
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412177
milestone: '16.1'
type: development
group: group::threat insights
default_enabled: false

View File

@ -489,6 +489,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :candidates, only: [:show, :destroy], controller: 'candidates', param: :iid
resources :models, only: [:index], controller: 'models'
end
namespace :service_desk do
resource :custom_email, only: [:show, :create, :update, :destroy], controller: 'custom_email'
end
end
# End of the /-/ scope.

10
db/docs/ml_models.yml Normal file
View File

@ -0,0 +1,10 @@
---
table_name: ml_models
classes:
- Ml::Model
feature_categories:
- mlops
description: A machine learning model for the model registry
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125302
milestone: '16.2'
gitlab_schema: gitlab_main

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class CreateMlModels < Gitlab::Database::Migration[2.1]
enable_lock_retries!
def up
create_table :ml_models do |t|
t.timestamps_with_timezone null: false
t.references :project, foreign_key: true, index: true, on_delete: :cascade, null: false
t.text :name, limit: 255, null: false
t.index [:project_id, :name], unique: true
end
end
def down
drop_table :ml_models
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddColumnModelIdToMlExperiments < Gitlab::Database::Migration[2.1]
def change
# rubocop:disable Migration/AddReference
add_reference :ml_experiments,
:model,
index: true,
null: true,
unique: true,
foreign_key: { on_delete: :cascade, to_table: :ml_models }
# rubocop:enable Migration/AddReference
end
end

View File

@ -0,0 +1 @@
5954829dd244b4536beeb9b92a157539feb5207bc1309e177310895f269f54d8

View File

@ -0,0 +1 @@
3a355ebb2299786d9aa5ce9bf1f07b2bbd3b6aca719c12c78c4a945c4a96cfe3

View File

@ -18592,6 +18592,7 @@ CREATE TABLE ml_experiments (
user_id bigint,
name text NOT NULL,
deleted_on timestamp with time zone,
model_id bigint,
CONSTRAINT check_ee07a0be2c CHECK ((char_length(name) <= 255))
);
@ -18604,6 +18605,24 @@ CREATE SEQUENCE ml_experiments_id_seq
ALTER SEQUENCE ml_experiments_id_seq OWNED BY ml_experiments.id;
CREATE TABLE ml_models (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
project_id bigint NOT NULL,
name text NOT NULL,
CONSTRAINT check_1fd2cc7d93 CHECK ((char_length(name) <= 255))
);
CREATE SEQUENCE ml_models_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE ml_models_id_seq OWNED BY ml_models.id;
CREATE TABLE namespace_admin_notes (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -25629,6 +25648,8 @@ ALTER TABLE ONLY ml_experiment_metadata ALTER COLUMN id SET DEFAULT nextval('ml_
ALTER TABLE ONLY ml_experiments ALTER COLUMN id SET DEFAULT nextval('ml_experiments_id_seq'::regclass);
ALTER TABLE ONLY ml_models ALTER COLUMN id SET DEFAULT nextval('ml_models_id_seq'::regclass);
ALTER TABLE ONLY namespace_admin_notes ALTER COLUMN id SET DEFAULT nextval('namespace_admin_notes_id_seq'::regclass);
ALTER TABLE ONLY namespace_bans ALTER COLUMN id SET DEFAULT nextval('namespace_bans_id_seq'::regclass);
@ -27848,6 +27869,9 @@ ALTER TABLE ONLY ml_experiment_metadata
ALTER TABLE ONLY ml_experiments
ADD CONSTRAINT ml_experiments_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ml_models
ADD CONSTRAINT ml_models_pkey PRIMARY KEY (id);
ALTER TABLE ONLY namespace_admin_notes
ADD CONSTRAINT namespace_admin_notes_pkey PRIMARY KEY (id);
@ -31974,12 +31998,18 @@ CREATE INDEX index_ml_candidates_on_user_id ON ml_candidates USING btree (user_i
CREATE UNIQUE INDEX index_ml_experiment_metadata_on_experiment_id_and_name ON ml_experiment_metadata USING btree (experiment_id, name);
CREATE INDEX index_ml_experiments_on_model_id ON ml_experiments USING btree (model_id);
CREATE UNIQUE INDEX index_ml_experiments_on_project_id_and_iid ON ml_experiments USING btree (project_id, iid);
CREATE UNIQUE INDEX index_ml_experiments_on_project_id_and_name ON ml_experiments USING btree (project_id, name);
CREATE INDEX index_ml_experiments_on_user_id ON ml_experiments USING btree (user_id);
CREATE INDEX index_ml_models_on_project_id ON ml_models USING btree (project_id);
CREATE UNIQUE INDEX index_ml_models_on_project_id_and_name ON ml_models USING btree (project_id, name);
CREATE UNIQUE INDEX index_mr_blocks_on_blocking_and_blocked_mr_ids ON merge_request_blocks USING btree (blocking_merge_request_id, blocked_merge_request_id);
CREATE INDEX index_mr_cleanup_schedules_timestamps_status ON merge_request_cleanup_schedules USING btree (scheduled_at) WHERE ((completed_at IS NULL) AND (status = 0));
@ -36969,6 +36999,9 @@ ALTER TABLE ONLY project_repository_storage_moves
ALTER TABLE ONLY ml_candidate_metadata
ADD CONSTRAINT fk_rails_5117dddf22 FOREIGN KEY (candidate_id) REFERENCES ml_candidates(id) ON DELETE CASCADE;
ALTER TABLE ONLY ml_models
ADD CONSTRAINT fk_rails_51e87f7c50 FOREIGN KEY (project_id) REFERENCES projects(id);
ALTER TABLE ONLY elastic_group_index_statuses
ADD CONSTRAINT fk_rails_52b9969b12 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@ -37428,6 +37461,9 @@ ALTER TABLE ONLY boards_epic_board_recent_visits
ALTER TABLE ONLY packages_dependency_links
ADD CONSTRAINT fk_rails_96ef1c00d3 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
ALTER TABLE ONLY ml_experiments
ADD CONSTRAINT fk_rails_97194a054e FOREIGN KEY (model_id) REFERENCES ml_models(id) ON DELETE CASCADE;
ALTER TABLE ONLY group_repository_storage_moves
ADD CONSTRAINT fk_rails_982bb5daf1 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;

View File

@ -60,8 +60,8 @@ concepts to look at as an example.
App Continuum:
An illustration of how an application can evolve from a small, unstructed app, through various
stages including a modular well-structured, monolith, all the way to a microservices architecture.
An illustration of how an application can evolve from a small, unstructured app, through various
stages including a modular well-structured monolith, all the way to a microservices architecture.
Includes discussion of why you might want to stop at various stages, and specifically the
challenges/concerns with making the jump to microservices, and why sticking with a

View File

@ -13,6 +13,12 @@ to AWS. You can reference these images in your CI/CD pipeline.
If you're using GitLab.com and deploying to the [Amazon Elastic Container Service](https://aws.amazon.com/ecs/) (ECS),
read about [deploying to ECS](ecs/deploy_to_aws_ecs.md).
NOTE:
If you are comfortable configuring a deployment yourself and just need to retrieve
AWS credentials, consider using [ID tokens and OpenID Connect](../cloud_services/aws/index.md).
ID tokens are more secure than storing credentials in CI/CD variables, but do not
work with the guidance on this page.
## Authenticate GitLab with AWS
To use GitLab CI/CD to connect to AWS, you must authenticate.

View File

@ -55,18 +55,16 @@ RSpec.describe "with feature flag enabled", feature_flag: {
let(:project) { Resource::Project.fabricate_via_api! }
before do
around do |example|
Runtime::Feature.enable(:feature_flag_name, project: project)
example.run
Runtime::Feature.disable(:feature_flag_name, project: project)
end
it "feature flag test" do
# Execute the test with the feature flag enabled.
# It will only affect the project created in this test.
end
after do
Runtime::Feature.disable(:feature_flag_name, project: project)
end
end
```

View File

@ -108,3 +108,12 @@ Check [`production.log`](../../administration/logs/index.md#productionlog) to se
```
If that's the case, ensure the **Due date** field is visible for issues in the integrated Jira project.
## `An error occurred while requesting data from Jira` when viewing the Jira issues list in GitLab
You might see a `An error occurred while requesting data from Jira` message when you attempt to view the Jira issues list in GitLab.
You can see this error when the authentication details in the Jira integration settings are incomplete or incorrect.
To attempt to resolve this error, try [configuring the integration](configure.md#configure-the-integration) again. Verify that the
authentication details are correct, re-enter your API token or password, and save your changes.

View File

@ -15,6 +15,7 @@ inherit_mode:
AllCops:
# Target the current Ruby version. For example, "3.0" or "3.1".
TargetRubyVersion: <%= RUBY_VERSION[/^\d+\.\d+/, 0] %>
SuggestExtensions: false
# This cop doesn't make sense in the context of gems
CodeReuse/ActiveRecord:
@ -59,6 +60,18 @@ Naming/FileName:
Exclude:
- spec/**/*.rb
RSpec/ContextWording:
Prefixes:
- 'when'
- 'with'
- 'without'
- 'for'
- 'and'
- 'on'
- 'in'
- 'as'
- 'if'
# This cop doesn't make sense in the context of gems
RSpec/MissingFeatureCategory:
Enabled: false

View File

@ -4,8 +4,25 @@ inherit_from:
CodeReuse/ActiveRecord:
Enabled: false
Gitlab/Json:
Enabled: false
# FIXME
Gitlab/RSpec/AvoidSetup:
Enabled: false
Naming/FileName:
Exclude:
- spec/**/*.rb
- lib/gitlab/rspec.rb
- lib/gitlab/rspec/all.rb
Rails/Pluck:
Enabled: false
RSpec/AvoidConditionalStatements:
Enabled: false
RSpec/MultipleMemoizedHelpers:
Max: 6
AllowSubject: true

View File

@ -1,26 +1,46 @@
PATH
remote: .
specs:
gitlab-ipynbdiff (0.4.7)
ipynbdiff (0.4.7)
diffy (~> 3.4)
oj (~> 3.13.16)
GEM
remote: https://rubygems.org/
specs:
activesupport (7.0.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
ast (2.4.2)
benchmark-memory (0.2.0)
memory_profiler (~> 1)
binding_ninja (0.2.3)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
coderay (1.1.3)
concurrent-ruby (1.2.2)
debug_inspector (1.1.0)
diff-lcs (1.5.0)
diffy (3.4.2)
docile (1.4.0)
gitlab-styles (10.1.0)
rubocop (~> 1.50.2)
rubocop-graphql (~> 0.18)
rubocop-performance (~> 1.15)
rubocop-rails (~> 2.17)
rubocop-rspec (~> 2.22)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
json (2.6.3)
memory_profiler (1.0.0)
method_source (1.0.0)
minitest (5.18.1)
oj (3.13.23)
parser (3.1.2.0)
parallel (1.23.0)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
proc_to_ast (0.1.0)
coderay
parser
@ -28,7 +48,12 @@ GEM
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
racc (1.7.1)
rack (3.0.8)
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.1)
rexml (3.2.5)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
@ -41,22 +66,60 @@ GEM
rspec-mocks (3.11.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-parameterized (0.5.2)
binding_ninja (>= 0.2.3)
rspec-parameterized (1.0.0)
rspec-parameterized-core (< 2)
rspec-parameterized-table_syntax (< 2)
rspec-parameterized-core (1.0.0)
parser
proc_to_ast
rspec (>= 2.13, < 4)
unparser
rspec-parameterized-table_syntax (1.0.0)
binding_of_caller
rspec-parameterized-core (< 2)
rspec-support (3.11.0)
rubocop (1.50.2)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-graphql (0.19.0)
rubocop (>= 0.87, < 2)
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.20.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.22.0)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
unparser (0.6.5)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.4.2)
unparser (0.6.8)
diff-lcs (~> 1.3)
parser (>= 3.1.0)
parser (>= 3.2.0)
PLATFORMS
ruby
@ -64,12 +127,13 @@ PLATFORMS
DEPENDENCIES
benchmark-memory (~> 0.2.0)
bundler (~> 2.2)
gitlab-ipynbdiff!
gitlab-styles (~> 10.1.0)
ipynbdiff!
pry (~> 0.14)
rake (~> 13.0)
rspec (~> 3.10)
rspec-parameterized (~> 0.5.1)
simplecov
rspec-parameterized (~> 1.0)
simplecov (~> 0.22.0)
BUNDLED WITH
2.3.16

View File

@ -24,9 +24,10 @@ Gem::Specification.new do |s|
s.add_development_dependency 'benchmark-memory', '~>0.2.0'
s.add_development_dependency 'bundler', '~> 2.2'
s.add_development_dependency 'gitlab-styles', '~> 10.1.0'
s.add_development_dependency 'pry', '~> 0.14'
s.add_development_dependency 'rake', '~> 13.0'
s.add_development_dependency 'rspec', '~> 3.10'
s.add_development_dependency 'rspec-parameterized', '~> 0.5.1'
s.add_development_dependency 'simplecov', '~> 0.12.0'
s.add_development_dependency 'rspec-parameterized', '~> 1.0'
s.add_development_dependency 'simplecov', '~> 0.22.0'
end

View File

@ -8,7 +8,7 @@ describe IpynbDiff::SymbolMap do
end
describe '.parse' do
subject { described_class.parse(JSON.pretty_generate(source)) }
subject(:symbol_map) { described_class.parse(JSON.pretty_generate(source)) }
context 'when object has blank key' do
let(:source) { { "": { "": 5 } } }
@ -37,8 +37,8 @@ describe IpynbDiff::SymbolMap do
context 'when object has inner object and number, string and array with object' do
let(:source) { { obj1: { obj2: [123, 2, true], obj3: "hel\nlo", obj4: true, obj5: 123, obj6: 'a' } } }
it do
is_expected.to match_array(
specify do
expect(symbol_map).to match_array(
res(['.obj1', 2],
['.obj1.obj2', 3],
['.obj1.obj2.0', 4],

View File

@ -4,7 +4,7 @@ require_relative 'test_helper'
describe IpynbDiff do
def diff_signs(diff)
diff.to_s(:text).scan(/.*\n/).map { |l| l[0] }.join('') # rubocop:disable Rails/Pluck
diff.to_s(:text).scan(/.*\n/).map { |l| l[0] }.join('')
end
describe '.diff' do
@ -18,9 +18,7 @@ describe IpynbDiff do
subject { described_class.diff(from, to, include_frontmatter: include_frontmatter, hide_images: hide_images) }
context 'if preprocessing is active' do
it 'html tables are stripped' do
is_expected.not_to include('<td>')
end
it { is_expected.not_to include('<td>') }
end
context 'when to is nil' do

View File

@ -44,7 +44,7 @@ pre-push:
tags: backend style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: '*.{rb,rake}'
run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --force-exclusion {files}
run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --config .rubocop.yml --parallel --force-exclusion {files}
sidekiq-queues:
tags: backend
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
@ -134,7 +134,7 @@ auto-fix:
tags: backend style
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: '*.{rb,rake}'
run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --parallel --autocorrect --force-exclusion {files}
run: REVEAL_RUBOCOP_TODO=0 bundle exec rubocop --config .rubocop.yml --parallel --autocorrect --force-exclusion {files}
gettext:
tags: backend frontend view haml
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD | while read file;do git diff --unified=1 $(git merge-base origin/master HEAD)..HEAD $file | grep -Fqe '_(' && echo $file;done; true

View File

@ -42370,6 +42370,9 @@ msgstr ""
msgid "ServiceDesk|Cannot create custom email"
msgstr ""
msgid "ServiceDesk|Cannot update custom email"
msgstr ""
msgid "ServiceDesk|Custom email address could not be verified."
msgstr ""

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do
factory :ml_models, class: '::Ml::Model' do
sequence(:name) { |n| "model#{n}" }
project
default_experiment { association :ml_experiments, project_id: project.id, name: name }
end
end

View File

@ -27,7 +27,7 @@ RSpec.describe 'Group milestones', feature_category: :groups_and_projects do
click_button("Preview")
preview = find('.js-md-preview')
preview = find('.js-vue-md-preview')
expect(preview).to have_content('Nothing to preview.')

View File

@ -1,6 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import htmlNewMilestone from 'test_fixtures/milestones/new-milestone.html';
import mock from 'xhr-mock';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
@ -9,6 +8,7 @@ import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
import dropzoneInput from '~/dropzone_input';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import htmlNewMilestone from 'test_fixtures_static/textarea.html';
const TEST_FILE = new File([], 'somefile.jpg');
TEST_FILE.upload = {};

View File

@ -1,43 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::MilestonesController, '(JavaScript fixtures)', :with_license, feature_category: :team_planning, type: :controller do
include JavaScriptFixturesHelpers
let_it_be(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let_it_be(:project) { create(:project_empty_repo, namespace: namespace, path: 'milestones-project') }
render_views
before do
project.add_maintainer(user)
sign_in(user)
end
after do
remove_repository(project)
end
it 'milestones/new-milestone.html' do
get :new, params: {
namespace_id: project.namespace.to_param,
project_id: project
}
expect(response).to be_successful
end
private
def render_milestone(milestone)
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: milestone.to_param
}
expect(response).to be_successful
end
end

View File

@ -0,0 +1,27 @@
<body>
<meta charset="utf-8">
<title>Document with Textarea</title>
<form class="milestone-form common-note-form js-quick-submit js-requires-input" id="new_milestone"
action="http://test.host/frontend-fixtures/milestones-project/-/milestones"
accept-charset="UTF-8" method="post">
<div class="form-group">
<div class="md-write-holder">
<div class="zen-backdrop">
<textarea class="note-textarea js-gfm-input js-autosize markdown-area"
placeholder="Write milestone description..." dir="auto"
data-supports-quick-actions="false" data-supports-autocomplete="true"
data-qa-selector="milestone_description_field" data-autofocus="false"
name="milestone[description]"
id="milestone_description"></textarea>
<a class="zen-control zen-control-leave js-zen-leave gl-text-gray-500"
href="#">
<svg class="s16" data-testid="minimize-icon">
<use href="http://test.host/assets/icons-b8c5a9711f73b1de3c81754da0aca72f43b0e6844aa06dd03092b601a493f45b.svg#minimize"></use>
</svg>
</a>
</div>
</div>
</div>
</form>
</body>

View File

@ -27,11 +27,11 @@ describe('packages_list_row', () => {
const defaultProvide = {
isGroupPage: false,
canDeletePackages: true,
};
const packageWithoutTags = { ...packageData(), project: packageProject(), ...linksData };
const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
const packageCannotDestroy = { ...packageData(), ...linksData, canDestroy: false };
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findDeleteDropdown = () => wrapper.findByTestId('action-delete');
@ -105,7 +105,9 @@ describe('packages_list_row', () => {
describe('delete button', () => {
it('does not exist when package cannot be destroyed', () => {
mountComponent({ packageEntity: packageCannotDestroy });
mountComponent({
packageEntity: { ...packageWithoutTags, canDestroy: false },
});
expect(findDeleteDropdown().exists()).toBe(false);
});
@ -168,7 +170,10 @@ describe('packages_list_row', () => {
describe('left action template', () => {
it('does not render checkbox if not permitted', () => {
mountComponent({
packageEntity: { ...packageWithoutTags, canDestroy: false },
provide: {
...defaultProvide,
canDeletePackages: false,
},
});
expect(findBulkDeleteAction().exists()).toBe(false);
@ -248,6 +253,7 @@ describe('packages_list_row', () => {
it('if the package is published through CI show the project and author name', () => {
mountComponent({
provide: {
...defaultProvide,
isGroupPage: true,
},
packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } },
@ -261,6 +267,7 @@ describe('packages_list_row', () => {
it('if the package is published manually dont show project and the author name', () => {
mountComponent({
provide: {
...defaultProvide,
isGroupPage: true,
},
packageEntity: { ...packageWithoutTags },

View File

@ -41,6 +41,10 @@ describe('packages_list', () => {
groupSettings: defaultPackageGroupSettings,
};
const defaultProvide = {
canDeletePackages: true,
};
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
@ -52,8 +56,9 @@ describe('packages_list', () => {
const showMock = jest.fn();
const mountComponent = (props) => {
const mountComponent = ({ props = {}, provide = defaultProvide } = {}) => {
wrapper = shallowMountExtended(PackagesList, {
provide,
propsData: {
...defaultProps,
...props,
@ -75,7 +80,7 @@ describe('packages_list', () => {
describe('when is loading', () => {
beforeEach(() => {
mountComponent({ isLoading: true });
mountComponent({ props: { isLoading: true } });
});
it('shows skeleton loader', () => {
@ -109,6 +114,7 @@ describe('packages_list', () => {
title: '2 packages',
items: defaultProps.list,
pagination: defaultProps.pageInfo,
hiddenDelete: false,
isLoading: false,
});
});
@ -137,6 +143,16 @@ describe('packages_list', () => {
});
});
describe('when the user does not have permission to destroy packages', () => {
beforeEach(() => {
mountComponent({ provide: { canDeletePackages: false } });
});
it('sets the hidden delete prop of registry list to true', () => {
expect(findRegistryList().props('hiddenDelete')).toBe(true);
});
});
describe.each`
description | finderFunction | deletePayload
${'when the user can destroy the package'} | ${findPackagesListRow} | ${firstPackage}
@ -262,7 +278,7 @@ describe('packages_list', () => {
describe('when an error package is present', () => {
beforeEach(() => {
mountComponent({ list: [firstPackage, errorPackage] });
mountComponent({ props: { list: [firstPackage, errorPackage] } });
return nextTick();
});
@ -290,7 +306,7 @@ describe('packages_list', () => {
describe('when the list is empty', () => {
beforeEach(() => {
mountComponent({ list: [] });
mountComponent({ props: { list: [] } });
});
it('show the empty slot', () => {
@ -301,7 +317,7 @@ describe('packages_list', () => {
describe('pagination', () => {
beforeEach(() => {
mountComponent({ pageInfo: { hasPreviousPage: true } });
mountComponent({ props: { pageInfo: { hasPreviousPage: true } } });
});
it('emits prev-page events when the prev event is fired', () => {

View File

@ -132,7 +132,6 @@ RSpec.describe PackagesHelper, feature_category: :package_registry do
describe '#show_container_registry_settings' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
before do
allow(helper).to receive(:current_user) { user }
@ -252,4 +251,114 @@ RSpec.describe PackagesHelper, feature_category: :package_registry do
end
end
end
describe '#can_delete_packages?' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
before do
allow(helper).to receive(:current_user) { user }
end
subject { helper.can_delete_packages?(project) }
context 'with package registry config enabled' do
before do
stub_config(packages: { enabled: true })
end
context 'when user has permission' do
before do
allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(true)
end
it { is_expected.to be(true) }
end
context 'when user does not have permission' do
before do
allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(false)
end
it { is_expected.to be(false) }
end
end
context 'with package registry config disabled' do
before do
stub_config(packages: { enabled: false })
end
context 'when user has permission' do
before do
allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(true)
end
it { is_expected.to be(false) }
end
context 'when user does not have permission' do
before do
allow(Ability).to receive(:allowed?).with(user, :destroy_package, project).and_return(false)
end
it { is_expected.to be(false) }
end
end
end
describe '#can_delete_group_packages?' do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
before do
allow(helper).to receive(:current_user) { user }
end
subject { helper.can_delete_group_packages?(group) }
context 'with package registry config enabled' do
before do
stub_config(packages: { enabled: true })
end
context 'when user has permission' do
before do
allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(true)
end
it { is_expected.to be(true) }
end
context 'when user does not have permission' do
before do
allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(false)
end
it { is_expected.to be(false) }
end
end
context 'with package registry config disabled' do
before do
stub_config(packages: { enabled: false })
end
context 'when user has permission' do
before do
allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(true)
end
it { is_expected.to be(false) }
end
context 'when user does not have permission' do
before do
allow(Ability).to receive(:allowed?).with(user, :destroy_package, group).and_return(false)
end
it { is_expected.to be(false) }
end
end
end
end

View File

@ -14,6 +14,21 @@ RSpec.describe Ml::Experiment, feature_category: :mlops do
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:candidates) }
it { is_expected.to have_many(:metadata) }
it { is_expected.to belong_to(:model).class_name('Ml::Model') }
end
describe '#destroy' do
it 'allow experiment without model to be destroyed' do
experiment = create(:ml_experiments, project: exp.project)
expect { experiment.destroy! }.to change { Ml::Experiment.count }.by(-1)
end
it 'throws error when destroying experiment with model' do
experiment = create(:ml_models, project: exp.project).default_experiment
expect { experiment.destroy! }.to raise_error(ActiveRecord::ActiveRecordError)
end
end
describe '.package_name' do

View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ml::Model, feature_category: :mlops do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_one(:default_experiment) }
end
describe '#valid?' do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project) }
let_it_be(:existing_model) { create(:ml_models, name: 'an_existing_model', project: project) }
let_it_be(:valid_name) { 'a_valid_name' }
let_it_be(:default_experiment) { create(:ml_experiments, name: valid_name, project: project) }
let(:name) { valid_name }
subject(:errors) do
m = described_class.new(name: name, project: project, default_experiment: default_experiment)
m.validate
m.errors
end
it 'validates a valid model version' do
expect(errors).to be_empty
end
describe 'name' do
where(:ctx, :name) do
'name is blank' | ''
'name is not valid package name' | '!!()()'
'name is too large' | ('a' * 256)
'name is not unique in the project' | 'an_existing_model'
end
with_them do
it { expect(errors).to include(:name) }
end
end
describe 'default_experiment' do
context 'when experiment name name is different than model name' do
before do
allow(default_experiment).to receive(:name).and_return("#{name}a")
end
it { expect(errors).to include(:default_experiment) }
end
context 'when model version project is different than model project' do
before do
allow(default_experiment).to receive(:project_id).and_return(project.id + 1)
end
it { expect(errors).to include(:default_experiment) }
end
end
end
end

View File

@ -0,0 +1,380 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::ServiceDesk::CustomEmailController, feature_category: :service_desk do
let_it_be_with_reload(:project) do
create(:project, :private, service_desk_enabled: true)
end
let_it_be(:custom_email_path) { project_service_desk_custom_email_path(project, format: :json) }
let_it_be(:user) { create(:user) }
let_it_be(:illegitimite_user) { create(:user) }
let(:message) { instance_double(Mail::Message) }
let(:error_cannot_create_custom_email) { s_("ServiceDesk|Cannot create custom email") }
let(:error_cannot_update_custom_email) { s_("ServiceDesk|Cannot update custom email") }
let(:error_does_not_exist) { s_('ServiceDesk|Custom email does not exist') }
let(:error_custom_email_exists) { s_('ServiceDesk|Custom email already exists') }
let(:custom_email_params) do
{
custom_email: 'user@example.com',
smtp_address: 'smtp.example.com',
smtp_port: '587',
smtp_username: 'user@example.com',
smtp_password: 'supersecret'
}
end
let(:empty_json_response) do
{
"custom_email" => nil,
"custom_email_enabled" => false,
"custom_email_verification_state" => nil,
"custom_email_verification_error" => nil,
"custom_email_smtp_address" => nil,
"error_message" => nil
}
end
before_all do
project.add_developer(illegitimite_user)
project.add_maintainer(user)
end
shared_examples 'a json response with empty values' do
it 'returns json response with empty values' do
perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include(empty_json_response)
end
end
shared_examples 'a controller that responds with status' do |status|
it "responds with #{status} for GET custom email" do
get custom_email_path
expect(response).to have_gitlab_http_status(status)
end
it "responds with #{status} for POST custom email" do
post custom_email_path
expect(response).to have_gitlab_http_status(status)
end
it "responds with #{status} for PUT custom email" do
put custom_email_path
expect(response).to have_gitlab_http_status(status)
end
it "responds with #{status} for DELETE custom email" do
delete custom_email_path
expect(response).to have_gitlab_http_status(status)
end
end
shared_examples 'a controller with disabled feature flag with status' do |status|
context 'when feature flag service_desk_custom_email is disabled' do
before do
stub_feature_flags(service_desk_custom_email: false)
end
it_behaves_like 'a controller that responds with status', status
end
end
shared_examples 'a deletable resource' do
describe 'DELETE custom email' do
let(:perform_request) { delete custom_email_path }
it_behaves_like 'a json response with empty values'
end
end
context 'with legitimate user signed in' do
before do
sign_out(illegitimite_user)
sign_in(user)
end
# because CustomEmailController check_feature_flag_enabled responds
it_behaves_like 'a controller with disabled feature flag with status', :not_found
describe 'GET custom email' do
let(:perform_request) { get custom_email_path }
it_behaves_like 'a json response with empty values'
end
describe 'POST custom email' do
before do
# We send verification email directly
allow(message).to receive(:deliver)
allow(Notify).to receive(:service_desk_custom_email_verification_email).and_return(message)
end
it 'adds custom email and kicks of verification' do
post custom_email_path, params: custom_email_params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include(
"custom_email" => custom_email_params[:custom_email],
"custom_email_enabled" => false,
"custom_email_verification_state" => "started",
"custom_email_verification_error" => nil,
"custom_email_smtp_address" => custom_email_params[:smtp_address],
"error_message" => nil
)
end
context 'when custom_email param is not valid' do
it 'does not add custom email' do
post custom_email_path, params: custom_email_params.merge(custom_email: 'useratexample.com')
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include(
empty_json_response.merge("error_message" => error_cannot_create_custom_email)
)
end
end
context 'when smtp_password param is not valid' do
it 'does not add custom email' do
post custom_email_path, params: custom_email_params.merge(smtp_password: '2short')
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include(
empty_json_response.merge("error_message" => error_cannot_create_custom_email)
)
end
end
context 'when the verification process fails fast' do
before do
# Could not establish connection, invalid host etc.
allow(message).to receive(:deliver).and_raise(SocketError)
end
it 'adds custom email and kicks of verification and returns verification error state' do
post custom_email_path, params: custom_email_params
# In terms of "custom email object creation", failing fast on the
# verification is a legit state that we don't treat as an error.
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include(
"custom_email" => custom_email_params[:custom_email],
"custom_email_enabled" => false,
"custom_email_verification_state" => "failed",
"custom_email_verification_error" => "smtp_host_issue",
"custom_email_smtp_address" => custom_email_params[:smtp_address],
"error_message" => nil
)
end
end
end
describe 'PUT custom email' do
let(:custom_email_params) { { custom_email_enabled: true } }
it 'does not update records' do
put custom_email_path, params: custom_email_params
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include(
empty_json_response.merge("error_message" => error_cannot_update_custom_email)
)
end
end
describe 'DELETE custom email' do
it 'does not touch any records' do
delete custom_email_path
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include(
empty_json_response.merge("error_message" => error_does_not_exist)
)
end
end
context 'when custom email is set up' do
let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') }
let!(:credential) { create(:service_desk_custom_email_credential, project: project) }
before do
project.reset
end
context 'and verification started' do
let!(:verification) do
create(:service_desk_custom_email_verification, project: project)
end
it_behaves_like 'a deletable resource'
describe 'GET custom email' do
it 'returns custom email in its current state' do
get custom_email_path
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include(
"custom_email" => "user@example.com",
"custom_email_enabled" => false,
"custom_email_verification_state" => "started",
"custom_email_verification_error" => nil,
"custom_email_smtp_address" => "smtp.example.com",
"error_message" => nil
)
end
end
describe 'POST custom email' do
it 'returns custom email in its current state' do
post custom_email_path, params: custom_email_params
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include(
"custom_email" => custom_email_params[:custom_email],
"custom_email_enabled" => false,
"custom_email_verification_state" => "started",
"custom_email_verification_error" => nil,
"custom_email_smtp_address" => custom_email_params[:smtp_address],
"error_message" => error_custom_email_exists
)
end
end
describe 'PUT custom email' do
let(:custom_email_params) { { custom_email_enabled: true } }
it 'marks custom email as enabled' do
put custom_email_path, params: custom_email_params
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include(
"custom_email" => "user@example.com",
"custom_email_enabled" => false,
"custom_email_verification_state" => "started",
"custom_email_verification_error" => nil,
"custom_email_smtp_address" => "smtp.example.com",
"error_message" => error_cannot_update_custom_email
)
end
end
end
context 'and verification finished' do
let!(:verification) do
create(:service_desk_custom_email_verification, project: project, state: :finished, token: nil)
end
it_behaves_like 'a deletable resource'
describe 'GET custom email' do
it 'returns custom email in its current state' do
get custom_email_path
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include(
"custom_email" => "user@example.com",
"custom_email_enabled" => false,
"custom_email_verification_state" => "finished",
"custom_email_verification_error" => nil,
"custom_email_smtp_address" => "smtp.example.com",
"error_message" => nil
)
end
end
describe 'PUT custom email' do
let(:custom_email_params) { { custom_email_enabled: true } }
it 'marks custom email as enabled' do
put custom_email_path, params: custom_email_params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include(
"custom_email" => "user@example.com",
"custom_email_enabled" => true,
"custom_email_verification_state" => "finished",
"custom_email_verification_error" => nil,
"custom_email_smtp_address" => "smtp.example.com",
"error_message" => nil
)
end
end
end
context 'and verification failed' do
let!(:verification) do
create(:service_desk_custom_email_verification,
project: project,
state: :failed,
token: nil,
error: :smtp_host_issue
)
end
it_behaves_like 'a deletable resource'
describe 'GET custom email' do
it 'returns custom email in its current state' do
get custom_email_path
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include(
"custom_email" => "user@example.com",
"custom_email_enabled" => false,
"custom_email_verification_state" => "failed",
"custom_email_verification_error" => "smtp_host_issue",
"custom_email_smtp_address" => "smtp.example.com",
"error_message" => nil
)
end
end
describe 'PUT custom email' do
let(:custom_email_params) { { custom_email_enabled: true } }
it 'does not mark custom email as enabled' do
put custom_email_path, params: custom_email_params
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include(
"custom_email" => "user@example.com",
"custom_email_enabled" => false,
"custom_email_verification_state" => "failed",
"custom_email_verification_error" => "smtp_host_issue",
"custom_email_smtp_address" => "smtp.example.com",
"error_message" => error_cannot_update_custom_email
)
end
end
end
end
end
context 'when user is anonymous' do
before do
sign_out(user)
sign_out(illegitimite_user)
end
# because Projects::ApplicationController :authenticate_user! responds
# with redirect to login page
it_behaves_like 'a controller that responds with status', :found
it_behaves_like 'a controller with disabled feature flag with status', :found
end
context 'with illegitimate user signed in' do
before do
sign_out(user)
sign_in(illegitimite_user)
end
it_behaves_like 'a controller that responds with status', :not_found
# because CustomEmailController check_feature_flag_enabled responds
it_behaves_like 'a controller with disabled feature flag with status', :not_found
end
end

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
RSpec.shared_examples 'milestone handling version conflicts' do
it 'warns about version conflict when milestone has been updated in the background' do
it 'warns about version conflict when milestone has been updated in the background', :js do
wait_for_all_requests
# Update the milestone in the background in order to trigger a version conflict
milestone.update!(title: "New title")

View File

@ -36,4 +36,22 @@ RSpec.describe 'groups/packages/index.html.haml', feature_category: :package_reg
)
end
end
describe 'can_delete_packages' do
it 'without permission sets false' do
allow(view).to receive(:can_delete_group_packages?).and_return(false)
render
expect(rendered).to have_selector('[data-can-delete-packages="false"]')
end
it 'with permission sets true' do
allow(view).to receive(:can_delete_group_packages?).and_return(true)
render
expect(rendered).to have_selector('[data-can-delete-packages="true"]')
end
end
end

View File

@ -36,4 +36,22 @@ RSpec.describe 'projects/packages/packages/index.html.haml', feature_category: :
)
end
end
describe 'can_delete_packages' do
it 'without permission sets empty settings path' do
allow(view).to receive(:can_delete_packages?).and_return(false)
render
expect(rendered).to have_selector('[data-can-delete-packages="false"]')
end
it 'with permission sets project settings path' do
allow(view).to receive(:can_delete_packages?).and_return(true)
render
expect(rendered).to have_selector('[data-can-delete-packages="true"]')
end
end
end

View File

@ -245,6 +245,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Geo::RepositoryVerification::Primary::SingleWorker' => false,
'Geo::RepositoryVerification::Secondary::SingleWorker' => false,
'Geo::ReverificationBatchWorker' => 0,
'Geo::BulkMarkPendingBatchWorker' => 0,
'Geo::Scheduler::Primary::SchedulerWorker' => false,
'Geo::Scheduler::SchedulerWorker' => false,
'Geo::Scheduler::Secondary::SchedulerWorker' => false,