Commit 201dd19f authored by Maxime Orefice's avatar Maxime Orefice

Add pipeline create artifact service

This new service will be used to persist generated
coverage report once a pipeline has been completed.
parent 968cf42f
...@@ -228,6 +228,12 @@ module Ci ...@@ -228,6 +228,12 @@ module Ci
end end
end end
after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
pipeline.run_after_commit do
::Ci::Pipelines::CreateArtifactWorker.perform_async(pipeline.id)
end
end
after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline| after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
next unless pipeline.bridge_triggered? next unless pipeline.bridge_triggered?
next unless pipeline.bridge_waiting? next unless pipeline.bridge_waiting?
...@@ -856,6 +862,10 @@ module Ci ...@@ -856,6 +862,10 @@ module Ci
complete? && latest_report_builds(reports_scope).exists? complete? && latest_report_builds(reports_scope).exists?
end end
def has_coverage_reports?
self.has_reports?(Ci::JobArtifact.coverage_reports)
end
def test_report_summary def test_report_summary
Gitlab::Ci::Reports::TestReportSummary.new(latest_builds_report_results) Gitlab::Ci::Reports::TestReportSummary.new(latest_builds_report_results)
end end
......
...@@ -14,6 +14,11 @@ module Ci ...@@ -14,6 +14,11 @@ module Ci
].freeze ].freeze
FILE_SIZE_LIMIT = 10.megabytes.freeze FILE_SIZE_LIMIT = 10.megabytes.freeze
EXPIRATION_DATE = 1.week.freeze
DEFAULT_FILE_NAMES = {
code_coverage: 'code_coverage.json'
}.freeze
belongs_to :project, class_name: "Project", inverse_of: :pipeline_artifacts belongs_to :project, class_name: "Project", inverse_of: :pipeline_artifacts
belongs_to :pipeline, class_name: "Ci::Pipeline", inverse_of: :pipeline_artifacts belongs_to :pipeline, class_name: "Ci::Pipeline", inverse_of: :pipeline_artifacts
...@@ -24,14 +29,13 @@ module Ci ...@@ -24,14 +29,13 @@ module Ci
validates :file_type, presence: true validates :file_type, presence: true
mount_file_store_uploader Ci::PipelineArtifactUploader mount_file_store_uploader Ci::PipelineArtifactUploader
before_save :set_size, if: :file_changed?
enum file_type: { enum file_type: {
code_coverage: 1 code_coverage: 1
} }
def set_size def self.has_code_coverage?
self.size = file.size where(file_type: :code_coverage).exists?
end end
end end
end end
# frozen_string_literal: true
module Ci
module Pipelines
class CreateArtifactService
def execute(pipeline)
return unless ::Gitlab::Ci::Features.coverage_report_view?(pipeline.project)
return unless pipeline.has_coverage_reports?
return if pipeline.pipeline_artifacts.has_code_coverage?
file = build_carrierwave_file(pipeline)
pipeline.pipeline_artifacts.create!(
project_id: pipeline.project_id,
file_type: :code_coverage,
file_format: :raw,
size: file["tempfile"].size,
file: file,
expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now
)
end
private
def build_carrierwave_file(pipeline)
CarrierWaveStringFile.new_file(
file_content: pipeline.coverage_reports.to_json,
filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_coverage),
content_type: 'application/json'
)
end
end
end
end
...@@ -899,6 +899,14 @@ ...@@ -899,6 +899,14 @@
:weight: 1 :weight: 1
:idempotent: true :idempotent: true
:tags: [] :tags: []
- :name: pipeline_background:ci_pipelines_create_artifact
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: pipeline_background:ci_ref_delete_unlock_artifacts - :name: pipeline_background:ci_ref_delete_unlock_artifacts
:feature_category: :continuous_integration :feature_category: :continuous_integration
:has_external_dependencies: :has_external_dependencies:
......
# frozen_string_literal: true
module Ci
module Pipelines
class CreateArtifactWorker
include ApplicationWorker
include PipelineBackgroundQueue
idempotent!
def perform(pipeline_id)
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
Ci::Pipelines::CreateArtifactService.new.execute(pipeline)
end
end
end
end
end
...@@ -4,4 +4,12 @@ class CarrierWaveStringFile < StringIO ...@@ -4,4 +4,12 @@ class CarrierWaveStringFile < StringIO
def original_filename def original_filename
"" ""
end end
def self.new_file(file_content:, filename:, content_type: "application/octet-stream")
{
"tempfile" => StringIO.new(file_content),
"filename" => filename,
"content_type" => content_type
}
end
end end
...@@ -83,6 +83,10 @@ module Gitlab ...@@ -83,6 +83,10 @@ module Gitlab
def self.project_transactionless_destroy?(project) def self.project_transactionless_destroy?(project)
Feature.enabled?(:project_transactionless_destroy, project, default_enabled: false) Feature.enabled?(:project_transactionless_destroy, project, default_enabled: false)
end end
def self.coverage_report_view?(project)
::Feature.enabled?(:coverage_report_view, project)
end
end end
end end
end end
......
...@@ -13,5 +13,16 @@ FactoryBot.define do ...@@ -13,5 +13,16 @@ FactoryBot.define do
artifact.file = fixture_file_upload( artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/pipeline_artifacts/code_coverage.json'), 'application/json') Rails.root.join('spec/fixtures/pipeline_artifacts/code_coverage.json'), 'application/json')
end end
trait :with_multibyte_characters do
size { { "utf8" => "✓" }.to_json.size }
after(:build) do |artifact, _evaluator|
artifact.file = CarrierWaveStringFile.new_file(
file_content: { "utf8" => "✓" }.to_json,
filename: 'filename',
content_type: 'application/json'
)
end
end
end end
end end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::PipelineArtifact, type: :model do RSpec.describe Ci::PipelineArtifact, type: :model do
let_it_be(:coverage_report) { create(:ci_pipeline_artifact) } let(:coverage_report) { create(:ci_pipeline_artifact) }
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:pipeline) } it { is_expected.to belong_to(:pipeline) }
...@@ -44,24 +44,6 @@ RSpec.describe Ci::PipelineArtifact, type: :model do ...@@ -44,24 +44,6 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end end
end end
describe '#set_size' do
subject { create(:ci_pipeline_artifact) }
context 'when file is being created' do
it 'sets the size' do
expect(subject.size).to eq(85)
end
end
context 'when file is being updated' do
it 'updates the size' do
subject.update!(file: fixture_file_upload('spec/fixtures/dk.png'))
expect(subject.size).to eq(1062)
end
end
end
describe 'file is being stored' do describe 'file is being stored' do
subject { create(:ci_pipeline_artifact) } subject { create(:ci_pipeline_artifact) }
...@@ -78,5 +60,31 @@ RSpec.describe Ci::PipelineArtifact, type: :model do ...@@ -78,5 +60,31 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
it_behaves_like 'mounted file in object store' it_behaves_like 'mounted file in object store'
end end
end end
context 'when file contains multi-byte characters' do
let(:coverage_report_multibyte) { create(:ci_pipeline_artifact, :with_multibyte_characters) }
it 'sets the size in bytesize' do
expect(coverage_report_multibyte.size).to eq(12)
end
end
end
describe '.has_code_coverage?' do
subject { Ci::PipelineArtifact.has_code_coverage? }
context 'when pipeline artifact has a code coverage' do
let!(:pipeline_artifact) { create(:ci_pipeline_artifact) }
it 'returns true' do
expect(subject).to be_truthy
end
end
context 'when pipeline artifact does not have a code coverage' do
it 'returns false' do
expect(subject).to be_falsey
end
end
end end
end end
...@@ -2948,6 +2948,38 @@ RSpec.describe Ci::Pipeline, :mailer do ...@@ -2948,6 +2948,38 @@ RSpec.describe Ci::Pipeline, :mailer do
end end
end end
describe '#has_coverage_reports?' do
subject { pipeline.has_coverage_reports? }
context 'when pipeline has builds with coverage reports' do
before do
create(:ci_build, :coverage_reports, pipeline: pipeline, project: project)
end
context 'when pipeline status is running' do
let(:pipeline) { create(:ci_pipeline, :running, project: project) }
it { expect(subject).to be_falsey }
end
context 'when pipeline status is success' do
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { expect(subject).to be_truthy }
end
end
context 'when pipeline does not have builds with coverage reports' do
before do
create(:ci_build, :artifacts, pipeline: pipeline, project: project)
end
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { expect(subject).to be_falsey }
end
end
describe '#test_report_summary' do describe '#test_report_summary' do
subject { pipeline.test_report_summary } subject { pipeline.test_report_summary }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Ci::Pipelines::CreateArtifactService do
describe '#execute' do
subject { described_class.new.execute(pipeline) }
context 'when pipeline has coverage reports' do
let(:pipeline) { create(:ci_pipeline, :with_coverage_reports) }
context 'when pipeline is finished' do
it 'creates a pipeline artifact' do
subject
expect(Ci::PipelineArtifact.count).to eq(1)
end
it 'persists the default file name' do
subject
file = Ci::PipelineArtifact.first.file
expect(file.filename).to eq('code_coverage.json')
end
it 'sets expire_at to 1 week' do
freeze_time do
subject
pipeline_artifact = Ci::PipelineArtifact.first
expect(pipeline_artifact.expire_at).to eq(1.week.from_now)
end
end
end
context 'when feature is disabled' do
it 'does not create a pipeline artifact' do
stub_feature_flags(coverage_report_view: false)
subject
expect(Ci::PipelineArtifact.count).to eq(0)
end
end
context 'when pipeline artifact has already been created' do
it 'do not raise an error and do not persist the same artifact twice' do
expect { 2.times { described_class.new.execute(pipeline) } }.not_to raise_error(ActiveRecord::RecordNotUnique)
expect(Ci::PipelineArtifact.count).to eq(1)
end
end
end
context 'when pipeline is running and coverage report does not exist' do
let(:pipeline) { create(:ci_pipeline, :running) }
it 'does not persist data' do
subject
expect(Ci::PipelineArtifact.count).to eq(0)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Ci::Pipelines::CreateArtifactWorker do
describe '#perform' do
subject { described_class.new.perform(pipeline_id) }
context 'when pipeline exists' do
let(:pipeline) { create(:ci_pipeline) }
let(:pipeline_id) { pipeline.id }
it 'calls pipeline report result service' do
expect_next_instance_of(::Ci::Pipelines::CreateArtifactService) do |create_artifact_service|
expect(create_artifact_service).to receive(:execute)
end
subject
end
end
context 'when pipeline does not exist' do
let(:pipeline_id) { non_existing_record_id }
it 'does not call pipeline create artifact service' do
expect(Ci::Pipelines::CreateArtifactService).not_to receive(:execute)
subject
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment