Commit 914cfbd2 authored by Kamil Trzcinski's avatar Kamil Trzcinski

Implement Commit Status API

parent 5ffbf5fe
......@@ -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,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
......
......@@ -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
......
......@@ -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
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
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
......@@ -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)
......
......@@ -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}
......
......@@ -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
- gl_project = build.project.gl_project
%tr.build
%tr.commit_status
%td.status
= ci_status_with_icon(build.status)
= ci_status_with_icon(commit_status.status)
%td.build-link
= link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do
%strong Build ##{build.id}
%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
= build.ref
= commit_status.ref
%td
= build.stage
= commit_status.stage
%td
= build.name
= commit_status.description
.pull-right
- if build.tags.any?
- build.tag_list.each do |tag|
- if commit_status.tags.any?
- commit_status.tags.each do |tag|
%span.label.label-primary
= tag
- if build.trigger_request
- if commit_status.try(:trigger_request)
%span.label.label-info triggered
- if build.allow_failure
- if commit_status.try(:allow_failure)
%span.label.label-danger allowed to fail
%td.duration
- if build.duration
#{duration_in_words(build.finished_at, build.started_at)}
- if commit_status.duration
#{duration_in_words(commit_status.finished_at, commit_status.started_at)}
%td.timestamp
- if build.finished_at
%span #{time_ago_in_words build.finished_at} ago
- if commit_status.finished_at
%span #{time_ago_in_words commit_status.finished_at} ago
- if build.project.coverage_enabled?
- if defined?(coverage)
%td.coverage
- if build.coverage
#{build.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 build.active?
= link_to cancel_namespace_project_build_path(gl_project.namespace, gl_project, build, return_to: request.original_url), title: 'Cancel build' do
- 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 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
- 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
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
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"
}
```
......
......@@ -46,6 +46,7 @@ module API
mount Services
mount Files
mount Commits
mount CommitStatus
mount Namespaces
mount Branches
mount Labels
......
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