diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/models/ci/build.rb | 5 | ||||
-rw-r--r-- | app/models/ci/build_annotation.rb | 42 | ||||
-rw-r--r-- | app/models/ci/job_artifact.rb | 7 | ||||
-rw-r--r-- | app/services/ci/create_build_annotations_service.rb | 46 | ||||
-rw-r--r-- | app/workers/build_finished_worker.rb | 1 | ||||
-rw-r--r-- | app/workers/ci/create_build_annotations_worker.rb | 15 |
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 |