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 # frozen_string_literal: true
class ReleasesFinder class ReleasesFinder
def initialize(project, current_user = nil) attr_reader :project, :current_user, :params
def initialize(project, current_user = nil, params = {})
@project = project @project = project
@current_user = current_user @current_user = current_user
@params = params
end end
def execute(preload: true) 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 # 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 = releases.preloaded if preload
releases.sorted releases.sorted
end 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 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 ...@@ -217,6 +217,20 @@ module Types
null: true, null: true,
description: 'A single Alert Management alert of the project', description: 'A single Alert Management alert of the project',
resolver: Resolvers::AlertManagementAlertResolver.single 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
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 { ...@@ -7265,6 +7265,41 @@ type Project {
""" """
publicJobs: Boolean 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 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 { ...@@ -8032,6 +8067,118 @@ enum RegistryState {
SYNCED 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 Autogenerated input type of RemoveAwardEmoji
""" """
......
...@@ -1073,6 +1073,7 @@ Information about pagination in a connection. ...@@ -1073,6 +1073,7 @@ Information about pagination in a connection.
| `path` | String! | Path of the project | | `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 | | `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 | | `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 | | `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 | | `repository` | Repository | Git repository of the project |
| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project | | `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
...@@ -1154,6 +1155,21 @@ Information about pagination in a connection. ...@@ -1154,6 +1155,21 @@ Information about pagination in a connection.
| `storageSize` | Float! | Storage size of the project | | `storageSize` | Float! | Storage size of the project |
| `wikiSize` | Float | Wiki 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 ## RemoveAwardEmojiPayload
Autogenerated return type of RemoveAwardEmoji Autogenerated return type of RemoveAwardEmoji
......
...@@ -5,10 +5,11 @@ require 'spec_helper' ...@@ -5,10 +5,11 @@ require 'spec_helper'
describe ReleasesFinder do describe ReleasesFinder do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:params) { {} }
let(:repository) { project.repository } let(:repository) { project.repository }
let(:v1_0_0) { create(:release, project: project, tag: 'v1.0.0') } 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(: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 before do
v1_0_0.update_attribute(:released_at, 2.days.ago) v1_0_0.update_attribute(:released_at, 2.days.ago)
...@@ -64,6 +65,14 @@ describe ReleasesFinder do ...@@ -64,6 +65,14 @@ describe ReleasesFinder do
expect(subject).to eq([v1_1_0]) expect(subject).to eq([v1_1_0])
end end
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 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 ...@@ -24,7 +24,7 @@ describe GitlabSchema.types['Project'] do
namespace group statistics repository merge_requests merge_request issues namespace group statistics repository merge_requests merge_request issues
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments 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) expect(described_class).to include_graphql_fields(*expected_fields)
...@@ -96,4 +96,18 @@ describe GitlabSchema.types['Project'] do ...@@ -96,4 +96,18 @@ describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::Projects::ServiceType.connection_type) } it { is_expected.to have_graphql_type(Types::Projects::ServiceType.connection_type) }
end 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 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