diff --git a/app/finders/ci/jobs_finder.rb b/app/finders/ci/jobs_finder.rb
index b35637d0e4f16b96c66d10bdfd1c340baba3b5ef..efacd8143bc88af543c392851ca716abe1d528cc 100644
--- a/app/finders/ci/jobs_finder.rb
+++ b/app/finders/ci/jobs_finder.rb
@@ -16,9 +16,7 @@ def initialize(current_user:, pipeline: nil, project: nil, runner: nil, params:
def execute
builds = init_collection.order_id_desc
- builds = filter_by_with_artifacts(builds)
- builds = filter_by_runner_types(builds)
- filter_by_scope(builds)
+ filter_builds(builds)
rescue Gitlab::Access::AccessDeniedError
type.none
end
@@ -59,6 +57,13 @@ def pipeline_jobs
params[:include_retried] ? jobs_scope : jobs_scope.latest
end
+ # Overriden in EE
+ def filter_builds(builds)
+ builds = filter_by_with_artifacts(builds)
+ builds = filter_by_runner_types(builds)
+ filter_by_scope(builds)
+ end
+
def filter_by_scope(builds)
return filter_by_statuses!(builds) if params[:scope].is_a?(Array)
@@ -80,16 +85,15 @@ def filter_by_runner_types(builds)
builds.with_runner_type(params[:runner_type])
end
+ # Overriden in EE
def use_runner_type_filter?
params[:runner_type].present? && Feature.enabled?(:admin_jobs_filter_runner_type, project, type: :ops)
end
def filter_by_with_artifacts(builds)
- if params[:with_artifacts]
- builds.with_any_artifacts
- else
- builds
- end
+ return builds.with_any_artifacts if params[:with_artifacts]
+
+ builds
end
def filter_by_statuses!(builds)
@@ -111,3 +115,5 @@ def jobs_by_type(relation, type)
end
end
end
+
+Ci::JobsFinder.prepend_mod
diff --git a/app/graphql/resolvers/ci/all_jobs_resolver.rb b/app/graphql/resolvers/ci/all_jobs_resolver.rb
index 85b0f8da8772e660e028bf4ef5e3ebdd8313ecf8..3012a7defa63f94065d495d74ba2b0610655a9b9 100644
--- a/app/graphql/resolvers/ci/all_jobs_resolver.rb
+++ b/app/graphql/resolvers/ci/all_jobs_resolver.rb
@@ -17,15 +17,18 @@ class AllJobsResolver < BaseResolver
description: 'Filter jobs by runner type if ' \
'feature flag `:admin_jobs_filter_runner_type` is enabled.'
- def resolve_with_lookahead(statuses: nil, runner_types: nil)
- jobs = ::Ci::JobsFinder.new(current_user: current_user,
-params: { scope: statuses, runner_type: runner_types }).execute
+ def resolve_with_lookahead(**args)
+ jobs = ::Ci::JobsFinder.new(current_user: current_user, params: params_data(args)).execute
apply_lookahead(jobs)
end
private
+ def params_data(args)
+ { scope: args[:statuses], runner_type: args[:runner_types] }
+ end
+
def preloads
{
previous_stage_jobs_or_needs: [:needs, :pipeline],
@@ -55,3 +58,5 @@ def nested_preloads
end
end
end
+
+Resolvers::Ci::AllJobsResolver.prepend_mod
diff --git a/app/graphql/types/ci/job_failure_reason_enum.rb b/app/graphql/types/ci/job_failure_reason_enum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3b9c13536d68db26fc952ee733f6edd57bd273c7
--- /dev/null
+++ b/app/graphql/types/ci/job_failure_reason_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class JobFailureReasonEnum < BaseEnum
+ graphql_name 'CiJobFailureReason'
+
+ ::Enums::Ci::CommitStatus.failure_reasons.each_key do |reason|
+ value reason.to_s.upcase,
+ description: "A job that failed due to #{reason.to_s.tr('_', ' ')}.",
+ value: reason
+ end
+ end
+ end
+end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index fe88e2e4129ca13184960a7d13ee993ed695da0d..2f88921757505207dec74c0503698cf70cfe24bd 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -462,6 +462,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| `failureReason` **{warning-solid}** | [`CiJobFailureReason`](#cijobfailurereason) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Filter jobs by failure reason. Currently only `RUNNER_SYSTEM_FAILURE` together with `runnerTypes: INSTANCE_TYPE` is supported. |
| `runnerTypes` **{warning-solid}** | [`[CiRunnerType!]`](#cirunnertype) | **Introduced** in 16.4. This feature is an Experiment. It can be changed or removed at any time. Filter jobs by runner type if feature flag `:admin_jobs_filter_runner_type` is enabled. |
| `statuses` | [`[CiJobStatus!]`](#cijobstatus) | Filter jobs by status. |
@@ -26259,6 +26260,47 @@ Values for sorting inherited variables.
| `KEY_ASC` | Key by ascending order. |
| `KEY_DESC` | Key by descending order. |
+### `CiJobFailureReason`
+
+| Value | Description |
+| ----- | ----------- |
+| `API_FAILURE` | A job that failed due to api failure. |
+| `ARCHIVED_FAILURE` | A job that failed due to archived failure. |
+| `BRIDGE_PIPELINE_IS_CHILD_PIPELINE` | A job that failed due to bridge pipeline is child pipeline. |
+| `BUILDS_DISABLED` | A job that failed due to builds disabled. |
+| `CI_QUOTA_EXCEEDED` | A job that failed due to ci quota exceeded. |
+| `DATA_INTEGRITY_FAILURE` | A job that failed due to data integrity failure. |
+| `DEPLOYMENT_REJECTED` | A job that failed due to deployment rejected. |
+| `DOWNSTREAM_BRIDGE_PROJECT_NOT_FOUND` | A job that failed due to downstream bridge project not found. |
+| `DOWNSTREAM_PIPELINE_CREATION_FAILED` | A job that failed due to downstream pipeline creation failed. |
+| `ENVIRONMENT_CREATION_FAILURE` | A job that failed due to environment creation failure. |
+| `FAILED_OUTDATED_DEPLOYMENT_JOB` | A job that failed due to failed outdated deployment job. |
+| `FORWARD_DEPLOYMENT_FAILURE` | A job that failed due to forward deployment failure. |
+| `INSUFFICIENT_BRIDGE_PERMISSIONS` | A job that failed due to insufficient bridge permissions. |
+| `INSUFFICIENT_UPSTREAM_PERMISSIONS` | A job that failed due to insufficient upstream permissions. |
+| `INVALID_BRIDGE_TRIGGER` | A job that failed due to invalid bridge trigger. |
+| `IP_RESTRICTION_FAILURE` | A job that failed due to ip restriction failure. |
+| `JOB_EXECUTION_TIMEOUT` | A job that failed due to job execution timeout. |
+| `MISSING_DEPENDENCY_FAILURE` | A job that failed due to missing dependency failure. |
+| `NO_MATCHING_RUNNER` | A job that failed due to no matching runner. |
+| `PIPELINE_LOOP_DETECTED` | A job that failed due to pipeline loop detected. |
+| `PROJECT_DELETED` | A job that failed due to project deleted. |
+| `PROTECTED_ENVIRONMENT_FAILURE` | A job that failed due to protected environment failure. |
+| `REACHED_MAX_DESCENDANT_PIPELINES_DEPTH` | A job that failed due to reached max descendant pipelines depth. |
+| `REACHED_MAX_PIPELINE_HIERARCHY_SIZE` | A job that failed due to reached max pipeline hierarchy size. |
+| `RUNNER_SYSTEM_FAILURE` | A job that failed due to runner system failure. |
+| `RUNNER_UNSUPPORTED` | A job that failed due to runner unsupported. |
+| `SCHEDULER_FAILURE` | A job that failed due to scheduler failure. |
+| `SCRIPT_FAILURE` | A job that failed due to script failure. |
+| `SECRETS_PROVIDER_NOT_FOUND` | A job that failed due to secrets provider not found. |
+| `STALE_SCHEDULE` | A job that failed due to stale schedule. |
+| `STUCK_OR_TIMEOUT_FAILURE` | A job that failed due to stuck or timeout failure. |
+| `TRACE_SIZE_EXCEEDED` | A job that failed due to trace size exceeded. |
+| `UNKNOWN_FAILURE` | A job that failed due to unknown failure. |
+| `UNMET_PREREQUISITES` | A job that failed due to unmet prerequisites. |
+| `UPSTREAM_BRIDGE_PROJECT_NOT_FOUND` | A job that failed due to upstream bridge project not found. |
+| `USER_BLOCKED` | A job that failed due to user blocked. |
+
### `CiJobKind`
| Value | Description |
diff --git a/ee/app/finders/ee/ci/jobs_finder.rb b/ee/app/finders/ee/ci/jobs_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..377bdc82adee1019922384cf061578e0860472c6
--- /dev/null
+++ b/ee/app/finders/ee/ci/jobs_finder.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module EE
+ module Ci
+ module JobsFinder
+ extend ::Gitlab::Utils::Override
+
+ private
+
+ override :filter_builds
+ def filter_builds(builds)
+ filter_by_failure_reason(super)
+ end
+
+ override :use_runner_type_filter?
+ def use_runner_type_filter?
+ # we don't need to use the runner_type scope if we know that the user only cares about instance runners
+ # with a job failure reason
+ return false if use_failure_reason_filter?
+
+ super
+ end
+
+ def filter_by_failure_reason(builds)
+ return builds unless use_failure_reason_filter?
+
+ builds.recently_failed_on_instance_runner(params[:failure_reason]) # currently limited to instance runners
+ end
+
+ def use_failure_reason_filter?
+ failure_reason = params[:failure_reason]
+ runner_type = params[:runner_type]
+
+ if failure_reason.present?
+ unless failure_reason == :runner_system_failure
+ raise ArgumentError, 'failure_reason only supports runner_system_failure'
+ end
+
+ unless runner_type == %w[instance_type]
+ raise ArgumentError, 'failure_reason can only be used together with runner_type: instance_type'
+ end
+
+ return true
+ end
+
+ false
+ end
+ end
+ end
+end
diff --git a/ee/app/graphql/ee/resolvers/ci/all_jobs_resolver.rb b/ee/app/graphql/ee/resolvers/ci/all_jobs_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..09694a7e938de4ccb0f7b82ac26a3ee946b0c81d
--- /dev/null
+++ b/ee/app/graphql/ee/resolvers/ci/all_jobs_resolver.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module EE
+ module Resolvers
+ module Ci
+ module AllJobsResolver
+ extend ActiveSupport::Concern
+ extend ::Gitlab::Utils::Override
+
+ prepended do
+ argument :failure_reason, ::Types::Ci::JobFailureReasonEnum,
+ required: false,
+ alpha: { milestone: '16.4' },
+ description: 'Filter jobs by failure reason. Currently only `RUNNER_SYSTEM_FAILURE` together with ' \
+ '`runnerTypes: INSTANCE_TYPE` is supported.'
+ end
+
+ override :resolve_with_lookahead
+ def resolve_with_lookahead(**args)
+ super
+ rescue ArgumentError => e
+ raise ::Gitlab::Graphql::Errors::ArgumentError, e.message
+ end
+
+ private
+
+ override :params_data
+ def params_data(args)
+ super.merge(failure_reason: args[:failure_reason])
+ end
+ end
+ end
+ end
+end
diff --git a/ee/app/models/ci/instance_runner_failed_jobs.rb b/ee/app/models/ci/instance_runner_failed_jobs.rb
index b7e8c6a4fd6cae9e93d04d161abd41490c00a478..ab3fd09a7688d1575ab4981e8c02c95f6ad68675 100644
--- a/ee/app/models/ci/instance_runner_failed_jobs.rb
+++ b/ee/app/models/ci/instance_runner_failed_jobs.rb
@@ -5,6 +5,7 @@ class InstanceRunnerFailedJobs
# Safety margin for situations where there is a mismatch between the async insert order and the finished_at value
JOB_LIMIT_MARGIN = 10
JOB_LIMIT = 100
+ SUPPORTED_FAILURE_REASONS = %i[runner_system_failure].freeze
class << self
def track(build)
@@ -18,15 +19,23 @@ def track(build)
end
end
- def recent_jobs
+ def recent_jobs(failure_reason:)
+ unless SUPPORTED_FAILURE_REASONS.include?(failure_reason.to_sym)
+ raise ArgumentError, "The only failure reason(s) supported are #{SUPPORTED_FAILURE_REASONS.join(', ')}"
+ end
+
return Ci::Build.none unless License.feature_available?(:runner_performance_insights)
job_ids = with_redis do |redis|
- # Fetch a few more jobs in case there is a mismatch between the async insert order and the finished_at value
+ # Fetch a few more jobs in case there is a mismatch between the async insert order and
+ # the finished_at value
redis.lrange(key, 0, max_admissible_job_count - 1)
end
- Ci::Build.id_in(job_ids).order(finished_at: :desc, id: :desc).limit(JOB_LIMIT)
+ Ci::Build.id_in(job_ids)
+ .where(failure_reason: failure_reason)
+ .reorder(finished_at: :desc, id: :desc)
+ .limit(JOB_LIMIT)
end
private
@@ -45,7 +54,7 @@ def max_admissible_job_count
def track_job?(build)
License.feature_available?(:runner_performance_insights) &&
- build.failure_reason == 'runner_system_failure' &&
+ SUPPORTED_FAILURE_REASONS.include?(build.failure_reason.to_sym) &&
build.runner.instance_type?
end
end
diff --git a/ee/app/models/ee/ci/build.rb b/ee/app/models/ee/ci/build.rb
index be8690467c4854b0daad65dddac0bc7a1aabb5db..32131bc1e3b726893f0c5708910fd0eb9c937a22 100644
--- a/ee/app/models/ee/ci/build.rb
+++ b/ee/app/models/ee/ci/build.rb
@@ -52,6 +52,10 @@ module Build
.for_project_paths(project_path)
end
+ scope :recently_failed_on_instance_runner, -> (failure_reason) do
+ merge(::Ci::InstanceRunnerFailedJobs.recent_jobs(failure_reason: failure_reason))
+ end
+
state_machine :status do
after_transition any => [:success, :failed, :canceled] do |build|
build.run_after_commit do
diff --git a/ee/spec/finders/ee/ci/jobs_finder_spec.rb b/ee/spec/finders/ee/ci/jobs_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c4d0307c5f352b2bcdf45e90e561fd2c5a108d7d
--- /dev/null
+++ b/ee/spec/finders/ee/ci/jobs_finder_spec.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobsFinder, '#execute', feature_category: :continuous_integration do
+ let_it_be(:admin) { create(:user, :admin) }
+
+ let(:params) { {} }
+
+ context 'when project, pipeline, and runner are blank' do
+ subject(:finder_execute) { described_class.new(current_user: current_user, params: params).execute }
+
+ context 'when current user is an admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ let_it_be(:instance_runner) { create(:ci_runner, :instance) }
+ let_it_be(:group_runner) { create(:ci_runner, :group) }
+ let_it_be(:project_runner) { create(:ci_runner, :project) }
+ let_it_be(:job_args) { { runner: instance_runner, failure_reason: :runner_system_failure } }
+ let_it_be(:job1) { create(:ci_build, :failed, finished_at: 1.minute.ago, **job_args) }
+ let_it_be(:job2) { create(:ci_build, :failed, finished_at: 2.minutes.ago, **job_args) }
+ let_it_be(:job7) { create(:ci_build, :failed, finished_at: 2.minutes.ago, **job_args) }
+ let_it_be(:job3) { create(:ci_build, :failed, finished_at: 2.minutes.ago, runner: group_runner) }
+ let_it_be(:job4) { create(:ci_build, :failed, finished_at: 2.minutes.ago, runner: project_runner) }
+ let_it_be(:job5) do
+ create(:ci_build, :failed, finished_at: 2.minutes.ago, runner: group_runner,
+ failure_reason: :runner_system_failure)
+ end
+
+ let_it_be(:job6) do
+ create(:ci_build, :failed, finished_at: 2.minutes.ago, runner: project_runner,
+ failure_reason: :runner_system_failure)
+ end
+
+ before do
+ stub_licensed_features(runner_performance_insights: runner_performance_insights)
+
+ ::Ci::InstanceRunnerFailedJobs.track(job1)
+ ::Ci::InstanceRunnerFailedJobs.track(job2)
+ end
+
+ context 'with param `failure_reason` set to :runner_system_failure', :clean_gitlab_redis_shared_state,
+ feature_category: :runner_fleet do
+ let(:params) { { failure_reason: :runner_system_failure } }
+
+ context 'without runner_performance_insights license' do
+ let(:runner_performance_insights) { false }
+
+ it 'raises ArgumentError due to lack of runner_type' do
+ expect(::Ci::Build).not_to receive(:recently_failed_on_instance_runner)
+
+ expect { finder_execute }.to raise_error(
+ ArgumentError, 'failure_reason can only be used together with runner_type: instance_type'
+ )
+ end
+
+ context 'with param :runner_type set to [instance_type]' do
+ let(:params) { { failure_reason: :runner_system_failure, runner_type: %w[instance_type] } }
+
+ it 'returns no jobs due to lack of license' do
+ expect(::Ci::Build).to receive(:recently_failed_on_instance_runner)
+ .with(:runner_system_failure).once.and_call_original
+
+ expect(finder_execute.ids).to be_empty
+ end
+ end
+ end
+
+ context 'with runner_performance_insights license' do
+ let(:runner_performance_insights) { true }
+
+ context 'with param :runner_type set to [instance_type]' do
+ let(:params) { { failure_reason: :runner_system_failure, runner_type: %w[instance_type] } }
+
+ it 'returns builds tracked by InstanceRunnerFailedJobs' do
+ expect(::Ci::Build).to receive(:recently_failed_on_instance_runner)
+ .with(:runner_system_failure).once.and_call_original
+
+ expect(finder_execute.ids).to contain_exactly(job1.id, job2.id)
+ end
+ end
+
+ context 'with param :runner_type set to multiple runner types', :aggregate_failures do
+ let(:params) { { failure_reason: :runner_system_failure, runner_type: %w[instance_type group_type] } }
+
+ it 'raises ArgumentError due to multiple runner types' do
+ expect(::Ci::Build).not_to receive(:recently_failed_on_instance_runner)
+
+ expect { finder_execute }.to raise_error(
+ ArgumentError, 'failure_reason can only be used together with runner_type: instance_type'
+ )
+ end
+
+ context 'with feature flag :admin_jobs_filter_runner_type disabled' do
+ before do
+ stub_feature_flags(admin_jobs_filter_runner_type: false)
+ end
+
+ it 'raises ArgumentError due to multiple runner types' do
+ expect(::Ci::Build).not_to receive(:recently_failed_on_instance_runner)
+
+ expect { finder_execute }.to raise_error(
+ ArgumentError, 'failure_reason can only be used together with runner_type: instance_type'
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context 'with param `failure_reason` not set to :runner_system_failure', :clean_gitlab_redis_shared_state,
+ feature_category: :runner_fleet do
+ let(:params) { { failure_reason: :runner_unsupported } }
+
+ context 'with runner_performance_insights license' do
+ let(:runner_performance_insights) { true }
+
+ context 'with param :runner_type set to [instance_type]' do
+ let(:params) { { failure_reason: :runner_unsupported, runner_type: %w[instance_type] } }
+
+ it 'raises ArgumentError due to unsupported failure_reason' do
+ expect(::Ci::Build).not_to receive(:recently_failed_on_instance_runner)
+
+ expect { finder_execute }
+ .to raise_error(ArgumentError, 'failure_reason only supports runner_system_failure')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/ee/spec/models/ci/build_spec.rb b/ee/spec/models/ci/build_spec.rb
index 162a7875d29aa30ab62c738c15ccf4566f0fbe3a..0b1da54037b15650b080043127074da7cd43fa39 100644
--- a/ee/spec/models/ci/build_spec.rb
+++ b/ee/spec/models/ci/build_spec.rb
@@ -670,6 +670,55 @@
end
end
+ describe '.recently_failed_on_instance_runner', :clean_gitlab_redis_shared_state, feature_category: :runner_fleet do
+ subject(:recently_failed_on_instance_runner) do
+ described_class.recently_failed_on_instance_runner(failure_reason)
+ end
+
+ before do
+ stub_licensed_features(runner_performance_insights: true)
+ end
+
+ let_it_be(:instance_runner) { create(:ci_runner, :instance) }
+ let_it_be(:job_args) { { runner: instance_runner, failure_reason: :runner_system_failure } }
+ let_it_be(:job1) { create(:ci_build, :failed, finished_at: 1.minute.ago, **job_args) }
+ let_it_be(:job2) { create(:ci_build, :failed, finished_at: 2.minutes.ago, **job_args) }
+
+ context 'with failure_reason set to :runner_system_failure' do
+ let(:failure_reason) { :runner_system_failure }
+
+ it 'returns no builds' do
+ is_expected.to be_empty
+ end
+
+ context 'with 2 jobs tracked' do
+ before do
+ ::Ci::InstanceRunnerFailedJobs.track(job2)
+ ::Ci::InstanceRunnerFailedJobs.track(job1)
+ end
+
+ it 'returns builds tracked by InstanceRunnerFailedJobs' do
+ is_expected.to match([
+ an_object_having_attributes(id: job1.id),
+ an_object_having_attributes(id: job2.id)
+ ])
+ end
+
+ it 'overrides the order of returned builds' do
+ expect(described_class.order(id: :asc).recently_failed_on_instance_runner(failure_reason)).to match([
+ an_object_having_attributes(id: job1.id),
+ an_object_having_attributes(id: job2.id)
+ ])
+
+ expect(described_class.order(id: :desc).recently_failed_on_instance_runner(failure_reason)).to match([
+ an_object_having_attributes(id: job1.id),
+ an_object_having_attributes(id: job2.id)
+ ])
+ end
+ end
+ end
+ end
+
describe 'ci_secrets_management_available?' do
subject { job.ci_secrets_management_available? }
diff --git a/ee/spec/models/ci/instance_runner_failed_jobs_spec.rb b/ee/spec/models/ci/instance_runner_failed_jobs_spec.rb
index 99df1913b090f8f5e7a6acd68bd1f9e2b76f614d..54ebec167d2f0bc1091dc485a2140ed03c104e75 100644
--- a/ee/spec/models/ci/instance_runner_failed_jobs_spec.rb
+++ b/ee/spec/models/ci/instance_runner_failed_jobs_spec.rb
@@ -66,7 +66,11 @@
end
describe '.recent_jobs' do
- subject(:recent_jobs) { described_class.recent_jobs }
+ def recent_jobs
+ described_class.recent_jobs(failure_reason: failure_reason)
+ end
+
+ subject(:scope) { recent_jobs }
let_it_be(:runner) { create(:ci_runner, :instance) }
let_it_be(:job_args) { { runner: runner, failure_reason: :runner_system_failure } }
@@ -77,57 +81,71 @@
context 'with runner_performance_insights licensed feature' do
let(:runner_performance_insights) { true }
- context 'when content is not set' do
- it { is_expected.to be_empty }
- end
+ context 'when failure_reason is not runner_system_failure' do
+ let(:failure_reason) { :runner_unsupported }
- context 'when jobs are added' do
- before do
- described_class.track(job)
+ it 'raises an error' do
+ expect { recent_jobs }.to raise_error(
+ ArgumentError, 'The only failure reason(s) supported are runner_system_failure'
+ )
end
+ end
- it 'returns 3 most recently finished jobs' do
- expect(described_class.recent_jobs).to contain_exactly(an_object_having_attributes(id: job.id))
-
- described_class.track(job2)
- described_class.track(job3)
+ context 'when failure_reason is runner_system_failure' do
+ let(:failure_reason) { :runner_system_failure }
- expect(described_class.recent_jobs).to match([
- an_object_having_attributes(id: job.id),
- an_object_having_attributes(id: job2.id),
- an_object_having_attributes(id: job3.id)
- ])
+ context 'when content is not set' do
+ it { is_expected.to be_empty }
end
- context 'when jobs are added in different order' do
+ context 'when jobs are added' do
+ before do
+ described_class.track(job)
+ end
+
it 'returns 3 most recently finished jobs' do
- expect(described_class.recent_jobs).to contain_exactly(an_object_having_attributes(id: job.id))
+ expect(recent_jobs).to contain_exactly(an_object_having_attributes(id: job.id))
- described_class.track(job3)
described_class.track(job2)
+ described_class.track(job3)
- expect(described_class.recent_jobs).to match([
+ expect(recent_jobs).to match([
an_object_having_attributes(id: job.id),
an_object_having_attributes(id: job2.id),
an_object_having_attributes(id: job3.id)
])
end
- end
- context 'when trimming is required' do
- before do
- stub_const("#{described_class}::JOB_LIMIT", 1)
- stub_const("#{described_class}::JOB_LIMIT_MARGIN", 1)
+ context 'when jobs are added in different order' do
+ it 'returns 3 most recently finished jobs' do
+ expect(recent_jobs).to contain_exactly(an_object_having_attributes(id: job.id))
+
+ described_class.track(job3)
+ described_class.track(job2)
+
+ expect(recent_jobs).to match([
+ an_object_having_attributes(id: job.id),
+ an_object_having_attributes(id: job2.id),
+ an_object_having_attributes(id: job3.id)
+ ])
+ end
end
- it 'returns 2 most recently finished jobs and purges the rest', :aggregate_failures do
- described_class.track(job3)
- described_class.track(job2)
+ context 'when trimming is required' do
+ before do
+ stub_const("#{described_class}::JOB_LIMIT", 1)
+ stub_const("#{described_class}::JOB_LIMIT_MARGIN", 1)
+ end
+
+ it 'returns 2 most recently finished jobs and purges the rest', :aggregate_failures do
+ described_class.track(job3)
+ described_class.track(job2)
- # Only the last 2 jobs saved will be retained
- expect(redis_stored_job_ids).to eq(formatted_job_ids_for(job2, job3))
- # and of those 2, the most recently finished will be returned (JOB_LIMIT)
- expect(described_class.recent_jobs).to contain_exactly(an_object_having_attributes(id: job2.id))
+ # Only the last 2 jobs saved will be retained
+ expect(redis_stored_job_ids).to eq(formatted_job_ids_for(job2, job3))
+ # and of those 2, the most recently finished will be returned (JOB_LIMIT)
+ is_expected.to contain_exactly(an_object_having_attributes(id: job2.id))
+ end
end
end
end
@@ -136,7 +154,11 @@
context 'without runner_performance_insights licensed feature' do
let(:runner_performance_insights) { false }
- it { is_expected.to be_empty }
+ context 'when failure_reason is runner_system_failure' do
+ let(:failure_reason) { :runner_system_failure }
+
+ it { is_expected.to be_empty }
+ end
end
end
diff --git a/ee/spec/requests/api/graphql/ci/jobs_spec.rb b/ee/spec/requests/api/graphql/ci/jobs_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f30cf0159d772de5844ea15b533569027cde5e1
--- /dev/null
+++ b/ee/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.jobs', feature_category: :continuous_integration do
+ include GraphqlHelpers
+
+ let_it_be(:instance_runner) { create(:ci_runner, :instance) }
+
+ let_it_be(:successful_job) { create(:ci_build, :success, name: 'successful_job') }
+ let_it_be(:failed_job) { create(:ci_build, :failed, name: 'failed_job') }
+ let_it_be(:pending_job) { create(:ci_build, :pending, name: 'pending_job') }
+ let_it_be(:system_failure_job) do
+ create(:ci_build, :failed, failure_reason: :runner_system_failure, runner: instance_runner,
+ name: 'system_failure_job')
+ end
+
+ let(:query_path) do
+ [
+ [:jobs, query_jobs_args],
+ [:nodes]
+ ]
+ end
+
+ let(:query) do
+ wrap_fields(query_graphql_path(query_path, 'id'))
+ end
+
+ let(:jobs_graphql_data) { graphql_data_at(:jobs, :nodes) }
+
+ subject(:request) { post_graphql(query, current_user: current_user) }
+
+ context 'when current user is an admin' do
+ let_it_be(:current_user) { create(:admin) }
+
+ context "with argument `failure_reason`", feature_category: :runner_fleet do
+ let(:query_jobs_args) do
+ graphql_args(failure_reason: failure_reason)
+ end
+
+ let_it_be(:_system_failure_on_project_runner) do
+ project_runner = create(:ci_runner, :project)
+
+ create(:ci_build, :failed, failure_reason: :runner_system_failure, runner: project_runner,
+ name: 'system_failure_job2')
+ end
+
+ before do
+ stub_licensed_features(runner_performance_insights: true)
+
+ Ci::Build.all.each { |build| ::Ci::InstanceRunnerFailedJobs.track(build) }
+ end
+
+ context 'as RUNNER_SYSTEM_FAILURE' do
+ let(:failure_reason) { :RUNNER_SYSTEM_FAILURE }
+
+ it 'generates an error' do
+ request
+
+ expect_graphql_errors_to_include 'failure_reason can only be used together with runner_type: instance_type'
+ end
+
+ context 'with argument `runnerTypes`' do
+ let(:query_jobs_args) do
+ graphql_args(runner_types: runner_types, failure_reason: failure_reason)
+ end
+
+ context 'as INSTANCE_TYPE' do
+ let(:runner_types) { [:INSTANCE_TYPE] }
+
+ it_behaves_like 'a working graphql query that returns data' do
+ before do
+ request
+ end
+
+ it { expect(jobs_graphql_data).to contain_exactly(a_graphql_entity_for(system_failure_job)) }
+ end
+ end
+ end
+ end
+
+ context 'as RUNNER_UNSUPPORTED' do
+ let(:failure_reason) { :RUNNER_UNSUPPORTED }
+
+ context 'with argument `runnerTypes`' do
+ let(:query_jobs_args) do
+ graphql_args(runner_types: runner_types, failure_reason: failure_reason)
+ end
+
+ context 'as INSTANCE_TYPE' do
+ let(:runner_types) { [:INSTANCE_TYPE] }
+
+ it 'generates an error' do
+ request
+
+ expect_graphql_errors_to_include 'failure_reason only supports runner_system_failure'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/ci/jobs_finder_spec.rb b/spec/finders/ci/jobs_finder_spec.rb
index e86ac65df61dd963f52ea1a5eaed4b9009123e7c..57046baafab7d024440b34b789d9e30428c99995 100644
--- a/spec/finders/ci/jobs_finder_spec.rb
+++ b/spec/finders/ci/jobs_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobsFinder, '#execute', feature_category: :runner_fleet do
+RSpec.describe Ci::JobsFinder, '#execute', feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:project) { create(:project, :private, public_builds: false) }
@@ -13,8 +13,8 @@
let(:params) { {} }
- context 'when project, pipeline or runner are blank' do
- subject { described_class.new(current_user: current_user, params: params).execute }
+ context 'when project, pipeline, and runner are blank' do
+ subject(:finder_execute) { described_class.new(current_user: current_user, params: params).execute }
context 'with admin' do
let(:current_user) { admin }
@@ -278,28 +278,30 @@
let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
let_it_be(:job_4) { create(:ci_build, :success, runner: runner) }
- subject { described_class.new(current_user: user, runner: runner, params: params).execute }
+ subject(:execute) { described_class.new(current_user: user, runner: runner, params: params).execute }
- context 'with admin and admin mode enabled', :enable_admin_mode do
+ context 'when current user is an admin' do
let(:user) { admin }
- it 'returns jobs for the specified project' do
- expect(subject).to match_array([job_4])
- end
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'returns jobs for the specified project' do
+ expect(subject).to contain_exactly job_4
+ end
- context "with params" do
- using RSpec::Parameterized::TableSyntax
+ context 'with params' do
+ using RSpec::Parameterized::TableSyntax
- where(:param_runner_type, :param_scope, :expected_jobs) do
- 'project_type' | 'success' | lazy { [job_4] }
- 'instance_type' | nil | lazy { [] }
- nil | 'pending' | lazy { [] }
- end
+ where(:param_runner_type, :param_scope, :expected_jobs) do
+ 'project_type' | 'success' | lazy { [job_4] }
+ 'instance_type' | nil | lazy { [] }
+ nil | 'pending' | lazy { [] }
+ end
- with_them do
- let(:params) { { runner_type: param_runner_type, scope: param_scope } }
+ with_them do
+ let(:params) { { runner_type: param_runner_type, scope: param_scope } }
- it { is_expected.to match_array(expected_jobs) }
+ it { is_expected.to match_array(expected_jobs) }
+ end
end
end
end
diff --git a/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb
index 933abf31470d46707ddfe5e332f06e895f6d39fb..6b9e3a484b14fab5d938457c6df3ba7efadffbd6 100644
--- a/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/all_jobs_resolver_spec.rb
@@ -5,6 +5,7 @@
RSpec.describe Resolvers::Ci::AllJobsResolver, feature_category: :continuous_integration do
include GraphqlHelpers
+ let_it_be(:instance_runner) { create(:ci_runner, :instance) }
let_it_be(:successful_job) { create(:ci_build, :success, name: 'successful_job') }
let_it_be(:successful_job_two) { create(:ci_build, :success, name: 'successful_job_two') }
let_it_be(:failed_job) { create(:ci_build, :failed, name: 'failed_job') }
@@ -12,11 +13,11 @@
let(:args) { {} }
- subject { resolve_jobs(args) }
-
describe '#resolve' do
- context 'with admin' do
- let(:current_user) { create(:admin) }
+ subject(:request) { resolve_jobs(args) }
+
+ context 'when current user is an admin' do
+ let_it_be(:current_user) { create(:admin) }
shared_examples 'executes as admin' do
context "with argument `statuses`" do
@@ -40,8 +41,7 @@
context "with argument `runner_types`" do
let_it_be(:successful_job_with_instance_runner) do
- create(:ci_build, :success, name: 'successful_job_with_instance_runner',
- runner: create(:ci_runner, :instance))
+ create(:ci_build, :success, name: 'successful_job_with_instance_runner', runner: instance_runner)
end
context 'with feature flag :admin_jobs_filter_runner_type enabled' do
@@ -80,7 +80,7 @@
:ci_build,
:success,
name: 'successful_job_with_instance_runner',
- runner: create(:ci_runner, :instance)
+ runner: instance_runner
)
end
@@ -132,7 +132,9 @@
end
context 'with unauthorized user' do
- let(:current_user) { nil }
+ let_it_be(:unauth_user) { create(:user) }
+
+ let(:current_user) { unauth_user }
it { is_expected.to be_empty }
end