Commit 873bb62e authored by Nathan Friend's avatar Nathan Friend

Add basic Release data to GraphQL endpoint

This commit adds basic Release data to our GraphQL endpoint.
parent bac995be
# frozen_string_literal: true
class ReleasesFinder
def initialize(project, current_user = nil)
attr_reader :project, :current_user, :params
def initialize(project, current_user = nil, params = {})
@project = project
@current_user = current_user
@params = params
end
def execute(preload: true)
return Release.none unless Ability.allowed?(@current_user, :read_release, @project)
return Release.none unless Ability.allowed?(current_user, :read_release, project)
# See https://gitlab.com/gitlab-org/gitlab/-/issues/211988
releases = @project.releases.where.not(tag: nil) # rubocop:disable CodeReuse/ActiveRecord
releases = project.releases.where.not(tag: nil) # rubocop:disable CodeReuse/ActiveRecord
releases = by_tag(releases)
releases = releases.preloaded if preload
releases.sorted
end
private
# rubocop: disable CodeReuse/ActiveRecord
def by_tag(releases)
return releases unless params[:tag].present?
releases.where(tag: params[:tag])
end
# rubocop: enable CodeReuse/ActiveRecord
end
# frozen_string_literal: true
module Resolvers
class ReleaseResolver < BaseResolver
type Types::ReleaseType, null: true
argument :tag_name, GraphQL::STRING_TYPE,
required: true,
description: 'The name of the tag associated to the release'
alias_method :project, :object
def self.single
self
end
def resolve(tag_name:)
ReleasesFinder.new(
project,
current_user,
{ tag: tag_name }
).execute.first
end
end
end
# frozen_string_literal: true
module Resolvers
class ReleasesResolver < BaseResolver
type Types::ReleaseType.connection_type, null: true
alias_method :project, :object
# This resolver has a custom singular resolver
def self.single
Resolvers::ReleaseResolver
end
def resolve(**args)
ReleasesFinder.new(
project,
current_user
).execute
end
end
end
......@@ -217,6 +217,20 @@ module Types
null: true,
description: 'A single Alert Management alert of the project',
resolver: Resolvers::AlertManagementAlertResolver.single
field :releases,
Types::ReleaseType.connection_type,
null: true,
description: 'Releases of the project',
resolver: Resolvers::ReleasesResolver,
feature_flag: :graphql_release_data
field :release,
Types::ReleaseType,
null: true,
description: 'A single release of the project',
resolver: Resolvers::ReleasesResolver.single,
feature_flag: :graphql_release_data
end
end
......
# frozen_string_literal: true
module Types
class ReleaseType < BaseObject
graphql_name 'Release'
authorize :read_release
alias_method :release, :object
present_using ReleasePresenter
field :tag_name, GraphQL::STRING_TYPE, null: false, method: :tag,
description: 'Name of the tag associated with the release'
field :tag_path, GraphQL::STRING_TYPE, null: true,
description: 'Relative web path to the tag associated with the release'
field :description, GraphQL::STRING_TYPE, null: true,
description: 'Description (also known as "release notes") of the release'
markdown_field :description_html, null: true
field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the release'
field :evidence_sha, GraphQL::STRING_TYPE, null: true,
description: "SHA of the release's evidence"
field :created_at, Types::TimeType, null: true,
description: 'Timestamp of when the release was created'
field :released_at, Types::TimeType, null: true,
description: 'Timestamp of when the release was released'
field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Milestones associated to the release'
field :author, Types::UserType, null: true,
description: 'User that created the release'
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find
end
field :commit, Types::CommitType, null: true,
complexity: 10, calls_gitaly: true,
description: 'The commit associated with the release',
authorize: :reporter_access
def commit
return if release.sha.nil?
release.project.commit_by(oid: release.sha)
end
end
end
......@@ -7265,6 +7265,41 @@ type Project {
"""
publicJobs: Boolean
"""
A single release of the project. Available only when feature flag `graphql_release_data` is enabled
"""
release(
"""
The name of the tag associated to the release
"""
tagName: String!
): Release
"""
Releases of the project. Available only when feature flag `graphql_release_data` is enabled
"""
releases(
"""
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
): ReleaseConnection
"""
Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project
"""
......@@ -8032,6 +8067,118 @@ enum RegistryState {
SYNCED
}
type Release {
"""
User that created the release
"""
author: User
"""
The commit associated with the release
"""
commit: Commit
"""
Timestamp of when the release was created
"""
createdAt: Time
"""
Description (also known as "release notes") of the release
"""
description: String
"""
The GitLab Flavored Markdown rendering of `description`
"""
descriptionHtml: String
"""
SHA of the release's evidence
"""
evidenceSha: String
"""
Milestones associated to the release
"""
milestones(
"""
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
): MilestoneConnection
"""
Name of the release
"""
name: String
"""
Timestamp of when the release was released
"""
releasedAt: Time
"""
Name of the tag associated with the release
"""
tagName: String!
"""
Relative web path to the tag associated with the release
"""
tagPath: String
}
"""
The connection type for Release.
"""
type ReleaseConnection {
"""
A list of edges.
"""
edges: [ReleaseEdge]
"""
A list of nodes.
"""
nodes: [Release]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type ReleaseEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Release
}
"""
Autogenerated input type of RemoveAwardEmoji
"""
......
......@@ -21612,6 +21612,86 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "release",
"description": "A single release of the project. Available only when feature flag `graphql_release_data` is enabled",
"args": [
{
"name": "tagName",
"description": "The name of the tag associated to the release",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "Release",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releases",
"description": "Releases of the project. Available only when feature flag `graphql_release_data` is enabled",
"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": "ReleaseConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "removeSourceBranchAfterMerge",
"description": "Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project",
......@@ -23853,6 +23933,328 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Release",
"description": null,
"fields": [
{
"name": "author",
"description": "User that created the release",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "User",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "commit",
"description": "The commit associated with the release",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Commit",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "Timestamp of when the release was created",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": "Description (also known as \"release notes\") of the release",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "descriptionHtml",
"description": "The GitLab Flavored Markdown rendering of `description`",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "evidenceSha",
"description": "SHA of the release's evidence",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "milestones",
"description": "Milestones associated to 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": "MilestoneConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the release",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releasedAt",
"description": "Timestamp of when the release was released",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tagName",
"description": "Name of the tag associated with the release",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tagPath",
"description": "Relative web path to the tag associated with the release",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseConnection",
"description": "The connection type for Release.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ReleaseEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Release",
"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": "ReleaseEdge",
"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": "Release",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "RemoveAwardEmojiInput",
......
......@@ -1073,6 +1073,7 @@ Information about pagination in a connection.
| `path` | String! | Path of the project |
| `printingMergeRequestLinkEnabled` | Boolean | Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line |
| `publicJobs` | Boolean | Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts |
| `release` | Release | A single release of the project. Available only when feature flag `graphql_release_data` is enabled |
| `removeSourceBranchAfterMerge` | Boolean | Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project |
| `repository` | Repository | Git repository of the project |
| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
......@@ -1154,6 +1155,21 @@ Information about pagination in a connection.
| `storageSize` | Float! | Storage size of the project |
| `wikiSize` | Float | Wiki size of the project |
## Release
| Name | Type | Description |
| --- | ---- | ---------- |
| `author` | User | User that created the release |
| `commit` | Commit | The commit associated with the release |
| `createdAt` | Time | Timestamp of when the release was created |
| `description` | String | Description (also known as "release notes") of the release |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `evidenceSha` | String | SHA of the release's evidence |
| `name` | String | Name of the release |
| `releasedAt` | Time | Timestamp of when the release was released |
| `tagName` | String! | Name of the tag associated with the release |
| `tagPath` | String | Relative web path to the tag associated with the release |
## RemoveAwardEmojiPayload
Autogenerated return type of RemoveAwardEmoji
......
......@@ -5,10 +5,11 @@ require 'spec_helper'
describe ReleasesFinder do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:params) { {} }
let(:repository) { project.repository }
let(:v1_0_0) { create(:release, project: project, tag: 'v1.0.0') }
let(:v1_1_0) { create(:release, project: project, tag: 'v1.1.0') }
let(:finder) { described_class.new(project, user) }
let(:finder) { described_class.new(project, user, params) }
before do
v1_0_0.update_attribute(:released_at, 2.days.ago)
......@@ -64,6 +65,14 @@ describe ReleasesFinder do
expect(subject).to eq([v1_1_0])
end
end
context 'when a tag parameter is passed' do
let(:params) { { tag: 'v1.0.0' } }
it 'only returns the release with the matching tag' do
expect(subject).to eq([v1_0_0])
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::ReleaseResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project, :private) }
let_it_be(:release) { create(:release, project: project) }
let_it_be(:developer) { create(:user) }
let_it_be(:public_user) { create(:user) }
let(:args) { { tag_name: release.tag } }
before do
project.add_developer(developer)
end
describe '#resolve' do
context 'when the user does not have access to the project' do
let(:current_user) { public_user }
it 'returns nil' do
expect(resolve_release).to be_nil
end
end
context "when the user has full access to the project's releases" do
let(:current_user) { developer }
it 'returns the release associated with the specified tag' do
expect(resolve_release).to eq(release)
end
context 'when no tag_name argument was passed' do
let(:args) { {} }
it 'raises an error' do
expect { resolve_release }.to raise_error(ArgumentError, "missing keyword: tag_name")
end
end
end
end
private
def resolve_release
context = { current_user: current_user }
resolve(described_class, obj: project, args: args, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::ReleasesResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project, :private) }
let_it_be(:release_v1) { create(:release, project: project, tag: 'v1.0.0') }
let_it_be(:release_v2) { create(:release, project: project, tag: 'v2.0.0') }
let_it_be(:developer) { create(:user) }
let_it_be(:public_user) { create(:user) }
before do
project.add_developer(developer)
end
describe '#resolve' do
context 'when the user does not have access to the project' do
let(:current_user) { public_user }
it 'returns an empty array' do
expect(resolve_releases).to eq([])
end
end
context "when the user has full access to the project's releases" do
let(:current_user) { developer }
it 'returns all releases associated to the project' do
expect(resolve_releases).to eq([release_v1, release_v2])
end
end
end
private
def resolve_releases
context = { current_user: current_user }
resolve(described_class, obj: project, args: {}, ctx: context)
end
end
......@@ -24,7 +24,7 @@ describe GitlabSchema.types['Project'] do
namespace group statistics repository merge_requests merge_request issues
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
boards jira_import_status jira_imports services
boards jira_import_status jira_imports services releases release
]
expect(described_class).to include_graphql_fields(*expected_fields)
......@@ -96,4 +96,18 @@ describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::Projects::ServiceType.connection_type) }
end
describe 'releases field' do
subject { described_class.fields['release'] }
it { is_expected.to have_graphql_type(Types::ReleaseType) }
it { is_expected.to have_graphql_resolver(Resolvers::ReleaseResolver) }
end
describe 'release field' do
subject { described_class.fields['releases'] }
it { is_expected.to have_graphql_type(Types::ReleaseType.connection_type) }
it { is_expected.to have_graphql_resolver(Resolvers::ReleasesResolver) }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['Release'] do
it { expect(described_class).to require_graphql_authorizations(:read_release) }
it 'has the expected fields' do
expected_fields = %w[
tag_name tag_path
description description_html
name evidence_sha milestones author commit
created_at released_at
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
describe 'milestones field' do
subject { described_class.fields['milestones'] }
it { is_expected.to have_graphql_type(Types::MilestoneType.connection_type) }
end
describe 'author field' do
subject { described_class.fields['author'] }
it { is_expected.to have_graphql_type(Types::UserType) }
end
describe 'commit field' do
subject { described_class.fields['commit'] }
it { is_expected.to have_graphql_type(Types::CommitType) }
it { is_expected.to require_graphql_authorizations(:reporter_access) }
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