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
# 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
......@@ -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
......
......@@ -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')
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
......
......@@ -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(
......
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
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/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
......@@ -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