Commit 9a4c9afb authored by Sean Carroll's avatar Sean Carroll
parent 056ee1bf
# frozen_string_literal: true
module Types
class EvidenceType < BaseObject
graphql_name 'ReleaseEvidence'
description 'Evidence for a release'
authorize :download_code
present_using Releases::EvidencePresenter
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the evidence'
field :sha, GraphQL::STRING_TYPE, null: true,
description: 'SHA1 ID of the evidence hash'
field :filepath, GraphQL::STRING_TYPE, null: true,
description: 'URL from where the evidence can be downloaded'
field :collected_at, Types::TimeType, null: true,
description: 'Timestamp when the evidence was collected'
end
end
...@@ -27,6 +27,8 @@ module Types ...@@ -27,6 +27,8 @@ module Types
description: 'Assets of the release' description: 'Assets of the release'
field :milestones, Types::MilestoneType.connection_type, null: true, field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Milestones associated to the release' description: 'Milestones associated to the release'
field :evidences, Types::EvidenceType.connection_type, null: true,
description: 'Evidence for the release'
field :author, Types::UserType, null: true, field :author, Types::UserType, null: true,
description: 'User that created the release' description: 'User that created the release'
......
# frozen_string_literal: true # frozen_string_literal: true
class Releases::Evidence < ApplicationRecord module Releases
include ShaAttribute class Evidence < ApplicationRecord
include Presentable include ShaAttribute
include Presentable
belongs_to :release, inverse_of: :evidences belongs_to :release, inverse_of: :evidences
default_scope { order(created_at: :asc) } default_scope { order(created_at: :asc) }
sha_attribute :summary_sha sha_attribute :summary_sha
alias_attribute :collected_at, :created_at alias_attribute :collected_at, :created_at
alias_attribute :sha, :summary_sha
def milestones def milestones
@milestones ||= release.milestones.includes(:issues) @milestones ||= release.milestones.includes(:issues)
end end
## ##
# Return `summary` without sensitive information. # Return `summary` without sensitive information.
# #
# Removing issues from summary in order to prevent leaking confidential ones. # Removing issues from summary in order to prevent leaking confidential ones.
# See more https://gitlab.com/gitlab-org/gitlab/issues/121930 # See more https://gitlab.com/gitlab-org/gitlab/issues/121930
def summary def summary
safe_summary = read_attribute(:summary) safe_summary = read_attribute(:summary)
safe_summary.dig('release', 'milestones')&.each do |milestone| safe_summary.dig('release', 'milestones')&.each do |milestone|
milestone.delete('issues') milestone.delete('issues')
end end
safe_summary safe_summary
end
end end
end end
---
title: Add Evidence to Releases GraphQL endpoint
merge_request: 33254
author:
type: added
...@@ -9971,6 +9971,31 @@ type Release { ...@@ -9971,6 +9971,31 @@ type Release {
""" """
descriptionHtml: String descriptionHtml: String
"""
Evidence for the release
"""
evidences(
"""
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
): ReleaseEvidenceConnection
""" """
Milestones associated to the release Milestones associated to the release
""" """
...@@ -10109,6 +10134,66 @@ type ReleaseEdge { ...@@ -10109,6 +10134,66 @@ type ReleaseEdge {
node: Release node: Release
} }
"""
Evidence for a release
"""
type ReleaseEvidence {
"""
Timestamp when the evidence was collected
"""
collectedAt: Time
"""
URL from where the evidence can be downloaded
"""
filepath: String
"""
ID of the evidence
"""
id: ID!
"""
SHA1 ID of the evidence hash
"""
sha: String
}
"""
The connection type for ReleaseEvidence.
"""
type ReleaseEvidenceConnection {
"""
A list of edges.
"""
edges: [ReleaseEvidenceEdge]
"""
A list of nodes.
"""
nodes: [ReleaseEvidence]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type ReleaseEvidenceEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: ReleaseEvidence
}
type ReleaseLink { type ReleaseLink {
""" """
Indicates the link points to an external resource Indicates the link points to an external resource
......
...@@ -29243,6 +29243,59 @@ ...@@ -29243,6 +29243,59 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "evidences",
"description": "Evidence for the release",
"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": "ReleaseEvidenceConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "milestones", "name": "milestones",
"description": "Milestones associated to the release", "description": "Milestones associated to the release",
...@@ -29609,6 +29662,191 @@ ...@@ -29609,6 +29662,191 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "ReleaseEvidence",
"description": "Evidence for a release",
"fields": [
{
"name": "collectedAt",
"description": "Timestamp when the evidence was collected",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "filepath",
"description": "URL from where the evidence can be downloaded",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the evidence",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sha",
"description": "SHA1 ID of the evidence hash",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseEvidenceConnection",
"description": "The connection type for ReleaseEvidence.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ReleaseEvidenceEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ReleaseEvidence",
"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": "ReleaseEvidenceEdge",
"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": "ReleaseEvidence",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "ReleaseLink", "name": "ReleaseLink",
...@@ -1406,6 +1406,17 @@ Represents a Project Member ...@@ -1406,6 +1406,17 @@ Represents a Project Member
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `assetsCount` | Int | Number of assets of the release | | `assetsCount` | Int | Number of assets of the release |
## ReleaseEvidence
Evidence for a release
| Name | Type | Description |
| --- | ---- | ---------- |
| `collectedAt` | Time | Timestamp when the evidence was collected |
| `filepath` | String | URL from where the evidence can be downloaded |
| `id` | ID! | ID of the evidence |
| `sha` | String | SHA1 ID of the evidence hash |
## ReleaseLink ## ReleaseLink
| Name | Type | Description | | Name | Type | Description |
......
...@@ -6,7 +6,7 @@ module API ...@@ -6,7 +6,7 @@ module API
class Evidence < Grape::Entity class Evidence < Grape::Entity
include ::API::Helpers::Presentable include ::API::Helpers::Presentable
expose :summary_sha, as: :sha expose :sha
expose :filepath expose :filepath
expose :collected_at expose :collected_at
end end
......
...@@ -3,5 +3,7 @@ ...@@ -3,5 +3,7 @@
FactoryBot.define do FactoryBot.define do
factory :evidence, class: 'Releases::Evidence' do factory :evidence, class: 'Releases::Evidence' do
release release
summary_sha { "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d" }
summary { { "release": { "tag": "v4.0", "name": "New release", "project_name": "Project name" } } }
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['ReleaseEvidence'] do
it { expect(described_class).to require_graphql_authorizations(:download_code) }
it 'has the expected fields' do
expected_fields = %w[
id sha filepath collected_at
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
...@@ -9,7 +9,7 @@ describe GitlabSchema.types['Release'] do ...@@ -9,7 +9,7 @@ describe GitlabSchema.types['Release'] do
expected_fields = %w[ expected_fields = %w[
tag_name tag_path tag_name tag_path
description description_html description description_html
name assets milestones author commit name assets milestones evidences author commit
created_at released_at created_at released_at
] ]
...@@ -28,6 +28,12 @@ describe GitlabSchema.types['Release'] do ...@@ -28,6 +28,12 @@ describe GitlabSchema.types['Release'] do
it { is_expected.to have_graphql_type(Types::MilestoneType.connection_type) } it { is_expected.to have_graphql_type(Types::MilestoneType.connection_type) }
end end
describe 'evidences field' do
subject { described_class.fields['evidences'] }
it { is_expected.to have_graphql_type(Types::EvidenceType.connection_type) }
end
describe 'author field' do describe 'author field' do
subject { described_class.fields['author'] } subject { described_class.fields['author'] }
......
...@@ -5,11 +5,12 @@ require 'pp' ...@@ -5,11 +5,12 @@ require 'pp'
describe 'Query.project(fullPath).release(tagName)' do describe 'Query.project(fullPath).release(tagName)' do
include GraphqlHelpers include GraphqlHelpers
include Presentable
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:milestone_1) { create(:milestone, project: project) } let_it_be(:milestone_1) { create(:milestone, project: project) }
let_it_be(:milestone_2) { create(:milestone, project: project) } let_it_be(:milestone_2) { create(:milestone, project: project) }
let_it_be(:release) { create(:release, project: project, milestones: [milestone_1, milestone_2]) } let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2]) }
let_it_be(:release_link_1) { create(:release_link, release: release) } let_it_be(:release_link_1) { create(:release_link, release: release) }
let_it_be(:release_link_2) { create(:release_link, release: release) } let_it_be(:release_link_2) { create(:release_link, release: release) }
let_it_be(:developer) { create(:user) } let_it_be(:developer) { create(:user) }
...@@ -164,5 +165,42 @@ describe 'Query.project(fullPath).release(tagName)' do ...@@ -164,5 +165,42 @@ describe 'Query.project(fullPath).release(tagName)' do
expect(data).to match_array(expected) expect(data).to match_array(expected)
end end
end end
describe 'evidences' do
let(:path) { path_prefix + %w[evidences] }
let(:release_fields) do
query_graphql_field(:evidences, nil, 'nodes { id sha filepath collectedAt }')
end
context 'for a developer' do
it 'finds all evidence fields' do
post_query
evidence = release.evidences.first.present
expected = {
'id' => global_id_of(evidence),
'sha' => evidence.sha,
'filepath' => evidence.filepath,
'collectedAt' => evidence.collected_at.utc.iso8601
}
expect(data["nodes"].first).to eq(expected)
end
end
context 'for a guest' do
let(:current_user) { create :user }
before do
project.add_guest(current_user)
end
it 'denies access' do
post_query
expect(data['node']).to be_nil
end
end
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