Commit e3edd53a authored by Kamil Trzciński's avatar Kamil Trzciński

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
parents 5313c388 e7cc554c
......@@ -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?
end
@status ||= begin
latest = latest_statuses
latest.reject! { |status| status.try(&:allow_failure?) }
if latest.none?
'skipped'
elsif latest.all?(&:success?)
'success'
elsif pending?
elsif latest.all?(&:pending?)
'pending'
elsif running?
elsif latest.any?(&:running?) || latest.any?(&:pending?)
'running'
elsif canceled?
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
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
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)
unless commit.skip_ci?
commit.update_committed!
commit.create_builds(ref, tag, user)
end
commit
end
......
- 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,19 +20,19 @@
.bs-callout.bs-callout-warning
\.gitlab-ci.yml not found in this commit
- @ci_commit.refs.each do |ref|
.gray-content-block.second-block
Builds for #{ref}
- if @ci_commit.duration_for_ref(ref) > 0
.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_for_ref(ref)}
#{time_interval_in_words @ci_commit.duration}
%table.table.builds
%table.table.builds
%thead
%tr
%th Status
%th Build ID
%th Ref
%th Stage
%th Name
%th Duration
......@@ -40,10 +40,11 @@
- 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
- @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_builds.any?
%h3
- if @ci_commit.retried.any?
.gray-content-block.second-block
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?)
%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
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"
}
```
......@@ -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
......
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'
......
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(
......
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
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
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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment