Commit 60dc1611 authored by Kerri Miller's avatar Kerri Miller

Merge branch '251015-artifact-download' into 'master'

Add JobArtifactType with downloadPath field

See merge request gitlab-org/gitlab!48207
parents af495ea3 4ddf9636
# frozen_string_literal: true
module Types
module Ci
class JobArtifactFileTypeEnum < BaseEnum
graphql_name 'JobArtifactFileType'
::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS.keys.each do |file_type|
value file_type.to_s.upcase, value: file_type.to_s
end
end
end
end
# frozen_string_literal: true
module Types
module Ci
# rubocop: disable Graphql/AuthorizeTypes
class JobArtifactType < BaseObject
graphql_name 'CiJobArtifact'
field :download_path, GraphQL::STRING_TYPE, null: true,
description: "URL for downloading the artifact's file"
field :file_type, ::Types::Ci::JobArtifactFileTypeEnum, null: true,
description: 'File type of the artifact'
def download_path
::Gitlab::Routing.url_helpers.download_project_job_artifacts_path(
object.project,
object.job,
file_type: object.file_type
)
end
end
end
end
...@@ -9,15 +9,28 @@ module Types ...@@ -9,15 +9,28 @@ module Types
field :pipeline, Types::Ci::PipelineType, null: false, field :pipeline, Types::Ci::PipelineType, null: false,
description: 'Pipeline the job belongs to', description: 'Pipeline the job belongs to',
resolve: -> (build, _, _) { Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, build.pipeline_id).find } resolve: -> (build, _, _) { Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, build.pipeline_id).find }
field :name, GraphQL::STRING_TYPE, null: true, field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the job' description: 'Name of the job'
field :needs, JobType.connection_type, null: true, field :needs, JobType.connection_type, null: true,
description: 'Builds that must complete before the jobs run' description: 'Builds that must complete before the jobs run'
field :detailed_status, Types::Ci::DetailedStatusType, null: true, field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the job', description: 'Detailed status of the job',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) } resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
field :scheduled_at, Types::TimeType, null: true, field :scheduled_at, Types::TimeType, null: true,
description: 'Schedule for the build' description: 'Schedule for the build'
field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true,
description: 'Artifacts generated by the job'
def artifacts
if object.is_a?(::Ci::Build)
object.job_artifacts
end
end
end end
end end
end end
---
title: Add artifacts field to JobType
merge_request: 48207
author:
type: added
...@@ -2315,6 +2315,31 @@ type CiGroupEdge { ...@@ -2315,6 +2315,31 @@ type CiGroupEdge {
} }
type CiJob { type CiJob {
"""
Artifacts generated by the job
"""
artifacts(
"""
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
): CiJobArtifactConnection
""" """
Detailed status of the job Detailed status of the job
""" """
...@@ -2361,6 +2386,53 @@ type CiJob { ...@@ -2361,6 +2386,53 @@ type CiJob {
scheduledAt: Time scheduledAt: Time
} }
type CiJobArtifact {
"""
URL for downloading the artifact's file
"""
downloadPath: String
"""
File type of the artifact
"""
fileType: JobArtifactFileType
}
"""
The connection type for CiJobArtifact.
"""
type CiJobArtifactConnection {
"""
A list of edges.
"""
edges: [CiJobArtifactEdge]
"""
A list of nodes.
"""
nodes: [CiJobArtifact]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type CiJobArtifactEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: CiJobArtifact
}
""" """
The connection type for CiJob. The connection type for CiJob.
""" """
...@@ -12038,6 +12110,36 @@ input JiraUsersMappingInputType { ...@@ -12038,6 +12110,36 @@ input JiraUsersMappingInputType {
jiraAccountId: String! jiraAccountId: String!
} }
enum JobArtifactFileType {
ACCESSIBILITY
API_FUZZING
ARCHIVE
BROWSER_PERFORMANCE
CLUSTER_APPLICATIONS
COBERTURA
CODEQUALITY
CONTAINER_SCANNING
COVERAGE_FUZZING
DAST
DEPENDENCY_SCANNING
DOTENV
JUNIT
LICENSE_MANAGEMENT
LICENSE_SCANNING
LOAD_PERFORMANCE
LSIF
METADATA
METRICS
METRICS_REFEREE
NETWORK_REFEREE
PERFORMANCE
REQUIREMENTS
SAST
SECRET_DETECTION
TERRAFORM
TRACE
}
type Label { type Label {
""" """
Background color of the label Background color of the label
......
...@@ -380,12 +380,20 @@ Represents the total number of issues and their weights for a particular day. ...@@ -380,12 +380,20 @@ Represents the total number of issues and their weights for a particular day.
| Field | Type | Description | | Field | Type | Description |
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `artifacts` | CiJobArtifactConnection | Artifacts generated by the job |
| `detailedStatus` | DetailedStatus | Detailed status of the job | | `detailedStatus` | DetailedStatus | Detailed status of the job |
| `name` | String | Name of the job | | `name` | String | Name of the job |
| `needs` | CiJobConnection | Builds that must complete before the jobs run | | `needs` | CiJobConnection | Builds that must complete before the jobs run |
| `pipeline` | Pipeline! | Pipeline the job belongs to | | `pipeline` | Pipeline! | Pipeline the job belongs to |
| `scheduledAt` | Time | Schedule for the build | | `scheduledAt` | Time | Schedule for the build |
### CiJobArtifact
| Field | Type | Description |
| ----- | ---- | ----------- |
| `downloadPath` | String | URL for downloading the artifact's file |
| `fileType` | JobArtifactFileType | File type of the artifact |
### CiStage ### CiStage
| Field | Type | Description | | Field | Type | Description |
...@@ -4145,6 +4153,38 @@ Iteration ID wildcard values. ...@@ -4145,6 +4153,38 @@ Iteration ID wildcard values.
| `ANY` | An iteration is assigned | | `ANY` | An iteration is assigned |
| `NONE` | No iteration is assigned | | `NONE` | No iteration is assigned |
### JobArtifactFileType
| Value | Description |
| ----- | ----------- |
| `ACCESSIBILITY` | |
| `API_FUZZING` | |
| `ARCHIVE` | |
| `BROWSER_PERFORMANCE` | |
| `CLUSTER_APPLICATIONS` | |
| `COBERTURA` | |
| `CODEQUALITY` | |
| `CONTAINER_SCANNING` | |
| `COVERAGE_FUZZING` | |
| `DAST` | |
| `DEPENDENCY_SCANNING` | |
| `DOTENV` | |
| `JUNIT` | |
| `LICENSE_MANAGEMENT` | |
| `LICENSE_SCANNING` | |
| `LOAD_PERFORMANCE` | |
| `LSIF` | |
| `METADATA` | |
| `METRICS` | |
| `METRICS_REFEREE` | |
| `NETWORK_REFEREE` | |
| `PERFORMANCE` | |
| `REQUIREMENTS` | |
| `SAST` | |
| `SECRET_DETECTION` | |
| `TERRAFORM` | |
| `TRACE` | |
### ListLimitMetric ### ListLimitMetric
List limit metric setting. List limit metric setting.
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['JobArtifactFileType'] do
it 'exposes all job artifact file types' do
expect(described_class.values.keys).to contain_exactly(
*::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS.keys.map(&:to_s).map(&:upcase)
)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['CiJobArtifact'] do
it 'has the correct fields' do
expected_fields = [:download_path, :file_type]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
...@@ -12,6 +12,7 @@ RSpec.describe Types::Ci::JobType do ...@@ -12,6 +12,7 @@ RSpec.describe Types::Ci::JobType do
needs needs
detailedStatus detailedStatus
scheduledAt scheduledAt
artifacts
] ]
expect(described_class).to have_graphql_fields(*expected_fields) expect(described_class).to have_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.project(fullPath).pipelines.jobs.artifacts' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:user) { create(:user) }
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
pipelines {
nodes {
jobs {
nodes {
artifacts {
nodes {
downloadPath
fileType
}
}
}
}
}
}
}
}
)
end
it 'returns the fields for the artifacts' do
job = create(:ci_build, pipeline: pipeline)
create(:ci_job_artifact, :junit, job: job)
post_graphql(query, current_user: user)
expect(response).to have_gitlab_http_status(:ok)
pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes')
jobs_data = pipelines_data.first.dig('jobs', 'nodes')
artifact_data = jobs_data.first.dig('artifacts', 'nodes').first
expect(artifact_data['downloadPath']).to eq(
"/#{project.full_path}/-/jobs/#{job.id}/artifacts/download?file_type=junit"
)
expect(artifact_data['fileType']).to eq('JUNIT')
end
end
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do RSpec.describe 'Query.project.pipeline' do
include GraphqlHelpers include GraphqlHelpers
let(:project) { create(:project, :repository, :public) } let_it_be(:project) { create(:project, :repository, :public) }
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
def first(field)
[field.pluralize, 'nodes', 0]
end
describe '.stages.groups.jobs' do
let(:pipeline) do let(:pipeline) do
pipeline = create(:ci_pipeline, project: project, user: user) pipeline = create(:ci_pipeline, project: project, user: user)
stage = create(:ci_stage_entity, pipeline: pipeline, name: 'first') stage = create(:ci_stage_entity, pipeline: pipeline, name: 'first')
...@@ -14,10 +20,6 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do ...@@ -14,10 +20,6 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
pipeline pipeline
end end
def first(field)
[field.pluralize, 'nodes', 0]
end
let(:jobs_graphql_data) { graphql_data.dig(*%w[project pipeline], *first('stage'), *first('group'), 'jobs', 'nodes') } let(:jobs_graphql_data) { graphql_data.dig(*%w[project pipeline], *first('stage'), *first('group'), 'jobs', 'nodes') }
let(:query) do let(:query) do
...@@ -55,7 +57,6 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do ...@@ -55,7 +57,6 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job')) expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job'))
end end
context 'when fetching jobs from the pipeline' do
it 'avoids N+1 queries', :aggregate_failures do it 'avoids N+1 queries', :aggregate_failures do
control_count = ActiveRecord::QueryRecorder.new do control_count = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: user) post_graphql(query, current_user: user)
...@@ -112,4 +113,50 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do ...@@ -112,4 +113,50 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
]) ])
end end
end end
describe '.jobs.artifacts' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
pipeline(iid: "#{pipeline.iid}") {
jobs {
nodes {
artifacts {
nodes {
downloadPath
}
}
}
}
}
}
}
)
end
context 'when the job is a build' do
it "returns the build's artifacts" do
create(:ci_build, :artifacts, pipeline: pipeline)
post_graphql(query, current_user: user)
job_data = graphql_data.dig('project', 'pipeline', 'jobs', 'nodes').first
expect(job_data.dig('artifacts', 'nodes').count).to be(2)
end
end
context 'when the job is not a build' do
it 'returns nil' do
create(:ci_bridge, pipeline: pipeline)
post_graphql(query, current_user: user)
job_data = graphql_data.dig('project', 'pipeline', 'jobs', 'nodes').first
expect(job_data['artifacts']).to be_nil
end
end
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