summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG1
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/ci/build.rb87
-rw-r--r--app/models/ci/commit.rb88
-rw-r--r--app/models/commit.rb8
-rw-r--r--app/models/commit_status.rb79
-rw-r--r--app/models/generic_commit_status.rb15
-rw-r--r--app/services/ci/create_commit_service.rb2
-rw-r--r--app/views/projects/builds/_build.html.haml50
-rw-r--r--app/views/projects/builds/show.html.haml6
-rw-r--r--app/views/projects/commit/ci.html.haml36
-rw-r--r--app/views/projects/commit_statuses/_commit_status.html.haml52
-rw-r--r--db/migrate/20151008123042_add_type_and_description_to_builds.rb9
-rw-r--r--db/migrate/20151008130321_migrate_name_to_description_for_builds.rb5
-rw-r--r--db/schema.rb7
-rw-r--r--doc/api/commits.md3
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/commit_statuses.rb79
-rw-r--r--lib/api/entities.rb8
-rw-r--r--lib/ci/api/entities.rb4
-rw-r--r--spec/factories/ci/builds.rb1
-rw-r--r--spec/factories/commit_statuses.rb15
-rw-r--r--spec/features/commits_spec.rb1
-rw-r--r--spec/models/build_spec.rb (renamed from spec/models/ci/build_spec.rb)111
-rw-r--r--spec/models/ci/commit_spec.rb57
-rw-r--r--spec/models/ci/project_services/mail_service_spec.rb8
-rw-r--r--spec/models/commit_status_spec.rb157
-rw-r--r--spec/models/generic_commit_status_spec.rb39
-rw-r--r--spec/requests/api/commit_status_spec.rb135
-rw-r--r--spec/requests/api/commits_spec.rb13
30 files changed, 767 insertions, 312 deletions
diff --git a/CHANGELOG b/CHANGELOG
index a3d796bea66..e38d465dd96 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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
diff --git a/app/models/ability.rb b/app/models/ability.rb
index a020b24a550..77c121ca5e8 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -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,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 5d17f4418ed..41ce522b2ff 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -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,15 @@ module Ci
"#{dir_to_trace}/#{id}.log"
end
+ def description
+ name
+ end
+
+ def target_url
+ Gitlab::Application.routes.url_helpers.
+ namespace_project_build_url(gl_project.namespace, gl_project, self)
+ end
+
private
def yaml_variables
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
index fde754a92a1..042a68681bb 100644
--- a/app/models/ci/commit.rb
+++ b/app/models/ci/commit.rb
@@ -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
@@ -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
+ 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,39 +106,47 @@ module Ci
end
def refs
- builds.group(:ref).pluck(:ref)
+ statuses.pluck(:ref).compact.uniq
end
- def last_ref
- builds.latest.first.try(:ref)
- end
-
- def builds_without_retry
- builds.latest
+ def statuses_for_ref(ref = nil)
+ if ref
+ statuses.for_ref(ref)
+ else
+ statuses
+ end
end
- def builds_without_retry_for_ref(ref)
- builds.for_ref(ref).latest
+ def builds_without_retry(ref = nil)
+ if ref
+ builds.for_ref(ref).latest
+ else
+ builds.latest
+ end
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?
+ def status(ref = nil)
+ if yaml_errors.present?
return 'failed'
- elsif builds.none?
+ end
+
+ latest_statuses = statuses.latest.to_a
+ latest_statuses.reject! { |status| status.try(&:allow_failure?) }
+ latest_statuses.select! { |status| status.ref == nil || status.ref == ref } if ref
+
+ if latest_statuses.none?
return 'skipped'
- elsif success?
+ elsif latest_statuses.all?(&:success?)
'success'
- elsif pending?
+ elsif latest_statuses.all?(&:pending?)
'pending'
- elsif running?
+ elsif latest_statuses.any?(&:running?) || latest_statuses.any?(&:pending?)
'running'
- elsif canceled?
+ elsif latest_statuses.all?(&:canceled?)
'canceled'
else
'failed'
@@ -147,21 +154,15 @@ module Ci
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,21 +170,15 @@ module Ci
end
def canceled?
- builds_without_retry.all? do |build|
- build.canceled?
- end
- end
-
- def duration
- @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i
+ status == 'canceled'
end
- def duration_for_ref(ref)
- builds_without_retry_for_ref(ref).select(&:duration).sum(&:duration).to_i
+ def duration(ref = nil)
+ statuses_for_ref(ref).latest.select(&:duration).sum(&:duration).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
@@ -195,8 +190,8 @@ module Ci
end
end
- def matrix_for_ref?(ref)
- builds_without_retry_for_ref(ref).pluck(:id).size > 1
+ def matrix?(ref)
+ builds_without_retry(ref).pluck(:id).size > 1
end
def config_processor
@@ -217,7 +212,6 @@ module Ci
end
def skip_ci?
- return false if builds.any?
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index aff329d71fa..d5c50013525 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -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
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
new file mode 100644
index 00000000000..4c6de18527b
--- /dev/null
+++ b/app/models/commit_status.rb
@@ -0,0 +1,79 @@
+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
+
+ 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)).order(stage_idx: :asc) }
+ scope :for_ref, ->(ref) { where(ref: [ref, nil]) }
+ 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
+
+ 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
+end
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
new file mode 100644
index 00000000000..fa54e3540d0
--- /dev/null
+++ b/app/models/generic_commit_status.rb
@@ -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
diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb
index fc1ae5774d5..0ae35387579 100644
--- a/app/services/ci/create_commit_service.rb
+++ b/app/services/ci/create_commit_service.rb
@@ -17,6 +17,8 @@ module Ci
tag = origin_ref.start_with?('refs/tags/')
commit = project.gl_project.ensure_ci_commit(sha)
+ return false if commit.skip_ci?
+
commit.update_committed!
commit.create_builds(ref, tag, user)
diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml
deleted file mode 100644
index 65fd9413b60..00000000000
--- a/app/views/projects/builds/_build.html.haml
+++ /dev/null
@@ -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
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index b561078e8c7..66e668f3771 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -7,9 +7,9 @@
%code #{@build.ref}
#up-build-trace
- - if @commit.matrix_for_ref?(@build.ref)
+ - if @commit.matrix?(@build.ref)
%ul.center-top-menu.build-top-menu
- - @commit.builds_without_retry_for_ref(@build.ref).each do |build|
+ - @commit.builds_without_retry(@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.builds_without_retry(@build.ref).include?(@build)
%li.active
%a
Build ##{@build.id}
diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml
index 26ab38445c2..7f106631cac 100644
--- a/app/views/projects/commit/ci.html.haml
+++ b/app/views/projects/commit/ci.html.haml
@@ -20,13 +20,35 @@
.bs-callout.bs-callout-warning
\.gitlab-ci.yml not found in this commit
-- @ci_commit.refs.each do |ref|
+- if @ci_commit.refs.blank?
+ .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 Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @ci_project && @ci_project.coverage_enabled?
+ %th Coverage
+ %th
+ = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.latest, coverage: @ci_project.try(:coverage_enabled?), controls: true
+
+- @ci_commit.refs.sort.each do |ref|
.gray-content-block.second-block
Builds for #{ref}
- - if @ci_commit.duration_for_ref(ref) > 0
+ - if @ci_commit.duration(ref) > 0
%small.pull-right
%i.fa.fa-time
- #{time_interval_in_words @ci_commit.duration_for_ref(ref)}
+ #{time_interval_in_words @ci_commit.duration(ref)}
%table.table.builds
%thead
@@ -40,10 +62,10 @@
- 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
+ = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest, coverage: @ci_project.try(:coverage_enabled?), controls: true
-- if @ci_commit.retried_builds.any?
- %h3
+- if @ci_commit.retried.any?
+ .gray-content-block.second-block
Retried builds
%table.table.builds
@@ -59,4 +81,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?), ref: true
diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml
new file mode 100644
index 00000000000..f79929c70bf
--- /dev/null
+++ b/app/views/projects/commit_statuses/_commit_status.html.haml
@@ -0,0 +1,52 @@
+%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}
+
+ - if defined?(ref)
+ %td
+ = commit_status.ref
+
+ %td
+ = commit_status.stage
+
+ %td
+ = commit_status.description
+ .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)
+ %td.coverage
+ - if commit_status.try(:coverage)
+ #{commit_status.coverage}%
+
+ %td
+ - if defined?(controls) && current_user && can?(current_user, :manage_builds, gl_project)
+ .pull-right
+ - if commit_status.active?
+ = link_to cancel_namespace_project_build_path(gl_project.namespace, gl_project, commit_status, return_to: request.original_url), title: 'Cancel commit_status' do
+ %i.fa.fa-remove.cred
+ - elsif commit_status.commands.present?
+ = link_to retry_namespace_project_build_path(gl_project.namespace, gl_project, commit_status, return_to: request.original_url), method: :post, title: 'Retry commit_status' do
+ %i.fa.fa-repeat
diff --git a/db/migrate/20151008123042_add_type_and_description_to_builds.rb b/db/migrate/20151008123042_add_type_and_description_to_builds.rb
new file mode 100644
index 00000000000..c72b1c611c6
--- /dev/null
+++ b/db/migrate/20151008123042_add_type_and_description_to_builds.rb
@@ -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
diff --git a/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb
new file mode 100644
index 00000000000..f5c44babd84
--- /dev/null
+++ b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb
@@ -0,0 +1,5 @@
+class MigrateNameToDescriptionForBuilds < ActiveRecord::Migration
+ def change
+ execute("UPDATE ci_builds SET type='Ci::Build' WHERE type IS NULL")
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c5c462c2e57..7a11dfca034 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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
diff --git a/doc/api/commits.md b/doc/api/commits.md
index eb8d6a43592..b22d040bf0d 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -62,7 +62,8 @@ Parameters:
"authored_date": "2012-09-20T09:06:12+03:00",
"parent_ids": [
"ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
- ]
+ ],
+ "status": "running"
}
```
diff --git a/lib/api/api.rb b/lib/api/api.rb
index c09488d3547..afc0402f9e1 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -46,6 +46,7 @@ module API
mount Services
mount Files
mount Commits
+ mount CommitStatus
mount Namespaces
mount Branches
mount Labels
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
new file mode 100644
index 00000000000..cfe8739b175
--- /dev/null
+++ b/lib/api/commit_statuses.rb
@@ -0,0 +1,79 @@
+require 'mime/types'
+
+module API
+ # Project commit statuses API
+ class CommitStatus < Grape::API
+ resource :projects do
+ before { authenticate! }
+ before { authorize! :read_commit_statuses, user_project }
+
+ # 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
+ 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(name: 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/repository/commits/:sha/status
+ post ':id/statuses/:sha' do
+ 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) unless status
+ 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
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9620d36ac41..e1c5a459751 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -65,7 +65,7 @@ module API
expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at
expose :creator_id
expose :namespace
- expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? }
+ expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda { |project, options| project.forked? }
expose :avatar_url
expose :star_count, :forks_count
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,11 @@ 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
+ end
+
class Event < Grape::Entity
expose :title, :project_id, :action_name
expose :target_id, :target_type, :author_id
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
index f47bc1236b8..b80c0b8b273 100644
--- a/lib/ci/api/entities.rb
+++ b/lib/ci/api/entities.rb
@@ -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|
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 21b582afba4..2fcd70182b9 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -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'
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
new file mode 100644
index 00000000000..52de437052d
--- /dev/null
+++ b/spec/factories/commit_statuses.rb
@@ -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
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 5da220859e3..cbb6360069b 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -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
diff --git a/spec/models/ci/build_spec.rb b/spec/models/build_spec.rb
index da56f6e31ae..d875015b991 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -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 }
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
index acff1ddf0fc..371add4ee59 100644
--- a/spec/models/ci/commit_spec.rb
+++ b/spec/models/ci/commit_spec.rb
@@ -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')
+ 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 }
@@ -252,10 +283,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 +297,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 +317,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 +327,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
diff --git a/spec/models/ci/project_services/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb
index 04e870dce7f..2ce5c2b4707 100644
--- a/spec/models/ci/project_services/mail_service_spec.rb
+++ b/spec/models/ci/project_services/mail_service_spec.rb
@@ -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(
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
new file mode 100644
index 00000000000..c1af50afb7c
--- /dev/null
+++ b/spec/models/commit_status_spec.rb
@@ -0,0 +1,157 @@
+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 :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, @commit3])
+ 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
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
new file mode 100644
index 00000000000..f442fa5fbe5
--- /dev/null
+++ b/spec/models/generic_commit_status_spec.rb
@@ -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
diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb
new file mode 100644
index 00000000000..d1dc97e1846
--- /dev/null
+++ b/spec/requests/api/commit_status_spec.rb
@@ -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/repository/commits/:sha/status' do
+ let(:post_url) { "/projects/#{project.id}/repository/commits/#{commit.id}/status" }
+
+ 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}/repository/commits/invalid_sha/status", 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
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index a1c248c636e..49acc3368f4 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -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