Merge branch 'commit_status' into 'master'
Implement Commit Status API This is preliminary implementation of Commit Status API, pretty much compatible with GitHub. 1. The Commit Statuses are stored in separate table: ci_commit_status. 2. The POST inserts a new row. 3. To POST execute GitLab API `post :id/repository/commits/:sha/status`. This accepts dual authorization: - Using authorized user - Using ci-token to allow easy posting from CI Services 4. This adds predefined variable to GitLab CI build environment: CI_BUILD_STATUS_URL, allowing to easy post status from within build (ex. with code coverage or other metrics). 5. This adds statuses to commit's builds view. 6. The commit's status is calculated taking into account status of all builds and all posted statuses. 7. The commit statuses doesn't trigger notifications. 8. The commit status API introduces two new privileges: `read_commit_statuses` and `create_commit_status`. 9. We still miss a few tests and documentation updates for API and CI. @dzaporozhets @sytses What do you think? See merge request !1530
This commit is contained in:
commit
e3edd53ae4
|
@ -16,6 +16,7 @@ v 8.1.0 (unreleased)
|
|||
- Move CI charts to project graphs area
|
||||
- Fix cases where Markdown did not render links in activity feed (Stan Hu)
|
||||
- Add first and last to pagination (Zeger-Jan van de Weg)
|
||||
- Added Commit Status API
|
||||
- Show CI status on commit page
|
||||
- Show CI status on Your projects page and Starred projects page
|
||||
- Remove "Continuous Integration" page from dashboard
|
||||
|
|
|
@ -135,6 +135,8 @@ class Ability
|
|||
|
||||
def project_report_rules
|
||||
project_guest_rules + [
|
||||
:create_commit_status,
|
||||
:read_commit_statuses,
|
||||
:download_code,
|
||||
:fork_project,
|
||||
:create_project_snippet,
|
||||
|
|
|
@ -24,32 +24,19 @@
|
|||
#
|
||||
|
||||
module Ci
|
||||
class Build < ActiveRecord::Base
|
||||
extend Ci::Model
|
||||
|
||||
class Build < CommitStatus
|
||||
LAZY_ATTRIBUTES = ['trace']
|
||||
|
||||
belongs_to :commit, class_name: 'Ci::Commit'
|
||||
belongs_to :runner, class_name: 'Ci::Runner'
|
||||
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
|
||||
belongs_to :user
|
||||
|
||||
serialize :options
|
||||
|
||||
validates :commit, presence: true
|
||||
validates :status, presence: true
|
||||
validates :coverage, numericality: true, allow_blank: true
|
||||
validates_presence_of :ref
|
||||
|
||||
scope :running, ->() { where(status: "running") }
|
||||
scope :pending, ->() { where(status: "pending") }
|
||||
scope :success, ->() { where(status: "success") }
|
||||
scope :failed, ->() { where(status: "failed") }
|
||||
scope :unstarted, ->() { where(runner_id: nil) }
|
||||
scope :running_or_pending, ->() { where(status:[:running, :pending]) }
|
||||
scope :latest, ->() { where(id: unscope(:select).select('max(id)').group(:name, :ref)).order(stage_idx: :asc) }
|
||||
scope :ignore_failures, ->() { where(allow_failure: false) }
|
||||
scope :for_ref, ->(ref) { where(ref: ref) }
|
||||
scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
|
||||
|
||||
acts_as_taggable
|
||||
|
@ -74,13 +61,14 @@ module Ci
|
|||
|
||||
def create_from(build)
|
||||
new_build = build.dup
|
||||
new_build.status = :pending
|
||||
new_build.status = 'pending'
|
||||
new_build.runner_id = nil
|
||||
new_build.trigger_request_id = nil
|
||||
new_build.save
|
||||
end
|
||||
|
||||
def retry(build)
|
||||
new_build = Ci::Build.new(status: :pending)
|
||||
new_build = Ci::Build.new(status: 'pending')
|
||||
new_build.ref = build.ref
|
||||
new_build.tag = build.tag
|
||||
new_build.options = build.options
|
||||
|
@ -98,28 +86,7 @@ module Ci
|
|||
end
|
||||
|
||||
state_machine :status, initial: :pending do
|
||||
event :run do
|
||||
transition pending: :running
|
||||
end
|
||||
|
||||
event :drop do
|
||||
transition running: :failed
|
||||
end
|
||||
|
||||
event :success do
|
||||
transition running: :success
|
||||
end
|
||||
|
||||
event :cancel do
|
||||
transition [:pending, :running] => :canceled
|
||||
end
|
||||
|
||||
after_transition pending: :running do |build, transition|
|
||||
build.update_attributes started_at: Time.now
|
||||
end
|
||||
|
||||
after_transition any => [:success, :failed, :canceled] do |build, transition|
|
||||
build.update_attributes finished_at: Time.now
|
||||
project = build.project
|
||||
|
||||
if project.web_hooks?
|
||||
|
@ -136,19 +103,10 @@ module Ci
|
|||
build.update_coverage
|
||||
end
|
||||
end
|
||||
|
||||
state :pending, value: 'pending'
|
||||
state :running, value: 'running'
|
||||
state :failed, value: 'failed'
|
||||
state :success, value: 'success'
|
||||
state :canceled, value: 'canceled'
|
||||
end
|
||||
|
||||
delegate :sha, :short_sha, :project, :gl_project,
|
||||
to: :commit, prefix: false
|
||||
|
||||
def before_sha
|
||||
Gitlab::Git::BLANK_SHA
|
||||
def ignored?
|
||||
failed? && allow_failure?
|
||||
end
|
||||
|
||||
def trace_html
|
||||
|
@ -156,22 +114,6 @@ module Ci
|
|||
html || ''
|
||||
end
|
||||
|
||||
def started?
|
||||
!pending? && !canceled? && started_at
|
||||
end
|
||||
|
||||
def active?
|
||||
running? || pending?
|
||||
end
|
||||
|
||||
def complete?
|
||||
canceled? || success? || failed?
|
||||
end
|
||||
|
||||
def ignored?
|
||||
failed? && allow_failure?
|
||||
end
|
||||
|
||||
def timeout
|
||||
project.timeout
|
||||
end
|
||||
|
@ -180,14 +122,6 @@ module Ci
|
|||
yaml_variables + project_variables + trigger_variables
|
||||
end
|
||||
|
||||
def duration
|
||||
if started_at && finished_at
|
||||
finished_at - started_at
|
||||
elsif started_at
|
||||
Time.now - started_at
|
||||
end
|
||||
end
|
||||
|
||||
def project
|
||||
commit.project
|
||||
end
|
||||
|
@ -278,6 +212,25 @@ module Ci
|
|||
"#{dir_to_trace}/#{id}.log"
|
||||
end
|
||||
|
||||
def target_url
|
||||
Gitlab::Application.routes.url_helpers.
|
||||
namespace_project_build_url(gl_project.namespace, gl_project, self)
|
||||
end
|
||||
|
||||
def cancel_url
|
||||
if active?
|
||||
Gitlab::Application.routes.url_helpers.
|
||||
cancel_namespace_project_build_path(gl_project.namespace, gl_project, self, return_to: request.original_url)
|
||||
end
|
||||
end
|
||||
|
||||
def retry_url
|
||||
if commands.present?
|
||||
Gitlab::Application.routes.url_helpers.
|
||||
cancel_namespace_project_build_path(gl_project.namespace, gl_project, self, return_to: request.original_url)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def yaml_variables
|
||||
|
|
|
@ -20,7 +20,8 @@ module Ci
|
|||
extend Ci::Model
|
||||
|
||||
belongs_to :gl_project, class_name: '::Project', foreign_key: :gl_project_id
|
||||
has_many :builds, dependent: :destroy, class_name: 'Ci::Build'
|
||||
has_many :statuses, dependent: :destroy, class_name: 'CommitStatus'
|
||||
has_many :builds, class_name: 'Ci::Build'
|
||||
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
|
||||
|
||||
validates_presence_of :sha
|
||||
|
@ -47,7 +48,7 @@ module Ci
|
|||
end
|
||||
|
||||
def retry
|
||||
builds_without_retry.each do |build|
|
||||
latest_builds.each do |build|
|
||||
Ci::Build.retry(build)
|
||||
end
|
||||
end
|
||||
|
@ -81,12 +82,11 @@ module Ci
|
|||
end
|
||||
|
||||
def stage
|
||||
running_or_pending = builds_without_retry.running_or_pending
|
||||
running_or_pending.limit(1).pluck(:stage).first
|
||||
running_or_pending = statuses.latest.running_or_pending.ordered
|
||||
running_or_pending.first.try(:stage)
|
||||
end
|
||||
|
||||
def create_builds(ref, tag, user, trigger_request = nil)
|
||||
return if skip_ci? && trigger_request.blank?
|
||||
return unless config_processor
|
||||
config_processor.stages.any? do |stage|
|
||||
CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request).present?
|
||||
|
@ -94,7 +94,6 @@ module Ci
|
|||
end
|
||||
|
||||
def create_next_builds(ref, tag, user, trigger_request)
|
||||
return if skip_ci? && trigger_request.blank?
|
||||
return unless config_processor
|
||||
|
||||
stages = builds.where(ref: ref, tag: tag, trigger_request: trigger_request).group_by(&:stage)
|
||||
|
@ -107,61 +106,60 @@ module Ci
|
|||
end
|
||||
|
||||
def refs
|
||||
builds.group(:ref).pluck(:ref)
|
||||
statuses.order(:ref).pluck(:ref).uniq
|
||||
end
|
||||
|
||||
def last_ref
|
||||
builds.latest.first.try(:ref)
|
||||
def latest_statuses
|
||||
@latest_statuses ||= statuses.latest.to_a
|
||||
end
|
||||
|
||||
def builds_without_retry
|
||||
builds.latest
|
||||
def latest_builds
|
||||
@latest_builds ||= builds.latest.to_a
|
||||
end
|
||||
|
||||
def builds_without_retry_for_ref(ref)
|
||||
builds.for_ref(ref).latest
|
||||
def latest_builds_for_ref(ref)
|
||||
latest_builds.select { |build| build.ref == ref }
|
||||
end
|
||||
|
||||
def retried_builds
|
||||
@retried_builds ||= (builds.order(id: :desc) - builds_without_retry)
|
||||
def retried
|
||||
@retried ||= (statuses.order(id: :desc) - statuses.latest)
|
||||
end
|
||||
|
||||
def status
|
||||
if skip_ci?
|
||||
return 'skipped'
|
||||
elsif yaml_errors.present?
|
||||
if yaml_errors.present?
|
||||
return 'failed'
|
||||
elsif builds.none?
|
||||
return 'skipped'
|
||||
elsif success?
|
||||
'success'
|
||||
elsif pending?
|
||||
'pending'
|
||||
elsif running?
|
||||
'running'
|
||||
elsif canceled?
|
||||
'canceled'
|
||||
else
|
||||
'failed'
|
||||
end
|
||||
|
||||
@status ||= begin
|
||||
latest = latest_statuses
|
||||
latest.reject! { |status| status.try(&:allow_failure?) }
|
||||
|
||||
if latest.none?
|
||||
'skipped'
|
||||
elsif latest.all?(&:success?)
|
||||
'success'
|
||||
elsif latest.all?(&:pending?)
|
||||
'pending'
|
||||
elsif latest.any?(&:running?) || latest.any?(&:pending?)
|
||||
'running'
|
||||
elsif latest.all?(&:canceled?)
|
||||
'canceled'
|
||||
else
|
||||
'failed'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def pending?
|
||||
builds_without_retry.all? do |build|
|
||||
build.pending?
|
||||
end
|
||||
status == 'pending'
|
||||
end
|
||||
|
||||
def running?
|
||||
builds_without_retry.any? do |build|
|
||||
build.running? || build.pending?
|
||||
end
|
||||
status == 'running'
|
||||
end
|
||||
|
||||
def success?
|
||||
builds_without_retry.all? do |build|
|
||||
build.success? || build.ignored?
|
||||
end
|
||||
status == 'success'
|
||||
end
|
||||
|
||||
def failed?
|
||||
|
@ -169,26 +167,21 @@ module Ci
|
|||
end
|
||||
|
||||
def canceled?
|
||||
builds_without_retry.all? do |build|
|
||||
build.canceled?
|
||||
end
|
||||
status == 'canceled'
|
||||
end
|
||||
|
||||
def duration
|
||||
@duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i
|
||||
end
|
||||
|
||||
def duration_for_ref(ref)
|
||||
builds_without_retry_for_ref(ref).select(&:duration).sum(&:duration).to_i
|
||||
duration_array = latest_statuses.map(&:duration).compact
|
||||
duration_array.reduce(:+).to_i
|
||||
end
|
||||
|
||||
def finished_at
|
||||
@finished_at ||= builds.order('finished_at DESC').first.try(:finished_at)
|
||||
@finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at)
|
||||
end
|
||||
|
||||
def coverage
|
||||
if project.coverage_enabled?
|
||||
coverage_array = builds_without_retry.map(&:coverage).compact
|
||||
coverage_array = latest_builds.map(&:coverage).compact
|
||||
if coverage_array.size >= 1
|
||||
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
|
||||
end
|
||||
|
@ -196,7 +189,7 @@ module Ci
|
|||
end
|
||||
|
||||
def matrix_for_ref?(ref)
|
||||
builds_without_retry_for_ref(ref).pluck(:id).size > 1
|
||||
latest_builds_for_ref(ref).size > 1
|
||||
end
|
||||
|
||||
def config_processor
|
||||
|
@ -217,7 +210,6 @@ module Ci
|
|||
end
|
||||
|
||||
def skip_ci?
|
||||
return false if builds.any?
|
||||
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
|
||||
end
|
||||
|
||||
|
|
|
@ -184,4 +184,12 @@ class Commit
|
|||
def parents
|
||||
@parents ||= Commit.decorate(super, project)
|
||||
end
|
||||
|
||||
def ci_commit
|
||||
project.ci_commit(sha)
|
||||
end
|
||||
|
||||
def status
|
||||
ci_commit.try(:status) || :not_found
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
class CommitStatus < ActiveRecord::Base
|
||||
self.table_name = 'ci_builds'
|
||||
|
||||
belongs_to :commit, class_name: 'Ci::Commit'
|
||||
belongs_to :user
|
||||
|
||||
validates :commit, presence: true
|
||||
validates :status, inclusion: { in: %w(pending running failed success canceled) }
|
||||
|
||||
validates_presence_of :name
|
||||
|
||||
alias_attribute :author, :user
|
||||
|
||||
scope :running, -> { where(status: 'running') }
|
||||
scope :pending, -> { where(status: 'pending') }
|
||||
scope :success, -> { where(status: 'success') }
|
||||
scope :failed, -> { where(status: 'failed') }
|
||||
scope :running_or_pending, -> { where(status:[:running, :pending]) }
|
||||
scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) }
|
||||
scope :ordered, -> { order(:ref, :stage_idx, :name) }
|
||||
scope :for_ref, ->(ref) { where(ref: ref) }
|
||||
scope :running_or_pending, -> { where(status: [:running, :pending]) }
|
||||
|
||||
state_machine :status, initial: :pending do
|
||||
event :run do
|
||||
transition pending: :running
|
||||
end
|
||||
|
||||
event :drop do
|
||||
transition running: :failed
|
||||
end
|
||||
|
||||
event :success do
|
||||
transition [:pending, :running] => :success
|
||||
end
|
||||
|
||||
event :cancel do
|
||||
transition [:pending, :running] => :canceled
|
||||
end
|
||||
|
||||
after_transition pending: :running do |build, transition|
|
||||
build.update_attributes started_at: Time.now
|
||||
end
|
||||
|
||||
after_transition any => [:success, :failed, :canceled] do |build, transition|
|
||||
build.update_attributes finished_at: Time.now
|
||||
end
|
||||
|
||||
state :pending, value: 'pending'
|
||||
state :running, value: 'running'
|
||||
state :failed, value: 'failed'
|
||||
state :success, value: 'success'
|
||||
state :canceled, value: 'canceled'
|
||||
end
|
||||
|
||||
delegate :sha, :short_sha, :gl_project,
|
||||
to: :commit, prefix: false
|
||||
|
||||
# TODO: this should be removed with all references
|
||||
def before_sha
|
||||
Gitlab::Git::BLANK_SHA
|
||||
end
|
||||
|
||||
def started?
|
||||
!pending? && !canceled? && started_at
|
||||
end
|
||||
|
||||
def active?
|
||||
running? || pending?
|
||||
end
|
||||
|
||||
def complete?
|
||||
canceled? || success? || failed?
|
||||
end
|
||||
|
||||
def duration
|
||||
if started_at && finished_at
|
||||
finished_at - started_at
|
||||
elsif started_at
|
||||
Time.now - started_at
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_url
|
||||
nil
|
||||
end
|
||||
|
||||
def retry_url
|
||||
nil
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
class GenericCommitStatus < CommitStatus
|
||||
before_validation :set_default_values
|
||||
|
||||
# GitHub compatible API
|
||||
alias_attribute :context, :name
|
||||
|
||||
def set_default_values
|
||||
self.context ||= 'default'
|
||||
self.stage ||= 'external'
|
||||
end
|
||||
|
||||
def tags
|
||||
[:external]
|
||||
end
|
||||
end
|
|
@ -49,7 +49,7 @@ module Ci
|
|||
|
||||
commit = build.commit
|
||||
return unless commit
|
||||
return unless commit.builds_without_retry.include? build
|
||||
return unless commit.latest_builds.include? build
|
||||
|
||||
case commit.status.to_sym
|
||||
when :failed
|
||||
|
|
|
@ -48,7 +48,7 @@ module Ci
|
|||
# it doesn't make sense to send emails for retried builds
|
||||
commit = build.commit
|
||||
return unless commit
|
||||
return unless commit.builds_without_retry.include?(build)
|
||||
return unless commit.latest_builds.include?(build)
|
||||
|
||||
case build.status.to_sym
|
||||
when :failed
|
||||
|
|
|
@ -23,7 +23,7 @@ module Ci
|
|||
def attachments
|
||||
fields = []
|
||||
|
||||
commit.builds_without_retry.each do |build|
|
||||
commit.latest_builds.each do |build|
|
||||
next if build.allow_failure?
|
||||
next unless build.failed?
|
||||
fields << {
|
||||
|
|
|
@ -48,7 +48,7 @@ module Ci
|
|||
|
||||
commit = build.commit
|
||||
return unless commit
|
||||
return unless commit.builds_without_retry.include?(build)
|
||||
return unless commit.latest_builds.include?(build)
|
||||
|
||||
case commit.status.to_sym
|
||||
when :failed
|
||||
|
|
|
@ -17,8 +17,10 @@ module Ci
|
|||
|
||||
tag = origin_ref.start_with?('refs/tags/')
|
||||
commit = project.gl_project.ensure_ci_commit(sha)
|
||||
commit.update_committed!
|
||||
commit.create_builds(ref, tag, user)
|
||||
unless commit.skip_ci?
|
||||
commit.update_committed!
|
||||
commit.create_builds(ref, tag, user)
|
||||
end
|
||||
|
||||
commit
|
||||
end
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
- gl_project = build.project.gl_project
|
||||
%tr.build
|
||||
%td.status
|
||||
= ci_status_with_icon(build.status)
|
||||
|
||||
%td.build-link
|
||||
= link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do
|
||||
%strong Build ##{build.id}
|
||||
|
||||
- if defined?(ref)
|
||||
%td
|
||||
= build.ref
|
||||
|
||||
%td
|
||||
= build.stage
|
||||
|
||||
%td
|
||||
= build.name
|
||||
.pull-right
|
||||
- if build.tags.any?
|
||||
- build.tag_list.each do |tag|
|
||||
%span.label.label-primary
|
||||
= tag
|
||||
- if build.trigger_request
|
||||
%span.label.label-info triggered
|
||||
- if build.allow_failure
|
||||
%span.label.label-danger allowed to fail
|
||||
|
||||
%td.duration
|
||||
- if build.duration
|
||||
#{duration_in_words(build.finished_at, build.started_at)}
|
||||
|
||||
%td.timestamp
|
||||
- if build.finished_at
|
||||
%span #{time_ago_in_words build.finished_at} ago
|
||||
|
||||
- if build.project.coverage_enabled?
|
||||
%td.coverage
|
||||
- if build.coverage
|
||||
#{build.coverage}%
|
||||
|
||||
%td
|
||||
- if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project)
|
||||
.pull-right
|
||||
- if build.active?
|
||||
= link_to cancel_namespace_project_build_path(gl_project.namespace, gl_project, build, return_to: request.original_url), title: 'Cancel build' do
|
||||
%i.fa.fa-remove.cred
|
||||
- elsif build.commands.present?
|
||||
= link_to retry_namespace_project_build_path(gl_project.namespace, gl_project, build, return_to: request.original_url), method: :post, title: 'Retry build' do
|
||||
%i.fa.fa-repeat
|
|
@ -9,7 +9,7 @@
|
|||
#up-build-trace
|
||||
- if @commit.matrix_for_ref?(@build.ref)
|
||||
%ul.center-top-menu.build-top-menu
|
||||
- @commit.builds_without_retry_for_ref(@build.ref).each do |build|
|
||||
- @commit.latest_builds_for_ref(@build.ref).each do |build|
|
||||
%li{class: ('active' if build == @build) }
|
||||
= link_to namespace_project_build_path(@project.namespace, @project, build) do
|
||||
= ci_icon_for_status(build.status)
|
||||
|
@ -20,7 +20,7 @@
|
|||
= build.id
|
||||
|
||||
|
||||
- unless @commit.builds_without_retry_for_ref(@build.ref).include?(@build)
|
||||
- unless @commit.latest_builds_for_ref(@build.ref).include?(@build)
|
||||
%li.active
|
||||
%a
|
||||
Build ##{@build.id}
|
||||
|
|
|
@ -20,30 +20,31 @@
|
|||
.bs-callout.bs-callout-warning
|
||||
\.gitlab-ci.yml not found in this commit
|
||||
|
||||
- @ci_commit.refs.each do |ref|
|
||||
.gray-content-block.second-block
|
||||
Latest builds
|
||||
- if @ci_commit.duration > 0
|
||||
%small.pull-right
|
||||
%i.fa.fa-time
|
||||
#{time_interval_in_words @ci_commit.duration}
|
||||
|
||||
%table.table.builds
|
||||
%thead
|
||||
%tr
|
||||
%th Status
|
||||
%th Build ID
|
||||
%th Ref
|
||||
%th Stage
|
||||
%th Name
|
||||
%th Duration
|
||||
%th Finished at
|
||||
- if @ci_project && @ci_project.coverage_enabled?
|
||||
%th Coverage
|
||||
%th
|
||||
- @ci_commit.refs.each do |ref|
|
||||
= render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, coverage: @ci_project.try(:coverage_enabled?), controls: true
|
||||
|
||||
- if @ci_commit.retried.any?
|
||||
.gray-content-block.second-block
|
||||
Builds for #{ref}
|
||||
- if @ci_commit.duration_for_ref(ref) > 0
|
||||
%small.pull-right
|
||||
%i.fa.fa-time
|
||||
#{time_interval_in_words @ci_commit.duration_for_ref(ref)}
|
||||
|
||||
%table.table.builds
|
||||
%thead
|
||||
%tr
|
||||
%th Status
|
||||
%th Build ID
|
||||
%th Stage
|
||||
%th Name
|
||||
%th Duration
|
||||
%th Finished at
|
||||
- if @ci_project && @ci_project.coverage_enabled?
|
||||
%th Coverage
|
||||
%th
|
||||
= render partial: "projects/builds/build", collection: @ci_commit.builds_without_retry.for_ref(ref), controls: true
|
||||
|
||||
- if @ci_commit.retried_builds.any?
|
||||
%h3
|
||||
Retried builds
|
||||
|
||||
%table.table.builds
|
||||
|
@ -59,4 +60,4 @@
|
|||
- if @ci_project && @ci_project.coverage_enabled?
|
||||
%th Coverage
|
||||
%th
|
||||
= render partial: "projects/builds/build", collection: @ci_commit.retried_builds, ref: true
|
||||
= render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, coverage: @ci_project.try(:coverage_enabled?)
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
%tr.commit_status
|
||||
%td.status
|
||||
= ci_status_with_icon(commit_status.status)
|
||||
|
||||
%td.commit_status-link
|
||||
- if commit_status.target_url
|
||||
= link_to commit_status.target_url do
|
||||
%strong Build ##{commit_status.id}
|
||||
- else
|
||||
%strong Build ##{commit_status.id}
|
||||
|
||||
%td
|
||||
= commit_status.ref
|
||||
|
||||
%td
|
||||
= commit_status.stage
|
||||
|
||||
%td
|
||||
= commit_status.name
|
||||
.pull-right
|
||||
- if commit_status.tags.any?
|
||||
- commit_status.tags.each do |tag|
|
||||
%span.label.label-primary
|
||||
= tag
|
||||
- if commit_status.try(:trigger_request)
|
||||
%span.label.label-info triggered
|
||||
- if commit_status.try(:allow_failure)
|
||||
%span.label.label-danger allowed to fail
|
||||
|
||||
%td.duration
|
||||
- if commit_status.duration
|
||||
#{duration_in_words(commit_status.finished_at, commit_status.started_at)}
|
||||
|
||||
%td.timestamp
|
||||
- if commit_status.finished_at
|
||||
%span #{time_ago_in_words commit_status.finished_at} ago
|
||||
|
||||
- if defined?(coverage) && coverage
|
||||
%td.coverage
|
||||
- if commit_status.try(:coverage)
|
||||
#{commit_status.coverage}%
|
||||
|
||||
%td
|
||||
- if defined?(controls) && controls && current_user && can?(current_user, :manage_builds, gl_project)
|
||||
.pull-right
|
||||
- if commit_status.cancel_url
|
||||
= link_to commit_status.cancel_url, title: 'Cancel' do
|
||||
%i.fa.fa-remove.cred
|
||||
- elsif commit_status.retry_url
|
||||
= link_to commit_status.retry_url, method: :post, title: 'Retry' do
|
||||
%i.fa.fa-repeat
|
|
@ -0,0 +1,9 @@
|
|||
class AddTypeAndDescriptionToBuilds < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :ci_builds, :type, :string
|
||||
add_column :ci_builds, :target_url, :string
|
||||
add_column :ci_builds, :description, :string
|
||||
add_index :ci_builds, [:commit_id, :type, :ref]
|
||||
add_index :ci_builds, [:commit_id, :type, :name, :ref]
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class MigrateNameToDescriptionForBuilds < ActiveRecord::Migration
|
||||
def change
|
||||
execute("UPDATE ci_builds SET type='Ci::Build' WHERE type IS NULL")
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20151007120511) do
|
||||
ActiveRecord::Schema.define(version: 20151008130321) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -103,9 +103,14 @@ ActiveRecord::Schema.define(version: 20151007120511) do
|
|||
t.boolean "tag"
|
||||
t.string "ref"
|
||||
t.integer "user_id"
|
||||
t.string "type"
|
||||
t.string "target_url"
|
||||
t.string "description"
|
||||
end
|
||||
|
||||
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
|
||||
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
|
||||
add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
|
||||
add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree
|
||||
add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
|
||||
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
|
||||
|
|
|
@ -62,7 +62,8 @@ Parameters:
|
|||
"authored_date": "2012-09-20T09:06:12+03:00",
|
||||
"parent_ids": [
|
||||
"ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
|
||||
]
|
||||
],
|
||||
"status": "running"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -156,3 +157,84 @@ Parameters:
|
|||
"line_type": "new"
|
||||
}
|
||||
```
|
||||
|
||||
## Get the status of a commit
|
||||
|
||||
Get the statuses of a commit in a project.
|
||||
|
||||
```
|
||||
GET /projects/:id/repository/commits/:sha/statuses
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
- `id` (required) - The ID of a project
|
||||
- `sha` (required) - The commit SHA
|
||||
- `ref` (optional) - Filter by ref name, it can be branch or tag
|
||||
- `stage` (optional) - Filter by stage
|
||||
- `name` (optional) - Filer by status name, eg. jenkins
|
||||
- `all` (optional) - The flag to return all statuses, not only latest ones
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 13,
|
||||
"sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27",
|
||||
"ref": "test",
|
||||
"status": "success",
|
||||
"name": "ci/jenkins",
|
||||
"target_url": "http://jenkins/project/url",
|
||||
"description": "Jenkins success",
|
||||
"created_at": "2015-10-12T09:47:16.250Z",
|
||||
"started_at": "2015-10-12T09:47:16.250Z"",
|
||||
"finished_at": "2015-10-12T09:47:16.262Z",
|
||||
"author": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"email": "admin@local.host",
|
||||
"name": "Administrator",
|
||||
"blocked": false,
|
||||
"created_at": "2012-04-29T08:46:00Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Post the status to commit
|
||||
|
||||
Adds or updates a status of a commit.
|
||||
|
||||
```
|
||||
POST /projects/:id/statuses/:sha
|
||||
```
|
||||
|
||||
- `id` (required) - The ID of a project
|
||||
- `sha` (required) - The commit SHA
|
||||
- `state` (required) - The state of the status. Can be: pending, running, success, failed, canceled
|
||||
- `ref` (optional) - The ref (branch or tag) to which the status refers
|
||||
- `name` or `context` (optional) - The label to differentiate this status from the status of other systems. Default: "default"
|
||||
- `target_url` (optional) - The target URL to associate with this status
|
||||
- `description` (optional) - The short description of the status
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 13,
|
||||
"sha": "b0b3a907f41409829b307a28b82fdbd552ee5a27",
|
||||
"ref": "test",
|
||||
"status": "success",
|
||||
"name": "ci/jenkins",
|
||||
"target_url": "http://jenkins/project/url",
|
||||
"description": "Jenkins success",
|
||||
"created_at": "2015-10-12T09:47:16.250Z",
|
||||
"started_at": "2015-10-12T09:47:16.250Z"",
|
||||
"finished_at": "2015-10-12T09:47:16.262Z",
|
||||
"author": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"email": "admin@local.host",
|
||||
"name": "Administrator",
|
||||
"blocked": false,
|
||||
"created_at": "2012-04-29T08:46:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -118,6 +118,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
|
|||
|
||||
step 'I see builds list' do
|
||||
expect(page).to have_content "build: pending"
|
||||
expect(page).to have_content "Builds for master"
|
||||
expect(page).to have_content "Latest builds"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -46,6 +46,7 @@ module API
|
|||
mount Services
|
||||
mount Files
|
||||
mount Commits
|
||||
mount CommitStatus
|
||||
mount Namespaces
|
||||
mount Branches
|
||||
mount Labels
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
require 'mime/types'
|
||||
|
||||
module API
|
||||
# Project commit statuses API
|
||||
class CommitStatus < Grape::API
|
||||
resource :projects do
|
||||
before { authenticate! }
|
||||
|
||||
# Get a commit's statuses
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of a project
|
||||
# sha (required) - The commit hash
|
||||
# ref (optional) - The ref
|
||||
# stage (optional) - The stage
|
||||
# name (optional) - The name
|
||||
# all (optional) - Show all statuses, default: false
|
||||
# Examples:
|
||||
# GET /projects/:id/repository/commits/:sha/statuses
|
||||
get ':id/repository/commits/:sha/statuses' do
|
||||
authorize! :read_commit_statuses, user_project
|
||||
sha = params[:sha]
|
||||
ci_commit = user_project.ci_commit(sha)
|
||||
not_found! 'Commit' unless ci_commit
|
||||
statuses = ci_commit.statuses
|
||||
statuses = statuses.latest unless parse_boolean(params[:all])
|
||||
statuses = statuses.where(ref: params[:ref]) if params[:ref].present?
|
||||
statuses = statuses.where(stage: params[:stage]) if params[:stage].present?
|
||||
statuses = statuses.where(name: params[:name]) if params[:name].present?
|
||||
present paginate(statuses), with: Entities::CommitStatus
|
||||
end
|
||||
|
||||
# Post status to commit
|
||||
#
|
||||
# Parameters:
|
||||
# id (required) - The ID of a project
|
||||
# sha (required) - The commit hash
|
||||
# ref (optional) - The ref
|
||||
# state (required) - The state of the status. Can be: pending, running, success, error or failure
|
||||
# target_url (optional) - The target URL to associate with this status
|
||||
# description (optional) - A short description of the status
|
||||
# name or context (optional) - A string label to differentiate this status from the status of other systems. Default: "default"
|
||||
# Examples:
|
||||
# POST /projects/:id/statuses/:sha
|
||||
post ':id/statuses/:sha' do
|
||||
authorize! :create_commit_status, user_project
|
||||
required_attributes! [:state]
|
||||
attrs = attributes_for_keys [:ref, :target_url, :description, :context, :name]
|
||||
commit = @project.commit(params[:sha])
|
||||
not_found! 'Commit' unless commit
|
||||
|
||||
ci_commit = @project.ensure_ci_commit(commit.sha)
|
||||
|
||||
name = params[:name] || params[:context]
|
||||
status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref])
|
||||
status ||= GenericCommitStatus.new(commit: ci_commit, user: current_user)
|
||||
status.update(attrs)
|
||||
|
||||
case params[:state].to_s
|
||||
when 'running'
|
||||
status.run
|
||||
when 'success'
|
||||
status.success
|
||||
when 'failed'
|
||||
status.drop
|
||||
when 'canceled'
|
||||
status.cancel
|
||||
else
|
||||
status.status = params[:state].to_s
|
||||
end
|
||||
|
||||
if status.save
|
||||
present status, with: Entities::CommitStatus
|
||||
else
|
||||
render_validation_error!(status)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -149,6 +149,7 @@ module API
|
|||
|
||||
class RepoCommitDetail < RepoCommit
|
||||
expose :parent_ids, :committed_date, :authored_date
|
||||
expose :status
|
||||
end
|
||||
|
||||
class ProjectSnippet < Grape::Entity
|
||||
|
@ -228,6 +229,12 @@ module API
|
|||
expose :created_at
|
||||
end
|
||||
|
||||
class CommitStatus < Grape::Entity
|
||||
expose :id, :sha, :ref, :status, :name, :target_url, :description,
|
||||
:created_at, :started_at, :finished_at
|
||||
expose :author, using: Entities::UserBasic
|
||||
end
|
||||
|
||||
class Event < Grape::Entity
|
||||
expose :title, :project_id, :action_name
|
||||
expose :target_id, :target_type, :author_id
|
||||
|
|
|
@ -2,7 +2,7 @@ module Ci
|
|||
module API
|
||||
module Entities
|
||||
class Commit < Grape::Entity
|
||||
expose :id, :ref, :sha, :project_id, :before_sha, :created_at
|
||||
expose :id, :sha, :project_id, :created_at
|
||||
expose :status, :finished_at, :duration
|
||||
expose :git_commit_message, :git_author_name, :git_author_email
|
||||
end
|
||||
|
@ -12,7 +12,7 @@ module Ci
|
|||
end
|
||||
|
||||
class Build < Grape::Entity
|
||||
expose :id, :commands, :ref, :sha, :project_id, :repo_url,
|
||||
expose :id, :commands, :ref, :sha, :status, :project_id, :repo_url,
|
||||
:before_sha, :allow_git_fetch, :project_name
|
||||
|
||||
expose :options do |model|
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
FactoryGirl.define do
|
||||
factory :ci_build, class: Ci::Build do
|
||||
name 'test'
|
||||
ref 'master'
|
||||
tag false
|
||||
started_at 'Di 29. Okt 09:51:28 CET 2013'
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
FactoryGirl.define do
|
||||
factory :commit_status, class: CommitStatus do
|
||||
started_at 'Di 29. Okt 09:51:28 CET 2013'
|
||||
finished_at 'Di 29. Okt 09:53:28 CET 2013'
|
||||
name 'default'
|
||||
status 'success'
|
||||
description 'commit status'
|
||||
commit factory: :ci_commit
|
||||
|
||||
factory :generic_commit_status, class: GenericCommitStatus do
|
||||
name 'generic'
|
||||
description 'external commit status'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,6 +12,7 @@ describe "Commits" do
|
|||
@ci_project = project.ensure_gitlab_ci_project
|
||||
@commit = FactoryGirl.create :ci_commit, gl_project: project, sha: project.commit.sha
|
||||
@build = FactoryGirl.create :ci_build, commit: @commit
|
||||
@generic_status = FactoryGirl.create :generic_commit_status, commit: @commit
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
|
@ -30,17 +30,9 @@ describe Ci::Build do
|
|||
let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
|
||||
let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
|
||||
let(:build) { FactoryGirl.create :ci_build, commit: commit }
|
||||
subject { build }
|
||||
|
||||
it { is_expected.to belong_to(:commit) }
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to validate_presence_of :status }
|
||||
it { is_expected.to validate_presence_of :ref }
|
||||
|
||||
it { is_expected.to respond_to :success? }
|
||||
it { is_expected.to respond_to :failed? }
|
||||
it { is_expected.to respond_to :running? }
|
||||
it { is_expected.to respond_to :pending? }
|
||||
it { is_expected.to respond_to :trace_html }
|
||||
|
||||
describe :first_pending do
|
||||
|
@ -67,72 +59,6 @@ describe Ci::Build do
|
|||
end
|
||||
end
|
||||
|
||||
describe :started? do
|
||||
subject { build.started? }
|
||||
|
||||
context 'without started_at' do
|
||||
before { build.started_at = nil }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
%w(running success failed).each do |status|
|
||||
context "if build status is #{status}" do
|
||||
before { build.status = status }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
%w(pending canceled).each do |status|
|
||||
context "if build status is #{status}" do
|
||||
before { build.status = status }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe :active? do
|
||||
subject { build.active? }
|
||||
|
||||
%w(pending running).each do |state|
|
||||
context "if build.status is #{state}" do
|
||||
before { build.status = state }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
%w(success failed canceled).each do |state|
|
||||
context "if build.status is #{state}" do
|
||||
before { build.status = state }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe :complete? do
|
||||
subject { build.complete? }
|
||||
|
||||
%w(success failed canceled).each do |state|
|
||||
context "if build.status is #{state}" do
|
||||
before { build.status = state }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
%w(pending running).each do |state|
|
||||
context "if build.status is #{state}" do
|
||||
before { build.status = state }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe :ignored? do
|
||||
subject { build.ignored? }
|
||||
|
||||
|
@ -200,31 +126,6 @@ describe Ci::Build do
|
|||
it { is_expected.to eq(commit.project.timeout) }
|
||||
end
|
||||
|
||||
describe :duration do
|
||||
subject { build.duration }
|
||||
|
||||
it { is_expected.to eq(120.0) }
|
||||
|
||||
context 'if the building process has not started yet' do
|
||||
before do
|
||||
build.started_at = nil
|
||||
build.finished_at = nil
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'if the building process has started' do
|
||||
before do
|
||||
build.started_at = Time.now - 1.minute
|
||||
build.finished_at = nil
|
||||
end
|
||||
|
||||
it { is_expected.to be_a(Float) }
|
||||
it { is_expected.to be > 0.0 }
|
||||
end
|
||||
end
|
||||
|
||||
describe :options do
|
||||
let(:options) do
|
||||
{
|
||||
|
@ -239,18 +140,6 @@ describe Ci::Build do
|
|||
it { is_expected.to eq(options) }
|
||||
end
|
||||
|
||||
describe :sha do
|
||||
subject { build.sha }
|
||||
|
||||
it { is_expected.to eq(commit.sha) }
|
||||
end
|
||||
|
||||
describe :short_sha do
|
||||
subject { build.short_sha }
|
||||
|
||||
it { is_expected.to eq(commit.short_sha) }
|
||||
end
|
||||
|
||||
describe :allow_git_fetch do
|
||||
subject { build.allow_git_fetch }
|
||||
|
|
@ -23,6 +23,8 @@ describe Ci::Commit do
|
|||
let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
|
||||
|
||||
it { is_expected.to belong_to(:gl_project) }
|
||||
it { is_expected.to have_many(:statuses) }
|
||||
it { is_expected.to have_many(:trigger_requests) }
|
||||
it { is_expected.to have_many(:builds) }
|
||||
it { is_expected.to validate_presence_of :sha }
|
||||
|
||||
|
@ -47,10 +49,12 @@ describe Ci::Commit do
|
|||
@second = FactoryGirl.create :ci_build, commit: commit
|
||||
end
|
||||
|
||||
it "creates new build" do
|
||||
it "creates only a new build" do
|
||||
expect(commit.builds.count(:all)).to eq 2
|
||||
expect(commit.statuses.count(:all)).to eq 2
|
||||
commit.retry
|
||||
expect(commit.builds.count(:all)).to eq 3
|
||||
expect(commit.statuses.count(:all)).to eq 3
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -78,8 +82,8 @@ describe Ci::Commit do
|
|||
subject { commit.stage }
|
||||
|
||||
before do
|
||||
@second = FactoryGirl.create :ci_build, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: :pending
|
||||
@first = FactoryGirl.create :ci_build, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: :pending
|
||||
@second = FactoryGirl.create :commit_status, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: 'pending'
|
||||
@first = FactoryGirl.create :commit_status, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: 'pending'
|
||||
end
|
||||
|
||||
it 'returns first running stage' do
|
||||
|
@ -88,7 +92,7 @@ describe Ci::Commit do
|
|||
|
||||
context 'first build succeeded' do
|
||||
before do
|
||||
@first.update_attributes(status: :success)
|
||||
@first.success
|
||||
end
|
||||
|
||||
it 'returns last running stage' do
|
||||
|
@ -98,8 +102,8 @@ describe Ci::Commit do
|
|||
|
||||
context 'all builds succeeded' do
|
||||
before do
|
||||
@first.update_attributes(status: :success)
|
||||
@second.update_attributes(status: :success)
|
||||
@first.success
|
||||
@second.success
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
|
@ -111,6 +115,33 @@ describe Ci::Commit do
|
|||
describe :create_next_builds do
|
||||
end
|
||||
|
||||
describe :refs do
|
||||
subject { commit.refs }
|
||||
|
||||
before do
|
||||
FactoryGirl.create :commit_status, commit: commit, name: 'deploy'
|
||||
FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'develop'
|
||||
FactoryGirl.create :commit_status, commit: commit, name: 'deploy', ref: 'master'
|
||||
end
|
||||
|
||||
it 'returns all refs' do
|
||||
is_expected.to contain_exactly('master', 'develop', nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe :retried do
|
||||
subject { commit.retried }
|
||||
|
||||
before do
|
||||
@commit1 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy'
|
||||
@commit2 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy'
|
||||
end
|
||||
|
||||
it 'returns old builds' do
|
||||
is_expected.to contain_exactly(@commit1)
|
||||
end
|
||||
end
|
||||
|
||||
describe :create_builds do
|
||||
let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
|
||||
|
||||
|
@ -194,9 +225,10 @@ describe Ci::Commit do
|
|||
it 'rebuilds commit' do
|
||||
expect(commit.status).to eq('skipped')
|
||||
expect(create_builds(trigger_request)).to be_truthy
|
||||
commit.builds.reload
|
||||
expect(commit.builds.size).to eq(2)
|
||||
expect(commit.status).to eq('pending')
|
||||
|
||||
# since everything in Ci::Commit is cached we need to fetch a new object
|
||||
new_commit = Ci::Commit.find_by_id(commit.id)
|
||||
expect(new_commit.status).to eq('pending')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -252,10 +284,10 @@ describe Ci::Commit do
|
|||
|
||||
describe :should_create_next_builds? do
|
||||
before do
|
||||
@build1 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: false, status: :success
|
||||
@build2 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'develop', tag: false, status: :failed
|
||||
@build3 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: true, status: :failed
|
||||
@build4 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: :success
|
||||
@build1 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: false, status: 'success'
|
||||
@build2 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'develop', tag: false, status: 'failed'
|
||||
@build3 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: true, status: 'failed'
|
||||
@build4 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: 'success'
|
||||
end
|
||||
|
||||
context 'for success' do
|
||||
|
@ -266,7 +298,7 @@ describe Ci::Commit do
|
|||
|
||||
context 'for failed' do
|
||||
before do
|
||||
@build4.update_attributes(status: :failed)
|
||||
@build4.update_attributes(status: 'failed')
|
||||
end
|
||||
|
||||
it 'to not create' do
|
||||
|
@ -286,7 +318,7 @@ describe Ci::Commit do
|
|||
|
||||
context 'for running' do
|
||||
before do
|
||||
@build4.update_attributes(status: :running)
|
||||
@build4.update_attributes(status: 'running')
|
||||
end
|
||||
|
||||
it 'to not create' do
|
||||
|
@ -296,7 +328,7 @@ describe Ci::Commit do
|
|||
|
||||
context 'for retried' do
|
||||
before do
|
||||
@build5 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: :failed
|
||||
@build5 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: 'failed'
|
||||
end
|
||||
|
||||
it 'to not create' do
|
||||
|
|
|
@ -35,7 +35,7 @@ describe Ci::MailService do
|
|||
let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) }
|
||||
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
|
||||
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) }
|
||||
|
||||
before do
|
||||
allow(mail).to receive_messages(
|
||||
|
@ -58,7 +58,7 @@ describe Ci::MailService do
|
|||
let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) }
|
||||
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
|
||||
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
|
||||
|
||||
before do
|
||||
allow(mail).to receive_messages(
|
||||
|
@ -86,7 +86,7 @@ describe Ci::MailService do
|
|||
end
|
||||
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
|
||||
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
|
||||
|
||||
before do
|
||||
allow(mail).to receive_messages(
|
||||
|
@ -115,7 +115,7 @@ describe Ci::MailService do
|
|||
end
|
||||
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
|
||||
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
|
||||
|
||||
before do
|
||||
allow(mail).to receive_messages(
|
||||
|
@ -144,7 +144,7 @@ describe Ci::MailService do
|
|||
end
|
||||
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
|
||||
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
|
||||
|
||||
before do
|
||||
allow(mail).to receive_messages(
|
||||
|
@ -167,7 +167,7 @@ describe Ci::MailService do
|
|||
end
|
||||
let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
|
||||
let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) }
|
||||
|
||||
before do
|
||||
allow(mail).to receive_messages(
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe CommitStatus do
|
||||
let(:commit) { FactoryGirl.create :ci_commit }
|
||||
let(:commit_status) { FactoryGirl.create :commit_status, commit: commit }
|
||||
|
||||
it { is_expected.to belong_to(:commit) }
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) }
|
||||
|
||||
it { is_expected.to delegate_method(:sha).to(:commit) }
|
||||
it { is_expected.to delegate_method(:short_sha).to(:commit) }
|
||||
it { is_expected.to delegate_method(:gl_project).to(:commit) }
|
||||
|
||||
it { is_expected.to respond_to :success? }
|
||||
it { is_expected.to respond_to :failed? }
|
||||
it { is_expected.to respond_to :running? }
|
||||
it { is_expected.to respond_to :pending? }
|
||||
|
||||
describe :author do
|
||||
subject { commit_status.author }
|
||||
before { commit_status.author = User.new }
|
||||
|
||||
it { is_expected.to eq(commit_status.user) }
|
||||
end
|
||||
|
||||
describe :started? do
|
||||
subject { commit_status.started? }
|
||||
|
||||
context 'without started_at' do
|
||||
before { commit_status.started_at = nil }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
%w(running success failed).each do |status|
|
||||
context "if commit status is #{status}" do
|
||||
before { commit_status.status = status }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
%w(pending canceled).each do |status|
|
||||
context "if commit status is #{status}" do
|
||||
before { commit_status.status = status }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe :active? do
|
||||
subject { commit_status.active? }
|
||||
|
||||
%w(pending running).each do |state|
|
||||
context "if commit_status.status is #{state}" do
|
||||
before { commit_status.status = state }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
%w(success failed canceled).each do |state|
|
||||
context "if commit_status.status is #{state}" do
|
||||
before { commit_status.status = state }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe :complete? do
|
||||
subject { commit_status.complete? }
|
||||
|
||||
%w(success failed canceled).each do |state|
|
||||
context "if commit_status.status is #{state}" do
|
||||
before { commit_status.status = state }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
%w(pending running).each do |state|
|
||||
context "if commit_status.status is #{state}" do
|
||||
before { commit_status.status = state }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe :duration do
|
||||
subject { commit_status.duration }
|
||||
|
||||
it { is_expected.to eq(120.0) }
|
||||
|
||||
context 'if the building process has not started yet' do
|
||||
before do
|
||||
commit_status.started_at = nil
|
||||
commit_status.finished_at = nil
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'if the building process has started' do
|
||||
before do
|
||||
commit_status.started_at = Time.now - 1.minute
|
||||
commit_status.finished_at = nil
|
||||
end
|
||||
|
||||
it { is_expected.to be_a(Float) }
|
||||
it { is_expected.to be > 0.0 }
|
||||
end
|
||||
end
|
||||
|
||||
describe :latest do
|
||||
subject { CommitStatus.latest.order(:id) }
|
||||
|
||||
before do
|
||||
@commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
|
||||
@commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
|
||||
@commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'cc', status: 'success'
|
||||
@commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'bb', status: 'success'
|
||||
@commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'success'
|
||||
end
|
||||
|
||||
it 'return unique statuses' do
|
||||
is_expected.to eq([@commit2, @commit3, @commit4, @commit5])
|
||||
end
|
||||
end
|
||||
|
||||
describe :for_ref do
|
||||
subject { CommitStatus.for_ref('bb').order(:id) }
|
||||
|
||||
before do
|
||||
@commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
|
||||
@commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
|
||||
@commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success'
|
||||
end
|
||||
|
||||
it 'return statuses with equal and nil ref set' do
|
||||
is_expected.to eq([@commit1])
|
||||
end
|
||||
end
|
||||
|
||||
describe :running_or_pending do
|
||||
subject { CommitStatus.running_or_pending.order(:id) }
|
||||
|
||||
before do
|
||||
@commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
|
||||
@commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
|
||||
@commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success'
|
||||
@commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed'
|
||||
@commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled'
|
||||
end
|
||||
|
||||
it 'return statuses that are running or pending' do
|
||||
is_expected.to eq([@commit1, @commit2])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,39 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe GenericCommitStatus do
|
||||
let(:commit) { FactoryGirl.create :ci_commit }
|
||||
let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, commit: commit }
|
||||
|
||||
describe :context do
|
||||
subject { generic_commit_status.context }
|
||||
before { generic_commit_status.context = 'my_context' }
|
||||
|
||||
it { is_expected.to eq(generic_commit_status.name) }
|
||||
end
|
||||
|
||||
describe :tags do
|
||||
subject { generic_commit_status.tags }
|
||||
|
||||
it { is_expected.to eq([:external]) }
|
||||
end
|
||||
|
||||
describe :set_default_values do
|
||||
before do
|
||||
generic_commit_status.context = nil
|
||||
generic_commit_status.stage = nil
|
||||
generic_commit_status.save
|
||||
end
|
||||
|
||||
describe :context do
|
||||
subject { generic_commit_status.context }
|
||||
|
||||
it { is_expected.to_not be_nil }
|
||||
end
|
||||
|
||||
describe :stage do
|
||||
subject { generic_commit_status.stage }
|
||||
|
||||
it { is_expected.to_not be_nil }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,135 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe API::API, api: true do
|
||||
include ApiHelpers
|
||||
let(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let!(:project) { create(:project, creator_id: user.id) }
|
||||
let!(:reporter) { create(:project_member, user: user, project: project, access_level: ProjectMember::REPORTER) }
|
||||
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
|
||||
let(:commit) { project.repository.commit }
|
||||
let!(:ci_commit) { project.ensure_ci_commit(commit.id) }
|
||||
let(:commit_status) { create(:commit_status, commit: ci_commit) }
|
||||
|
||||
describe "GET /projects/:id/repository/commits/:sha/statuses" do
|
||||
context "reporter user" do
|
||||
let(:statuses_id) { json_response.map { |status| status['id'] } }
|
||||
|
||||
before do
|
||||
@status1 = create(:commit_status, commit: ci_commit, status: 'running')
|
||||
@status2 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'pending')
|
||||
@status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running')
|
||||
@status4 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'success')
|
||||
@status5 = create(:commit_status, commit: ci_commit, ref: 'develop', status: 'success')
|
||||
@status6 = create(:commit_status, commit: ci_commit, status: 'success')
|
||||
end
|
||||
|
||||
it "should return latest commit statuses" do
|
||||
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user)
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
expect(json_response).to be_an Array
|
||||
expect(statuses_id).to contain_exactly(@status3.id, @status4.id, @status5.id, @status6.id)
|
||||
end
|
||||
|
||||
it "should return all commit statuses" do
|
||||
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?all=1", user)
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
expect(json_response).to be_an Array
|
||||
expect(statuses_id).to contain_exactly(@status1.id, @status2.id, @status3.id, @status4.id, @status5.id, @status6.id)
|
||||
end
|
||||
|
||||
it "should return latest commit statuses for specific ref" do
|
||||
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?ref=develop", user)
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
expect(json_response).to be_an Array
|
||||
expect(statuses_id).to contain_exactly(@status3.id, @status5.id)
|
||||
end
|
||||
|
||||
it "should return latest commit statuses for specific name" do
|
||||
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?name=coverage", user)
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
expect(json_response).to be_an Array
|
||||
expect(statuses_id).to contain_exactly(@status3.id, @status4.id)
|
||||
end
|
||||
end
|
||||
|
||||
context "guest user" do
|
||||
it "should not return project commits" do
|
||||
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user2)
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
context "unauthorized user" do
|
||||
it "should not return project commits" do
|
||||
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses")
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /projects/:id/statuses/:sha' do
|
||||
let(:post_url) { "/projects/#{project.id}/statuses/#{commit.id}" }
|
||||
|
||||
context 'reporter user' do
|
||||
context 'should create commit status' do
|
||||
it 'with only required parameters' do
|
||||
post api(post_url, user), state: 'success'
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response['sha']).to eq(commit.id)
|
||||
expect(json_response['status']).to eq('success')
|
||||
expect(json_response['name']).to eq('default')
|
||||
expect(json_response['ref']).to be_nil
|
||||
expect(json_response['target_url']).to be_nil
|
||||
expect(json_response['description']).to be_nil
|
||||
end
|
||||
|
||||
it 'with all optional parameters' do
|
||||
post api(post_url, user), state: 'success', context: 'coverage', ref: 'develop', target_url: 'url', description: 'test'
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response['sha']).to eq(commit.id)
|
||||
expect(json_response['status']).to eq('success')
|
||||
expect(json_response['name']).to eq('coverage')
|
||||
expect(json_response['ref']).to eq('develop')
|
||||
expect(json_response['target_url']).to eq('url')
|
||||
expect(json_response['description']).to eq('test')
|
||||
end
|
||||
end
|
||||
|
||||
context 'should not create commit status' do
|
||||
it 'with invalid state' do
|
||||
post api(post_url, user), state: 'invalid'
|
||||
expect(response.status).to eq(400)
|
||||
end
|
||||
|
||||
it 'without state' do
|
||||
post api(post_url, user)
|
||||
expect(response.status).to eq(400)
|
||||
end
|
||||
|
||||
it 'invalid commit' do
|
||||
post api("/projects/#{project.id}/statuses/invalid_sha", user), state: 'running'
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'guest user' do
|
||||
it 'should not create commit status' do
|
||||
post api(post_url, user2)
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'unauthorized user' do
|
||||
it 'should not create commit status' do
|
||||
post api(post_url)
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -47,6 +47,19 @@ describe API::API, api: true do
|
|||
get api("/projects/#{project.id}/repository/commits/invalid_sha", user)
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it "should return not_found for CI status" do
|
||||
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response['status']).to eq('not_found')
|
||||
end
|
||||
|
||||
it "should return status for CI" do
|
||||
ci_commit = project.ensure_ci_commit(project.repository.commit.sha)
|
||||
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response['status']).to eq(ci_commit.status)
|
||||
end
|
||||
end
|
||||
|
||||
context "unauthorized user" do
|
||||
|
|
Loading…
Reference in New Issue