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
field :pipeline, Types::Ci::PipelineType, null: false,
description: 'Pipeline the job belongs to',
resolve: -> (build, _, _) { Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, build.pipeline_id).find }
field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the job'
field :needs, JobType.connection_type, null: true,
description: 'Builds that must complete before the jobs run'
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the job',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
field :scheduled_at, Types::TimeType, null: true,
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
---
title: Add artifacts field to JobType
merge_request: 48207
author:
type: added
......@@ -2315,6 +2315,31 @@ type CiGroupEdge {
}
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
"""
......@@ -2361,6 +2386,53 @@ type CiJob {
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.
"""
......@@ -12038,6 +12110,36 @@ input JiraUsersMappingInputType {
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 {
"""
Background color of the label
......
......@@ -6225,6 +6225,59 @@
"name": "CiJob",
"description": null,
"fields": [
{
"name": "artifacts",
"description": "Artifacts generated by the job",
"args": [
{
"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": "CiJobArtifactConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "detailedStatus",
"description": "Detailed status of the job",
......@@ -6346,6 +6399,159 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "CiJobArtifact",
"description": null,
"fields": [
{
"name": "downloadPath",
"description": "URL for downloading the artifact's file",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "fileType",
"description": "File type of the artifact",
"args": [
],
"type": {
"kind": "ENUM",
"name": "JobArtifactFileType",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "CiJobArtifactConnection",
"description": "The connection type for CiJobArtifact.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "CiJobArtifactEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "CiJobArtifact",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "CiJobArtifactEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "CiJobArtifact",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "CiJobConnection",
......@@ -33064,6 +33270,179 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "JobArtifactFileType",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "ARCHIVE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "METADATA",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "TRACE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "JUNIT",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "METRICS",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "METRICS_REFEREE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "NETWORK_REFEREE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DOTENV",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "COBERTURA",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "CLUSTER_APPLICATIONS",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "LSIF",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SAST",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SECRET_DETECTION",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DEPENDENCY_SCANNING",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "CONTAINER_SCANNING",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DAST",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "LICENSE_MANAGEMENT",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "LICENSE_SCANNING",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ACCESSIBILITY",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "CODEQUALITY",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PERFORMANCE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "BROWSER_PERFORMANCE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "LOAD_PERFORMANCE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "TERRAFORM",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "REQUIREMENTS",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "COVERAGE_FUZZING",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "API_FUZZING",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Label",
......@@ -380,12 +380,20 @@ Represents the total number of issues and their weights for a particular day.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `artifacts` | CiJobArtifactConnection | Artifacts generated by the job |
| `detailedStatus` | DetailedStatus | Detailed status of the job |
| `name` | String | Name of the job |
| `needs` | CiJobConnection | Builds that must complete before the jobs run |
| `pipeline` | Pipeline! | Pipeline the job belongs to |
| `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
| Field | Type | Description |
......@@ -4145,6 +4153,38 @@ Iteration ID wildcard values.
| `ANY` | An 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
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
needs
detailedStatus
scheduledAt
artifacts
]
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
require 'spec_helper'
RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
RSpec.describe 'Query.project.pipeline' do
include GraphqlHelpers
let(:project) { create(:project, :repository, :public) }
let(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:user) { create(:user) }
def first(field)
[field.pluralize, 'nodes', 0]
end
describe '.stages.groups.jobs' do
let(:pipeline) do
pipeline = create(:ci_pipeline, project: project, user: user)
stage = create(:ci_stage_entity, pipeline: pipeline, name: 'first')
......@@ -14,10 +20,6 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
pipeline
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(:query) 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'))
end
context 'when fetching jobs from the pipeline' do
it 'avoids N+1 queries', :aggregate_failures do
control_count = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: user)
......@@ -112,4 +113,50 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
])
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
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