diff --git a/app/workers/import/jira_create_labels_job.rb b/app/workers/import/jira_create_labels_job.rb
new file mode 100644
index 000000000000..8057fe2b9daf
--- /dev/null
+++ b/app/workers/import/jira_create_labels_job.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+module Import
+ class JiraCreateLabelsJob < ApplicationJob
+ include Import::JiraOpenProjectReferenceCreation
+
+ DISTINCT_LABEL_NAMES = Arel.sql("DISTINCT jsonb_array_elements_text(payload -> 'fields' -> 'labels')")
+
+ def text
+ I18n.t(:"admin.jira.run.jobs.#{self.class.to_s.demodulize}.title")
+ end
+
+ def perform(jira_import_id)
+ @jira_import = Import::JiraImport.find(jira_import_id)
+ @system_user = User.system
+
+ jira_label_names.each { create_label(it) }
+ end
+
+ private
+
+ def jira_label_names
+ Import::JiraIssue
+ .where(jira_import_id: @jira_import.id)
+ .where("jsonb_typeof(payload -> 'fields' -> 'labels') = 'array'")
+ .pluck(DISTINCT_LABEL_NAMES)
+ .filter_map { it.to_s.squish.presence }
+ .uniq(&:downcase)
+ end
+
+ def create_label(name)
+ service_call = Labels::CreateService.new(user: @system_user).call(name:)
+ return reference_existing_label(name) if service_call.errors.symbols_for(:name).include?(:taken)
+ raise service_call.message if service_call.failure?
+
+ create_reference!(op_leg: service_call.result, jira_leg: nil, jira_import: @jira_import, uses_existing: false)
+ end
+
+ def reference_existing_label(name)
+ label = Label.named(name).first!
+ return if referenced?(label)
+
+ create_reference!(op_leg: label, jira_leg: nil, jira_import: @jira_import, uses_existing: true)
+ end
+
+ def referenced?(label)
+ Import::JiraOpenProjectReference.exists?(jira_import_id: @jira_import.id,
+ op_entity_class: label.class.to_s,
+ op_entity_id: label.id)
+ end
+ end
+end
diff --git a/app/workers/import/jira_create_project_work_packages_job.rb b/app/workers/import/jira_create_project_work_packages_job.rb
index 744f24c0cf9e..8a7f5ed2a5e5 100644
--- a/app/workers/import/jira_create_project_work_packages_job.rb
+++ b/app/workers/import/jira_create_project_work_packages_job.rb
@@ -276,6 +276,28 @@ def resolution_date_cf_attrs(jira_issue)
{ @resolution_date_cf.attribute_getter => Time.zone.parse(raw).iso8601 }
end
+ def label_attrs(jira_issue)
+ names = Array(jira_issue.payload.dig("fields", "labels")).filter_map { it.to_s.squish.presence }
+ issue_labels = names.index_with { find_label(it) }
+ log_missing_labels(issue_labels)
+
+ label_ids = issue_labels.values.compact.map(&:id)
+ label_ids.any? ? { label_ids: } : {}
+ end
+
+ def log_missing_labels(issue_labels)
+ missing = issue_labels.filter_map { |name, label| name if label.nil? }
+ return if missing.empty?
+
+ Rails.logger.warn("Skipping jira labels without an OpenProject label: #{missing.join(', ')}")
+ end
+
+ def find_label(name) = labels_by_name[name.downcase]
+
+ def labels_by_name
+ @labels_by_name ||= Hash.new { |cache, name| cache[name] = Label.named(name).first }
+ end
+
def update_workflows(type)
Rails.logger.tagged("jira_object_type:issueType", "jira_object_id_or_name:#{type.name}") do
statuses = Status.all
@@ -347,6 +369,7 @@ def create_work_package(jira_issue, project, type, status, priority, custom_fiel
estimated_hours: (original_estimate_seconds / 3600.0 if original_estimate_seconds),
remaining_hours: (remaining_estimate_seconds / 3600.0 if remaining_estimate_seconds),
skip_semantic_id_allocation: true,
+ **label_attrs(jira_issue),
**custom_field_attrs
)
unless service_call.success?
diff --git a/app/workers/import/jira_revert_import_job.rb b/app/workers/import/jira_revert_import_job.rb
index a4aaebba1184..225ce04c0600 100644
--- a/app/workers/import/jira_revert_import_job.rb
+++ b/app/workers/import/jira_revert_import_job.rb
@@ -38,6 +38,7 @@ class JiraRevertImportJob < ApplicationJob
delete_groups
delete_project_roles
delete_custom_fields
+ delete_labels
delete_references
delete_jira_objects].freeze
@@ -242,6 +243,22 @@ def delete_custom_fields
end
end
+ def delete_labels
+ Import::JiraOpenProjectReference
+ .where(jira_import_id: @jira_import.id, uses_existing: false)
+ .where(op_entity_class: "Label")
+ .find_each do |ref|
+ op_leg = ref.op_leg
+ # label is used by something outside this import run, keep it
+ next if op_leg.labelings.exists?
+
+ service_call = ::Labels::DeleteService.new(user: @user, model: op_leg).call
+ raise service_call.message if service_call.failure?
+ rescue Import::JiraOpenProjectReference::LegNotFoundError
+ next
+ end
+ end
+
def delete_references
Import::JiraOpenProjectReference.where(jira_import_id: @jira_import.id).delete_all
end
diff --git a/app/workers/import/jira_staged_import_job.rb b/app/workers/import/jira_staged_import_job.rb
index bd381c94ee17..82b95ca16a37 100644
--- a/app/workers/import/jira_staged_import_job.rb
+++ b/app/workers/import/jira_staged_import_job.rb
@@ -81,6 +81,7 @@ def perform_stage(batch, jira_import)
batch.enqueue(stage: 5) do
Import::JiraCreateProjectRoleJob.set(good_job_labels: ["stage_5"]).perform_later(jira_import.id)
Import::JiraCreateCustomFieldsJob.set(good_job_labels: ["stage_5"]).perform_later(jira_import.id)
+ Import::JiraCreateLabelsJob.set(good_job_labels: ["stage_5"]).perform_later(jira_import.id)
end
elsif batch.properties[:stage] == 5
Rails.logger.info "Starting stage 6"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 0fbb57997d02..0a942d20e828 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1683,6 +1683,8 @@ en:
jobs:
JiraCreateCustomFieldsJob:
title: "Create custom fields"
+ JiraCreateLabelsJob:
+ title: "Create labels"
JiraCreateProjectJob:
title: "Create project '%{jira_project_name}'"
JiraCreateProjectRoleJob:
diff --git a/docs/installation-and-operations/jira-migration/field-mapping/README.md b/docs/installation-and-operations/jira-migration/field-mapping/README.md
index f694e765bea0..b69d2f448aee 100644
--- a/docs/installation-and-operations/jira-migration/field-mapping/README.md
+++ b/docs/installation-and-operations/jira-migration/field-mapping/README.md
@@ -38,6 +38,7 @@ Every migrated project is created **private** and **active**, with no parent —
| Issue key (e.g. `PROJECT-123`) | Work package identifier | Old/renamed issue keys also keep working afterward, the same way as project keys |
| Issue links (blocks, relates to, etc.) | **Not migrated** | — (planned: [JIM-76](https://community.openproject.org/projects/JIM/work_packages/JIM-76)) |
| Issue type | Type | Matched by name (not case-sensitive); a new Type is created automatically if nothing matches, and it's attached to the right projects (mirroring Jira's per-project Issue Type Scheme). Only the name comes across. No migrated type is marked as the project's default, even if it was the default in Jira. |
+| Labels | Labels | Matched by name (not case-sensitive); created automatically if missing. |
| Logged work / time entries | **Not migrated** | - (planned: [JIM-93](https://community.openproject.org/projects/JIM/work_packages/JIM-93)) |
| Original time estimate | Work | - |
| Priority | Priority | Matched by name (not case-sensitive); created automatically if missing. If an issue has no priority in Jira, OpenProject's default priority is used. Only the name comes across — Jira's severity order, colors, and icons aren't preserved, and this matters beyond appearance: OpenProject falls back to whichever priority ends up first in the list when an issue has none set |
diff --git a/docs/installation-and-operations/jira-migration/pre-migration-checklist/README.md b/docs/installation-and-operations/jira-migration/pre-migration-checklist/README.md
index 21c60451a844..71d8923a9abb 100644
--- a/docs/installation-and-operations/jira-migration/pre-migration-checklist/README.md
+++ b/docs/installation-and-operations/jira-migration/pre-migration-checklist/README.md
@@ -31,7 +31,6 @@ Review this against how your teams actually use Jira. If anything here would be
- [ ] Sub-tasks and parent/child hierarchy (planned: [JIM-75](https://community.openproject.org/projects/JIM/work_packages/JIM-75))
- [ ] Issue links (blocks, relates to, duplicates, etc.) (planned: [JIM-76](https://community.openproject.org/projects/JIM/work_packages/JIM-76))
- [ ] Web links (workaround planned: [JIM-206](https://community.openproject.org/projects/JIM/work_packages/JIM-206)).
-- [ ] Labels (planned: [JIM-109](https://community.openproject.org/projects/JIM/work_packages/JIM-109))
- [ ] Logged work / time entries (only the *estimated* and *remaining* hours fields come across — actual time logged does not) (planned: [JIM-93](https://community.openproject.org/projects/JIM/work_packages/JIM-93))
- [ ] Votes (workaround planned: [JIM-207](https://community.openproject.org/projects/JIM/work_packages/JIM-207), open feature request: [OP-3251](https://community.openproject.org/projects/OP/work_packages/OP-3251))
- [ ] Environment
diff --git a/spec/support/shared/with_jira_project_import_data.rb b/spec/support/shared/with_jira_project_import_data.rb
index 09423c605726..b7fef336f525 100644
--- a/spec/support/shared/with_jira_project_import_data.rb
+++ b/spec/support/shared/with_jira_project_import_data.rb
@@ -60,6 +60,10 @@ def create_project_role
Import::JiraCreateProjectRoleJob.perform_now(jira_import.id)
end
+ def create_labels
+ Import::JiraCreateLabelsJob.perform_now(jira_import.id)
+ end
+
def create_project
Import::JiraCreateProjectJob.perform_now(jira_import.id, jira_project.id)
end
@@ -75,6 +79,7 @@ def create_work_package_attachments
# Runs every per-project stage of the import in the order Import::JiraStagedImportJob enqueues them.
def import_project
create_project_role
+ create_labels
create_project
create_work_packages
create_work_package_attachments
diff --git a/spec/workers/import/jira_create_labels_job_spec.rb b/spec/workers/import/jira_create_labels_job_spec.rb
new file mode 100644
index 000000000000..0a916f2ed8a0
--- /dev/null
+++ b/spec/workers/import/jira_create_labels_job_spec.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+
+require "spec_helper"
+
+RSpec.describe Import::JiraCreateLabelsJob do
+ include_context "with a jira demo issue"
+
+ def references
+ Import::JiraOpenProjectReference.where(jira_import:, op_entity_class: "Label")
+ end
+
+ describe "#perform" do
+ let(:jira_issue_payload) do
+ super().tap { |payload| payload["fields"]["labels"] = %w[backend urgent] }
+ end
+
+ it "creates a label per distinct jira label of the import" do
+ expect { create_labels }.to change(Label, :count).by(2)
+
+ expect(Label.pluck(:name)).to contain_exactly("backend", "urgent")
+ end
+
+ it "authors the labels as the system user" do
+ create_labels
+
+ expect(Label.distinct.pluck(:author_id)).to eq([User.system.id])
+ end
+
+ it "references the created labels so the revert can remove them" do
+ create_labels
+
+ expect(references.pluck(:uses_existing)).to eq([false, false])
+ expect(references.map { |ref| ref.op_leg.name }).to contain_exactly("backend", "urgent")
+ end
+
+ it "creates nothing new when the import is run again" do
+ create_labels
+
+ expect { create_labels }.to not_change(Label, :count).and not_change(references, :count)
+ end
+
+ it "keeps its own labels revertable when a half-finished run is retried" do
+ create_labels
+ create_labels
+
+ expect(references.pluck(:uses_existing)).to eq([false, false])
+ end
+
+ context "when several issues share a label" do
+ let!(:jira_issue2) do
+ payload = jira_issue_payload.deep_dup.tap do |p|
+ p["id"] = "10406"
+ p["key"] = "DPPP-7"
+ p["fields"]["labels"] = %w[backend Backend]
+ end
+ create(:jira_issue, jira_import:, origin_id: "10406", jira_project:, payload:)
+ end
+
+ it "creates it once, ignoring case" do
+ expect { create_labels }.to change(Label, :count).by(2)
+ end
+ end
+
+ context "when a label with the same name already exists in a different case" do
+ let!(:existing_label) { create(:label, name: "Backend") }
+
+ it "reuses it instead of creating a second one" do
+ expect { create_labels }.to change(Label, :count).by(1)
+
+ expect(Label.named("backend").first).to eq(existing_label)
+ end
+
+ it "references it as pre-existing, so the revert leaves it alone" do
+ create_labels
+
+ expect(references.where(uses_existing: true).map(&:op_leg)).to eq([existing_label])
+ expect(references.where(uses_existing: false).map { |ref| ref.op_leg.name }).to eq(["urgent"])
+ end
+
+ it "still references it as pre-existing when a half-finished run is retried" do
+ create_labels
+ create_labels
+
+ expect(references.where(uses_existing: true).map(&:op_leg)).to eq([existing_label])
+ end
+ end
+
+ context "when the issues of two projects share a label" do
+ let!(:other_jira_project) { create(:jira_project, jira_import:, payload: jira_project_payload) }
+
+ let!(:other_project_issue) do
+ payload = jira_issue_payload.deep_dup.tap { |p| p["fields"]["labels"] = %w[backend] }
+ create(:jira_issue, jira_import:, jira_project: other_jira_project, payload:)
+ end
+
+ it "creates the shared label once" do
+ expect { create_labels }.to change(Label, :count).by(2)
+
+ expect(Label.named("backend").count).to eq(1)
+ end
+ end
+
+ context "when the issues of another import carry labels" do
+ let(:other_jira_import) { create(:jira_import, jira:, author:) }
+
+ let!(:other_jira_issue) do
+ payload = jira_issue_payload.deep_dup.tap { |p| p["fields"]["labels"] = %w[elsewhere] }
+ create(:jira_issue, jira_import: other_jira_import, origin_id: "10407", jira_project:, payload:)
+ end
+
+ it "ignores them" do
+ create_labels
+
+ expect(Label.named("elsewhere")).to be_empty
+ end
+ end
+
+ context "when no issue carries a label" do
+ let(:jira_issue_payload) { super().tap { |payload| payload["fields"]["labels"] = [] } }
+
+ it "creates nothing" do
+ expect { create_labels }.not_to change(Label, :count)
+ end
+ end
+
+ context "when the labels field is hidden in the jira field configuration" do
+ let(:jira_issue_payload) { super().tap { |payload| payload["fields"].delete("labels") } }
+
+ it "creates nothing" do
+ expect { create_labels }.not_to change(Label, :count)
+ end
+ end
+
+ context "when label names are padded, blank or null" do
+ let(:jira_issue_payload) do
+ super().tap { |payload| payload["fields"]["labels"] = [" backend ", " ", nil] }
+ end
+
+ it "squishes them and skips the ones left empty" do
+ expect { create_labels }.to change(Label, :count).by(1)
+
+ expect(Label.pluck(:name)).to eq(["backend"])
+ end
+ end
+ end
+end
diff --git a/spec/workers/import/jira_create_project_work_packages_job_spec.rb b/spec/workers/import/jira_create_project_work_packages_job_spec.rb
index d207c89f26ce..a653864f5e8f 100644
--- a/spec/workers/import/jira_create_project_work_packages_job_spec.rb
+++ b/spec/workers/import/jira_create_project_work_packages_job_spec.rb
@@ -406,6 +406,90 @@ def stage_5_resolution_date_cf
end
end
+ context "with labels" do
+ let(:jira_issue_payload) do
+ super().tap { |payload| payload["fields"]["labels"] = %w[backend urgent] }
+ end
+
+ it "attaches the jira labels to the work package" do
+ create_labels
+ create_work_packages
+
+ expect(WorkPackage.find("DPPP-6").labels.pluck(:name)).to contain_exactly("backend", "urgent")
+ end
+
+ it "records the labels in the work package journal" do
+ create_labels
+ create_work_packages
+
+ work_package = WorkPackage.find("DPPP-6")
+ expect(work_package.journals.last.label_journals.pluck(:label_id)).to match_array(work_package.label_ids)
+ end
+
+ context "when a label with the same name already exists in a different case" do
+ let!(:existing_label) { create(:label, name: "Backend") }
+
+ it "attaches the existing label instead of a second one" do
+ create_labels
+ create_work_packages
+
+ expect(WorkPackage.find("DPPP-6").labels).to include(existing_label)
+ expect(Label.named("backend").count).to eq(1)
+ end
+ end
+
+ context "when a second issue carries the same label" do
+ let!(:jira_issue2) do
+ payload = jira_issue_payload.deep_dup
+ payload["id"] = "10406"
+ payload["key"] = "DPPP-7"
+ payload["fields"]["labels"] = %w[backend]
+ create(:jira_issue, jira_import:, origin_id: "10406", jira_project:, payload:)
+ end
+
+ it "attaches the one label to both work packages" do
+ create_labels
+ create_work_packages
+
+ expect(WorkPackage.find("DPPP-7").labels.pluck(:name)).to eq(["backend"])
+ expect(WorkPackage.find("DPPP-6").labels).to include(Label.named("backend").first)
+ end
+ end
+
+ context "when the issue has no labels" do
+ let(:jira_issue_payload) { super().tap { |payload| payload["fields"]["labels"] = [] } }
+
+ it "leaves the work package without labels" do
+ create_labels
+ create_work_packages
+
+ expect(WorkPackage.find("DPPP-6").labels).to be_empty
+ end
+ end
+
+ context "when the labels field is hidden in the jira field configuration" do
+ let(:jira_issue_payload) { super().tap { |payload| payload["fields"].delete("labels") } }
+
+ it "imports the work package without labels" do
+ create_labels
+ create_work_packages
+
+ expect(WorkPackage.find("DPPP-6").labels).to be_empty
+ end
+ end
+
+ context "when the labels stage did not run" do
+ it "imports the work package without labels and logs them" do
+ allow(Rails.logger).to receive(:warn)
+
+ create_work_packages
+
+ expect(WorkPackage.find("DPPP-6").labels).to be_empty
+ expect(Rails.logger).to have_received(:warn).with(/backend, urgent/)
+ end
+ end
+ end
+
context "with resolution fields" do
context "when an issue's resolution is set" do
let(:jira_issue_payload) do
diff --git a/spec/workers/import/jira_revert_import_job_spec.rb b/spec/workers/import/jira_revert_import_job_spec.rb
index 34cbfdd50902..6cecc9b8f711 100644
--- a/spec/workers/import/jira_revert_import_job_spec.rb
+++ b/spec/workers/import/jira_revert_import_job_spec.rb
@@ -258,6 +258,47 @@ def transition_to_reverting
end
end
+ describe "#delete_labels" do
+ let(:label) { create(:label) }
+ let!(:reference) { reference_for(label, "Label") }
+
+ it "deletes the referenced label" do
+ expect { job.send(:delete_labels) }.to change(Label, :count).by(-1)
+ end
+
+ context "when the label is already gone" do
+ before { label.destroy }
+
+ it "skips the dangling reference" do
+ expect { job.send(:delete_labels) }.not_to raise_error
+ end
+ end
+
+ context "when the label existed before the import" do
+ let(:pre_existing_label) { create(:label) }
+
+ before { reference_for(pre_existing_label, "Label", uses_existing: true) }
+
+ it "keeps it" do
+ job.send(:delete_labels)
+
+ expect(Label).to exist(pre_existing_label.id)
+ end
+ end
+
+ context "when the label is still attached to a work package outside the import" do
+ let(:work_package) { create(:work_package) }
+
+ before { create(:labeling, label:, labelable: work_package) }
+
+ it "keeps it" do
+ job.send(:delete_labels)
+
+ expect(Label).to exist(label.id)
+ end
+ end
+ end
+
describe "#delete_references" do
let(:other_import) { create(:jira_import, jira:, author:) }
@@ -341,15 +382,15 @@ def transition_to_reverting
describe "#progress" do
it "is 0 while no step has been completed" do
- expect(job.progress).to eq({ current: 0, percentage: 0, total: 8 })
+ expect(job.progress).to eq({ current: 0, percentage: 0, total: 9 })
end
it "grows with every completed step" do
jira_import.set_job_cursor(job, :delete_projects)
- expect(job.progress).to eq({ current: 1, percentage: 12.5, total: 8 })
+ expect(job.progress).to eq({ current: 1, percentage: 11.11, total: 9 })
jira_import.set_job_cursor(job, :delete_jira_objects)
- expect(job.progress).to eq({ current: 8, percentage: 100, total: 8 })
+ expect(job.progress).to eq({ current: 9, percentage: 100, total: 9 })
end
end
diff --git a/spec/workers/import/jira_staged_import_job_spec.rb b/spec/workers/import/jira_staged_import_job_spec.rb
index 06eb94de31e6..d57a0af0ef17 100644
--- a/spec/workers/import/jira_staged_import_job_spec.rb
+++ b/spec/workers/import/jira_staged_import_job_spec.rb
@@ -82,6 +82,7 @@ def transition_to_importing
expect(GoodJob::Job.where(job_class: "Import::JiraCreateProjectRoleJob").count).to eq(0)
expect(GoodJob::Job.where(job_class: "Import::JiraCreateCustomFieldsJob").count).to eq(0)
+ expect(GoodJob::Job.where(job_class: "Import::JiraCreateLabelsJob").count).to eq(0)
end
it "transitions the import to import_error" do
@@ -216,11 +217,12 @@ def transition_to_importing
describe "stage 4" do
let(:stage) { 4 }
- it "enqueues the project role and custom fields creation jobs" do
+ it "enqueues the project role, custom fields and labels creation jobs" do
run_callback
expect(GoodJob::Job.where(job_class: "Import::JiraCreateProjectRoleJob").count).to eq(1)
expect(GoodJob::Job.where(job_class: "Import::JiraCreateCustomFieldsJob").count).to eq(1)
+ expect(GoodJob::Job.where(job_class: "Import::JiraCreateLabelsJob").count).to eq(1)
end
it "labels enqueued jobs with stage_5" do
@@ -228,6 +230,7 @@ def transition_to_importing
expect(GoodJob::Job.where(job_class: "Import::JiraCreateProjectRoleJob").last.labels).to include("stage_5")
expect(GoodJob::Job.where(job_class: "Import::JiraCreateCustomFieldsJob").last.labels).to include("stage_5")
+ expect(GoodJob::Job.where(job_class: "Import::JiraCreateLabelsJob").last.labels).to include("stage_5")
end
end