summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-18 12:09:13 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-18 12:09:13 +0000
commit1363ca12f1f07c634647cf55c4c16b7401098673 (patch)
treed932caf09c8148322edb51ae954ed159ff7d00f8
parent6763d2787670bc03a36a8eb601703e88fc70dece (diff)
downloadgitlab-ce-1363ca12f1f07c634647cf55c4c16b7401098673.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue4
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue4
-rw-r--r--app/controllers/projects/import/jira_controller.rb4
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/ci_variables_helper.rb2
-rw-r--r--app/models/ci/group_variable.rb4
-rw-r--r--app/models/ci/job_variable.rb2
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb2
-rw-r--r--app/models/ci/pipeline_variable.rb2
-rw-r--r--app/models/ci/variable.rb4
-rw-r--r--app/models/concerns/ci/has_variable.rb36
-rw-r--r--app/models/concerns/ci/maskable.rb25
-rw-r--r--app/models/concerns/ci/new_has_variable.rb16
-rw-r--r--app/models/concerns/has_variable.rb34
-rw-r--r--app/models/concerns/maskable.rb23
-rw-r--r--app/models/concerns/new_has_variable.rb14
-rw-r--r--app/models/jira_import_data.rb31
-rw-r--r--app/models/project.rb14
-rw-r--r--app/services/milestones/transfer_service.rb4
-rw-r--r--app/workers/all_queues.yml49
-rw-r--r--app/workers/concerns/gitlab/jira_import/import_worker.rb35
-rw-r--r--app/workers/concerns/gitlab/jira_import/queue_options.rb16
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb4
-rw-r--r--app/workers/gitlab/jira_import/advance_stage_worker.rb26
-rw-r--r--app/workers/gitlab/jira_import/stage/finish_import_worker.rb20
-rw-r--r--app/workers/gitlab/jira_import/stage/import_attachments_worker.rb22
-rw-r--r--app/workers/gitlab/jira_import/stage/import_issues_worker.rb22
-rw-r--r--app/workers/gitlab/jira_import/stage/import_labels_worker.rb20
-rw-r--r--app/workers/gitlab/jira_import/stage/import_notes_worker.rb22
-rw-r--r--app/workers/gitlab/jira_import/stage/start_import_worker.rb44
-rw-r--r--changelogs/unreleased/208894-fix-showing-only-free-namespaces-when-applying-licenses.yml5
-rw-r--r--changelogs/unreleased/208923-enable-batch-counting-for-some-individual-queries-5.yml5
-rw-r--r--changelogs/unreleased/fix-invalid-milestones-when-moving-projects.yml5
-rw-r--r--config/gitlab.yml.example5
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20200311141943_insert_ci_pipeline_schedules_plan_limits.rb2
-rw-r--r--db/migrate/20200312160532_add_index_on_mirror_and_id_to_projects.rb21
-rw-r--r--db/schema.rb2
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md17
-rw-r--r--lib/gitlab/ci/pipeline/seed/build/cache.rb2
-rw-r--r--lib/gitlab/ci/variables/collection/item.rb2
-rw-r--r--spec/frontend/repository/components/table/row_spec.js5
-rw-r--r--spec/helpers/blob_helper_spec.rb73
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb10
-rw-r--r--spec/models/ci/group_variable_spec.rb2
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/concerns/ci/has_variable_spec.rb (renamed from spec/models/concerns/has_variable_spec.rb)2
-rw-r--r--spec/models/concerns/ci/maskable_spec.rb (renamed from spec/models/concerns/maskable_spec.rb)4
-rw-r--r--spec/models/jira_import_data_spec.rb134
-rw-r--r--spec/models/project_spec.rb57
-rw-r--r--spec/services/milestones/transfer_service_spec.rb10
-rw-r--r--spec/support/shared_examples/models/ci_variable_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb32
-rw-r--r--spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb63
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb40
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb40
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb40
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb40
-rw-r--r--spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb86
61 files changed, 1069 insertions, 157 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 8006e45259a..3bbe3020b70 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -750,7 +750,7 @@ GEM
orm_adapter (0.5.0)
os (1.0.0)
parallel (1.19.1)
- parser (2.6.5.0)
+ parser (2.7.0.4)
ast (~> 2.4.0)
parslet (1.8.2)
peek (1.1.0)
@@ -1094,13 +1094,13 @@ GEM
uniform_notifier (1.13.0)
unleash (0.1.5)
murmurhash3 (~> 0.1.6)
- unparser (0.4.5)
+ unparser (0.4.7)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
diff-lcs (~> 1.3)
equalizer (~> 0.0.9)
- parser (~> 2.6.3)
+ parser (>= 2.6.5)
procto (~> 0.0.2)
validate_email (0.1.6)
activemodel (>= 3.0)
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index be70bfc7399..03766c4877e 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -1,6 +1,6 @@
<script>
import { GlDropdown, GlDropdownDivider, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui';
-import { joinPaths } from '~/lib/utils/url_utility';
+import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '../../locale';
import Icon from '../../vue_shared/components/icon.vue';
import getRefMixin from '../mixins/get_ref';
@@ -103,7 +103,7 @@ export default {
.filter(p => p !== '')
.reduce(
(acc, name, i) => {
- const path = joinPaths(i > 0 ? acc[i].path : '', encodeURIComponent(name));
+ const path = joinPaths(i > 0 ? acc[i].path : '', escapeFileUrl(name));
return acc.concat({
name,
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index b81e6a38b4c..f3e6e3686a3 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -1,7 +1,7 @@
<script>
import { escapeRegExp } from 'lodash';
import { GlBadge, GlLink, GlSkeletonLoading, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
-import { visitUrl } from '~/lib/utils/url_utility';
+import { visitUrl, escapeFileUrl } from '~/lib/utils/url_utility';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { getIconName } from '../../utils/icon';
@@ -92,7 +92,7 @@ export default {
computed: {
routerLinkTo() {
return this.isFolder
- ? { path: `/-/tree/${escape(this.ref)}/${encodeURIComponent(this.path)}` }
+ ? { path: `/-/tree/${escape(this.ref)}/${escapeFileUrl(this.path)}` }
: null;
},
iconName() {
diff --git a/app/controllers/projects/import/jira_controller.rb b/app/controllers/projects/import/jira_controller.rb
index c74c180fa20..d38d9e27347 100644
--- a/app/controllers/projects/import/jira_controller.rb
+++ b/app/controllers/projects/import/jira_controller.rb
@@ -42,11 +42,13 @@ module Projects
def schedule_import(params)
import_data = @project.create_or_update_import_data(data: {}).becomes(JiraImportData)
- import_data << JiraImportData::JiraProjectDetails.new(
+ jira_project_details = JiraImportData::JiraProjectDetails.new(
params[:jira_project_key],
Time.now.strftime('%Y-%m-%d %H:%M:%S'),
{ user_id: current_user.id, name: current_user.name }
)
+ import_data << jira_project_details
+ import_data.force_import!
@project.import_type = 'jira'
@project.import_state.schedule if @project.save
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index cc5ae32856a..4debf66db64 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -344,8 +344,8 @@ module BlobHelper
def show_suggest_pipeline_creation_celebration?
experiment_enabled?(:suggest_pipeline) &&
- @blob.auxiliary_viewer.valid?(project: @project, sha: @commit.sha, user: current_user) &&
@blob.path == Gitlab::FileDetector::PATTERNS[:gitlab_ci] &&
+ @blob.auxiliary_viewer.valid?(project: @project, sha: @commit.sha, user: current_user) &&
@project.uses_default_ci_config? &&
cookies[suggest_pipeline_commit_cookie_name].present?
end
diff --git a/app/helpers/ci_variables_helper.rb b/app/helpers/ci_variables_helper.rb
index 3f4c04070b5..b271f069778 100644
--- a/app/helpers/ci_variables_helper.rb
+++ b/app/helpers/ci_variables_helper.rb
@@ -45,6 +45,6 @@ module CiVariablesHelper
end
def ci_variable_maskable_regex
- Maskable::REGEX.inspect.sub('\\A', '^').sub('\\z', '$').sub(/^\//, '').sub(/\/[a-z]*$/, '').gsub('\/', '/')
+ Ci::Maskable::REGEX.inspect.sub('\\A', '^').sub('\\z', '$').sub(/^\//, '').sub(/\/[a-z]*$/, '').gsub('\/', '/')
end
end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 0e50265c7ba..1e1dd68ee6c 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -3,9 +3,9 @@
module Ci
class GroupVariable < ApplicationRecord
extend Gitlab::Ci::Model
- include HasVariable
+ include Ci::HasVariable
include Presentable
- include Maskable
+ include Ci::Maskable
belongs_to :group, class_name: "::Group"
diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb
index f2968c037c7..7eea8a37150 100644
--- a/app/models/ci/job_variable.rb
+++ b/app/models/ci/job_variable.rb
@@ -3,7 +3,7 @@
module Ci
class JobVariable < ApplicationRecord
extend Gitlab::Ci::Model
- include NewHasVariable
+ include Ci::NewHasVariable
include BulkInsertSafe
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
index be6e5e76c31..adef9911ae1 100644
--- a/app/models/ci/pipeline_schedule_variable.rb
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -3,7 +3,7 @@
module Ci
class PipelineScheduleVariable < ApplicationRecord
extend Gitlab::Ci::Model
- include HasVariable
+ include Ci::HasVariable
belongs_to :pipeline_schedule
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index 51a6272e1ff..84ca4833cd7 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -3,7 +3,7 @@
module Ci
class PipelineVariable < ApplicationRecord
extend Gitlab::Ci::Model
- include HasVariable
+ include Ci::HasVariable
belongs_to :pipeline
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 760872d3e6b..08d39595c61 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -3,9 +3,9 @@
module Ci
class Variable < ApplicationRecord
extend Gitlab::Ci::Model
- include HasVariable
+ include Ci::HasVariable
include Presentable
- include Maskable
+ include Ci::Maskable
prepend HasEnvironmentScope
belongs_to :project
diff --git a/app/models/concerns/ci/has_variable.rb b/app/models/concerns/ci/has_variable.rb
new file mode 100644
index 00000000000..9bf2b409080
--- /dev/null
+++ b/app/models/concerns/ci/has_variable.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Ci
+ module HasVariable
+ extend ActiveSupport::Concern
+
+ included do
+ enum variable_type: {
+ env_var: 1,
+ file: 2
+ }
+
+ validates :key,
+ presence: true,
+ length: { maximum: 255 },
+ format: { with: /\A[a-zA-Z0-9_]+\z/,
+ message: "can contain only letters, digits and '_'." }
+
+ scope :order_key_asc, -> { reorder(key: :asc) }
+
+ attr_encrypted :value,
+ mode: :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ key: Settings.attr_encrypted_db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ def key=(new_key)
+ super(new_key.to_s.strip)
+ end
+ end
+
+ def to_runner_variable
+ { key: key, value: value, public: false, file: file? }
+ end
+ end
+end
diff --git a/app/models/concerns/ci/maskable.rb b/app/models/concerns/ci/maskable.rb
new file mode 100644
index 00000000000..15bc48bf964
--- /dev/null
+++ b/app/models/concerns/ci/maskable.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Ci
+ module Maskable
+ extend ActiveSupport::Concern
+
+ # * Single line
+ # * No escape characters
+ # * No variables
+ # * No spaces
+ # * Minimal length of 8 characters
+ # * Characters must be from the Base64 alphabet (RFC4648) with the addition of @ and :
+ # * Absolutely no fun is allowed
+ REGEX = /\A[a-zA-Z0-9_+=\/@:-]{8,}\z/.freeze
+
+ included do
+ validates :masked, inclusion: { in: [true, false] }
+ validates :value, format: { with: REGEX }, if: :masked?
+ end
+
+ def to_runner_variable
+ super.merge(masked: masked?)
+ end
+ end
+end
diff --git a/app/models/concerns/ci/new_has_variable.rb b/app/models/concerns/ci/new_has_variable.rb
new file mode 100644
index 00000000000..546d243e5de
--- /dev/null
+++ b/app/models/concerns/ci/new_has_variable.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Ci
+ module NewHasVariable
+ extend ActiveSupport::Concern
+ include Ci::HasVariable
+
+ included do
+ attr_encrypted :value,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm',
+ key: Settings.attr_encrypted_db_key_base_32,
+ insecure_mode: false
+ end
+ end
+end
diff --git a/app/models/concerns/has_variable.rb b/app/models/concerns/has_variable.rb
deleted file mode 100644
index b4e99569071..00000000000
--- a/app/models/concerns/has_variable.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-module HasVariable
- extend ActiveSupport::Concern
-
- included do
- enum variable_type: {
- env_var: 1,
- file: 2
- }
-
- validates :key,
- presence: true,
- length: { maximum: 255 },
- format: { with: /\A[a-zA-Z0-9_]+\z/,
- message: "can contain only letters, digits and '_'." }
-
- scope :order_key_asc, -> { reorder(key: :asc) }
-
- attr_encrypted :value,
- mode: :per_attribute_iv_and_salt,
- insecure_mode: true,
- key: Settings.attr_encrypted_db_key_base,
- algorithm: 'aes-256-cbc'
-
- def key=(new_key)
- super(new_key.to_s.strip)
- end
- end
-
- def to_runner_variable
- { key: key, value: value, public: false, file: file? }
- end
-end
diff --git a/app/models/concerns/maskable.rb b/app/models/concerns/maskable.rb
deleted file mode 100644
index d70e47bc4ff..00000000000
--- a/app/models/concerns/maskable.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module Maskable
- extend ActiveSupport::Concern
-
- # * Single line
- # * No escape characters
- # * No variables
- # * No spaces
- # * Minimal length of 8 characters
- # * Characters must be from the Base64 alphabet (RFC4648) with the addition of @ and :
- # * Absolutely no fun is allowed
- REGEX = /\A[a-zA-Z0-9_+=\/@:-]{8,}\z/.freeze
-
- included do
- validates :masked, inclusion: { in: [true, false] }
- validates :value, format: { with: REGEX }, if: :masked?
- end
-
- def to_runner_variable
- super.merge(masked: masked?)
- end
-end
diff --git a/app/models/concerns/new_has_variable.rb b/app/models/concerns/new_has_variable.rb
deleted file mode 100644
index 429bf496872..00000000000
--- a/app/models/concerns/new_has_variable.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-module NewHasVariable
- extend ActiveSupport::Concern
- include HasVariable
-
- included do
- attr_encrypted :value,
- mode: :per_attribute_iv,
- algorithm: 'aes-256-gcm',
- key: Settings.attr_encrypted_db_key_base_32,
- insecure_mode: false
- end
-end
diff --git a/app/models/jira_import_data.rb b/app/models/jira_import_data.rb
index 3f882deb24d..63be190aa0d 100644
--- a/app/models/jira_import_data.rb
+++ b/app/models/jira_import_data.rb
@@ -3,17 +3,40 @@
class JiraImportData < ProjectImportData
JiraProjectDetails = Struct.new(:key, :scheduled_at, :scheduled_by)
+ FORCE_IMPORT_KEY = 'force-import'
+
def projects
return [] unless data
- projects = data.dig('jira', 'projects').map do |p|
+ projects = data.dig('jira', 'projects')&.map do |p|
JiraProjectDetails.new(p['key'], p['scheduled_at'], p['scheduled_by'])
end
- projects.sort_by { |jp| jp.scheduled_at }
+
+ projects&.sort_by { |jp| jp.scheduled_at } || []
end
def <<(project)
- self.data ||= { jira: { projects: [] } }
- self.data['jira']['projects'] << project.to_h.deep_stringify_keys!
+ self.data ||= { 'jira' => { 'projects' => [] } }
+ self.data['jira'] ||= { 'projects' => [] }
+ self.data['jira']['projects'] = [] if data['jira']['projects'].blank? || !data['jira']['projects'].is_a?(Array)
+
+ self.data['jira']['projects'] << project.to_h
+ self.data.deep_stringify_keys!
+ end
+
+ def force_import!
+ self.data ||= {}
+ self.data.deep_merge!({ 'jira' => { FORCE_IMPORT_KEY => true } })
+ self.data.deep_stringify_keys!
+ end
+
+ def force_import?
+ !!data&.dig('jira', FORCE_IMPORT_KEY) && !projects.blank?
+ end
+
+ def finish_import!
+ return if data&.dig('jira', FORCE_IMPORT_KEY).nil?
+
+ data['jira'].delete(FORCE_IMPORT_KEY)
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 4892c5310ec..7e006e734c5 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -868,6 +868,8 @@ class Project < ApplicationRecord
elsif gitlab_project_import?
# Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-foss/issues/26189 is solved.
RepositoryImportWorker.set(retry: false).perform_async(self.id)
+ elsif jira_import?
+ Gitlab::JiraImport::Stage::StartImportWorker.perform_async(self.id)
else
RepositoryImportWorker.perform_async(self.id)
end
@@ -900,7 +902,7 @@ class Project < ApplicationRecord
# This method is overridden in EE::Project model
def remove_import_data
- import_data&.destroy
+ import_data&.destroy unless jira_import?
end
def ci_config_path=(value)
@@ -947,7 +949,7 @@ class Project < ApplicationRecord
end
def import?
- external_import? || forked? || gitlab_project_import? || bare_repository_import?
+ external_import? || forked? || gitlab_project_import? || jira_import? || bare_repository_import?
end
def external_import?
@@ -962,6 +964,14 @@ class Project < ApplicationRecord
import_type == 'bare_repository'
end
+ def jira_import?
+ import_type == 'jira' && Feature.enabled?(:jira_issue_import, self)
+ end
+
+ def jira_force_import?
+ jira_import? && import_data&.becomes(JiraImportData)&.force_import?
+ end
+
def gitlab_project_import?
import_type == 'gitlab_project'
end
diff --git a/app/services/milestones/transfer_service.rb b/app/services/milestones/transfer_service.rb
index 213c6f8f1dd..18d7e41adc7 100644
--- a/app/services/milestones/transfer_service.rb
+++ b/app/services/milestones/transfer_service.rb
@@ -46,7 +46,7 @@ module Milestones
Milestone.joins(:issues)
.where(
issues: { project_id: project.id },
- group_id: old_group.id
+ group_id: old_group.self_and_ancestors
)
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -56,7 +56,7 @@ module Milestones
Milestone.joins(:merge_requests)
.where(
merge_requests: { target_project_id: project.id },
- group_id: old_group.id
+ group_id: old_group.self_and_ancestors
)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 3df86e3314d..dd0eeaa9359 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -528,6 +528,55 @@
:resource_boundary: :unknown
:weight: 2
:idempotent:
+- :name: jira_importer:jira_import_advance_stage
+ :feature_category: :importers
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+- :name: jira_importer:jira_import_stage_finish_import
+ :feature_category: :importers
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+- :name: jira_importer:jira_import_stage_import_attachments
+ :feature_category: :importers
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+- :name: jira_importer:jira_import_stage_import_issues
+ :feature_category: :importers
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+- :name: jira_importer:jira_import_stage_import_labels
+ :feature_category: :importers
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+- :name: jira_importer:jira_import_stage_import_notes
+ :feature_category: :importers
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+- :name: jira_importer:jira_import_stage_start_import
+ :feature_category: :importers
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
- :name: mail_scheduler:mail_scheduler_issue_due
:feature_category: :issue_tracking
:has_external_dependencies:
diff --git a/app/workers/concerns/gitlab/jira_import/import_worker.rb b/app/workers/concerns/gitlab/jira_import/import_worker.rb
new file mode 100644
index 00000000000..7cc650bfc29
--- /dev/null
+++ b/app/workers/concerns/gitlab/jira_import/import_worker.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ module ImportWorker
+ extend ActiveSupport::Concern
+
+ included do
+ include ApplicationWorker
+ include Gitlab::JiraImport::QueueOptions
+ end
+
+ def perform(project_id)
+ project = Project.find_by(id: project_id) # rubocop: disable CodeReuse/ActiveRecord
+
+ return unless can_import?(project)
+
+ import(project)
+ end
+
+ private
+
+ def import(project)
+ raise NotImplementedError
+ end
+
+ def can_import?(project)
+ return false unless project
+ return false if Feature.disabled?(:jira_issue_import, project)
+
+ project.import_state.started?
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/jira_import/queue_options.rb b/app/workers/concerns/gitlab/jira_import/queue_options.rb
new file mode 100644
index 00000000000..bc1148f7d3b
--- /dev/null
+++ b/app/workers/concerns/gitlab/jira_import/queue_options.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ module QueueOptions
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :jira_importer
+ feature_category :importers
+
+ sidekiq_options retry: 5
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index 8fbf88a1762..8bbfb10ed6e 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -13,8 +13,6 @@ module Gitlab
sidekiq_options dead: false
feature_category :importers
- private
-
# The known importer stages and their corresponding Sidekiq workers.
STAGES = {
issues_and_diff_notes: Stage::ImportIssuesAndDiffNotesWorker,
@@ -23,6 +21,8 @@ module Gitlab
finish: Stage::FinishImportWorker
}.freeze
+ private
+
def next_stage_worker(next_stage)
STAGES.fetch(next_stage.to_sym)
end
diff --git a/app/workers/gitlab/jira_import/advance_stage_worker.rb b/app/workers/gitlab/jira_import/advance_stage_worker.rb
new file mode 100644
index 00000000000..1b6fc54151e
--- /dev/null
+++ b/app/workers/gitlab/jira_import/advance_stage_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ class AdvanceStageWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+ include QueueOptions
+ include ::Gitlab::Import::AdvanceStage
+
+ # The known importer stages and their corresponding Sidekiq workers.
+ STAGES = {
+ labels: Gitlab::JiraImport::Stage::ImportLabelsWorker,
+ issues: Gitlab::JiraImport::Stage::ImportIssuesWorker,
+ attachments: Gitlab::JiraImport::Stage::ImportAttachmentsWorker,
+ notes: Gitlab::JiraImport::Stage::ImportNotesWorker,
+ finish: Gitlab::JiraImport::Stage::FinishImportWorker
+ }.freeze
+
+ private
+
+ def next_stage_worker(next_stage)
+ STAGES.fetch(next_stage.to_sym)
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/jira_import/stage/finish_import_worker.rb b/app/workers/gitlab/jira_import/stage/finish_import_worker.rb
new file mode 100644
index 00000000000..5b1661d68c6
--- /dev/null
+++ b/app/workers/gitlab/jira_import/stage/finish_import_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ module Stage
+ class FinishImportWorker # rubocop:disable Scalability/IdempotentWorker
+ include Gitlab::JiraImport::ImportWorker
+
+ private
+
+ def import(project)
+ project.after_import
+ ensure
+ project.import_data.becomes(JiraImportData).finish_import!
+ project.import_data.save!
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/jira_import/stage/import_attachments_worker.rb b/app/workers/gitlab/jira_import/stage/import_attachments_worker.rb
new file mode 100644
index 00000000000..3b209a279b5
--- /dev/null
+++ b/app/workers/gitlab/jira_import/stage/import_attachments_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ module Stage
+ class ImportAttachmentsWorker # rubocop:disable Scalability/IdempotentWorker
+ include Gitlab::JiraImport::ImportWorker
+
+ private
+
+ def import(project)
+ # fake a attahcments import workers for now.
+ # new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage
+ fake_waiter = JobWaiter.new
+
+ project.import_state.refresh_jid_expiration
+ Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { fake_waiter.key => fake_waiter.jobs_remaining }, :notes)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/jira_import/stage/import_issues_worker.rb b/app/workers/gitlab/jira_import/stage/import_issues_worker.rb
new file mode 100644
index 00000000000..79ed8e1f2da
--- /dev/null
+++ b/app/workers/gitlab/jira_import/stage/import_issues_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ module Stage
+ class ImportIssuesWorker # rubocop:disable Scalability/IdempotentWorker
+ include Gitlab::JiraImport::ImportWorker
+
+ private
+
+ def import(project)
+ # fake issues import workers for now
+ # new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage
+ jobs_waiter = JobWaiter.new
+ project.import_state.refresh_jid_expiration
+
+ Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { jobs_waiter.key => jobs_waiter.jobs_remaining }, :attachments)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/jira_import/stage/import_labels_worker.rb b/app/workers/gitlab/jira_import/stage/import_labels_worker.rb
new file mode 100644
index 00000000000..b96bb1bbdda
--- /dev/null
+++ b/app/workers/gitlab/jira_import/stage/import_labels_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ module Stage
+ class ImportLabelsWorker # rubocop:disable Scalability/IdempotentWorker
+ include Gitlab::JiraImport::ImportWorker
+
+ private
+
+ def import(project)
+ # fake labels import workers for now
+ # new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage
+ fake_waiter = JobWaiter.new
+ Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { fake_waiter.key => fake_waiter.jobs_remaining }, :issues)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/jira_import/stage/import_notes_worker.rb b/app/workers/gitlab/jira_import/stage/import_notes_worker.rb
new file mode 100644
index 00000000000..9eef0d31a8c
--- /dev/null
+++ b/app/workers/gitlab/jira_import/stage/import_notes_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ module Stage
+ class ImportNotesWorker # rubocop:disable Scalability/IdempotentWorker
+ include Gitlab::JiraImport::ImportWorker
+
+ private
+
+ def import(project)
+ # fake notes import workers for now
+ # new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage
+ jobs_waiter = JobWaiter.new
+ project.import_state.refresh_jid_expiration
+
+ Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { jobs_waiter.key => jobs_waiter.jobs_remaining }, :finish)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/jira_import/stage/start_import_worker.rb b/app/workers/gitlab/jira_import/stage/start_import_worker.rb
new file mode 100644
index 00000000000..8abbfab647b
--- /dev/null
+++ b/app/workers/gitlab/jira_import/stage/start_import_worker.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ module Stage
+ class StartImportWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+ include ProjectStartImport
+ include ProjectImportOptions
+ include Gitlab::JiraImport::QueueOptions
+
+ attr_reader :project
+
+ def perform(project_id)
+ @project = Project.find_by(id: project_id) # rubocop: disable CodeReuse/ActiveRecord
+
+ return unless start_import
+
+ Gitlab::Import::SetAsyncJid.set_jid(project)
+
+ Gitlab::JiraImport::Stage::ImportLabelsWorker.perform_async(project.id)
+ end
+
+ private
+
+ def start_import
+ return false unless project
+ return false if Feature.disabled?(:jira_issue_import, project)
+ return true if start(project.import_state)
+
+ Gitlab::Import::Logger.info(
+ {
+ project_id: project.id,
+ project_path: project.full_path,
+ state: project&.import_status,
+ message: 'inconsistent state while importing'
+ }
+ )
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/changelogs/unreleased/208894-fix-showing-only-free-namespaces-when-applying-licenses.yml b/changelogs/unreleased/208894-fix-showing-only-free-namespaces-when-applying-licenses.yml
new file mode 100644
index 00000000000..40495fc2d06
--- /dev/null
+++ b/changelogs/unreleased/208894-fix-showing-only-free-namespaces-when-applying-licenses.yml
@@ -0,0 +1,5 @@
+---
+title: Fix managed_free_namespaces scope to only groups without a license or a free license
+merge_request: 27356
+author:
+type: fixed
diff --git a/changelogs/unreleased/208923-enable-batch-counting-for-some-individual-queries-5.yml b/changelogs/unreleased/208923-enable-batch-counting-for-some-individual-queries-5.yml
new file mode 100644
index 00000000000..93f07635a31
--- /dev/null
+++ b/changelogs/unreleased/208923-enable-batch-counting-for-some-individual-queries-5.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize projects_mirrored_with_pipelines_enabled query performance in usage data
+merge_request: 27110
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-invalid-milestones-when-moving-projects.yml b/changelogs/unreleased/fix-invalid-milestones-when-moving-projects.yml
new file mode 100644
index 00000000000..24a0e303176
--- /dev/null
+++ b/changelogs/unreleased/fix-invalid-milestones-when-moving-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Fix invalid ancestor group milestones when moving projects
+merge_request: 27262
+author:
+type: fixed
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 81085d4641e..eb41e4ac423 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -459,6 +459,11 @@ production: &base
elastic_index_bulk_cron_worker:
cron: "*/1 * * * *"
+ # Elasticsearch metrics
+ # NOTE: This will only take effect if Elasticsearch is enabled.
+ elastic_metrics_update_worker:
+ cron: "*/1 * * * *"
+
registry:
# enabled: true
# host: registry.example.com
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 8d88d1bcf7c..81de0ac6818 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -546,6 +546,9 @@ Gitlab.ee do
Settings.cron_jobs['elastic_index_bulk_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['elastic_index_bulk_cron_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['elastic_index_bulk_cron_worker']['job_class'] ||= 'ElasticIndexBulkCronWorker'
+ Settings.cron_jobs['elastic_metrics_update_worker'] ||= Settingslogic.new({})
+ Settings.cron_jobs['elastic_metrics_update_worker']['cron'] ||= '*/1 * * * *'
+ Settings.cron_jobs['elastic_metrics_update_worker']['job_class'] ||= 'ElasticMetricsUpdateWorker'
Settings.cron_jobs['sync_seat_link_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['sync_seat_link_worker']['cron'] ||= "#{rand(60)} 0 * * *"
Settings.cron_jobs['sync_seat_link_worker']['job_class'] = 'SyncSeatLinkWorker'
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 2dc2f33e71e..858766f19ca 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -128,6 +128,8 @@
- 1
- - jira_connect
- 1
+- - jira_importer
+ - 1
- - ldap_group_sync
- 2
- - mail_scheduler
diff --git a/db/migrate/20200311141943_insert_ci_pipeline_schedules_plan_limits.rb b/db/migrate/20200311141943_insert_ci_pipeline_schedules_plan_limits.rb
index d1ad5be5f85..849d95667a7 100644
--- a/db/migrate/20200311141943_insert_ci_pipeline_schedules_plan_limits.rb
+++ b/db/migrate/20200311141943_insert_ci_pipeline_schedules_plan_limits.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class InsertCiPipelineSchedulesPlanLimits < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
DOWNTIME = false
def change
diff --git a/db/migrate/20200312160532_add_index_on_mirror_and_id_to_projects.rb b/db/migrate/20200312160532_add_index_on_mirror_and_id_to_projects.rb
new file mode 100644
index 00000000000..3a64b915931
--- /dev/null
+++ b/db/migrate/20200312160532_add_index_on_mirror_and_id_to_projects.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddIndexOnMirrorAndIdToProjects < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ OLD_INDEX_NAME = 'index_projects_on_mirror_and_mirror_trigger_builds_both_true'
+ NEW_INDEX_NAME = 'index_projects_on_mirror_id_where_mirror_and_trigger_builds'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :projects, :id, where: 'mirror = TRUE AND mirror_trigger_builds = TRUE', name: NEW_INDEX_NAME
+ remove_concurrent_index_by_name :projects, OLD_INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :projects, :id, where: 'mirror IS TRUE AND mirror_trigger_builds IS TRUE', name: OLD_INDEX_NAME
+ remove_concurrent_index_by_name :projects, NEW_INDEX_NAME
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1e32f4e6bd4..9edc1d9853c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -3507,7 +3507,7 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
t.index ["id"], name: "index_on_id_partial_with_legacy_storage", where: "((storage_version < 2) OR (storage_version IS NULL))"
t.index ["id"], name: "index_projects_on_id_partial_for_visibility", unique: true, where: "(visibility_level = ANY (ARRAY[10, 20]))"
t.index ["id"], name: "index_projects_on_id_service_desk_enabled", where: "(service_desk_enabled = true)"
- t.index ["id"], name: "index_projects_on_mirror_and_mirror_trigger_builds_both_true", where: "((mirror IS TRUE) AND (mirror_trigger_builds IS TRUE))"
+ t.index ["id"], name: "index_projects_on_mirror_id_where_mirror_and_trigger_builds", where: "((mirror = true) AND (mirror_trigger_builds = true))"
t.index ["last_activity_at", "id"], name: "index_projects_api_last_activity_at_id_desc", order: { id: :desc }
t.index ["last_activity_at", "id"], name: "index_projects_api_vis20_last_activity_at", where: "(visibility_level = 20)"
t.index ["last_activity_at", "id"], name: "index_projects_on_last_activity_at_and_id"
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index b789f2ddd02..186b848f8bd 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -17,7 +17,13 @@ GitLab monitors its own internal service metrics, and makes them available at th
`/-/metrics` endpoint. Unlike other [Prometheus](https://prometheus.io) exporters, in order to access
it, the client IP needs to be [included in a whitelist](../ip_whitelist.md).
-For Omnibus and Chart installations, these metrics are automatically enabled and collected as of [GitLab 9.4](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/1702). For source installations or earlier versions, these metrics will need to be enabled manually and collected by a Prometheus server.
+For Omnibus and Chart installations, these metrics are automatically enabled
+and collected as of [GitLab
+9.4](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/1702). For
+source installations or earlier versions, these metrics will need to be enabled
+manually and collected by a Prometheus server.
+
+See also [Sidekiq metrics](#sidekiq-metrics) for how to enable and view metrics from Sidekiq nodes.
## Metrics available
@@ -105,10 +111,12 @@ The following metrics can be controlled by feature flags:
| `gitlab_method_call_duration_seconds` | `prometheus_metrics_method_instrumentation` |
| `gitlab_view_rendering_duration_seconds` | `prometheus_metrics_view_instrumentation` |
-## Sidekiq Metrics available for Geo **(PREMIUM)**
+## Sidekiq metrics
-Sidekiq jobs may also gather metrics, and these metrics can be accessed if the Sidekiq exporter is enabled (e.g. via
-the `monitoring.sidekiq_exporter` configuration option in `gitlab.yml`.
+Sidekiq jobs may also gather metrics, and these metrics can be accessed if the
+Sidekiq exporter is enabled (for example, using the `monitoring.sidekiq_exporter`
+configuration option in `gitlab.yml`. These metrics are served from the
+`/metrics` path on the configured port.
| Metric | Type | Since | Description | Labels |
|:---------------------------------------------- |:------- |:----- |:----------- |:------ |
@@ -145,6 +153,7 @@ the `monitoring.sidekiq_exporter` configuration option in `gitlab.yml`.
| `geo_repositories_checked_failed_count` | Gauge | 11.1 | Number of repositories that have a failure from `git fsck` | url |
| `geo_repositories_retrying_verification_count` | Gauge | 11.2 | Number of repositories verification failures that Geo is actively trying to correct on secondary | url |
| `geo_wikis_retrying_verification_count` | Gauge | 11.2 | Number of wikis verification failures that Geo is actively trying to correct on secondary | url |
+| `global_search_bulk_cron_queue_size` | Gauge | 12.10 | Number of database records waiting to be synchronized to Elasticsearch | |
## Database load balancing metrics **(PREMIUM ONLY)**
diff --git a/lib/gitlab/ci/pipeline/seed/build/cache.rb b/lib/gitlab/ci/pipeline/seed/build/cache.rb
index 7671035b896..a4127ea0be2 100644
--- a/lib/gitlab/ci/pipeline/seed/build/cache.rb
+++ b/lib/gitlab/ci/pipeline/seed/build/cache.rb
@@ -51,8 +51,6 @@ module Gitlab
end
def hash_of_the_latest_changes
- return unless Feature.enabled?(:ci_file_based_cache, @pipeline.project, default_enabled: true)
-
ids = files.map { |path| last_commit_id_for_path(path) }
ids = ids.compact.sort.uniq
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index aab10aef398..a072036daa8 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -37,7 +37,7 @@ module Gitlab
case resource
when Hash
self.new(resource.symbolize_keys)
- when ::HasVariable
+ when ::Ci::HasVariable
self.new(resource.to_runner_variable)
when self
resource.dup
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index a51846023ac..22aacdc735c 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -4,7 +4,10 @@ import { visitUrl } from '~/lib/utils/url_utility';
import TableRow from '~/repository/components/table/row.vue';
import Icon from '~/vue_shared/components/icon.vue';
-jest.mock('~/lib/utils/url_utility');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
let vm;
let $router;
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index dec7d6b2df3..2631c219222 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -204,7 +204,6 @@ describe BlobHelper do
end
describe '#show_suggest_pipeline_creation_celebration?' do
- let(:blob) { fake_blob(path: Gitlab::FileDetector::PATTERNS[:gitlab_ci]) }
let(:current_user) { create(:user) }
before do
@@ -212,52 +211,68 @@ describe BlobHelper do
assign(:blob, blob)
assign(:commit, double('Commit', sha: 'whatever'))
helper.request.cookies["suggest_gitlab_ci_yml_commit_#{project.id}"] = 'true'
- allow(blob).to receive(:auxiliary_viewer).and_return(double('viewer', valid?: true))
allow(helper).to receive(:current_user).and_return(current_user)
end
- context 'experiment enabled' do
- before do
- allow(helper).to receive(:experiment_enabled?).and_return(true)
- end
-
- it 'is true' do
- expect(helper.show_suggest_pipeline_creation_celebration?).to be_truthy
- end
+ context 'when file is a pipeline config file' do
+ let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
+ let(:blob) { fake_blob(path: Gitlab::FileDetector::PATTERNS[:gitlab_ci], data: data) }
- context 'file is invalid format' do
+ context 'experiment enabled' do
before do
- allow(blob).to receive(:auxiliary_viewer).and_return(double('viewer', valid?: false))
+ allow(helper).to receive(:experiment_enabled?).and_return(true)
end
- it 'is false' do
- expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
+ it 'is true' do
+ expect(helper.show_suggest_pipeline_creation_celebration?).to be_truthy
end
- end
- context 'path is not a ci file' do
- before do
- allow(blob).to receive(:path).and_return('something_bad')
+ context 'file is invalid format' do
+ let(:data) { 'foo' }
+
+ it 'is false' do
+ expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
+ end
end
- it 'is false' do
- expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
+ context 'does not use the default ci config' do
+ before do
+ project.ci_config_path = 'something_bad'
+ end
+
+ it 'is false' do
+ expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
+ end
+ end
+
+ context 'does not have the needed cookie' do
+ before do
+ helper.request.cookies.delete "suggest_gitlab_ci_yml_commit_#{project.id}"
+ end
+
+ it 'is false' do
+ expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
+ end
end
end
- context 'does not use the default ci config' do
+ context 'experiment disabled' do
before do
- project.ci_config_path = 'something_bad'
+ allow(helper).to receive(:experiment_enabled?).and_return(false)
end
it 'is false' do
expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
end
end
+ end
- context 'does not have the needed cookie' do
+ context 'when file is not a pipeline config file' do
+ let(:blob) { fake_blob(path: 'LICENSE') }
+
+ context 'experiment enabled' do
before do
- helper.request.cookies.delete "suggest_gitlab_ci_yml_commit_#{project.id}"
+ allow(helper).to receive(:experiment_enabled?).and_return(true)
end
it 'is false' do
@@ -265,16 +280,6 @@ describe BlobHelper do
end
end
end
-
- context 'experiment disabled' do
- before do
- allow(helper).to receive(:experiment_enabled?).and_return(false)
- end
-
- it 'is false' do
- expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
- end
- end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
index 6a8b804597c..fe19244659f 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
@@ -83,16 +83,6 @@ describe Gitlab::Ci::Pipeline::Seed::Build::Cache do
it_behaves_like 'version and gemfile files'
end
- context 'with feature flag disabled' do
- let(:files) { ['VERSION', 'Gemfile.zip'] }
-
- before do
- stub_feature_flags(ci_file_based_cache: false)
- end
-
- it_behaves_like 'default key'
- end
-
context 'with files ending with /' do
let(:files) { ['Gemfile.zip/'] }
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
index 406a69f3bbc..610db9bf0e5 100644
--- a/spec/models/ci/group_variable_spec.rb
+++ b/spec/models/ci/group_variable_spec.rb
@@ -8,7 +8,7 @@ describe Ci::GroupVariable do
it_behaves_like "CI variable"
it { is_expected.to include_module(Presentable) }
- it { is_expected.to include_module(Maskable) }
+ it { is_expected.to include_module(Ci::Maskable) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id).with_message(/\(\w+\) has already been taken/) }
describe '.unprotected' do
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 3ff547456c6..810a0ddfd2e 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -9,7 +9,7 @@ describe Ci::Variable do
describe 'validations' do
it { is_expected.to include_module(Presentable) }
- it { is_expected.to include_module(Maskable) }
+ it { is_expected.to include_module(Ci::Maskable) }
it { is_expected.to include_module(HasEnvironmentScope) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope).with_message(/\(\w+\) has already been taken/) }
end
diff --git a/spec/models/concerns/has_variable_spec.rb b/spec/models/concerns/ci/has_variable_spec.rb
index 2bb21d7934e..c132fe47c3c 100644
--- a/spec/models/concerns/has_variable_spec.rb
+++ b/spec/models/concerns/ci/has_variable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe HasVariable do
+describe Ci::HasVariable do
subject { build(:ci_variable) }
it { is_expected.to validate_presence_of(:key) }
diff --git a/spec/models/concerns/maskable_spec.rb b/spec/models/concerns/ci/maskable_spec.rb
index aeba7ad862f..22ffb294819 100644
--- a/spec/models/concerns/maskable_spec.rb
+++ b/spec/models/concerns/ci/maskable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe Maskable do
+describe Ci::Maskable do
let(:variable) { build(:ci_variable) }
describe 'masked value validations' do
@@ -34,7 +34,7 @@ describe Maskable do
end
describe 'REGEX' do
- subject { Maskable::REGEX }
+ subject { Ci::Maskable::REGEX }
it 'does not match strings shorter than 8 letters' do
expect(subject.match?('hello')).to eq(false)
diff --git a/spec/models/jira_import_data_spec.rb b/spec/models/jira_import_data_spec.rb
new file mode 100644
index 00000000000..ad7a704236b
--- /dev/null
+++ b/spec/models/jira_import_data_spec.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe JiraImportData do
+ let(:symbol_keys_project) do
+ { key: 'AA', scheduled_at: 2.days.ago.strftime('%Y-%m-%d %H:%M:%S'), scheduled_by: { 'user_id' => 1, 'name' => 'tester1' } }
+ end
+
+ let(:string_keys_project) do
+ { 'key': 'BB', 'scheduled_at': 1.hour.ago.strftime('%Y-%m-%d %H:%M:%S'), 'scheduled_by': { 'user_id': 2, 'name': 'tester2' } }
+ end
+
+ let(:jira_project_details) do
+ JiraImportData::JiraProjectDetails.new('CC', 1.day.ago.strftime('%Y-%m-%d %H:%M:%S'), { user_id: 3, name: 'tester3' })
+ end
+
+ describe '#projects' do
+ it 'returns empty array if no data' do
+ expect(described_class.new.projects).to eq([])
+ end
+
+ it 'returns empty array if no projects' do
+ import_data = described_class.new(data: { 'some-key' => 10 })
+ expect(import_data.projects).to eq([])
+ end
+
+ it 'returns JiraProjectDetails sorted by scheduled_at time' do
+ import_data = described_class.new(data: { jira: { projects: [symbol_keys_project, string_keys_project, jira_project_details] } })
+
+ expect(import_data.projects.size).to eq 3
+ expect(import_data.projects.map(&:key)).to eq(%w(AA CC BB))
+ expect(import_data.projects.map(&:scheduled_by).map {|e| e['name']}).to eq %w(tester1 tester3 tester2)
+ expect(import_data.projects.map(&:scheduled_by).map {|e| e['user_id']}).to eq [1, 3, 2]
+ end
+ end
+
+ describe 'add projects' do
+ it 'adds project when data is nil' do
+ import_data = described_class.new
+ expect(import_data.data).to be nil
+
+ import_data << string_keys_project
+
+ expect(import_data.data).to eq({ 'jira' => { 'projects' => [string_keys_project] } })
+ end
+
+ it 'adds project when data has some random info' do
+ import_data = described_class.new(data: { 'one-key': 10 })
+ expect(import_data.data).to eq({ 'one-key' => 10 })
+
+ import_data << string_keys_project
+
+ expect(import_data.data).to eq({ 'one-key' => 10, 'jira' => { 'projects' => [string_keys_project] } })
+ end
+
+ it 'adds project when data already has some jira projects' do
+ import_data = described_class.new(data: { jira: { projects: [symbol_keys_project] } })
+ expect(import_data.projects.map(&:to_h)).to eq [symbol_keys_project]
+
+ import_data << string_keys_project
+
+ expect(import_data.data['jira']['projects'].size).to eq 2
+ expect(import_data.projects.map(&:key)).to eq(%w(AA BB))
+ expect(import_data.projects.map(&:scheduled_by).map {|e| e['name']}).to eq %w(tester1 tester2)
+ expect(import_data.projects.map(&:scheduled_by).map {|e| e['user_id']}).to eq [1, 2]
+ end
+ end
+
+ describe '#force_import!' do
+ it 'sets force import when data is nil' do
+ import_data = described_class.new
+
+ import_data.force_import!
+
+ expect(import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be true
+ expect(import_data.force_import?).to be false
+ end
+
+ it 'sets force import when data is present but no jira key' do
+ import_data = described_class.new(data: { 'some-key': 'some-data' })
+
+ import_data.force_import!
+
+ expect(import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be true
+ expect(import_data.data).to eq({ 'some-key' => 'some-data', 'jira' => { JiraImportData::FORCE_IMPORT_KEY => true } })
+ expect(import_data.force_import?).to be false
+ end
+
+ it 'sets force import when data and jira keys exist' do
+ import_data = described_class.new(data: { 'some-key': 'some-data', 'jira': {} })
+
+ import_data.force_import!
+
+ expect(import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be true
+ expect(import_data.data).to eq({ 'some-key' => 'some-data', 'jira' => { JiraImportData::FORCE_IMPORT_KEY => true } })
+ expect(import_data.force_import?).to be false
+ end
+
+ it 'sets force import when data and jira project data exist' do
+ import_data = described_class.new(data: { jira: { projects: [symbol_keys_project], JiraImportData::FORCE_IMPORT_KEY => false }, 'some-key': 'some-data' })
+
+ import_data.force_import!
+
+ expect(import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be true
+ expect(import_data.data).to eq({ 'some-key' => 'some-data', 'jira' => { 'projects' => [symbol_keys_project.deep_stringify_keys!], JiraImportData::FORCE_IMPORT_KEY => true } })
+ expect(import_data.force_import?).to be true
+ end
+ end
+
+ describe '#force_import?' do
+ it 'returns false when data blank' do
+ expect(described_class.new.force_import?).to be false
+ end
+
+ it 'returns false if there is no project data present' do
+ import_data = described_class.new(data: { jira: { JiraImportData::FORCE_IMPORT_KEY => true }, 'one-key': 10 })
+
+ expect(import_data.force_import?).to be false
+ end
+
+ it 'returns false when force import set to false' do
+ import_data = described_class.new(data: { jira: { projects: [symbol_keys_project], JiraImportData::FORCE_IMPORT_KEY => false }, 'one-key': 10 })
+
+ expect(import_data.force_import?).to be false
+ end
+
+ it 'returns true when force import set to true' do
+ import_data = described_class.new(data: { jira: { projects: [symbol_keys_project], JiraImportData::FORCE_IMPORT_KEY => true } })
+
+ expect(import_data.force_import?).to be true
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index ae97e5340e2..15b409b2dcf 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -2353,6 +2353,63 @@ describe Project do
expect(project.add_import_job).to eq(import_jid)
end
end
+
+ context 'jira import' do
+ it 'schedules a jira import job' do
+ project = create(:project, import_type: 'jira')
+
+ expect(Gitlab::JiraImport::Stage::StartImportWorker).to receive(:perform_async).with(project.id).and_return(import_jid)
+ expect(project.add_import_job).to eq(import_jid)
+ end
+ end
+ end
+
+ describe '#jira_import?' do
+ subject(:project) { build(:project, import_type: 'jira') }
+
+ it { expect(project.jira_import?).to be true }
+ it { expect(project.import?).to be true }
+ end
+
+ describe '#jira_force_import?' do
+ let(:imported_jira_project) do
+ JiraImportData::JiraProjectDetails.new('xx', Time.now.strftime('%Y-%m-%d %H:%M:%S'), { user_id: 1, name: 'root' })
+ end
+ let(:jira_import_data) do
+ data = JiraImportData.new
+ data << imported_jira_project
+ data.force_import!
+ data
+ end
+
+ subject(:project) { build(:project, import_type: 'jira', import_data: jira_import_data) }
+
+ it { expect(project.jira_force_import?).to be true }
+ end
+
+ describe '#remove_import_data' do
+ let(:import_data) { ProjectImportData.new(data: { 'test' => 'some data' }) }
+
+ context 'when jira import' do
+ let!(:project) { create(:project, import_type: 'jira', import_data: import_data) }
+
+ it 'does not remove import data' do
+ expect(project.mirror?).to be false
+ expect(project.jira_import?).to be true
+ expect { project.remove_import_data }.not_to change { ProjectImportData.count }
+ end
+ end
+
+ context 'when not mirror neither jira import' do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, import_type: 'github', import_data: import_data) }
+
+ it 'removes import data' do
+ expect(project.mirror?).to be false
+ expect(project.jira_import?).to be false
+ expect { project.remove_import_data }.to change { ProjectImportData.count }.by(-1)
+ end
+ end
end
describe '#gitlab_project_import?' do
diff --git a/spec/services/milestones/transfer_service_spec.rb b/spec/services/milestones/transfer_service_spec.rb
index 9b087b07cea..9f94d2d320b 100644
--- a/spec/services/milestones/transfer_service_spec.rb
+++ b/spec/services/milestones/transfer_service_spec.rb
@@ -40,6 +40,16 @@ describe Milestones::TransferService do
expect(new_milestone.project_milestone?).to be_truthy
end
+ context 'when milestone is from an ancestor group' do
+ let(:old_group_ancestor) { create(:group) }
+ let(:old_group) { create(:group, parent: old_group_ancestor) }
+ let(:group_milestone) { create(:milestone, group: old_group_ancestor)}
+
+ it 'recreates the missing group milestones at project level' do
+ expect { service.execute }.to change(project.milestones, :count).by(1)
+ end
+ end
+
it 'deletes milestone issue counters cache for both milestones' do
new_milestone = create(:milestone, project: project, title: group_milestone.title)
diff --git a/spec/support/shared_examples/models/ci_variable_shared_examples.rb b/spec/support/shared_examples/models/ci_variable_shared_examples.rb
index 6cc922b4101..e5463f26369 100644
--- a/spec/support/shared_examples/models/ci_variable_shared_examples.rb
+++ b/spec/support/shared_examples/models/ci_variable_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'CI variable' do
- it { is_expected.to include_module(HasVariable) }
+ it { is_expected.to include_module(Ci::HasVariable) }
describe "variable type" do
it 'defines variable types' do
diff --git a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb
new file mode 100644
index 00000000000..5448526f954
--- /dev/null
+++ b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+shared_examples 'include import workers modules' do
+ it { expect(described_class).to include_module(ApplicationWorker) }
+ it { expect(described_class).to include_module(Gitlab::JiraImport::QueueOptions) }
+
+ if described_class == Gitlab::JiraImport::Stage::StartImportWorker
+ it { expect(described_class).to include_module(ProjectStartImport) }
+ it { expect(described_class).to include_module(ProjectImportOptions) }
+ else
+ it { expect(described_class).to include_module(Gitlab::JiraImport::ImportWorker) }
+ end
+end
+
+shared_examples 'exit import not started' do
+ it 'does nothing, and exits' do
+ expect(Gitlab::JiraImport::AdvanceStageWorker).not_to receive(:perform_async)
+
+ worker.perform(project.id)
+ end
+end
+
+shared_examples 'advance to next stage' do |next_stage|
+ let(:job_waiter) { Gitlab::JobWaiter.new(2, 'some-job-key') }
+
+ it "advances to #{next_stage} stage" do
+ expect(Gitlab::JobWaiter).to receive(:new).and_return(job_waiter)
+ expect(Gitlab::JiraImport::AdvanceStageWorker).to receive(:perform_async).with(project.id, { job_waiter.key => job_waiter.jobs_remaining }, next_stage.to_sym)
+
+ worker.perform(project.id)
+ end
+end
diff --git a/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb
new file mode 100644
index 00000000000..fa0c7d83851
--- /dev/null
+++ b/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::JiraImport::Stage::FinishImportWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe 'modules' do
+ it_behaves_like 'include import workers modules'
+ end
+
+ describe '#perform' do
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: false)
+ end
+
+ it_behaves_like 'exit import not started'
+ end
+
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: true)
+ end
+
+ context 'when import did not start' do
+ let!(:import_state) { create(:import_state, project: project) }
+
+ it_behaves_like 'exit import not started'
+ end
+
+ context 'when import started' do
+ let(:imported_jira_project) do
+ JiraImportData::JiraProjectDetails.new('xx', Time.now.strftime('%Y-%m-%d %H:%M:%S'), { user_id: 1, name: 'root' })
+ end
+ let(:jira_import_data) do
+ data = JiraImportData.new
+ data << imported_jira_project
+ data.force_import!
+ data
+ end
+ let(:import_state) { create(:import_state, status: :started) }
+ let(:project) { create(:project, import_type: 'jira', import_data: jira_import_data, import_state: import_state) }
+
+ it 'changes import state to finished' do
+ worker.perform(project.id)
+
+ expect(project.reload.import_state.status).to eq "finished"
+ end
+
+ it 'removes force-import flag' do
+ expect(project.reload.import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be true
+
+ worker.perform(project.id)
+
+ expect(project.reload.import_data.data['jira'][JiraImportData::FORCE_IMPORT_KEY]).to be nil
+ expect(project.reload.import_data.data['jira']).not_to be nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb
new file mode 100644
index 00000000000..fa2f3501973
--- /dev/null
+++ b/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::JiraImport::Stage::ImportAttachmentsWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe 'modules' do
+ it_behaves_like 'include import workers modules'
+ end
+
+ describe '#perform' do
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: false)
+ end
+
+ it_behaves_like 'exit import not started'
+ end
+
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: true)
+ end
+
+ context 'when import did not start' do
+ let!(:import_state) { create(:import_state, project: project) }
+
+ it_behaves_like 'exit import not started'
+ end
+
+ context 'when import started' do
+ let!(:import_state) { create(:import_state, status: :started, project: project) }
+
+ it_behaves_like 'advance to next stage', :notes
+ end
+ end
+ end
+end
diff --git a/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
new file mode 100644
index 00000000000..b43519a3e5d
--- /dev/null
+++ b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::JiraImport::Stage::ImportIssuesWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe 'modules' do
+ it_behaves_like 'include import workers modules'
+ end
+
+ describe '#perform' do
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: false)
+ end
+
+ it_behaves_like 'exit import not started'
+ end
+
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: true)
+ end
+
+ context 'when import did not start' do
+ let!(:import_state) { create(:import_state, project: project) }
+
+ it_behaves_like 'exit import not started'
+ end
+
+ context 'when import started' do
+ let!(:import_state) { create(:import_state, status: :started, project: project) }
+
+ it_behaves_like 'advance to next stage', :attachments
+ end
+ end
+ end
+end
diff --git a/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb
new file mode 100644
index 00000000000..827efb85a17
--- /dev/null
+++ b/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::JiraImport::Stage::ImportLabelsWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe 'modules' do
+ it_behaves_like 'include import workers modules'
+ end
+
+ describe '#perform' do
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: false)
+ end
+
+ it_behaves_like 'exit import not started'
+ end
+
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: true)
+ end
+
+ context 'when import did not start' do
+ let!(:import_state) { create(:import_state, project: project) }
+
+ it_behaves_like 'exit import not started'
+ end
+
+ context 'when import started' do
+ let!(:import_state) { create(:import_state, status: :started, project: project) }
+
+ it_behaves_like 'advance to next stage', :issues
+ end
+ end
+ end
+end
diff --git a/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb
new file mode 100644
index 00000000000..bd6b36613cc
--- /dev/null
+++ b/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::JiraImport::Stage::ImportNotesWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+
+ describe 'modules' do
+ it_behaves_like 'include import workers modules'
+ end
+
+ describe '#perform' do
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: false)
+ end
+
+ it_behaves_like 'exit import not started'
+ end
+
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: true)
+ end
+
+ context 'when import did not start' do
+ let!(:import_state) { create(:import_state, project: project) }
+
+ it_behaves_like 'exit import not started'
+ end
+
+ context 'when import started' do
+ let!(:import_state) { create(:import_state, status: :started, project: project) }
+
+ it_behaves_like 'advance to next stage', :finish
+ end
+ end
+ end
+end
diff --git a/spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb
new file mode 100644
index 00000000000..cc70277384d
--- /dev/null
+++ b/spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::JiraImport::Stage::StartImportWorker do
+ let(:project) { create(:project) }
+ let(:worker) { described_class.new }
+ let(:jid) { '12345678' }
+
+ describe 'modules' do
+ it_behaves_like 'include import workers modules'
+ end
+
+ describe '#perform' do
+ context 'when feature flag not enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: false)
+ end
+
+ it 'exits because import not allowed' do
+ expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).not_to receive(:perform_async)
+
+ worker.perform(project.id)
+ end
+ end
+
+ context 'when feature flag not enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: true)
+ end
+
+ context 'when import is not scheudled' do
+ let!(:import_state) { create(:import_state, project: project, status: :none, jid: jid) }
+
+ it 'exits because import not started' do
+ expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).not_to receive(:perform_async)
+
+ worker.perform(project.id)
+ end
+ end
+
+ context 'when import is scheduled' do
+ let!(:import_state) { create(:import_state, project: project, status: :scheduled, jid: jid) }
+
+ it 'advances to importing labels' do
+ expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).to receive(:perform_async)
+
+ worker.perform(project.id)
+ end
+ end
+
+ context 'when import is started' do
+ let!(:import_state) { create(:import_state, project: project, status: :started, jid: jid) }
+
+ context 'when this is the same worker that stated import' do
+ it 'advances to importing labels' do
+ allow(worker).to receive(:jid).and_return(jid)
+ expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).to receive(:perform_async)
+
+ worker.perform(project.id)
+ end
+ end
+
+ context 'when this is a different worker that stated import' do
+ it 'advances to importing labels' do
+ allow(worker).to receive(:jid).and_return('87654321')
+ expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).not_to receive(:perform_async)
+
+ worker.perform(project.id)
+ end
+ end
+ end
+
+ context 'when import is finished' do
+ let!(:import_state) { create(:import_state, project: project, status: :finished, jid: jid) }
+
+ it 'advances to importing labels' do
+ allow(worker).to receive(:jid).and_return(jid)
+ expect(Gitlab::JiraImport::Stage::ImportLabelsWorker).not_to receive(:perform_async)
+
+ worker.perform(project.id)
+ end
+ end
+ end
+ end
+end