Commit 08b9532e authored by Lin Jen-Shin's avatar Lin Jen-Shin

API for downloading latest successful build:

This was extracted from !5142 and implementing part of #4255.
We split it from !5142 because we want to ship it in 8.10
while !5142 was not ready yet.
parent 1cd573ee
...@@ -15,6 +15,9 @@ module Ci ...@@ -15,6 +15,9 @@ module Ci
scope :with_artifacts, ->() { where.not(artifacts_file: nil) } scope :with_artifacts, ->() { where.not(artifacts_file: nil) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :latest_successful_with_artifacts, ->() do
with_artifacts.success.order(id: :desc)
end
mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader
......
...@@ -20,6 +20,14 @@ module Ci ...@@ -20,6 +20,14 @@ module Ci
after_touch :update_state after_touch :update_state
after_save :keep_around_commits after_save :keep_around_commits
# ref can't be HEAD, can only be branch/tag name or SHA
scope :latest_successful_for, ->(ref) do
table = quoted_table_name
# TODO: Use `where(ref: ref).or(sha: ref)` in Rails 5
where("#{table}.ref = ? OR #{table}.sha = ?", ref, ref).
success.order(id: :desc)
end
def self.truncate_sha(sha) def self.truncate_sha(sha)
sha[0...8] sha[0...8]
end end
...@@ -222,7 +230,7 @@ module Ci ...@@ -222,7 +230,7 @@ module Ci
def keep_around_commits def keep_around_commits
return unless project return unless project
project.repository.keep_around(self.sha) project.repository.keep_around(self.sha)
project.repository.keep_around(self.before_sha) project.repository.keep_around(self.before_sha)
end end
......
...@@ -429,6 +429,13 @@ class Project < ActiveRecord::Base ...@@ -429,6 +429,13 @@ class Project < ActiveRecord::Base
repository.commit(ref) repository.commit(ref)
end end
# ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = 'master')
Ci::Build.joins(:pipeline).
merge(pipelines.latest_successful_for(ref)).
latest_successful_with_artifacts
end
def merge_base_commit(first_commit_id, second_commit_id) def merge_base_commit(first_commit_id, second_commit_id)
sha = repository.merge_base(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id)
repository.commit(sha) if sha repository.commit(sha) if sha
......
...@@ -52,8 +52,7 @@ module API ...@@ -52,8 +52,7 @@ module API
get ':id/builds/:build_id' do get ':id/builds/:build_id' do
authorize_read_builds! authorize_read_builds!
build = get_build(params[:build_id]) build = get_build!(params[:build_id])
return not_found!(build) unless build
present build, with: Entities::Build, present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project) user_can_download_artifacts: can?(current_user, :read_build, user_project)
...@@ -69,18 +68,25 @@ module API ...@@ -69,18 +68,25 @@ module API
get ':id/builds/:build_id/artifacts' do get ':id/builds/:build_id/artifacts' do
authorize_read_builds! authorize_read_builds!
build = get_build(params[:build_id]) build = get_build!(params[:build_id])
return not_found!(build) unless build
artifacts_file = build.artifacts_file present_artifact!(build.artifacts_file)
end
unless artifacts_file.file_storage?
return redirect_to build.artifacts_file.url
end
return not_found! unless artifacts_file.exists? # Download the artifacts file from ref_name and job
#
# Parameters:
# id (required) - The ID of a project
# ref_name (required) - The ref from repository
# job (required) - The name for the build
# Example Request:
# GET /projects/:id/artifacts/:ref_name/download?job=name
get ':id/builds/artifacts/:ref_name/download',
requirements: { ref_name: /.+/ } do
builds = user_project.latest_successful_builds_for(params[:ref_name])
latest_build = builds.find_by!(name: params[:job])
present_file!(artifacts_file.path, artifacts_file.filename) present_artifact!(latest_build.artifacts_file)
end end
# Get a trace of a specific build of a project # Get a trace of a specific build of a project
...@@ -97,8 +103,7 @@ module API ...@@ -97,8 +103,7 @@ module API
get ':id/builds/:build_id/trace' do get ':id/builds/:build_id/trace' do
authorize_read_builds! authorize_read_builds!
build = get_build(params[:build_id]) build = get_build!(params[:build_id])
return not_found!(build) unless build
header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
content_type 'text/plain' content_type 'text/plain'
...@@ -118,8 +123,7 @@ module API ...@@ -118,8 +123,7 @@ module API
post ':id/builds/:build_id/cancel' do post ':id/builds/:build_id/cancel' do
authorize_update_builds! authorize_update_builds!
build = get_build(params[:build_id]) build = get_build!(params[:build_id])
return not_found!(build) unless build
build.cancel build.cancel
...@@ -137,8 +141,7 @@ module API ...@@ -137,8 +141,7 @@ module API
post ':id/builds/:build_id/retry' do post ':id/builds/:build_id/retry' do
authorize_update_builds! authorize_update_builds!
build = get_build(params[:build_id]) build = get_build!(params[:build_id])
return not_found!(build) unless build
return forbidden!('Build is not retryable') unless build.retryable? return forbidden!('Build is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user) build = Ci::Build.retry(build, current_user)
...@@ -157,8 +160,7 @@ module API ...@@ -157,8 +160,7 @@ module API
post ':id/builds/:build_id/erase' do post ':id/builds/:build_id/erase' do
authorize_update_builds! authorize_update_builds!
build = get_build(params[:build_id]) build = get_build!(params[:build_id])
return not_found!(build) unless build
return forbidden!('Build is not erasable!') unless build.erasable? return forbidden!('Build is not erasable!') unless build.erasable?
build.erase(erased_by: current_user) build.erase(erased_by: current_user)
...@@ -176,8 +178,8 @@ module API ...@@ -176,8 +178,8 @@ module API
post ':id/builds/:build_id/artifacts/keep' do post ':id/builds/:build_id/artifacts/keep' do
authorize_update_builds! authorize_update_builds!
build = get_build(params[:build_id]) build = get_build!(params[:build_id])
return not_found!(build) unless build && build.artifacts? return not_found!(build) unless build.artifacts?
build.keep_artifacts! build.keep_artifacts!
...@@ -192,6 +194,20 @@ module API ...@@ -192,6 +194,20 @@ module API
user_project.builds.find_by(id: id.to_i) user_project.builds.find_by(id: id.to_i)
end end
def get_build!(id)
get_build(id) || not_found!
end
def present_artifact!(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) def filter_builds(builds, scope)
return builds if scope.nil? || scope.empty? return builds if scope.nil? || scope.empty?
......
...@@ -5,7 +5,9 @@ describe Ci::Build, models: true do ...@@ -5,7 +5,9 @@ describe Ci::Build, models: true do
let(:pipeline) do let(:pipeline) do
create(:ci_pipeline, project: project, create(:ci_pipeline, project: project,
sha: project.commit.id) sha: project.commit.id,
ref: 'fix',
status: 'success')
end end
let(:build) { create(:ci_build, pipeline: pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) }
...@@ -612,7 +614,7 @@ describe Ci::Build, models: true do ...@@ -612,7 +614,7 @@ describe Ci::Build, models: true do
describe '#erasable?' do describe '#erasable?' do
subject { build.erasable? } subject { build.erasable? }
it { is_expected.to eq true } it { is_expected.to be_truthy }
end end
describe '#erased?' do describe '#erased?' do
...@@ -620,7 +622,7 @@ describe Ci::Build, models: true do ...@@ -620,7 +622,7 @@ describe Ci::Build, models: true do
subject { build.erased? } subject { build.erased? }
context 'build has not been erased' do context 'build has not been erased' do
it { is_expected.to be false } it { is_expected.to be_falsey }
end end
context 'build has been erased' do context 'build has been erased' do
...@@ -628,12 +630,13 @@ describe Ci::Build, models: true do ...@@ -628,12 +630,13 @@ describe Ci::Build, models: true do
build.erase build.erase
end end
it { is_expected.to be true } it { is_expected.to be_truthy }
end end
end end
context 'metadata and build trace are not available' do context 'metadata and build trace are not available' do
let!(:build) { create(:ci_build, :success, :artifacts) } let!(:build) { create(:ci_build, :success, :artifacts) }
before do before do
build.remove_artifacts_metadata! build.remove_artifacts_metadata!
end end
...@@ -655,18 +658,58 @@ describe Ci::Build, models: true do ...@@ -655,18 +658,58 @@ describe Ci::Build, models: true do
describe '#retryable?' do describe '#retryable?' do
context 'when build is running' do context 'when build is running' do
before { build.run! } before do
build.run!
end
it 'should return false' do it 'returns false' do
expect(build.retryable?).to be false expect(build).not_to be_retryable
end end
end end
context 'when build is finished' do context 'when build is finished' do
before { build.success! } before do
build.success!
end
it 'returns true' do
expect(build).to be_retryable
end
end
end
describe 'Project#latest_successful_builds_for' do
let(:build) do
create(:ci_build, :artifacts, :success, pipeline: pipeline)
end
before do
build
end
context 'with succeed pipeline' do
it 'returns builds from ref' do
builds = project.latest_successful_builds_for('fix')
expect(builds).to contain_exactly(build)
end
it 'returns empty relation if the build cannot be found' do
builds = project.latest_successful_builds_for('TAIL').all
expect(builds).to be_empty
end
end
context 'with pending pipeline' do
before do
pipeline.update(status: 'pending')
end
it 'returns empty relation' do
builds = project.latest_successful_builds_for('fix').all
it 'should return true' do expect(builds).to be_empty
expect(build.retryable?).to be true
end end
end end
end end
......
...@@ -377,7 +377,7 @@ describe Project, models: true do ...@@ -377,7 +377,7 @@ describe Project, models: true do
describe '#repository' do describe '#repository' do
let(:project) { create(:project) } let(:project) { create(:project) }
it 'should return valid repo' do it 'returns valid repo' do
expect(project.repository).to be_kind_of(Repository) expect(project.repository).to be_kind_of(Repository)
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe API::API, api: true do describe API::API, api: true do
include ApiHelpers include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:api_user) { user } let(:api_user) { user }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) } let(:project) { create(:project, creator_id: user.id) }
let!(:developer) { create(:project_member, :developer, user: user, project: project) } let(:developer) { create(:project_member, :developer, user: user, project: project) }
let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) } let(:reporter) { create(:project_member, :reporter, user: user2, project: project) }
let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) } let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) }
let!(:build) { create(:ci_build, pipeline: pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) }
describe 'GET /projects/:id/builds ' do describe 'GET /projects/:id/builds ' do
let(:query) { '' } let(:query) { '' }
before { get api("/projects/#{project.id}/builds?#{query}", api_user) } before do
developer
build
get api("/projects/#{project.id}/builds?#{query}", api_user)
end
context 'authorized user' do context 'authorized user' do
it 'should return project builds' do it 'should return project builds' do
...@@ -77,9 +82,9 @@ describe API::API, api: true do ...@@ -77,9 +82,9 @@ describe API::API, api: true do
context 'when user is authorized' do context 'when user is authorized' do
context 'when pipeline has builds' do context 'when pipeline has builds' do
before do before do
create(:ci_pipeline, project: project, sha: project.commit.id) developer
build
create(:ci_build, pipeline: pipeline) create(:ci_build, pipeline: pipeline)
create(:ci_build)
get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
end end
...@@ -93,6 +98,8 @@ describe API::API, api: true do ...@@ -93,6 +98,8 @@ describe API::API, api: true do
context 'when pipeline has no builds' do context 'when pipeline has no builds' do
before do before do
developer
branch_head = project.commit('feature').id branch_head = project.commit('feature').id
get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user)
end end
...@@ -107,8 +114,7 @@ describe API::API, api: true do ...@@ -107,8 +114,7 @@ describe API::API, api: true do
context 'when user is not authorized' do context 'when user is not authorized' do
before do before do
create(:ci_pipeline, project: project, sha: project.commit.id) build
create(:ci_build, pipeline: pipeline)
get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
end end
...@@ -122,7 +128,11 @@ describe API::API, api: true do ...@@ -122,7 +128,11 @@ describe API::API, api: true do
end end
describe 'GET /projects/:id/builds/:build_id' do describe 'GET /projects/:id/builds/:build_id' do
before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) } before do
developer
get api("/projects/#{project.id}/builds/#{build.id}", api_user)
end
context 'authorized user' do context 'authorized user' do
it 'should return specific build data' do it 'should return specific build data' do
...@@ -141,7 +151,11 @@ describe API::API, api: true do ...@@ -141,7 +151,11 @@ describe API::API, api: true do
end end
describe 'GET /projects/:id/builds/:build_id/artifacts' do describe 'GET /projects/:id/builds/:build_id/artifacts' do
before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) } before do
developer
get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
end
context 'build with artifacts' do context 'build with artifacts' do
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
...@@ -172,10 +186,146 @@ describe API::API, api: true do ...@@ -172,10 +186,146 @@ describe API::API, api: true do
end end
end end
describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: project.commit('fix').sha,
ref: 'fix')
end
let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
before do
project.team << [user, :developer]
end
def path_from_ref(ref = pipeline.ref, job = build.name)
api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", user)
end
context 'not logging in' do
let(:user) { nil }
before do
get path_from_ref
end
it 'gives 401 for unauthorized user' do
expect(response).to have_http_status(401)
end
end
context 'non-existing build' do
def verify
expect(response).to have_http_status(404)
end
context 'has no such ref' do
before do
get path_from_ref('TAIL', build.name)
end
it('gives 404') { verify }
end
context 'has no such build' do
before do
get path_from_ref(pipeline.ref, 'NOBUILD')
end
it('gives 404') { verify }
end
end
context 'find proper build' do
def verify
download_headers =
{ 'Content-Transfer-Encoding' => 'binary',
'Content-Disposition' =>
"attachment; filename=#{build.artifacts_file.filename}" }
expect(response).to have_http_status(200)
expect(response.headers).to include(download_headers)
end
def create_new_pipeline(status)
new_pipeline = create(:ci_pipeline, status: 'success')
create(:ci_build, status, :artifacts, pipeline: new_pipeline)
end
context 'with sha' do
before do
get path_from_ref(pipeline.sha)
end
it('gives the file') { verify }
end
context 'with regular branch' do
before do
pipeline.update(ref: 'master',
sha: project.commit('master').sha)
end
before do
get path_from_ref('master')
end
it('gives the file') { verify }
end
context 'with branch name containing slash' do
before do
pipeline.update(ref: 'improve/awesome',
sha: project.commit('improve/awesome').sha)
end
before do
get path_from_ref('improve/awesome')
end
it('gives the file') { verify }
end
context 'with latest pipeline' do
before do
3.times do # creating some old pipelines
create_new_pipeline(:success)
end
end
before do
get path_from_ref
end
it('gives the file') { verify }
end
context 'with success pipeline' do
before do
build # make sure pipeline was old, but still the latest success one
create_new_pipeline(:pending)
end
before do
get path_from_ref
end
it('gives the file') { verify }
end
end
end
describe 'GET /projects/:id/builds/:build_id/trace' do describe 'GET /projects/:id/builds/:build_id/trace' do
let(:build) { create(:ci_build, :trace, pipeline: pipeline) } let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) } before do
developer
get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user)
end
context 'authorized user' do context 'authorized user' do
it 'should return specific build trace' do it 'should return specific build trace' do
...@@ -194,7 +344,12 @@ describe API::API, api: true do ...@@ -194,7 +344,12 @@ describe API::API, api: true do
end end
describe 'POST /projects/:id/builds/:build_id/cancel' do describe 'POST /projects/:id/builds/:build_id/cancel' do
before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) } before do
developer
reporter
post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user)
end
context 'authorized user' do context 'authorized user' do
context 'user with :update_build persmission' do context 'user with :update_build persmission' do
...@@ -225,7 +380,12 @@ describe API::API, api: true do ...@@ -225,7 +380,12 @@ describe API::API, api: true do
describe 'POST /projects/:id/builds/:build_id/retry' do describe 'POST /projects/:id/builds/:build_id/retry' do
let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) } before do
developer
reporter
post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user)
end
context 'authorized user' do context 'authorized user' do
context 'user with :update_build permission' do context 'user with :update_build permission' do
...@@ -256,6 +416,8 @@ describe API::API, api: true do ...@@ -256,6 +416,8 @@ describe API::API, api: true do
describe 'POST /projects/:id/builds/:build_id/erase' do describe 'POST /projects/:id/builds/:build_id/erase' do
before do before do
developer
post api("/projects/#{project.id}/builds/#{build.id}/erase", user) post api("/projects/#{project.id}/builds/#{build.id}/erase", user)
end end
...@@ -286,6 +448,8 @@ describe API::API, api: true do ...@@ -286,6 +448,8 @@ describe API::API, api: true do
describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do
before do before do
developer
post api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user) post api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user)
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