diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 5d316906bd3c7b21efd935664d51e8269478d69d..d78894078493aead17a7a7b3eeb42464966343fc 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1004,6 +1004,10 @@ def self_and_descendants
       object_hierarchy(project_condition: :same).base_and_descendants
     end
 
+    def self_and_descendants_complete?
+      self_and_descendants.all?(&:complete?)
+    end
+
     # Follow the parent-child relationships and return the top-level parent
     def root_ancestor
       return self unless child?
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index 2284a05bcc935a4bec17084a14e89cd49263d40a..cdc3d69f754318a95f72106438ada929d6a71b1b 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -51,6 +51,23 @@ def report_exists?(file_type)
       def find_by_file_type(file_type)
         find_by(file_type: file_type)
       end
+
+      def create_or_replace_for_pipeline!(pipeline:, file_type:, file:, size:)
+        transaction do
+          pipeline.pipeline_artifacts.find_by_file_type(file_type)&.destroy!
+
+          pipeline.pipeline_artifacts.create!(
+            file_type: file_type,
+            project_id: pipeline.project_id,
+            size: size,
+            file: file,
+            file_format: REPORT_TYPES[file_type],
+            expire_at: EXPIRATION_DATE.from_now
+          )
+        end
+      rescue ActiveRecord::ActiveRecordError => err
+        Gitlab::ErrorTracking.track_and_raise_exception(err, { pipeline_id: pipeline.id, file_type: file_type })
+      end
     end
 
     def present
diff --git a/app/services/ci/pipeline_artifacts/coverage_report_service.rb b/app/services/ci/pipeline_artifacts/coverage_report_service.rb
index b0acb1d5a0b98b65c328b0e7ac493c83a05455a9..c69cb721bb4238e0c2b4aec01730650bc3d7059c 100644
--- a/app/services/ci/pipeline_artifacts/coverage_report_service.rb
+++ b/app/services/ci/pipeline_artifacts/coverage_report_service.rb
@@ -9,17 +9,16 @@ def initialize(pipeline)
       end
 
       def execute
-        return if pipeline.has_coverage_reports?
+        unless Feature.enabled?(:ci_child_pipeline_coverage_reports, pipeline.project) ||
+          !pipeline.has_coverage_reports?
+          return
+        end
+
         return if report.empty?
 
-        pipeline.pipeline_artifacts.create!(
-          project_id: pipeline.project_id,
-          file_type: :code_coverage,
-          file_format: Ci::PipelineArtifact::REPORT_TYPES.fetch(:code_coverage),
-          size: carrierwave_file["tempfile"].size,
-          file: carrierwave_file,
-          expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now
-        )
+        Ci::PipelineArtifact.create_or_replace_for_pipeline!(**pipeline_artifact_params).tap do |pipeline_artifact|
+          Gitlab::AppLogger.info(log_params(pipeline_artifact))
+        end
       end
 
       private
@@ -32,6 +31,15 @@ def report
         end
       end
 
+      def pipeline_artifact_params
+        {
+          pipeline: pipeline,
+          file_type: :code_coverage,
+          file: carrierwave_file,
+          size: carrierwave_file['tempfile'].size
+        }
+      end
+
       def carrierwave_file
         strong_memoize(:carrier_wave_file) do
           CarrierWaveStringFile.new_file(
@@ -41,6 +49,15 @@ def carrierwave_file
           )
         end
       end
+
+      def log_params(pipeline_artifact)
+        {
+          project_id: pipeline.project_id,
+          pipeline_id: pipeline.id,
+          pipeline_artifact_id: pipeline_artifact.id,
+          message: "Created code coverage for pipeline."
+        }
+      end
     end
   end
 end
diff --git a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
index 8ee518e3ae6d6dfa0d47ed22a744a16c3bb3befa..5be40f64930f46ea91a0a6e090349f920523bfc1 100644
--- a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
+++ b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
@@ -15,7 +15,17 @@ class CoverageReportWorker
       idempotent!
 
       def perform(pipeline_id)
-        Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
+        pipeline = Ci::Pipeline.find_by_id(pipeline_id)
+
+        return unless pipeline
+
+        if Feature.enabled?(:ci_child_pipeline_coverage_reports, pipeline.project)
+          pipeline.root_ancestor.try do |root_ancestor_pipeline|
+            next unless root_ancestor_pipeline.self_and_descendants_complete?
+
+            Ci::PipelineArtifacts::CoverageReportService.new(root_ancestor_pipeline).execute
+          end
+        else
           Ci::PipelineArtifacts::CoverageReportService.new(pipeline).execute
         end
       end
diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb
index 801505f0231f4911708ae6c6df5a8bb2c3bd9f9a..b051f646bd4769cd766c97935e9d0302095f0f63 100644
--- a/spec/models/ci/pipeline_artifact_spec.rb
+++ b/spec/models/ci/pipeline_artifact_spec.rb
@@ -196,6 +196,80 @@
     end
   end
 
+  describe '.create_or_replace_for_pipeline!' do
+    let_it_be(:pipeline) { create(:ci_empty_pipeline) }
+
+    let(:file_type) { :code_coverage }
+    let(:file) { CarrierWaveStringFile.new_file(file_content: 'content', filename: 'file.json', content_type: 'json') }
+    let(:size) { file['tempfile'].size }
+
+    subject do
+      Ci::PipelineArtifact.create_or_replace_for_pipeline!(
+        pipeline: pipeline,
+        file_type: file_type,
+        file: file,
+        size: size
+      )
+    end
+
+    around do |example|
+      freeze_time { example.run }
+    end
+
+    context 'when there is no existing record' do
+      it 'creates a new pipeline artifact for the given parameters' do
+        expect { subject }.to change { Ci::PipelineArtifact.count }.from(0).to(1)
+
+        expect(subject.code_coverage?).to be(true)
+        expect(subject.pipeline).to eq(pipeline)
+        expect(subject.project_id).to eq(pipeline.project_id)
+        expect(subject.file.filename).to eq(file['filename'])
+        expect(subject.size).to eq(size)
+        expect(subject.file_format).to eq(Ci::PipelineArtifact::REPORT_TYPES[file_type].to_s)
+        expect(subject.expire_at).to eq(Ci::PipelineArtifact::EXPIRATION_DATE.from_now)
+      end
+    end
+
+    context 'when there are existing records with different types' do
+      let!(:existing_artifact) do
+        create(:ci_pipeline_artifact, pipeline: pipeline, file_type: file_type, expire_at: 1.day.from_now)
+      end
+
+      let!(:other_artifact) { create(:ci_pipeline_artifact, pipeline: pipeline, file_type: :code_quality_mr_diff) }
+
+      it 'replaces the existing pipeline artifact record with the given file type' do
+        expect { subject }.not_to change { Ci::PipelineArtifact.count }
+
+        expect(subject.id).not_to eq(existing_artifact.id)
+
+        expect(subject.code_coverage?).to be(true)
+        expect(subject.pipeline).to eq(pipeline)
+        expect(subject.project_id).to eq(pipeline.project_id)
+        expect(subject.file.filename).to eq(file['filename'])
+        expect(subject.size).to eq(size)
+        expect(subject.file_format).to eq(Ci::PipelineArtifact::REPORT_TYPES[file_type].to_s)
+        expect(subject.expire_at).to eq(Ci::PipelineArtifact::EXPIRATION_DATE.from_now)
+      end
+    end
+
+    context 'when ActiveRecordError is raised' do
+      let(:pipeline) { instance_double(Ci::Pipeline, id: 1) }
+      let(:file_type) { :code_coverage }
+      let(:error) { ActiveRecord::ActiveRecordError.new('something went wrong') }
+
+      before do
+        allow(pipeline).to receive(:pipeline_artifacts).and_raise(error)
+      end
+
+      it 'tracks and raise the exception' do
+        expect(Gitlab::ErrorTracking).to receive(:track_and_raise_exception)
+          .with(error, { pipeline_id: pipeline.id, file_type: file_type }).and_call_original
+
+        expect { subject }.to raise_error(ActiveRecord::ActiveRecordError, 'something went wrong')
+      end
+    end
+  end
+
   describe '#present' do
     subject(:presenter) { report.present }
 
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 31752f300f4bc6ccaad2c80a39831ca2375f50ce..b719adcdace2783aefba72d292792c1867e02481 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -106,6 +106,18 @@
     end
   end
 
+  describe 'state machine transitions' do
+    context 'from failed to success' do
+      let_it_be(:pipeline) { create(:ci_empty_pipeline, :failed) }
+
+      it 'schedules CoverageReportWorker' do
+        expect(Ci::PipelineArtifacts::CoverageReportWorker).to receive(:perform_async).with(pipeline.id)
+
+        pipeline.succeed!
+      end
+    end
+  end
+
   describe '#set_status' do
     let(:pipeline) { build(:ci_empty_pipeline, :created) }
 
@@ -3760,6 +3772,24 @@ def create_pipeline(status, ref, sha)
     end
   end
 
+  describe '#self_and_descendants_complete?' do
+    let_it_be(:pipeline) { create(:ci_pipeline, :success) }
+    let_it_be(:child_pipeline) { create(:ci_pipeline, :success, child_of: pipeline) }
+    let_it_be_with_reload(:grandchild_pipeline) { create(:ci_pipeline, :success, child_of: child_pipeline) }
+
+    context 'when all pipelines in the hierarchy is complete' do
+      it { expect(pipeline.self_and_descendants_complete?).to be(true) }
+    end
+
+    context 'when a pipeline in the hierarchy is not complete' do
+      before do
+        grandchild_pipeline.update!(status: :running)
+      end
+
+      it { expect(pipeline.self_and_descendants_complete?).to be(false) }
+    end
+  end
+
   describe '#builds_in_self_and_descendants' do
     subject(:builds) { pipeline.builds_in_self_and_descendants }
 
diff --git a/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb b/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb
index 403afde5da32c83261206d34fde45235dcf0aa97..4b85c52ebce8e316e9669222f712fcedb6394896 100644
--- a/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb
+++ b/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb
@@ -2,16 +2,18 @@
 
 require 'spec_helper'
 
-RSpec.describe ::Ci::PipelineArtifacts::CoverageReportService do
+RSpec.describe Ci::PipelineArtifacts::CoverageReportService do
   describe '#execute' do
     let_it_be(:project) { create(:project, :repository) }
 
     subject { described_class.new(pipeline).execute }
 
-    shared_examples 'creating a pipeline coverage report' do
+    shared_examples 'creating or updating a pipeline coverage report' do
       context 'when pipeline is finished' do
-        it 'creates a pipeline artifact' do
-          expect { subject }.to change { Ci::PipelineArtifact.count }.from(0).to(1)
+        it 'creates or updates a pipeline artifact' do
+          subject
+
+          expect(pipeline.reload.pipeline_artifacts.count).to eq(1)
         end
 
         it 'persists the default file name' do
@@ -22,7 +24,7 @@
           expect(file.filename).to eq('code_coverage.json')
         end
 
-        it 'sets expire_at to 1 week' do
+        it 'sets expire_at to 1 week from now' do
           freeze_time do
             subject
 
@@ -31,13 +33,16 @@
             expect(pipeline_artifact.expire_at).to eq(1.week.from_now)
           end
         end
-      end
 
-      context 'when pipeline artifact has already been created' do
-        it 'does not raise an error and does not persist the same artifact twice' do
-          expect { 2.times { described_class.new(pipeline).execute } }.not_to raise_error
+        it 'logs relevant information' do
+          expect(Gitlab::AppLogger).to receive(:info).with({
+                                                             project_id: project.id,
+                                                             pipeline_id: pipeline.id,
+                                                             pipeline_artifact_id: kind_of(Numeric),
+                                                             message: kind_of(String)
+                                                           })
 
-          expect(Ci::PipelineArtifact.count).to eq(1)
+          subject
         end
       end
     end
@@ -45,21 +50,42 @@
     context 'when pipeline has coverage report' do
       let!(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) }
 
-      it_behaves_like 'creating a pipeline coverage report'
+      it_behaves_like 'creating or updating a pipeline coverage report'
     end
 
     context 'when pipeline has coverage report from child pipeline' do
       let!(:pipeline) { create(:ci_pipeline, :success, project: project) }
       let!(:child_pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project, child_of: pipeline) }
 
-      it_behaves_like 'creating a pipeline coverage report'
+      it_behaves_like 'creating or updating a pipeline coverage report'
+    end
+
+    context 'when pipeline has existing pipeline artifact for coverage report' do
+      let!(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) }
+      let!(:child_pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project, child_of: pipeline) }
+
+      let!(:pipeline_artifact) do
+        create(:ci_pipeline_artifact, :with_coverage_report, pipeline: pipeline, expire_at: 1.day.from_now)
+      end
+
+      it_behaves_like 'creating or updating a pipeline coverage report'
+
+      context 'when the feature flag is disabled' do
+        before do
+          stub_feature_flags(ci_child_pipeline_coverage_reports: false)
+        end
+
+        it 'does not change existing pipeline artifact' do
+          expect { subject }.not_to change { pipeline_artifact.reload.updated_at }
+        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
-        expect { subject }.not_to change { Ci::PipelineArtifact.count }
+        expect { subject }.not_to change { Ci::PipelineArtifact.count }.from(0)
       end
     end
   end
diff --git a/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb b/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb
index 000eda055af2f6e65f443d77c8174e33611543b2..f51e66aa948a820ed3f1dd0dd9d9e91364b985e2 100644
--- a/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb
+++ b/spec/workers/ci/pipeline_artifacts/coverage_report_worker_spec.rb
@@ -2,28 +2,71 @@
 
 require 'spec_helper'
 
-RSpec.describe ::Ci::PipelineArtifacts::CoverageReportWorker do
+RSpec.describe Ci::PipelineArtifacts::CoverageReportWorker do
   describe '#perform' do
+    let(:pipeline_id) { pipeline.id }
+
     subject { described_class.new.perform(pipeline_id) }
 
     context 'when pipeline exists' do
-      let(:pipeline) { create(:ci_pipeline) }
-      let(:pipeline_id) { pipeline.id }
+      let(:pipeline) { create(:ci_pipeline, :success) }
 
-      it 'calls pipeline report result service' do
-        expect_next_instance_of(::Ci::PipelineArtifacts::CoverageReportService) do |create_artifact_service|
-          expect(create_artifact_service).to receive(:execute)
+      it 'calls the pipeline coverage report service' do
+        expect_next_instance_of(::Ci::PipelineArtifacts::CoverageReportService, pipeline) do |service|
+          expect(service).to receive(:execute)
         end
 
         subject
       end
     end
 
+    context 'when the pipeline is part of a hierarchy' do
+      let_it_be(:root_ancestor_pipeline) { create(:ci_pipeline, :success) }
+      let_it_be(:pipeline) { create(:ci_pipeline, :success, child_of: root_ancestor_pipeline) }
+      let_it_be(:another_child_pipeline) { create(:ci_pipeline, :success, child_of: root_ancestor_pipeline) }
+
+      context 'when all pipelines is complete' do
+        it 'calls the pipeline coverage report service on the root ancestor pipeline' do
+          expect_next_instance_of(::Ci::PipelineArtifacts::CoverageReportService, root_ancestor_pipeline) do |service|
+            expect(service).to receive(:execute)
+          end
+
+          subject
+        end
+      end
+
+      context 'when the pipeline hierarchy has incomplete pipeline' do
+        before do
+          another_child_pipeline.update!(status: :running)
+        end
+
+        it 'does not call pipeline coverage report service' do
+          expect(Ci::PipelineArtifacts::CoverageReportService).not_to receive(:new)
+
+          subject
+        end
+      end
+
+      context 'when feature flag is disabled' do
+        before do
+          stub_feature_flags(ci_child_pipeline_coverage_reports: false)
+        end
+
+        it 'calls the pipeline coverage report service on the pipeline' do
+          expect_next_instance_of(::Ci::PipelineArtifacts::CoverageReportService, pipeline) do |service|
+            expect(service).to receive(:execute)
+          end
+
+          subject
+        end
+      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::PipelineArtifacts::CoverageReportService).not_to receive(:execute)
+        expect(Ci::PipelineArtifacts::CoverageReportService).not_to receive(:new)
 
         subject
       end