summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/models/ci/build.rb5
-rw-r--r--app/models/ci/build_annotation.rb42
-rw-r--r--app/models/ci/job_artifact.rb7
-rw-r--r--app/services/ci/create_build_annotations_service.rb46
-rw-r--r--app/workers/build_finished_worker.rb1
-rw-r--r--app/workers/ci/create_build_annotations_worker.rb15
6 files changed, 114 insertions, 2 deletions
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 84010e40ef4..0c8a7cf6a94 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -755,6 +755,11 @@ module Ci
:creating
end
+ # @return [NilClass|Ci::JobArtifact]
+ def first_build_annotation_artifact
+ job_artifacts.build_annotation.first
+ end
+
private
def erase_old_artifacts!
diff --git a/app/models/ci/build_annotation.rb b/app/models/ci/build_annotation.rb
new file mode 100644
index 00000000000..f2ba9d0f529
--- /dev/null
+++ b/app/models/ci/build_annotation.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildAnnotation < ActiveRecord::Base
+ self.table_name = 'ci_build_annotations'
+
+ belongs_to :build, class_name: 'Ci::Build', foreign_key: :build_id
+
+ enum severity: {
+ info: 0,
+ warning: 1,
+ error: 2
+ }
+
+ # We deliberately validate just the presence of the ID, and not the target
+ # row. We do this for two reasons:
+ #
+ # 1. Foreign key checks already ensure the ID points to a valid row.
+ #
+ # 2. When parsing artifacts, we run validations for every row to make sure
+ # they are in the correct format. Validating an association would result
+ # in a database query being executed for every entry, slowing down the
+ # parsing process.
+ validates :build_id, presence: true
+
+ validates :severity, presence: true
+ validates :summary, presence: true, length: { maximum: 512 }
+
+ validates :line_number,
+ numericality: {
+ greater_than_or_equal_to: 1,
+ less_than_or_equal_to: 32767,
+ only_integer: true
+ },
+ allow_nil: true
+
+ # Only giving a file path or line number makes no sense, so if either is
+ # given we require both to be present.
+ validates :line_number, presence: true, if: :file_path?
+ validates :file_path, presence: true, if: :line_number?
+ end
+end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 789bb293811..fdb2db38349 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -21,7 +21,8 @@ module Ci
container_scanning: 'gl-container-scanning-report.json',
dast: 'gl-dast-report.json',
license_management: 'gl-license-management-report.json',
- performance: 'performance.json'
+ performance: 'performance.json',
+ build_annotation: 'gl-build-annotations.json'
}.freeze
TYPE_AND_FORMAT_PAIRS = {
@@ -29,6 +30,7 @@ module Ci
metadata: :gzip,
trace: :raw,
junit: :gzip,
+ build_annotation: :gzip,
# All these file formats use `raw` as we need to store them uncompressed
# for Frontend to fetch the files and do analysis
@@ -88,7 +90,8 @@ module Ci
dast: 8, ## EE-specific
codequality: 9, ## EE-specific
license_management: 10, ## EE-specific
- performance: 11 ## EE-specific
+ performance: 11, ## EE-specific
+ build_annotation: 12
}
enum file_format: {
diff --git a/app/services/ci/create_build_annotations_service.rb b/app/services/ci/create_build_annotations_service.rb
new file mode 100644
index 00000000000..665ba995bc0
--- /dev/null
+++ b/app/services/ci/create_build_annotations_service.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Ci
+ # Parses and stores the build annotations of a single CI build.
+ class CreateBuildAnnotationsService
+ attr_reader :build
+
+ # @param [Ci::Build] build
+ def initialize(build)
+ @build = build
+ end
+
+ def execute
+ artifact = build.first_build_annotation_artifact
+
+ return unless artifact
+
+ annotations = parse_annotations(artifact)
+
+ insert_annotations(annotations) if annotations.any?
+ end
+
+ # @param [Ci::JobArtifact] artifact
+ def parse_annotations(artifact)
+ Gitlab::Ci::Parsers.fabricate!(:build_annotation).parse!(artifact)
+ rescue Gitlab::Ci::Parsers::BuildAnnotation::ParserError => error
+ build_error_annotation(error.message)
+ end
+
+ # @param [Array<Hash>] rows
+ def insert_annotations(rows)
+ Gitlab::Database.bulk_insert(::Ci::BuildAnnotation.table_name, rows)
+ end
+
+ # @param [String] message
+ def build_error_annotation(message)
+ [
+ {
+ build_id: build.id,
+ severity: Ci::BuildAnnotation.severities[:error],
+ summary: message
+ }
+ ]
+ end
+ end
+end
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index ae853ec9316..b516d08999b 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -30,5 +30,6 @@ class BuildFinishedWorker
# We execute these async as these are independent operations.
BuildHooksWorker.perform_async(build.id)
ArchiveTraceWorker.perform_async(build.id)
+ Ci::CreateBuildAnnotationsService.perform_async(build.id)
end
end
diff --git a/app/workers/ci/create_build_annotations_worker.rb b/app/workers/ci/create_build_annotations_worker.rb
new file mode 100644
index 00000000000..2467b53a527
--- /dev/null
+++ b/app/workers/ci/create_build_annotations_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Ci
+ # Sidekiq worker for storing the build annotations produced by a CI build.
+ class CreateBuildAnnotationsWorker
+ include ApplicationWorker
+
+ # @param [Integer] build_id
+ def perform(build_id)
+ if (build = Ci::Build.find_by_id(build_id))
+ CreateBuildAnnotationsService.new(build).execute
+ end
+ end
+ end
+end