Commit 08f22901 authored by Kamil Trzciński's avatar Kamil Trzciński

Implement Composite Status in Ruby

parent 710f5b3c
...@@ -390,20 +390,15 @@ module Ci ...@@ -390,20 +390,15 @@ module Ci
def legacy_stages def legacy_stages
# TODO, this needs refactoring, see gitlab-foss#26481. # TODO, this needs refactoring, see gitlab-foss#26481.
stages_query = statuses Gitlab::Ci::Status::GroupedStatuses
.group('stage').select(:stage).order('max(stage_idx)') .new(statuses.latest, :stage, :stage_idx)
.group(:stage, :stage_idx)
status_sql = statuses.latest.where('stage=sg.stage').status_sql .map do |stage|
Ci::LegacyStage.new(self,
warnings_sql = statuses.latest.select('COUNT(*)') name: stage[:stage],
.where('stage=sg.stage').failed_but_allowed.to_sql status: stage[:status],
warnings: stage[:warnings])
stages_with_statuses = CommitStatus.from(stages_query, :sg) end
.pluck('sg.stage', status_sql, "(#{warnings_sql})")
stages_with_statuses.map do |stage|
Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])
end
end end
def valid_commit_sha def valid_commit_sha
......
...@@ -29,14 +29,6 @@ class CommitStatus < ApplicationRecord ...@@ -29,14 +29,6 @@ class CommitStatus < ApplicationRecord
where(allow_failure: true, status: [:failed, :canceled]) where(allow_failure: true, status: [:failed, :canceled])
end end
scope :exclude_ignored, -> do
# We want to ignore failed but allowed to fail jobs.
#
# TODO, we also skip ignored optional manual actions.
where("allow_failure = ? OR status IN (?)",
false, all_state_names - [:failed, :canceled, :manual])
end
scope :latest, -> { where(retried: [false, nil]) } scope :latest, -> { where(retried: [false, nil]) }
scope :retried, -> { where(retried: true) } scope :retried, -> { where(retried: true) }
scope :ordered, -> { order(:name) } scope :ordered, -> { order(:name) }
......
...@@ -10,6 +10,7 @@ module HasStatus ...@@ -10,6 +10,7 @@ module HasStatus
ACTIVE_STATUSES = %w[preparing pending running].freeze ACTIVE_STATUSES = %w[preparing pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
ORDERED_STATUSES = %w[failed preparing pending running manual scheduled canceled success skipped created].freeze ORDERED_STATUSES = %w[failed preparing pending running manual scheduled canceled success skipped created].freeze
WARNING_STATUSES = %w[manual failed canceled].to_set.freeze
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3, STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7, failed: 4, canceled: 5, skipped: 6, manual: 7,
scheduled: 8, preparing: 9 }.freeze scheduled: 8, preparing: 9 }.freeze
...@@ -17,44 +18,10 @@ module HasStatus ...@@ -17,44 +18,10 @@ module HasStatus
UnknownStatusError = Class.new(StandardError) UnknownStatusError = Class.new(StandardError)
class_methods do class_methods do
def status_sql
scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all
scope_warnings = respond_to?(:failed_but_allowed) ? failed_but_allowed : none
builds = scope_relevant.select('count(*)').to_sql
created = scope_relevant.created.select('count(*)').to_sql
success = scope_relevant.success.select('count(*)').to_sql
manual = scope_relevant.manual.select('count(*)').to_sql
scheduled = scope_relevant.scheduled.select('count(*)').to_sql
preparing = scope_relevant.preparing.select('count(*)').to_sql
pending = scope_relevant.pending.select('count(*)').to_sql
running = scope_relevant.running.select('count(*)').to_sql
skipped = scope_relevant.skipped.select('count(*)').to_sql
canceled = scope_relevant.canceled.select('count(*)').to_sql
warnings = scope_warnings.select('count(*) > 0').to_sql.presence || 'false'
Arel.sql(
"(CASE
WHEN (#{builds})=(#{skipped}) AND (#{warnings}) THEN 'success'
WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success}) THEN 'success'
WHEN (#{builds})=(#{created}) THEN 'created'
WHEN (#{builds})=(#{preparing}) THEN 'preparing'
WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})>0 THEN 'running'
WHEN (#{manual})>0 THEN 'manual'
WHEN (#{scheduled})>0 THEN 'scheduled'
WHEN (#{preparing})>0 THEN 'preparing'
WHEN (#{created})>0 THEN 'running'
ELSE 'failed'
END)"
)
end
def status def status
all.pluck(status_sql).first Gitlab::Ci::Status::GroupedStatuses
.new(all)
.one&.dig(:status)
end end
def started_at def started_at
......
...@@ -33,9 +33,9 @@ module Ci ...@@ -33,9 +33,9 @@ module Ci
return unless HasStatus::COMPLETED_STATUSES.include?(current_status) return unless HasStatus::COMPLETED_STATUSES.include?(current_status)
created_processables_in_stage_without_needs(index).select do |build| created_processables_in_stage_without_needs(index).find_each.select do |build|
process_build(build, current_status) process_build(build, current_status)
end end.any?
end end
def process_builds_with_needs(trigger_build_ids) def process_builds_with_needs(trigger_build_ids)
...@@ -92,6 +92,10 @@ module Ci ...@@ -92,6 +92,10 @@ module Ci
def created_processables_in_stage_without_needs(index) def created_processables_in_stage_without_needs(index)
created_processables_without_needs created_processables_without_needs
.preload(:project)
.preload(:taggings)
.preload(:deployment)
.preload(:user)
.for_stage(index) .for_stage(index)
end end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Status
class CompositeStatus
def initialize(all_statuses)
@status_set = build_status_set(all_statuses)
end
def status
case
when only?(:skipped, :warning)
:success
when only?(:skipped)
:skipped
when only?(:success)
:skipped
when only?(:created)
:created
when only?(:preparing)
:preparing
when only?(:success, :skipped)
:success
when only?(:success, :skipped, :canceled)
:canceled
when only?(:created, :skipped, :pending)
:pending
when include?(:running, :pending)
:running
when include?(:manual)
:manual
when include?(:scheduled)
:scheduled
when include?(:preparing)
:preparing
when include?(:created)
:created
when include?
:failed
end
end
def warnings?
include?(:warning)
end
private
def include?(*names)
names.all? { |name| @status_set.include?(name) }
end
def only?(*names)
@status_set.size == names.size &&
names.all? { |name| @status_set.include?(name) }
end
def build_status_set(all_statuses)
status_set = Set.new
all_statuses.each do |status|
if status[:allow_failure] && HasStatus::WARNING_STATUSES.include?(status[:status])
status_set.add(:warning)
else
status_set.add(status[:status].to_sym)
end
end
status_set
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Status
class GroupedStatuses
def initialize(subject, *keys)
@subject = subject
@keys = keys
end
def one(**query)
validate_keys!(query.keys)
item_hash = find_one(data_hash, query)
status_for_key(query, item_hash) if item_hash
end
def group(*keys)
validate_keys!(keys)
grouped_statuses(data_hash, keys)
end
private
def validate_keys!(keys)
missing_keys = @keys - keys
if missing_keys.present?
raise ArgumentError, "the keys '#{missing_keys.join(',')} are not subset of #{@keys.join(',')}"
end
end
def data_hash
@data_hash ||= hash_from_relation(@subject, @keys)
end
def hash_from_relation(subject, keys)
columns = keys.dup
columns << :status
# we request allow_failure when
# we don't have column_names, or such column does exist
columns << :allow_failure if !subject.respond_to?(:column_names) || subject.column_names.include?('allow_failure')
subject
.pluck(*columns)
.map { |attrs| columns.zip(attrs).to_h }
end
def find_one(subject, query)
subject.select do |attrs|
query.all? do |key, value|
attrs[key] == value
end
end.presence
end
def grouped_statuses(subject, keys)
subject
.group_by { |attrs| attrs.slice(*keys) }
.map { |key, all_attrs| status_for_key(key, all_attrs) }
end
def status_for_key(key, all_attrs)
composite_status = Gitlab::Ci::Status::CompositeStatus.new(all_attrs)
key.merge(
status: composite_status.status.to_s,
warnings: composite_status.warnings?)
end
end
end
end
end
...@@ -270,29 +270,6 @@ describe CommitStatus do ...@@ -270,29 +270,6 @@ describe CommitStatus do
end end
end end
describe '.exclude_ignored' do
subject { described_class.exclude_ignored.order(:id) }
let(:statuses) do
[create_status(when: 'manual', status: 'skipped'),
create_status(when: 'manual', status: 'success'),
create_status(when: 'manual', status: 'failed'),
create_status(when: 'on_failure', status: 'skipped'),
create_status(when: 'on_failure', status: 'success'),
create_status(when: 'on_failure', status: 'failed'),
create_status(allow_failure: true, status: 'success'),
create_status(allow_failure: true, status: 'failed'),
create_status(allow_failure: false, status: 'success'),
create_status(allow_failure: false, status: 'failed'),
create_status(allow_failure: true, status: 'manual'),
create_status(allow_failure: false, status: 'manual')]
end
it 'returns statuses without what we want to ignore' do
is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9, 11))
end
end
describe '.failed_but_allowed' do describe '.failed_but_allowed' do
subject { described_class.failed_but_allowed.order(:id) } subject { described_class.failed_but_allowed.order(:id) }
......
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