Commit bd0cecfd authored by Douwe Maan's avatar Douwe Maan

Merge branch 'with-pipeline-view' into 'master'

Add pipeline view

This is continuation of https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3653

cc @DouweM @grzesiek 

Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/17551
Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/15625


See merge request !3703
parents 53e2d30a 4f1c6368
.pipeline-stage {
overflow: hidden;
text-overflow: ellipsis;
}
class Projects::PipelinesController < Projects::ApplicationController
before_action :pipeline, except: [:index, :new, :create]
before_action :commit, only: [:show]
before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
def index
@scope = params[:scope]
all_pipelines = project.ci_commits
@pipelines_count = all_pipelines.count
@running_or_pending_count = all_pipelines.running_or_pending.count
@pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope)
@pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30)
end
def new
@pipeline = project.ci_commits.new(ref: @project.default_branch)
end
def create
@pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute
unless @pipeline.persisted?
render 'new'
return
end
redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline)
end
def show
end
def retry
pipeline.retry_failed
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
def cancel
pipeline.cancel_running
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
private
def create_params
params.require(:pipeline).permit(:ref)
end
def pipeline
@pipeline ||= project.ci_commits.find_by!(id: params[:id])
end
def commit
@commit ||= @pipeline.commit_data
end
end
class PipelinesFinder
attr_reader :project
def initialize(project)
@project = project
end
def execute(pipelines, scope)
case scope
when 'running'
pipelines.running_or_pending
when 'branches'
from_ids(pipelines, ids_for_ref(pipelines, branches))
when 'tags'
from_ids(pipelines, ids_for_ref(pipelines, tags))
else
pipelines
end
end
private
def ids_for_ref(pipelines, refs)
pipelines.where(ref: refs).group(:ref).select('max(id)')
end
def from_ids(pipelines, ids)
pipelines.unscoped.where(id: ids)
end
def branches
project.repository.branches.map(&:name)
end
def tags
project.repository.tags.map(&:name)
end
end
...@@ -38,19 +38,30 @@ module CiStatusHelper ...@@ -38,19 +38,30 @@ module CiStatusHelper
icon(icon_name + ' fw') icon(icon_name + ' fw')
end end
def render_ci_status(ci_commit, tooltip_placement: 'auto left') def render_commit_status(commit, tooltip_placement: 'auto left')
# TODO: split this method into project = commit.project
# - render_commit_status path = builds_namespace_project_commit_path(project.namespace, project, commit)
# - render_pipeline_status render_status_with_link('commit', commit.status, path, tooltip_placement)
link_to ci_icon_for_status(ci_commit.status), end
ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
title: "Build #{ci_label_for_status(ci_commit.status)}", project = pipeline.project
data: { toggle: 'tooltip', placement: tooltip_placement } path = namespace_project_pipeline_path(project.namespace, project, pipeline)
render_status_with_link('pipeline', pipeline.status, path, tooltip_placement)
end end
def no_runners_for_project?(project) def no_runners_for_project?(project)
project.runners.blank? && project.runners.blank? &&
Ci::Runner.shared.blank? Ci::Runner.shared.blank?
end end
private
def render_status_with_link(type, status, path, tooltip_placement)
link_to ci_icon_for_status(status),
path,
class: "ci-status-link ci-status-icon-#{status.dasherize}",
title: "#{type.titleize}: #{ci_label_for_status(status)}",
data: { toggle: 'tooltip', placement: tooltip_placement }
end
end end
...@@ -205,6 +205,7 @@ class Ability ...@@ -205,6 +205,7 @@ class Ability
:read_commit_status, :read_commit_status,
:read_build, :read_build,
:read_container_image, :read_container_image,
:read_pipeline,
] ]
end end
...@@ -216,6 +217,8 @@ class Ability ...@@ -216,6 +217,8 @@ class Ability
:update_commit_status, :update_commit_status,
:create_build, :create_build,
:update_build, :update_build,
:create_pipeline,
:update_pipeline,
:create_merge_request, :create_merge_request,
:create_wiki, :create_wiki,
:push_code, :push_code,
...@@ -248,6 +251,7 @@ class Ability ...@@ -248,6 +251,7 @@ class Ability
:admin_commit_status, :admin_commit_status,
:admin_build, :admin_build,
:admin_container_image, :admin_container_image,
:admin_pipeline
] ]
end end
...@@ -290,6 +294,7 @@ class Ability ...@@ -290,6 +294,7 @@ class Ability
unless project.builds_enabled unless project.builds_enabled
rules += named_abilities('build') rules += named_abilities('build')
rules += named_abilities('pipeline')
end end
unless project.container_registry_enabled unless project.container_registry_enabled
......
...@@ -8,8 +8,6 @@ module Ci ...@@ -8,8 +8,6 @@ module Ci
has_many :builds, class_name: 'Ci::Build' has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
delegate :stages, to: :statuses
validates_presence_of :sha validates_presence_of :sha
validates_presence_of :status validates_presence_of :status
validate :valid_commit_sha validate :valid_commit_sha
...@@ -22,7 +20,8 @@ module Ci ...@@ -22,7 +20,8 @@ module Ci
end end
def self.stages def self.stages
CommitStatus.where(commit: all).stages # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
CommitStatus.where(commit: pluck(:id)).stages
end end
def project_id def project_id
...@@ -67,6 +66,25 @@ module Ci ...@@ -67,6 +66,25 @@ module Ci
end end
end end
def cancel_running
builds.running_or_pending.each(&:cancel)
end
def retry_failed
builds.latest.failed.select(&:retryable?).each(&:retry)
end
def latest?
return false unless ref
commit = project.commit(ref)
return false unless commit
commit.sha == sha
end
def triggered?
trigger_requests.any?
end
def create_builds(user, trigger_request = nil) def create_builds(user, trigger_request = nil)
return unless config_processor return unless config_processor
config_processor.stages.any? do |stage| config_processor.stages.any? do |stage|
......
...@@ -14,7 +14,8 @@ class CommitStatus < ActiveRecord::Base ...@@ -14,7 +14,8 @@ class CommitStatus < ActiveRecord::Base
alias_attribute :author, :user alias_attribute :author, :user
scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) } scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) }
scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
state_machine :status, initial: :pending do state_machine :status, initial: :pending do
...@@ -54,13 +55,15 @@ class CommitStatus < ActiveRecord::Base ...@@ -54,13 +55,15 @@ class CommitStatus < ActiveRecord::Base
end end
def self.stages def self.stages
order_by = 'max(stage_idx)' # We group by stage name, but order stages by theirs' index
group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
end end
def self.stages_status def self.stages_status
all.stages.inject({}) do |h, stage| # We execute subquery for each stage to calculate a stage status
h[stage] = all.where(stage: stage).status statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql)
statuses.inject({}) do |h, k|
h[k.first] = k.last
h h
end end
end end
......
module Ci
class CreatePipelineService < BaseService
def execute
pipeline = project.ci_commits.new(params)
unless ref_names.include?(params[:ref])
pipeline.errors.add(:base, 'Reference not found')
return pipeline
end
unless commit
pipeline.errors.add(:base, 'Commit not found')
return pipeline
end
unless can?(current_user, :create_pipeline, project)
pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline')
return pipeline
end
begin
Ci::Commit.transaction do
pipeline.sha = commit.id
unless pipeline.config_processor
pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
raise ActiveRecord::Rollback
end
pipeline.save!
pipeline.create_builds(current_user)
end
rescue
pipeline.errors.add(:base, 'The pipeline could not be created. Please try again.')
end
pipeline
end
private
def ref_names
@ref_names ||= project.repository.ref_names
end
def commit
@commit ||= project.commit(params[:ref])
end
end
end
...@@ -39,6 +39,13 @@ ...@@ -39,6 +39,13 @@
Commits Commits
- if project_nav_tab? :builds - if project_nav_tab? :builds
= nav_link(controller: :pipelines) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
= icon('ship fw')
%span
Pipelines
%span.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count)
= nav_link(controller: %w(builds)) do = nav_link(controller: %w(builds)) do
= link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
= icon('cubes fw') = icon('cubes fw')
......
...@@ -13,7 +13,9 @@ ...@@ -13,7 +13,9 @@
%strong ##{build.id} %strong ##{build.id}
- if build.stuck? - if build.stuck?
%i.fa.fa-warning.text-warning = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
- if defined?(retried) && retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
- if defined?(commit_sha) && commit_sha - if defined?(commit_sha) && commit_sha
%td %td
...@@ -40,7 +42,7 @@ ...@@ -40,7 +42,7 @@
%td %td
= build.name = build.name
%td .pull-right
.label-container .label-container
- if build.tags.any? - if build.tags.any?
- build.tags.each do |tag| - build.tags.each do |tag|
...@@ -55,10 +57,14 @@ ...@@ -55,10 +57,14 @@
%td.duration %td.duration
- if build.duration - if build.duration
= icon("clock-o")
&nbsp;
#{duration_in_words(build.finished_at, build.started_at)} #{duration_in_words(build.finished_at, build.started_at)}
%td.timestamp %td.timestamp
- if build.finished_at - if build.finished_at
= icon("calendar")
&nbsp;
%span #{time_ago_with_tooltip(build.finished_at)} %span #{time_ago_with_tooltip(build.finished_at)}
- if defined?(coverage) && coverage - if defined?(coverage) && coverage
...@@ -70,11 +76,11 @@ ...@@ -70,11 +76,11 @@
.pull-right .pull-right
- if can?(current_user, :read_build, build) && build.artifacts? - if can?(current_user, :read_build, build) && build.artifacts?
= link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do
%i.fa.fa-download = icon('download')
- if can?(current_user, :update_build, build) - if can?(current_user, :update_build, build)
- if build.active? - if build.active?
= link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
%i.fa.fa-remove.cred = icon('remove', class: 'cred')
- elsif defined?(allow_retry) && allow_retry && build.retryable? - elsif defined?(allow_retry) && allow_retry && build.retryable?
= link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
%i.fa.fa-refresh = icon('refresh')
- status = commit.status
%tr.commit
%td.commit-link
= link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status)
%strong ##{commit.id}
%td
%div.branch-commit
- if commit.ref
= link_to commit.ref, namespace_project_commits_path(@project.namespace, @project, commit.ref), class: "monospace"
&middot;
= link_to commit.short_sha, namespace_project_commit_path(@project.namespace, @project, commit.sha), class: "commit-id monospace"
&nbsp;
- if commit.latest?
%span.label.label-success latest
- if commit.tag?
%span.label.label-primary tag
- if commit.triggered?
%span.label.label-primary triggered
- if commit.yaml_errors.present?
%span.label.label-danger.has-tooltip{ title: "#{commit.yaml_errors}" } yaml invalid
- if commit.builds.any?(&:stuck?)
%span.label.label-warning stuck
%p
%span
- if commit_data = commit.commit_data
= link_to_gfm commit_data.title, namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
- stages_status = commit.statuses.stages_status
- stages.each do |stage|
%td
- if status = stages_status[stage]
- tooltip = "#{stage.titleize}: #{status}"
%span.has-tooltip{ title: "#{tooltip}", class: "ci-status-icon-#{status}" }
= ci_icon_for_status(status)
%td
- if commit.started_at && commit.finished_at
%p
= icon("clock-o")
&nbsp;
#{duration_in_words(commit.finished_at, commit.started_at)}
- if commit.finished_at
%p
= icon("calendar")
&nbsp;
#{time_ago_with_tooltip(commit.finished_at)}
%td
.controls.hidden-xs.pull-right
- artifacts = commit.builds.latest.select { |b| b.artifacts? }
- if artifacts.present?
.dropdown.inline.build-artifacts
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
= icon('download')
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
- artifacts.each do |build|
%li
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do
= icon("download")
%span #{build.name}
- if can?(current_user, :update_pipeline, @project)
&nbsp;
- if commit.retryable? && commit.builds.failed.any?
= link_to retry_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do
= icon("repeat")
&nbsp;
- if commit.active?
= link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
= icon("remove")
- @ci_commits.each do |ci_commit| - @ci_commits.each do |ci_commit|
= render "ci_commit", ci_commit: ci_commit = render "ci_commit", ci_commit: ci_commit, pipeline_details: true
.row-content-block.build-content.middle-block .row-content-block.build-content.middle-block
.pull-right .pull-right
- if can?(current_user, :update_build, @project) - if can?(current_user, :update_pipeline, @project)
- if ci_commit.builds.latest.failed.any?(&:retryable?) - if ci_commit.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post = link_to "Retry failed", retry_namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), class: 'btn btn-grouped btn-primary', method: :post
- if ci_commit.builds.running_or_pending.any? - if ci_commit.builds.running_or_pending.any?
= link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post = link_to "Cancel running", cancel_namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
.oneline .oneline.clearfix
- if defined?(pipeline_details) && pipeline_details
Pipeline
= link_to "##{ci_commit.id}", namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), class: "monospace"
with
= pluralize ci_commit.statuses.count(:id), "build" = pluralize ci_commit.statuses.count(:id), "build"
- if ci_commit.ref - if ci_commit.ref
for for
%span.label.label-info = link_to ci_commit.ref, namespace_project_commits_path(@project.namespace, @project, ci_commit.ref), class: "monospace"
= ci_commit.ref
- if defined?(link_to_commit) && link_to_commit - if defined?(link_to_commit) && link_to_commit
for commit for commit
= link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace" = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace"
...@@ -34,38 +37,5 @@ ...@@ -34,38 +37,5 @@
.table-holder .table-holder
%table.table.builds %table.table.builds
%thead - ci_commit.statuses.stages.each do |stage|
%tr = render 'projects/commit/ci_stage', stage: stage, statuses: ci_commit.statuses.where(stage: stage)
%th Status
%th Build ID
%th Stage
%th Name
%th Tags
%th Duration
%th Finished at
- if @project.build_coverage_enabled?
%th Coverage
%th
- builds = ci_commit.statuses.latest.ordered
= render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true
- if ci_commit.retried.any?
.row-content-block.second-block
Retried builds
.table-holder
%table.table.builds
%thead
%tr
%th Status
%th Build ID
%th Ref
%th Stage
%th Name
%th Tags
%th Duration
%th Finished at
- if @project.build_coverage_enabled?
%th Coverage
%th
= render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false
%tr
%th{colspan: 10}
%strong
- status = statuses.latest.status
%span{class: "ci-status-link ci-status-icon-#{status}"}
= ci_icon_for_status(status)
- if stage
&nbsp;
= stage.titleize.pluralize
= render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
= render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
%tr
%td{colspan: 10}
&nbsp;
.pull-right.commit-action-buttons .pull-right.commit-action-buttons
%div %div
- if @notes_count > 0 - if defined?(@notes_count) && @notes_count > 0
%span.btn.disabled.btn-grouped %span.btn.disabled.btn-grouped
%i.fa.fa-comment %i.fa.fa-comment
= @notes_count = @notes_count
...@@ -23,11 +23,6 @@ ...@@ -23,11 +23,6 @@
%p %p
.commit-info-row .commit-info-row
- if @commit.status
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do
= ci_icon_for_status(@commit.status)
build:
= ci_label_for_status(@commit.status)
%span.light Authored by %span.light Authored by
%strong %strong
= commit_author_link(@commit, avatar: true, size: 24) = commit_author_link(@commit, avatar: true, size: 24)
...@@ -51,6 +46,17 @@ ...@@ -51,6 +46,17 @@
%span.commit-info.branches %span.commit-info.branches
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
- if @commit.status
.commit-info-row
Builds for
= pluralize(@commit.ci_commits.count, 'pipeline')
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do
= ci_icon_for_status(@commit.status)
= ci_label_for_status(@commit.status)
- if @commit.ci_commits.duration
in
= time_interval_in_words @commit.ci_commits.duration
.commit-box.content-block .commit-box.content-block
%h3.commit-title %h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line = markdown escape_once(@commit.title), pipeline: :single_line
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
.pull-right .pull-right
- if commit.status - if commit.status
= render_ci_status(commit) = render_commit_status(commit)
= clipboard_button(clipboard_text: commit.id) = clipboard_button(clipboard_text: commit.id)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
......
...@@ -12,6 +12,9 @@ ...@@ -12,6 +12,9 @@
- else - else
%strong ##{generic_commit_status.id} %strong ##{generic_commit_status.id}
- if defined?(retried) && retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.')
- if defined?(commit_sha) && commit_sha - if defined?(commit_sha) && commit_sha
%td %td
= link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace" = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
...@@ -42,13 +45,19 @@ ...@@ -42,13 +45,19 @@
- generic_commit_status.tags.each do |tag| - generic_commit_status.tags.each do |tag|
%span.label.label-primary %span.label.label-primary
= tag = tag
- if defined?(retried) && retried
%span.label.label-warning retried
%td.duration %td.duration
- if generic_commit_status.duration - if generic_commit_status.duration
= icon("clock-o")
&nbsp;
#{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)} #{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
%td.timestamp %td.timestamp
- if generic_commit_status.finished_at - if generic_commit_status.finished_at
= icon("calendar")
&nbsp;
%span #{time_ago_with_tooltip(generic_commit_status.finished_at)} %span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
- if defined?(coverage) && coverage - if defined?(coverage) && coverage
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
%li %li
%span.merge-request-ci-status %span.merge-request-ci-status
- if merge_request.ci_commit - if merge_request.ci_commit
= render_ci_status(merge_request.ci_commit) = render_pipeline_status(merge_request.ci_commit)
- elsif has_any_ci - elsif has_any_ci
= icon('blank fw') = icon('blank fw')
%span.merge-request-id %span.merge-request-id
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- ci_commit = @project.ci_commit(sha, branch) if sha - ci_commit = @project.ci_commit(sha, branch) if sha
- if ci_commit - if ci_commit
%span.related-branch-ci-status %span.related-branch-ci-status
= render_ci_status(ci_commit) = render_pipeline_status(ci_commit)
%span.related-branch-info %span.related-branch-info
%strong %strong
= link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
- if merge_request.ci_commit - if merge_request.ci_commit
%li %li
= render_ci_status(merge_request.ci_commit) = render_pipeline_status(merge_request.ci_commit)
- if merge_request.open? && merge_request.broken? - if merge_request.open? && merge_request.broken?
%li %li
......
- header_title project_title(@project, "Pipelines", project_pipelines_path(@project))
%p
.commit-info-row
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace"
with
= pluralize @pipeline.statuses.count(:id), "build"
- if @pipeline.ref
for
= link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
- if @pipeline.duration
in
= time_interval_in_words @pipeline.duration
.pull-right
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
= ci_icon_for_status(@pipeline.status)
= ci_label_for_status(@pipeline.status)
- if @commit
.commit-info-row
%span.light Authored by
%strong
= commit_author_link(@commit, avatar: true, size: 24)
#{time_ago_with_tooltip(@commit.authored_date)}
.commit-info-row
%span.light Commit
= link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace"
= clipboard_button(clipboard_text: @pipeline.sha)
- if @commit
.commit-box.content-block
%h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line
- if @commit.description.present?
%pre.commit-description
= preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
- page_title "Pipelines"
= render "header_title"
.top-area
%ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to project_pipelines_path(@project) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(@pipelines_count)
%li{class: ('active' if @scope == 'running')}
= link_to project_pipelines_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@running_or_pending_count)
%li{class: ('active' if @scope == 'branches')}
= link_to project_pipelines_path(@project, scope: :branches) do
Branches
%li{class: ('active' if @scope == 'tags')}
= link_to project_pipelines_path(@project, scope: :tags) do
Tags
.nav-controls
- if can? current_user, :create_pipeline, @project
= link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do
= icon('plus')
New pipeline
- unless @repository.gitlab_ci_yml
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do
= icon('wrench')
%span CI Lint
.row-content-block
- if @scope == 'running'
Running pipelines for this project
- elsif @scope.nil?
Pipelines for this project
- else
#{@scope.titleize} for this project
%ul.content-list
- stages = @pipelines.stages
- if @pipelines.blank?
%li
.nothing-here-block No pipelines to show
- else
.table-holder
%table.table.builds
%tbody
%th ID
%th Commit
- stages.each do |stage|
%th
%span.pipeline-stage.has-tooltip{ title: "#{stage.titleize}" }
= stage.titleize.pluralize
%th
%th
= render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
= paginate @pipelines, theme: 'gitlab'
- page_title "New Pipeline"
= render "header_title"
%h3.page-title
New Pipeline
%hr
= form_for @pipeline, as: :pipeline, url: namespace_project_pipelines_path(@project.namespace, @project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
.form-group
= f.label :ref, 'Create for', class: 'control-label'
.col-sm-10
= f.text_field :ref, required: true, tabindex: 2, class: 'form-control'
.help-block Existing branch name, tag
.form-actions
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', namespace_project_pipelines_path(@project.namespace, @project), class: 'btn btn-cancel'
:javascript
var availableRefs = #{@project.repository.ref_names.to_json};
new NewBranchForm($('.js-new-pipeline-form'), availableRefs)
- page_title "Pipeline"
= render "header_title"
.prepend-top-default
- if @commit
= render "projects/pipelines/info"
%div.block-connector
= render "projects/commit/ci_commit", ci_commit: @pipeline
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= project.main_language = project.main_language
- if project.commit.try(:status) - if project.commit.try(:status)
%span %span
= render_ci_status(project.commit) = render_commit_status(project.commit)
- if forks - if forks
%span %span
= icon('code-fork') = icon('code-fork')
......
...@@ -666,6 +666,13 @@ Rails.application.routes.draw do ...@@ -666,6 +666,13 @@ Rails.application.routes.draw do
resources :variables, only: [:index, :show, :update, :create, :destroy] resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :triggers, only: [:index, :create, :destroy] resources :triggers, only: [:index, :create, :destroy]
resources :pipelines, only: [:index, :new, :create, :show] do
member do
post :cancel
post :retry
end
end
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do collection do
post :cancel_all post :cancel_all
......
...@@ -13,7 +13,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps ...@@ -13,7 +13,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
end end
step 'I should see "Shop" project CI status' do step 'I should see "Shop" project CI status' do
expect(page).to have_link "Build skipped" expect(page).to have_link "Commit: skipped"
end end
step 'I should see last push widget' do step 'I should see last push widget' do
......
...@@ -173,7 +173,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps ...@@ -173,7 +173,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end end
step 'I see commit ci info' do step 'I see commit ci info' do
expect(page).to have_content "build: pending" expect(page).to have_content "Builds for 1 pipeline pending"
end end
step 'I click status link' do step 'I click status link' do
...@@ -181,7 +181,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps ...@@ -181,7 +181,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end end
step 'I see builds list' do step 'I see builds list' do
expect(page).to have_content "build: pending" expect(page).to have_content "Builds for 1 pipeline pending"
expect(page).to have_content "1 build" expect(page).to have_content "1 build"
end end
......
...@@ -525,7 +525,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -525,7 +525,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see merge request "Bug NS-05" with CI status' do step 'I should see merge request "Bug NS-05" with CI status' do
page.within ".mr-list" do page.within ".mr-list" do
expect(page).to have_link "Build pending" expect(page).to have_link "Pipeline: pending"
end end
end end
......
require 'spec_helper'
describe "Pipelines" do
include GitlabRoutingHelper
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
before do
login_as(user)
project.team << [user, :developer]
end
describe 'GET /:project/pipelines' do
let!(:pipeline) { create(:ci_commit, project: project, ref: 'master', status: 'running') }
[:all, :running, :branches].each do |scope|
context "displaying #{scope}" do
let(:project) { create(:project) }
before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) }
it { expect(page).to have_content(pipeline.short_sha) }
end
end
context 'cancelable pipeline' do
let!(:running) { create(:ci_build, :running, commit: pipeline, stage: 'test', commands: 'test') }
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_link('Cancel') }
it { expect(page).to have_selector('.ci-running') }
context 'when canceling' do
before { click_link('Cancel') }
it { expect(page).to_not have_link('Cancel') }
it { expect(page).to have_selector('.ci-canceled') }
end
end
context 'retryable pipelines' do
let!(:failed) { create(:ci_build, :failed, commit: pipeline, stage: 'test', commands: 'test') }
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_link('Retry') }
it { expect(page).to have_selector('.ci-failed') }
context 'when retrying' do
before { click_link('Retry') }
it { expect(page).to_not have_link('Retry') }
it { expect(page).to have_selector('.ci-pending') }
end
end
context 'downloadable pipelines' do
context 'with artifacts' do
let!(:with_artifacts) { create(:ci_build, :artifacts, :success, commit: pipeline, name: 'rspec tests', stage: 'test') }
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_selector('.build-artifacts') }
it { expect(page).to have_link(with_artifacts.name) }
end
context 'without artifacts' do
let!(:without_artifacts) { create(:ci_build, :success, commit: pipeline, name: 'rspec', stage: 'test') }
it { expect(page).to_not have_selector('.build-artifacts') }
end
end
end
describe 'GET /:project/pipelines/:id' do
let(:pipeline) { create(:ci_commit, project: project, ref: 'master') }
before do
@success = create(:ci_build, :success, commit: pipeline, stage: 'build', name: 'build')
@failed = create(:ci_build, :failed, commit: pipeline, stage: 'test', name: 'test', commands: 'test')
@running = create(:ci_build, :running, commit: pipeline, stage: 'deploy', name: 'deploy')
@external = create(:generic_commit_status, status: 'success', commit: pipeline, name: 'jenkins', stage: 'external')
end
before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
it 'showing a list of builds' do
expect(page).to have_content('Tests')
expect(page).to have_content(@success.id)
expect(page).to have_content('Deploy')
expect(page).to have_content(@failed.id)
expect(page).to have_content(@running.id)
expect(page).to have_content(@external.id)
expect(page).to have_content('Retry failed')
expect(page).to have_content('Cancel running')
end
context 'retrying builds' do
it { expect(page).to_not have_content('retried') }
context 'when retrying' do
before { click_on 'Retry failed' }
it { expect(page).to_not have_content('Retry failed') }
it { expect(page).to have_content('retried') }
end
end
context 'canceling builds' do
it { expect(page).to_not have_selector('.ci-canceled') }
context 'when canceling' do
before { click_on 'Cancel running' }
it { expect(page).to_not have_content('Cancel running') }
it { expect(page).to have_selector('.ci-canceled') }
end
end
end
describe 'POST /:project/pipelines' do
let(:project) { create(:project) }
before { visit new_namespace_project_pipeline_path(project.namespace, project) }
context 'for valid commit' do
before { fill_in('Create for', with: 'master') }
context 'with gitlab-ci.yml' do
before { stub_ci_commit_to_return_yaml_file }
it { expect{ click_on 'Create pipeline' }.to change{ Ci::Commit.count }.by(1) }
end
context 'without gitlab-ci.yml' do
before { click_on 'Create pipeline' }
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
end
end
context 'for invalid commit' do
before do
fill_in('Create for', with: 'invalid reference')
click_on 'Create pipeline'
end
it { expect(page).to have_content('Reference not found') }
end
end
end
...@@ -10,7 +10,6 @@ describe Ci::Commit, models: true do ...@@ -10,7 +10,6 @@ describe Ci::Commit, models: true do
it { is_expected.to have_many(:builds) } it { is_expected.to have_many(:builds) }
it { is_expected.to validate_presence_of :sha } it { is_expected.to validate_presence_of :sha }
it { is_expected.to validate_presence_of :status } it { is_expected.to validate_presence_of :status }
it { is_expected.to delegate_method(:stages).to(:statuses) }
it { is_expected.to respond_to :git_author_name } it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :git_author_email }
......
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