summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZ.J. van de Weg <git@zjvandeweg.nl>2017-02-22 11:13:59 +0100
committerZ.J. van de Weg <git@zjvandeweg.nl>2017-02-22 11:55:21 +0100
commit47a3d8b121b83c1993c4f54bc225859d77cf860b (patch)
treea9feae8aa9f45b7e84426f89e071d34a9080aa46
parenta1f05001e8e26d8bfe12b1ae7a8dbd35e050f5b6 (diff)
downloadgitlab-ce-zj-move-build-traces.tar.gz
Rename Builds to Jobs in the APIzj-move-build-traces
Fixes gitlab-org/gitlab-ce#28515 [ci skip]
-rw-r--r--lib/api/api.rb3
-rw-r--r--lib/api/entities.rb24
-rw-r--r--lib/api/jobs.rb (renamed from lib/api/builds.rb)119
-rw-r--r--lib/api/v3/builds.rb263
-rw-r--r--lib/api/v3/entities.rb10
-rw-r--r--spec/requests/api/jobs_spec.rb477
-rw-r--r--spec/requests/api/v3/builds_spec.rb (renamed from spec/requests/api/builds_spec.rb)2
7 files changed, 826 insertions, 72 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index e729c07f8c3..ebfbd2fb8ef 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -7,6 +7,7 @@ module API
version 'v3', using: :path do
mount ::API::V3::Boards
mount ::API::V3::Branches
+ mount ::API::V3::Builds
mount ::API::V3::DeployKeys
mount ::API::V3::Issues
mount ::API::V3::Labels
@@ -61,7 +62,6 @@ module API
mount ::API::Boards
mount ::API::Branches
mount ::API::BroadcastMessages
- mount ::API::Builds
mount ::API::Commits
mount ::API::CommitStatuses
mount ::API::DeployKeys
@@ -71,6 +71,7 @@ module API
mount ::API::Groups
mount ::API::Internal
mount ::API::Issues
+ mount ::API::Jobs
mount ::API::Keys
mount ::API::Labels
mount ::API::Lint
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 400ee7c92aa..40e9616c70a 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -49,7 +49,8 @@ module API
class ProjectHook < Hook
expose :project_id, :issues_events, :merge_requests_events
- expose :note_events, :build_events, :pipeline_events, :wiki_page_events
+ expose :note_events, :pipeline_events, :wiki_page_events
+ expose :job_events, as: :build_events
end
class BasicProjectDetails < Grape::Entity
@@ -81,7 +82,7 @@ module API
expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
- expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
+ expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
expose :created_at, :last_activity_at
@@ -94,7 +95,7 @@ module API
expose :star_count, :forks_count
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
- expose :public_builds
+ expose :public_jobs, as: :public_builds
expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links.all, options)
end
@@ -110,7 +111,7 @@ module API
expose :storage_size
expose :repository_size
expose :lfs_objects_size
- expose :build_artifacts_size
+ expose :job_artifacts_size, as: :build_artifacts_size
end
class Member < UserBasic
@@ -145,7 +146,7 @@ module API
expose :storage_size
expose :repository_size
expose :lfs_objects_size
- expose :build_artifacts_size
+ expose :job_artifacts_size, as: :build_artifacts_size
end
end
end
@@ -288,7 +289,7 @@ module API
expose :label_names, as: :labels
expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone
- expose :merge_when_build_succeeds
+ expose :merge_when_pipeline_succeeds, as: :merge_when_build_succeeds
expose :merge_status
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
@@ -451,7 +452,8 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :merge_requests_events
- expose :tag_push_events, :note_events, :build_events, :pipeline_events
+ expose :tag_push_events, :note_events, :pipeline_events
+ expose :job_events, as: :build_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
@@ -620,7 +622,7 @@ module API
end
end
- class BuildArtifactFile < Grape::Entity
+ class JobArtifactFile < Grape::Entity
expose :filename, :size
end
@@ -628,11 +630,11 @@ module API
expose :id, :sha, :ref, :status
end
- class Build < Grape::Entity
+ class Job < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
expose :user, with: User
- expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
+ expose :artifacts_file, using: JobArtifactFile, if: -> (build, opts) { build.artifacts? }
expose :commit, with: RepoCommit
expose :runner, with: Runner
expose :pipeline, with: PipelineBasic
@@ -667,7 +669,7 @@ module API
expose :id, :iid, :ref, :sha, :created_at
expose :user, using: Entities::UserBasic
expose :environment, using: Entities::EnvironmentBasic
- expose :deployable, using: Entities::Build
+ expose :deployable, using: Entities::Job
end
class RepoLicense < Grape::Entity
diff --git a/lib/api/builds.rb b/lib/api/jobs.rb
index 44fe0fc4a95..6cca8bee31a 100644
--- a/lib/api/builds.rb
+++ b/lib/api/jobs.rb
@@ -1,5 +1,5 @@
module API
- class Builds < Grape::API
+ class Jobs < Grape::API
include PaginationParams
before { authenticate! }
@@ -10,12 +10,13 @@ module API
resource :projects do
helpers do
params :optional_scope do
- optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
+ optional :scope, types: [String, Array[String]], desc: 'The scope of jobs to show',
values: ['pending', 'running', 'failed', 'success', 'canceled'],
coerce_with: ->(scope) {
- if scope.is_a?(String)
+ case scope
+ when String
[scope]
- elsif scope.is_a?(Hashie::Mash)
+ when Hashie::Mash
scope.values
else
['unknown']
@@ -24,30 +25,30 @@ module API
end
end
- desc 'Get a project builds' do
- success Entities::Build
+ desc 'Get a projects jobs' do
+ success Entities::Job
end
params do
use :optional_scope
use :pagination
end
- get ':id/builds' do
+ get ':id/jobs' do
builds = user_project.builds.order('id DESC')
builds = filter_builds(builds, params[:scope])
- present paginate(builds), with: Entities::Build,
+ present paginate(builds), with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- desc 'Get builds for a specific commit of a project' do
- success Entities::Build
+ desc 'Get jobs for a specific commit of a project' do
+ success Entities::Job
end
params do
requires :sha, type: String, desc: 'The SHA id of a commit'
use :optional_scope
use :pagination
end
- get ':id/repository/commits/:sha/builds' do
+ get ':id/repository/commits/:sha/jobs' do
authorize_read_builds!
return not_found! unless user_project.commit(params[:sha])
@@ -56,47 +57,47 @@ module API
builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
builds = filter_builds(builds, params[:scope])
- present paginate(builds), with: Entities::Build,
+ present paginate(builds), with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- desc 'Get a specific build of a project' do
- success Entities::Build
+ desc 'Get a specific job of a project' do
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- get ':id/builds/:build_id' do
+ get ':id/jobs/:job_id' do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- desc 'Download the artifacts file from build' do
+ desc 'Download the artifacts file from a job' do
detail 'This feature was introduced in GitLab 8.5'
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- get ':id/builds/:build_id/artifacts' do
+ get ':id/jobs/:job_id/artifacts' do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
present_artifacts!(build.artifacts_file)
end
- desc 'Download the artifacts file from build' do
+ desc 'Download the artifacts file from a job' do
detail 'This feature was introduced in GitLab 8.10'
end
params do
requires :ref_name, type: String, desc: 'The ref from repository'
- requires :job, type: String, desc: 'The name for the build'
+ requires :job, type: String, desc: 'The name for the job'
end
- get ':id/builds/artifacts/:ref_name/download',
+ get ':id/jobs/artifacts/:ref_name/download',
requirements: { ref_name: /.+/ } do
authorize_read_builds!
@@ -109,14 +110,14 @@ module API
# TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
# is saved in the DB instead of file). But before that, we need to consider how to replace the value of
# `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
- desc 'Get a trace of a specific build of a project'
+ desc 'Get a trace of a specific job of a project'
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- get ':id/builds/:build_id/trace' do
+ get ':id/jobs/:job_id/trace' do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
content_type 'text/plain'
@@ -126,95 +127,95 @@ module API
body trace
end
- desc 'Cancel a specific build of a project' do
- success Entities::Build
+ desc 'Cancel a specific job of a project' do
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- post ':id/builds/:build_id/cancel' do
+ post ':id/jobs/:job_id/cancel' do
authorize_update_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
build.cancel
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
desc 'Retry a specific build of a project' do
- success Entities::Build
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a build'
end
- post ':id/builds/:build_id/retry' do
+ post ':id/jobs/:job_id/retry' do
authorize_update_builds!
- build = get_build!(params[:build_id])
- return forbidden!('Build is not retryable') unless build.retryable?
+ build = get_build!(params[:job_id])
+ return forbidden!('Job is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user)
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- desc 'Erase build (remove artifacts and build trace)' do
- success Entities::Build
+ desc 'Erase job (remove artifacts and the trace)' do
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a build'
end
- post ':id/builds/:build_id/erase' do
+ post ':id/jobs/:job_id/erase' do
authorize_update_builds!
- build = get_build!(params[:build_id])
- return forbidden!('Build is not erasable!') unless build.erasable?
+ build = get_build!(params[:job_id])
+ return forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
end
desc 'Keep the artifacts to prevent them from being deleted' do
- success Entities::Build
+ success Entities::Job
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a build'
+ requires :job_id, type: Integer, desc: 'The ID of a job'
end
- post ':id/builds/:build_id/artifacts/keep' do
+ post ':id/jobs/:job_id/artifacts/keep' do
authorize_update_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
return not_found!(build) unless build.artifacts?
build.keep_artifacts!
status 200
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
- desc 'Trigger a manual build' do
- success Entities::Build
+ desc 'Trigger a manual job' do
+ success Entities::Job
detail 'This feature was added in GitLab 8.11'
end
params do
- requires :build_id, type: Integer, desc: 'The ID of a Build'
+ requires :job_id, type: Integer, desc: 'The ID of a Job'
end
- post ":id/builds/:build_id/play" do
+ post ":id/jobs/:job_id/play" do
authorize_read_builds!
- build = get_build!(params[:build_id])
+ build = get_build!(params[:job_id])
bad_request!("Unplayable Job") unless build.playable?
build.play(current_user)
status 200
- present build, with: Entities::Build,
+ present build, with: Entities::Job,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
end
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
new file mode 100644
index 00000000000..33f9cfa6927
--- /dev/null
+++ b/lib/api/v3/builds.rb
@@ -0,0 +1,263 @@
+module API
+ module V3
+ class Builds < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ helpers do
+ params :optional_scope do
+ optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
+ values: ['pending', 'running', 'failed', 'success', 'canceled'],
+ coerce_with: ->(scope) {
+ if scope.is_a?(String)
+ [scope]
+ elsif scope.is_a?(Hashie::Mash)
+ scope.values
+ else
+ ['unknown']
+ end
+ }
+ end
+ end
+
+ desc 'Get a project builds' do
+ success V3::Entities::Build
+ end
+ params do
+ use :optional_scope
+ use :pagination
+ end
+ get ':id/builds' do
+ builds = user_project.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Get builds for a specific commit of a project' do
+ success Entities::Build
+ end
+ params do
+ requires :sha, type: String, desc: 'The SHA id of a commit'
+ use :optional_scope
+ use :pagination
+ end
+ get ':id/repository/commits/:sha/builds' do
+ authorize_read_builds!
+
+ return not_found! unless user_project.commit(params[:sha])
+
+ pipelines = user_project.pipelines.where(sha: params[:sha])
+ builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Get a specific build of a project' do
+ success Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ get ':id/builds/:build_id' do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Download the artifacts file from build' do
+ detail 'This feature was introduced in GitLab 8.5'
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ get ':id/builds/:build_id/artifacts' do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ present_artifacts!(build.artifacts_file)
+ end
+
+ desc 'Download the artifacts file from build' do
+ detail 'This feature was introduced in GitLab 8.10'
+ end
+ params do
+ requires :ref_name, type: String, desc: 'The ref from repository'
+ requires :job, type: String, desc: 'The name for the build'
+ end
+ get ':id/builds/artifacts/:ref_name/download',
+ requirements: { ref_name: /.+/ } do
+ authorize_read_builds!
+
+ builds = user_project.latest_successful_builds_for(params[:ref_name])
+ latest_build = builds.find_by!(name: params[:job])
+
+ present_artifacts!(latest_build.artifacts_file)
+ end
+
+ # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
+ # is saved in the DB instead of file). But before that, we need to consider how to replace the value of
+ # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
+ desc 'Get a trace of a specific build of a project'
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ get ':id/builds/:build_id/trace' do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
+ content_type 'text/plain'
+ env['api.format'] = :binary
+
+ trace = build.trace
+ body trace
+ end
+
+ desc 'Cancel a specific build of a project' do
+ success Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/cancel' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+
+ build.cancel
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Retry a specific build of a project' do
+ success Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/retry' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return forbidden!('Build is not retryable') unless build.retryable?
+
+ build = Ci::Build.retry(build, current_user)
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Erase build (remove artifacts and build trace)' do
+ success Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/erase' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return forbidden!('Build is not erasable!') unless build.erasable?
+
+ build.erase(erased_by: current_user)
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+
+ desc 'Keep the artifacts to prevent them from being deleted' do
+ success Entities::Build
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a build'
+ end
+ post ':id/builds/:build_id/artifacts/keep' do
+ authorize_update_builds!
+
+ build = get_build!(params[:build_id])
+ return not_found!(build) unless build.artifacts?
+
+ build.keep_artifacts!
+
+ status 200
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+
+ desc 'Trigger a manual build' do
+ success Entities::Build
+ detail 'This feature was added in GitLab 8.11'
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a Build'
+ end
+ post ":id/builds/:build_id/play" do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ bad_request!("Unplayable Job") unless build.playable?
+
+ build.play(current_user)
+
+ status 200
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
+ end
+
+ helpers do
+ def get_build(id)
+ user_project.builds.find_by(id: id.to_i)
+ end
+
+ def get_build!(id)
+ get_build(id) || not_found!
+ end
+
+ def present_artifacts!(artifacts_file)
+ if !artifacts_file.file_storage?
+ redirect_to(build.artifacts_file.url)
+ elsif artifacts_file.exists?
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ else
+ not_found!
+ end
+ end
+
+ def filter_builds(builds, scope)
+ return builds if scope.nil? || scope.empty?
+
+ available_statuses = ::CommitStatus::AVAILABLE_STATUSES
+
+ unknown = scope - available_statuses
+ render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
+
+ builds.where(status: available_statuses && scope)
+ end
+
+ def authorize_read_builds!
+ authorize! :read_build, user_project
+ end
+
+ def authorize_update_builds!
+ authorize! :update_build, user_project
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 3cc0dc968a8..7daa653905a 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -11,6 +11,16 @@ module API
Gitlab::UrlBuilder.build(snippet)
end
end
+
+ class Build < Grape::Entity
+ expose :id, :status, :stage, :name, :ref, :tag, :coverage
+ expose :created_at, :started_at, :finished_at
+ expose :user, with: ::API::Entities::User
+ expose :artifacts_file, using: ::API::Entities::JobArtifactFile, if: -> (build, opts) { build.artifacts? }
+ expose :commit, with: ::API::Entities::RepoCommit
+ expose :runner, with: ::API::Entities::Runner
+ expose :pipeline, with: ::API::Entities::PipelineBasic
+ end
end
end
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
new file mode 100644
index 00000000000..f0e08e73763
--- /dev/null
+++ b/spec/requests/api/jobs_spec.rb
@@ -0,0 +1,477 @@
+require 'spec_helper'
+
+describe API::Jobs, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:api_user) { user }
+ let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
+ let!(:developer) { create(:project_member, :developer, user: user, project: project) }
+ let(:reporter) { create(:project_member, :reporter, project: project) }
+ let(:guest) { create(:project_member, :guest, project: project) }
+ let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+ describe 'GET /projects/:id/jobs' do
+ let(:query) { Hash.new }
+
+ before do
+ get api("/projects/#{project.id}/jobs", api_user), query
+ end
+
+ context 'authorized user' do
+ it 'returns project jobs' do
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ end
+
+ it 'returns pipeline data' do
+ json_build = json_response.first
+
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+
+ context 'filter project with one scope element' do
+ let(:query) { { 'scope' => 'pending' } }
+
+ it do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'filter project with array of scope elements' do
+ let(:query) { { 'scope[0]' => 'pending', 'scope[1]' => 'running' } }
+
+ it do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'respond 400 when scope contains invalid state' do
+ let(:query) { { 'scope[0]' => 'unknown', 'scope[1]' => 'running' } }
+
+ it { expect(response).to have_http_status(400) }
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return project builds' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/repository/commits/:sha/jobs' do
+ context 'when commit does not exist in repository' do
+ before do
+ get api("/projects/#{project.id}/repository/commits/1a271fd1/jobs", api_user)
+ end
+
+ it 'responds with 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when commit exists in repository' do
+ context 'when user is authorized' do
+ context 'when pipeline has jobs' do
+ before do
+ create(:ci_pipeline, project: project, sha: project.commit.id)
+ create(:ci_build, pipeline: pipeline)
+ create(:ci_build)
+
+ get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/jobs", api_user)
+ end
+
+ it 'returns project jobs for specific commit' do
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq 2
+ end
+
+ it 'returns pipeline data' do
+ json_build = json_response.first
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+ end
+
+ context 'when pipeline has no jobs' do
+ before do
+ branch_head = project.commit('feature').id
+ get api("/projects/#{project.id}/repository/commits/#{branch_head}/jobs", api_user)
+ end
+
+ it 'returns an empty array' do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response).to be_empty
+ end
+ end
+ end
+
+ context 'when user is not authorized' do
+ before do
+ create(:ci_pipeline, project: project, sha: project.commit.id)
+ create(:ci_build, pipeline: pipeline)
+
+ get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/jobs", nil)
+ end
+
+ it 'does not return project jobs' do
+ expect(response).to have_http_status(401)
+ expect(json_response.except('message')).to be_empty
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/jobs/:job_id' do
+ before do
+ get api("/projects/#{project.id}/jobs/#{build.id}", api_user)
+ end
+
+ context 'authorized user' do
+ it 'returns specific job data' do
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq('test')
+ end
+
+ it 'returns pipeline data' do
+ json_build = json_response
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job data' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/jobs/:job_id/artifacts' do
+ before do
+ get api("/projects/#{project.id}/jobs/#{build.id}/artifacts", api_user)
+ end
+
+ context 'job with artifacts' do
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ context 'authorized user' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+ end
+
+ it 'returns specific job artifacts' do
+ expect(response).to have_http_status(200)
+ expect(response.headers).to include(download_headers)
+ expect(response.body).to match_file(build.artifacts_file.file.file)
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job artifacts' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ it 'does not return job artifacts if not uploaded' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
+ let(:api_user) { reporter.user }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ before do
+ build.success
+ end
+
+ def get_for_ref(ref = pipeline.ref, job = build.name)
+ get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), job: job
+ end
+
+ context 'when not logged in' do
+ let(:api_user) { nil }
+
+ before do
+ get_for_ref
+ end
+
+ it 'gives 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when logging as guest' do
+ let(:api_user) { guest.user }
+
+ before do
+ get_for_ref
+ end
+
+ it 'gives 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'non-existing job' do
+ shared_examples 'not found' do
+ it { expect(response).to have_http_status(:not_found) }
+ end
+
+ context 'has no such ref' do
+ before do
+ get_for_ref('TAIL')
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no such job' do
+ before do
+ get_for_ref(pipeline.ref, 'NOBUILD')
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+
+ context 'find proper job' do
+ shared_examples 'a valid file' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' =>
+ "attachment; filename=#{build.artifacts_file.filename}" }
+ end
+
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.headers).to include(download_headers) }
+ end
+
+ context 'with regular branch' do
+ before do
+ pipeline.reload
+ pipeline.update(ref: 'master',
+ sha: project.commit('master').sha)
+
+ get_for_ref('master')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+
+ context 'with branch name containing slash' do
+ before do
+ pipeline.reload
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+ end
+
+ before do
+ get_for_ref('improve/awesome')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/jobs/:job_id/trace' do
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
+
+ before do
+ get api("/projects/#{project.id}/jobs/#{build.id}/trace", api_user)
+ end
+
+ context 'authorized user' do
+ it 'returns specific job trace' do
+ expect(response).to have_http_status(200)
+ expect(response.body).to eq(build.trace)
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job trace' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/jobs/:job_id/cancel' do
+ before do
+ post api("/projects/#{project.id}/jobs/#{build.id}/cancel", api_user)
+ end
+
+ context 'authorized user' do
+ context 'user with :update_build persmission' do
+ it 'cancels running or pending job' do
+ expect(response).to have_http_status(201)
+ expect(project.builds.first.status).to eq('canceled')
+ end
+ end
+
+ context 'user without :update_build permission' do
+ let(:api_user) { reporter.user }
+
+ it 'does not cancel job' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not cancel job' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/jobs/:job_id/retry' do
+ let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
+
+ before do
+ post api("/projects/#{project.id}/jobs/#{build.id}/retry", api_user)
+ end
+
+ context 'authorized user' do
+ context 'user with :update_build permission' do
+ it 'retries non-running job' do
+ expect(response).to have_http_status(201)
+ expect(project.builds.first.status).to eq('canceled')
+ expect(json_response['status']).to eq('pending')
+ end
+ end
+
+ context 'user without :update_build permission' do
+ let(:api_user) { reporter.user }
+
+ it 'does not retry job' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not retry job' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/jobs/:job_id/erase' do
+ before do
+ post api("/projects/#{project.id}/jobs/#{build.id}/erase", user)
+ end
+
+ context 'job is erasable' do
+ let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
+
+ it 'erases job content' do
+ expect(response).to have_http_status(201)
+ expect(build.trace).to be_empty
+ expect(build.artifacts_file.exists?).to be_falsy
+ expect(build.artifacts_metadata.exists?).to be_falsy
+ end
+
+ it 'updates job' do
+ build.reload
+ expect(build.erased_at).to be_truthy
+ expect(build.erased_by).to eq(user)
+ end
+ end
+
+ context 'job is not erasable' do
+ let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
+
+ it 'responds with forbidden' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/jobs/:build_id/artifacts/keep' do
+ before do
+ post api("/projects/#{project.id}/jobs/#{build.id}/artifacts/keep", user)
+ end
+
+ context 'artifacts did not expire' do
+ let(:build) do
+ create(:ci_build, :trace, :artifacts, :success,
+ project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
+ end
+
+ it 'keeps artifacts' do
+ expect(response).to have_http_status(200)
+ expect(build.reload.artifacts_expire_at).to be_nil
+ end
+ end
+
+ context 'no artifacts' do
+ let(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+ it 'responds with not found' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/jobs/:job_id/play' do
+ before do
+ post api("/projects/#{project.id}/jobs/#{build.id}/play", user)
+ end
+
+ context 'on an playable job' do
+ let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
+
+ it 'plays the job' do
+ expect(response).to have_http_status(200)
+ expect(json_response['user']['id']).to eq(user.id)
+ expect(json_response['id']).to eq(build.id)
+ end
+ end
+
+ context 'on a non-playable job' do
+ it 'returns a status code 400, Bad Request' do
+ expect(response).to have_http_status 400
+ expect(response.body).to match("Unplayable Job")
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index 38aef7f2767..e9530253116 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::Builds, api: true do
+describe API::V3::Builds, api: true do
include ApiHelpers
let(:user) { create(:user) }