Commit 99c9f636 authored by Avielle Wolfe's avatar Avielle Wolfe Committed by Stan Hu

Add jobs field to Ci::PipelineType

The field returns commit status data
parent a06b4ad9
......@@ -2,12 +2,21 @@
module Resolvers
class ProjectPipelinesResolver < BaseResolver
include LooksAhead
include ResolvesPipelines
alias_method :project, :object
def resolve(**args)
resolve_pipelines(project, args)
def resolve_with_lookahead(**args)
apply_lookahead(resolve_pipelines(project, args))
end
private
def preloads
{
jobs: [:statuses]
}
end
end
end
......@@ -56,6 +56,12 @@ module Types
description: 'Specifies if a pipeline can be canceled',
method: :cancelable?,
null: false
field :jobs,
::Types::Ci::JobType.connection_type,
null: true,
description: 'Jobs belonging to the pipeline',
method: :statuses
end
end
end
......
......@@ -191,6 +191,7 @@ module Types
Types::Ci::PipelineType.connection_type,
null: true,
description: 'Build pipelines of the project',
extras: [:lookahead],
resolver: Resolvers::ProjectPipelinesResolver
field :pipeline,
......
---
title: Add jobs field with secureReportTypes argument to Ci::PipelineType
merge_request: 45837
author:
type: added
......@@ -13373,6 +13373,36 @@ type Pipeline {
"""
iid: String!
"""
Jobs belonging to the pipeline
"""
jobs(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Filter jobs by the type of security report they produce
"""
securityReportTypes: [SecurityReportTypeEnum!]
): CiJobConnection
"""
Specifies if a pipeline can be retried
"""
......@@ -17840,6 +17870,43 @@ type SecurityReportSummarySection {
vulnerabilitiesCount: Int
}
enum SecurityReportTypeEnum {
"""
API FUZZING scan report
"""
API_FUZZING
"""
CONTAINER SCANNING scan report
"""
CONTAINER_SCANNING
"""
COVERAGE FUZZING scan report
"""
COVERAGE_FUZZING
"""
DAST scan report
"""
DAST
"""
DEPENDENCY SCANNING scan report
"""
DEPENDENCY_SCANNING
"""
SAST scan report
"""
SAST
"""
SECRET DETECTION scan report
"""
SECRET_DETECTION
}
"""
The type of the security scanner
"""
......
......@@ -39346,6 +39346,77 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jobs",
"description": "Jobs belonging to the pipeline",
"args": [
{
"name": "securityReportTypes",
"description": "Filter jobs by the type of security report they produce",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "SecurityReportTypeEnum",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "CiJobConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "retryable",
"description": "Specifies if a pipeline can be retried",
......@@ -51417,6 +51488,59 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "SecurityReportTypeEnum",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "SAST",
"description": "SAST scan report",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DAST",
"description": "DAST scan report",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DEPENDENCY_SCANNING",
"description": "DEPENDENCY SCANNING scan report",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "CONTAINER_SCANNING",
"description": "CONTAINER SCANNING scan report",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SECRET_DETECTION",
"description": "SECRET DETECTION scan report",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "COVERAGE_FUZZING",
"description": "COVERAGE FUZZING scan report",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "API_FUZZING",
"description": "API FUZZING scan report",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "SecurityScannerType",
......@@ -3739,6 +3739,18 @@ Size of UI component in SAST configuration page.
| `MEDIUM` | |
| `SMALL` | |
### SecurityReportTypeEnum
| Value | Description |
| ----- | ----------- |
| `API_FUZZING` | API FUZZING scan report |
| `CONTAINER_SCANNING` | CONTAINER SCANNING scan report |
| `COVERAGE_FUZZING` | COVERAGE FUZZING scan report |
| `DAST` | DAST scan report |
| `DEPENDENCY_SCANNING` | DEPENDENCY SCANNING scan report |
| `SAST` | SAST scan report |
| `SECRET_DETECTION` | SECRET DETECTION scan report |
### SecurityScannerType
The type of the security scanner.
......
......@@ -13,6 +13,12 @@ module EE
extras: [:lookahead],
description: 'Vulnerability and scanned resource counts for each security scanner of the pipeline',
resolver: ::Resolvers::SecurityReportSummaryResolver
field :jobs,
::Types::Ci::JobType.connection_type,
null: true,
description: 'Jobs belonging to the pipeline',
resolver: ::Resolvers::Ci::JobsResolver
end
end
end
......
# frozen_string_literal: true
module Resolvers
module Ci
class JobsResolver < BaseResolver
alias_method :pipeline, :object
argument :security_report_types, [Types::Security::ReportTypeEnum],
required: false,
description: 'Filter jobs by the type of security report they produce'
def resolve(security_report_types: [])
if security_report_types.present?
::Security::SecurityJobsFinder.new(
pipeline: pipeline,
job_types: security_report_types
).execute
else
pipeline.statuses
end
end
end
end
end
# frozen_string_literal: true
module Types
module Security
class ReportTypeEnum < BaseEnum
graphql_name 'SecurityReportTypeEnum'
::Security::SecurityJobsFinder.allowed_job_types.each do |report_type|
value report_type.upcase,
value: report_type,
description: "#{report_type.upcase.to_s.tr('_', ' ')} scan report"
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Ci::JobsResolver do
include GraphqlHelpers
let_it_be(:pipeline) { create(:ci_pipeline) }
before_all do
create(:ci_build, name: 'Normal job', pipeline: pipeline)
create(:ci_build, :sast, name: 'DAST job', pipeline: pipeline)
create(:ci_build, :dast, name: 'SAST job', pipeline: pipeline)
create(:ci_build, :container_scanning, name: 'Container scanning job', pipeline: pipeline)
end
describe '#resolve' do
context 'when security_report_types is empty' do
it "returns all of the pipeline's jobs" do
jobs = resolve(described_class, obj: pipeline, args: {}, ctx: {})
job_names = jobs.map(&:name)
expect(job_names).to contain_exactly('Normal job', 'DAST job', 'SAST job', 'Container scanning job')
end
end
context 'when security_report_types is present' do
it "returns the pipeline's jobs with the given security report types" do
report_types = [
::Types::Security::ReportTypeEnum.values['SAST'].value,
::Types::Security::ReportTypeEnum.values['DAST'].value
]
jobs = resolve(described_class, obj: pipeline, args: { security_report_types: report_types }, ctx: {})
job_names = jobs.map(&:name)
expect(job_names).to contain_exactly('DAST job', 'SAST job')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['SecurityReportTypeEnum'] do
it 'exposes all security report types' do
expect(described_class.values.keys).to contain_exactly(
*::Security::SecurityJobsFinder.allowed_job_types.map(&:to_s).map(&:upcase)
)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.project(fullPath).pipelines' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:first_user) { create(:user) }
let_it_be(:second_user) { create(:user) }
describe '.jobs' do
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
pipelines {
nodes {
jobs {
nodes {
name
}
}
}
}
}
}
)
end
it 'fetches the jobs without an N+1' do
pipeline = create(:ci_pipeline, project: project)
create(:ci_build, pipeline: pipeline, name: 'Job 1')
control_count = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: first_user)
end
pipeline = create(:ci_pipeline, project: project)
create(:ci_build, pipeline: pipeline, name: 'Job 2')
expect do
post_graphql(query, current_user: second_user)
end.not_to exceed_query_limit(control_count)
expect(response).to have_gitlab_http_status(:ok)
pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes')
job_names = pipelines_data.map do |pipeline_data|
jobs_data = pipeline_data.dig('jobs', 'nodes')
jobs_data.map { |job_data| job_data['name'] }
end.flatten
expect(job_names).to contain_exactly('Job 1', 'Job 2')
end
end
describe '.jobs(securityReportTypes)' do
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
pipelines {
nodes {
jobs(securityReportTypes: [SAST]) {
nodes {
name
}
}
}
}
}
}
)
end
it 'fetches the jobs matching the report type filter' do
pipeline = create(:ci_pipeline, project: project)
create(:ci_build, :dast, name: 'DAST Job 1', pipeline: pipeline)
create(:ci_build, :sast, name: 'SAST Job 1', pipeline: pipeline)
post_graphql(query, current_user: first_user)
expect(response).to have_gitlab_http_status(:ok)
pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes')
job_names = pipelines_data.map do |pipeline_data|
jobs_data = pipeline_data.dig('jobs', 'nodes')
jobs_data.map { |job_data| job_data['name'] }
end.flatten
expect(job_names).to contain_exactly('SAST Job 1')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.project(fullPath).pipelines' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:first_user) { create(:user) }
let_it_be(:second_user) { create(:user) }
describe '.jobs' do
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
pipelines {
nodes {
jobs {
nodes {
name
}
}
}
}
}
}
)
end
it 'fetches the jobs without an N+1' do
pipeline = create(:ci_pipeline, project: project)
create(:ci_build, pipeline: pipeline, name: 'Job 1')
control_count = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: first_user)
end
pipeline = create(:ci_pipeline, project: project)
create(:ci_build, pipeline: pipeline, name: 'Job 2')
expect do
post_graphql(query, current_user: second_user)
end.not_to exceed_query_limit(control_count)
expect(response).to have_gitlab_http_status(:ok)
pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes')
job_names = pipelines_data.map do |pipeline_data|
jobs_data = pipeline_data.dig('jobs', 'nodes')
jobs_data.map { |job_data| job_data['name'] }
end.flatten
expect(job_names).to contain_exactly('Job 1', 'Job 2')
end
end
end
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