Commit 15e87cea authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '8998_skip_pending_commits_if_not_head' into 'master'

Add auto-cancel for pending pipelines on branch, if they are not HEAD

See merge request !9362
parents c970fa97 38796f56
...@@ -116,7 +116,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -116,7 +116,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def pipeline def pipeline
@pipeline ||= project.pipelines.find_by!(id: params[:id]) @pipeline ||= project.pipelines.find_by!(id: params[:id]).present(current_user: current_user)
end end
def commit def commit
......
...@@ -23,7 +23,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -23,7 +23,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def update_params def update_params
params.require(:project).permit( params.require(:project).permit(
:runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds :public_builds, :auto_cancel_pending_pipelines
) )
end end
end end
...@@ -4,9 +4,14 @@ module Ci ...@@ -4,9 +4,14 @@ module Ci
include HasStatus include HasStatus
include Importable include Importable
include AfterCommitQueue include AfterCommitQueue
include Presentable
belongs_to :project belongs_to :project
belongs_to :user belongs_to :user
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
has_many :builds, foreign_key: :commit_id has_many :builds, foreign_key: :commit_id
...@@ -71,6 +76,10 @@ module Ci ...@@ -71,6 +76,10 @@ module Ci
pipeline.update_duration pipeline.update_duration
end end
before_transition canceled: any - [:canceled] do |pipeline|
pipeline.auto_canceled_by = nil
end
after_transition [:created, :pending] => :running do |pipeline| after_transition [:created, :pending] => :running do |pipeline|
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) } pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
end end
...@@ -216,9 +225,24 @@ module Ci ...@@ -216,9 +225,24 @@ module Ci
cancelable_statuses.any? cancelable_statuses.any?
end end
def auto_canceled?
canceled? && auto_canceled_by_id?
end
def cancel_running def cancel_running
Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable| Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable|
cancelable.find_each(&:cancel) cancelable.find_each do |job|
yield(job) if block_given?
job.cancel
end
end
end
def auto_cancel_running(pipeline)
update(auto_canceled_by: pipeline)
cancel_running do |job|
job.auto_canceled_by = pipeline
end end
end end
......
...@@ -7,6 +7,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -7,6 +7,7 @@ class CommitStatus < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :user belongs_to :user
delegate :commit, to: :pipeline delegate :commit, to: :pipeline
...@@ -137,6 +138,10 @@ class CommitStatus < ActiveRecord::Base ...@@ -137,6 +138,10 @@ class CommitStatus < ActiveRecord::Base
false false
end end
def auto_canceled?
canceled? && auto_canceled_by_id?
end
# Added in 9.0 to keep backward compatibility for projects exported in 8.17 # Added in 9.0 to keep backward compatibility for projects exported in 8.17
# and prior. # and prior.
def gl_project_id def gl_project_id
......
...@@ -76,6 +76,7 @@ module HasStatus ...@@ -76,6 +76,7 @@ module HasStatus
scope :canceled, -> { where(status: 'canceled') } scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') } scope :skipped, -> { where(status: 'skipped') }
scope :manual, -> { where(status: 'manual') } scope :manual, -> { where(status: 'manual') }
scope :created_or_pending, -> { where(status: [:created, :pending]) }
scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) }
scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) }
......
...@@ -262,6 +262,8 @@ class Project < ActiveRecord::Base ...@@ -262,6 +262,8 @@ class Project < ActiveRecord::Base
scope :with_builds_enabled, -> { with_feature_enabled(:builds) } scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
# project features may be "disabled", "internal" or "enabled". If "internal", # project features may be "disabled", "internal" or "enabled". If "internal",
# they are only available to team members. This scope returns projects where # they are only available to team members. This scope returns projects where
# the feature is either enabled, or internal with permission for the user. # the feature is either enabled, or internal with permission for the user.
......
...@@ -11,5 +11,11 @@ module Ci ...@@ -11,5 +11,11 @@ module Ci
def erased_by_name def erased_by_name
erased_by.name if erased_by_user? erased_by.name if erased_by_user?
end end
def status_title
if auto_canceled?
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
end
end
end end
end end
module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated
presents :pipeline
def status_title
if auto_canceled?
"Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
end
end
end
end
...@@ -53,6 +53,8 @@ module Ci ...@@ -53,6 +53,8 @@ module Ci
.execute(pipeline) .execute(pipeline)
end end
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
pipeline.tap(&:process!) pipeline.tap(&:process!)
end end
...@@ -63,6 +65,22 @@ module Ci ...@@ -63,6 +65,22 @@ module Ci
pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
end end
def cancel_pending_pipelines
Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
cancelables.find_each do |cancelable|
cancelable.auto_cancel_running(pipeline)
end
end
end
def auto_cancelable_pipelines
project.pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.id)
.where.not(sha: project.repository.sha_from_ref(pipeline.ref))
.created_or_pending
end
def commit def commit
@commit ||= project.commit(origin_sha || origin_ref) @commit ||= project.commit(origin_sha || origin_ref)
end end
......
- status = local_assigns.fetch(:status) - status = local_assigns.fetch(:status)
- link = local_assigns.fetch(:link, true) - link = local_assigns.fetch(:link, true)
- css_classes = "ci-status ci-#{status.group}" - title = local_assigns.fetch(:title, nil)
- css_classes = "ci-status ci-#{status.group} #{'has-tooltip' if title.present?}"
- if link && status.has_details? - if link && status.has_details?
= link_to status.details_path, class: css_classes do = link_to status.details_path, class: css_classes, title: title do
= custom_icon(status.icon) = custom_icon(status.icon)
= status.text = status.text
- else - else
%span{ class: css_classes } %span{ class: css_classes, title: title }
= custom_icon(status.icon) = custom_icon(status.icon)
= status.text = status.text
- pipeline = @build.pipeline
.content-block.build-header.top-area .content-block.build-header.top-area
.header-content .header-content
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
Job Job
%strong.js-build-id ##{@build.id} %strong.js-build-id ##{@build.id}
in pipeline in pipeline
= link_to pipeline_path(@build.pipeline) do = link_to pipeline_path(pipeline) do
%strong ##{@build.pipeline.id} %strong ##{pipeline.id}
for commit for commit
= link_to namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha) do = link_to namespace_project_commit_path(@project.namespace, @project, pipeline.sha) do
%strong= @build.pipeline.short_sha %strong= pipeline.short_sha
from from
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
%code %code
......
- job = build.present(current_user: current_user)
- pipeline = job.pipeline
- admin = local_assigns.fetch(:admin, false) - admin = local_assigns.fetch(:admin, false)
- ref = local_assigns.fetch(:ref, nil) - ref = local_assigns.fetch(:ref, nil)
- commit_sha = local_assigns.fetch(:commit_sha, nil) - commit_sha = local_assigns.fetch(:commit_sha, nil)
...@@ -8,101 +10,101 @@ ...@@ -8,101 +10,101 @@
%tr.build.commit{ class: ('retried' if retried) } %tr.build.commit{ class: ('retried' if retried) }
%td.status %td.status
= render "ci/status/badge", status: build.detailed_status(current_user) = render "ci/status/badge", status: job.detailed_status(current_user), title: job.status_title
%td.branch-commit %td.branch-commit
- if can?(current_user, :read_build, build) - if can?(current_user, :read_build, job)
= link_to namespace_project_build_url(build.project.namespace, build.project, build) do = link_to namespace_project_build_url(job.project.namespace, job.project, job) do
%span.build-link ##{build.id} %span.build-link ##{job.id}
- else - else
%span.build-link ##{build.id} %span.build-link ##{job.id}
- if ref - if ref
- if build.ref - if job.ref
.icon-container .icon-container
= build.tag? ? icon('tag') : icon('code-fork') = job.tag? ? icon('tag') : icon('code-fork')
= link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" = link_to job.ref, namespace_project_commits_path(job.project.namespace, job.project, job.ref), class: "monospace branch-name"
- else - else
.light none .light none
.icon-container.commit-icon .icon-container.commit-icon
= custom_icon("icon_commit") = custom_icon("icon_commit")
- if commit_sha - if commit_sha
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" = link_to job.short_sha, namespace_project_commit_path(job.project.namespace, job.project, job.sha), class: "commit-id monospace"
- if build.stuck? - if job.stuck?
= icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.') = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.')
- if retried - if retried
= icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried') = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried')
.label-container .label-container
- if build.tags.any? - if job.tags.any?
- build.tags.each do |tag| - job.tags.each do |tag|
%span.label.label-primary %span.label.label-primary
= tag = tag
- if build.try(:trigger_request) - if job.try(:trigger_request)
%span.label.label-info triggered %span.label.label-info triggered
- if build.try(:allow_failure) - if job.try(:allow_failure)
%span.label.label-danger allowed to fail %span.label.label-danger allowed to fail
- if build.action? - if job.action?
%span.label.label-info manual %span.label.label-info manual
- if pipeline_link - if pipeline_link
%td %td
= link_to pipeline_path(build.pipeline) do = link_to pipeline_path(pipeline) do
%span.pipeline-id ##{build.pipeline.id} %span.pipeline-id ##{pipeline.id}
%span by %span by
- if build.pipeline.user - if pipeline.user
= user_avatar(user: build.pipeline.user, size: 20) = user_avatar(user: pipeline.user, size: 20)
- else - else
%span.monospace API %span.monospace API
- if admin - if admin
%td %td
- if build.project - if job.project
= link_to build.project.name_with_namespace, admin_namespace_project_path(build.project.namespace, build.project) = link_to job.project.name_with_namespace, admin_namespace_project_path(job.project.namespace, job.project)
%td %td
- if build.try(:runner) - if job.try(:runner)
= runner_link(build.runner) = runner_link(job.runner)
- else - else
.light none .light none
- if stage - if stage
%td %td
= build.stage = job.stage
%td %td
= build.name = job.name
%td %td
- if build.duration - if job.duration
%p.duration %p.duration
= custom_icon("icon_timer") = custom_icon("icon_timer")
= duration_in_numbers(build.duration) = duration_in_numbers(job.duration)
- if build.finished_at - if job.finished_at
%p.finished-at %p.finished-at
= icon("calendar") = icon("calendar")
%span= time_ago_with_tooltip(build.finished_at) %span= time_ago_with_tooltip(job.finished_at)
%td.coverage %td.coverage
- if build.try(:coverage) - if job.try(:coverage)
#{build.coverage}% #{job.coverage}%
%td %td
.pull-right .pull-right
- if can?(current_user, :read_build, build) && build.artifacts? - if can?(current_user, :read_build, job) && job.artifacts?
= link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do = link_to download_namespace_project_build_artifacts_path(job.project.namespace, job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
= icon('download') = icon('download')
- if can?(current_user, :update_build, build) - if can?(current_user, :update_build, job)
- if build.active? - if job.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(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
= icon('remove', class: 'cred') = icon('remove', class: 'cred')
- elsif allow_retry - elsif allow_retry
- if build.playable? && !admin - if job.playable? && !admin
= link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do = link_to play_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
= custom_icon('icon_play') = custom_icon('icon_play')
- elsif build.retryable? - elsif job.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(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
= icon('repeat') = icon('repeat')
.page-content-header .page-content-header
.header-main-content .header-main-content
= render 'ci/status/badge', status: @pipeline.detailed_status(current_user) = render 'ci/status/badge', status: @pipeline.detailed_status(current_user), title: @pipeline.status_title
%strong Pipeline ##{@pipeline.id} %strong Pipeline ##{@pipeline.id}
triggered #{time_ago_with_tooltip(@pipeline.created_at)} triggered #{time_ago_with_tooltip(@pipeline.created_at)}
- if @pipeline.user - if @pipeline.user
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
Git strategy for pipelines Git strategy for pipelines
%p %p
Choose between <code>clone</code> or <code>fetch</code> to get the recent application code Choose between <code>clone</code> or <code>fetch</code> to get the recent application code
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy') = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank'
.radio .radio
= f.label :build_allow_git_fetch_false do = f.label :build_allow_git_fetch_false do
= f.radio_button :build_allow_git_fetch, 'false' = f.radio_button :build_allow_git_fetch, 'false'
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
= f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
%p.help-block %p.help-block
Per job in minutes. If a job passes this threshold, it will be marked as failed. Per job in minutes. If a job passes this threshold, it will be marked as failed.
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout') = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr %hr
.form-group .form-group
...@@ -53,7 +53,16 @@ ...@@ -53,7 +53,16 @@
%strong Public pipelines %strong Public pipelines
.help-block .help-block
Allow everyone to access pipelines for public and internal projects Allow everyone to access pipelines for public and internal projects
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines') = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank'
%hr
.form-group
.checkbox
= f.label :auto_cancel_pending_pipelines do
= f.check_box :auto_cancel_pending_pipelines, {}, 'enabled', 'disabled'
%strong Auto-cancel redundant, pending pipelines
.help-block
New pipelines will cancel older, pending pipelines on the same branch
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'auto-cancel-pending-pipelines'), target: '_blank'
%hr %hr
.form-group .form-group
...@@ -65,7 +74,7 @@ ...@@ -65,7 +74,7 @@
%p.help-block %p.help-block
A regular expression that will be used to find the test coverage A regular expression that will be used to find the test coverage
output in the job trace. Leave blank to disable output in the job trace. Leave blank to disable
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing') = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank'
.bs-callout.bs-callout-info .bs-callout.bs-callout-info
%p Below are examples of regex for existing tools: %p Below are examples of regex for existing tools:
%ul %ul
......
---
title: Cancel pending pipelines if commits not HEAD
merge_request: 9362
author: Rydkin Maxim
class AddAutoCancelPendingPipelinesToProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:projects, :auto_cancel_pending_pipelines, :integer, default: 0)
end
def down
remove_column(:projects, :auto_cancel_pending_pipelines)
end
end
class AddAutoCanceledByIdToPipeline < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_pipelines, :auto_canceled_by_id, :integer
end
end
class AddAutoCanceledByIdForeignKeyToPipeline < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
on_delete =
if Gitlab::Database.mysql?
:nullify
else
'SET NULL'
end
add_concurrent_foreign_key :ci_pipelines, :ci_pipelines, column: :auto_canceled_by_id, on_delete: on_delete
end
def down
remove_foreign_key :ci_pipelines, column: :auto_canceled_by_id
end
end
...@@ -15,7 +15,7 @@ class RemoveIndexForUsersCurrentSignInAt < ActiveRecord::Migration ...@@ -15,7 +15,7 @@ class RemoveIndexForUsersCurrentSignInAt < ActiveRecord::Migration
if Gitlab::Database.postgresql? if Gitlab::Database.postgresql?
execute 'DROP INDEX CONCURRENTLY index_users_on_current_sign_in_at;' execute 'DROP INDEX CONCURRENTLY index_users_on_current_sign_in_at;'
else else
remove_index :users, :current_sign_in_at remove_concurrent_index :users, :current_sign_in_at
end end
end end
end end
......
class AddAutoCanceledByIdToCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_builds, :auto_canceled_by_id, :integer
end
end
class AddAutoCanceledByIdForeignKeyToCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
on_delete =
if Gitlab::Database.mysql?
:nullify
else
'SET NULL'
end
add_concurrent_foreign_key :ci_builds, :ci_pipelines, column: :auto_canceled_by_id, on_delete: on_delete
end
def down
remove_foreign_key :ci_builds, column: :auto_canceled_by_id
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170405080720) do ActiveRecord::Schema.define(version: 20170406115029) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -223,6 +223,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do ...@@ -223,6 +223,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do
t.string "token" t.string "token"
t.integer "lock_version" t.integer "lock_version"
t.string "coverage_regex" t.string "coverage_regex"
t.integer "auto_canceled_by_id"
end 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", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
...@@ -251,6 +252,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do ...@@ -251,6 +252,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do
t.integer "duration" t.integer "duration"
t.integer "user_id" t.integer "user_id"
t.integer "lock_version" t.integer "lock_version"
t.integer "auto_canceled_by_id"
end end
add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree
...@@ -947,6 +949,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do ...@@ -947,6 +949,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do
t.boolean "lfs_enabled" t.boolean "lfs_enabled"
t.text "description_html" t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved" t.boolean "only_allow_merge_if_all_discussions_are_resolved"
t.integer "auto_cancel_pending_pipelines", default: 0, null: false
t.boolean "printing_merge_request_link_enabled", default: true, null: false t.boolean "printing_merge_request_link_enabled", default: true, null: false
t.string "import_jid" t.string "import_jid"
end end
...@@ -1328,6 +1331,8 @@ ActiveRecord::Schema.define(version: 20170405080720) do ...@@ -1328,6 +1331,8 @@ ActiveRecord::Schema.define(version: 20170405080720) do
add_foreign_key "boards", "projects" add_foreign_key "boards", "projects"
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
add_foreign_key "ci_trigger_schedules", "ci_triggers", column: "trigger_id", name: "fk_90a406cc94", on_delete: :cascade add_foreign_key "ci_trigger_schedules", "ci_triggers", column: "trigger_id", name: "fk_90a406cc94", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "container_repositories", "projects" add_foreign_key "container_repositories", "projects"
......
...@@ -60,6 +60,14 @@ anyone and those logged in respectively. If you wish to hide it so that only ...@@ -60,6 +60,14 @@ anyone and those logged in respectively. If you wish to hide it so that only
the members of the project or group have access to it, uncheck the **Public the members of the project or group have access to it, uncheck the **Public
pipelines** checkbox and save the changes. pipelines** checkbox and save the changes.
## Auto-cancel pending pipelines
> [Introduced][ce-9362] in GitLab 9.1.
If you want to auto-cancel all pending non-HEAD pipelines on branch, when
new pipeline will be created (after your git push or manually from UI),
check **Auto-cancel pending pipelines** checkbox and save the changes.
## Badges ## Badges
In the pipelines settings page you can find pipeline status and test coverage In the pipelines settings page you can find pipeline status and test coverage
...@@ -111,3 +119,4 @@ into your `README.md`: ...@@ -111,3 +119,4 @@ into your `README.md`:
[var]: ../../../ci/yaml/README.md#git-strategy [var]: ../../../ci/yaml/README.md#git-strategy
[coverage report]: #test-coverage-parsing [coverage report]: #test-coverage-parsing
[ce-9362]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9362
...@@ -32,5 +32,16 @@ feature "Pipelines settings", feature: true do ...@@ -32,5 +32,16 @@ feature "Pipelines settings", feature: true do
expect(page).to have_button('Save changes', disabled: false) expect(page).to have_button('Save changes', disabled: false)
expect(page).to have_field('Test coverage parsing', with: 'coverage_regex') expect(page).to have_field('Test coverage parsing', with: 'coverage_regex')
end end
scenario 'updates auto_cancel_pending_pipelines' do
page.check('Auto-cancel redundant, pending pipelines')
click_on 'Save changes'
expect(page.status_code).to eq(200)
expect(page).to have_button('Save changes', disabled: false)
checkbox = find_field('project_auto_cancel_pending_pipelines')
expect(checkbox).to be_checked
end
end end
end end
...@@ -89,6 +89,9 @@ pipelines: ...@@ -89,6 +89,9 @@ pipelines:
- statuses - statuses
- builds - builds
- trigger_requests - trigger_requests
- auto_canceled_by
- auto_canceled_pipelines
- auto_canceled_jobs
- pending_builds - pending_builds
- retryable_builds - retryable_builds
- cancelable_statuses - cancelable_statuses
...@@ -98,6 +101,7 @@ statuses: ...@@ -98,6 +101,7 @@ statuses:
- project - project
- pipeline - pipeline
- user - user
- auto_canceled_by
variables: variables:
- project - project
triggers: triggers:
......
...@@ -183,6 +183,7 @@ Ci::Pipeline: ...@@ -183,6 +183,7 @@ Ci::Pipeline:
- duration - duration
- user_id - user_id
- lock_version - lock_version
- auto_canceled_by_id
CommitStatus: CommitStatus:
- id - id
- project_id - project_id
...@@ -223,6 +224,7 @@ CommitStatus: ...@@ -223,6 +224,7 @@ CommitStatus:
- token - token
- lock_version - lock_version
- coverage_regex - coverage_regex
- auto_canceled_by_id
Ci::Variable: Ci::Variable:
- id - id
- project_id - project_id
......
...@@ -12,10 +12,13 @@ describe Ci::Pipeline, models: true do ...@@ -12,10 +12,13 @@ describe Ci::Pipeline, models: true do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:auto_canceled_by) }
it { is_expected.to have_many(:statuses) } it { is_expected.to have_many(:statuses) }
it { is_expected.to have_many(:trigger_requests) } it { is_expected.to have_many(:trigger_requests) }
it { is_expected.to have_many(:builds) } it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:auto_canceled_pipelines) }
it { is_expected.to have_many(:auto_canceled_jobs) }
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 }
...@@ -134,6 +137,43 @@ describe Ci::Pipeline, models: true do ...@@ -134,6 +137,43 @@ describe Ci::Pipeline, models: true do
end end
end end
describe '#auto_canceled?' do
subject { pipeline.auto_canceled? }
context 'when it is canceled' do
before do
pipeline.cancel
end
context 'when there is auto_canceled_by' do
before do
pipeline.update(auto_canceled_by: create(:ci_empty_pipeline))
end
it 'is auto canceled' do
is_expected.to be_truthy
end
end
context 'when there is no auto_canceled_by' do
it 'is not auto canceled' do
is_expected.to be_falsey
end
end
context 'when it is retried and canceled manually' do
before do
pipeline.enqueue
pipeline.cancel
end
it 'is not auto canceled' do
is_expected.to be_falsey
end
end
end
end
describe 'pipeline stages' do describe 'pipeline stages' do
before do before do
create(:commit_status, pipeline: pipeline, create(:commit_status, pipeline: pipeline,
......
...@@ -16,6 +16,7 @@ describe CommitStatus, :models do ...@@ -16,6 +16,7 @@ describe CommitStatus, :models do
it { is_expected.to belong_to(:pipeline) } it { is_expected.to belong_to(:pipeline) }
it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:auto_canceled_by) }
it { is_expected.to validate_presence_of(:name) } 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 validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) }
...@@ -101,6 +102,32 @@ describe CommitStatus, :models do ...@@ -101,6 +102,32 @@ describe CommitStatus, :models do
end end
end end
describe '#auto_canceled?' do
subject { commit_status.auto_canceled? }
context 'when it is canceled' do
before do
commit_status.update(status: 'canceled')
end
context 'when there is auto_canceled_by' do
before do
commit_status.update(auto_canceled_by: create(:ci_empty_pipeline))
end
it 'is auto canceled' do
is_expected.to be_truthy
end
end
context 'when there is no auto_canceled_by' do
it 'is not auto canceled' do
is_expected.to be_falsey
end
end
end
end
describe '#duration' do describe '#duration' do
subject { commit_status.duration } subject { commit_status.duration }
......
...@@ -231,6 +231,18 @@ describe HasStatus do ...@@ -231,6 +231,18 @@ describe HasStatus do
end end
end end
describe '.created_or_pending' do
subject { CommitStatus.created_or_pending }
%i[created pending].each do |status|
it_behaves_like 'containing the job', status
end
%i[running failed success].each do |status|
it_behaves_like 'not containing the job', status
end
end
describe '.finished' do describe '.finished' do
subject { CommitStatus.finished } subject { CommitStatus.finished }
......
...@@ -57,6 +57,32 @@ describe Ci::BuildPresenter do ...@@ -57,6 +57,32 @@ describe Ci::BuildPresenter do
end end
end end
describe '#status_title' do
context 'when build is auto-canceled' do
before do
expect(build).to receive(:auto_canceled?).and_return(true)
expect(build).to receive(:auto_canceled_by_id).and_return(1)
end
it 'shows that the build is auto-canceled' do
status_title = presenter.status_title
expect(status_title).to include('auto-canceled')
expect(status_title).to include('Pipeline #1')
end
end
context 'when build is not auto-canceled' do
before do
expect(build).to receive(:auto_canceled?).and_return(false)
end
it 'does not have a status title' do
expect(presenter.status_title).to be_nil
end
end
end
describe 'quack like a Ci::Build permission-wise' do describe 'quack like a Ci::Build permission-wise' do
context 'user is not allowed' do context 'user is not allowed' do
let(:project) { build_stubbed(:empty_project, public_builds: false) } let(:project) { build_stubbed(:empty_project, public_builds: false) }
......
require 'spec_helper'
describe Ci::PipelinePresenter do
let(:project) { create(:empty_project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
subject(:presenter) do
described_class.new(pipeline)
end
it 'inherits from Gitlab::View::Presenter::Delegated' do
expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
end
describe '#initialize' do
it 'takes a pipeline and optional params' do
expect { presenter }.not_to raise_error
end
it 'exposes pipeline' do
expect(presenter.pipeline).to eq(pipeline)
end
it 'forwards missing methods to pipeline' do
expect(presenter.ref).to eq(pipeline.ref)
end
end
describe '#status_title' do
context 'when pipeline is auto-canceled' do
before do
expect(pipeline).to receive(:auto_canceled?).and_return(true)
expect(pipeline).to receive(:auto_canceled_by_id).and_return(1)
end
it 'shows that the pipeline is auto-canceled' do
status_title = presenter.status_title
expect(status_title).to include('auto-canceled')
expect(status_title).to include('Pipeline #1')
end
end
context 'when pipeline is not auto-canceled' do
before do
expect(pipeline).to receive(:auto_canceled?).and_return(false)
end
it 'does not have a status title' do
expect(presenter.status_title).to be_nil
end
end
end
end
...@@ -9,72 +9,140 @@ describe Ci::CreatePipelineService, services: true do ...@@ -9,72 +9,140 @@ describe Ci::CreatePipelineService, services: true do
end end
describe '#execute' do describe '#execute' do
def execute(params) def execute_service(after: project.commit.id, message: 'Message', ref: 'refs/heads/master')
params = { ref: ref,
before: '00000000',
after: after,
commits: [{ message: message }] }
described_class.new(project, user, params).execute described_class.new(project, user, params).execute
end end
context 'valid params' do context 'valid params' do
let(:pipeline) do let(:pipeline) { execute_service }
execute(ref: 'refs/heads/master',
before: '00000000', let(:pipeline_on_previous_commit) do
after: project.commit.id, execute_service(
commits: [{ message: "Message" }]) after: previous_commit_sha_from_ref('master')
)
end end
it { expect(pipeline).to be_kind_of(Ci::Pipeline) } it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
it { expect(pipeline).to be_valid } it { expect(pipeline).to be_valid }
it { expect(pipeline).to be_persisted }
it { expect(pipeline).to eq(project.pipelines.last) } it { expect(pipeline).to eq(project.pipelines.last) }
it { expect(pipeline).to have_attributes(user: user) } it { expect(pipeline).to have_attributes(user: user) }
it { expect(pipeline).to have_attributes(status: 'pending') }
it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
context 'auto-cancel enabled' do
before do
project.update(auto_cancel_pending_pipelines: 'enabled')
end
it 'does not cancel HEAD pipeline' do
pipeline
pipeline_on_previous_commit
expect(pipeline.reload).to have_attributes(status: 'pending', auto_canceled_by_id: nil)
end
it 'auto cancel pending non-HEAD pipelines' do
pipeline_on_previous_commit
pipeline
expect(pipeline_on_previous_commit.reload).to have_attributes(status: 'canceled', auto_canceled_by_id: pipeline.id)
end
it 'does not cancel running outdated pipelines' do
pipeline_on_previous_commit.run
execute_service
expect(pipeline_on_previous_commit.reload).to have_attributes(status: 'running', auto_canceled_by_id: nil)
end
it 'cancel created outdated pipelines' do
pipeline_on_previous_commit.update(status: 'created')
pipeline
expect(pipeline_on_previous_commit.reload).to have_attributes(status: 'canceled', auto_canceled_by_id: pipeline.id)
end
it 'does not cancel pipelines from the other branches' do
pending_pipeline = execute_service(
ref: 'refs/heads/feature',
after: previous_commit_sha_from_ref('feature')
)
pipeline
expect(pending_pipeline.reload).to have_attributes(status: 'pending', auto_canceled_by_id: nil)
end
end
context 'auto-cancel disabled' do
before do
project.update(auto_cancel_pending_pipelines: 'disabled')
end
it 'does not auto cancel pending non-HEAD pipelines' do
pipeline_on_previous_commit
pipeline
expect(pipeline_on_previous_commit.reload)
.to have_attributes(status: 'pending', auto_canceled_by_id: nil)
end
end
def previous_commit_sha_from_ref(ref)
project.commit(ref).parent.sha
end
end end
context "skip tag if there is no build for it" do context "skip tag if there is no build for it" do
it "creates commit if there is appropriate job" do it "creates commit if there is appropriate job" do
result = execute(ref: 'refs/heads/master', expect(execute_service).to be_persisted
before: '00000000',
after: project.commit.id,
commits: [{ message: "Message" }])
expect(result).to be_persisted
end end
it "creates commit if there is no appropriate job but deploy job has right ref setting" do it "creates commit if there is no appropriate job but deploy job has right ref setting" do
config = YAML.dump({ deploy: { script: "ls", only: ["master"] } }) config = YAML.dump({ deploy: { script: "ls", only: ["master"] } })
stub_ci_pipeline_yaml_file(config) stub_ci_pipeline_yaml_file(config)
result = execute(ref: 'refs/heads/master',
before: '00000000',
after: project.commit.id,
commits: [{ message: "Message" }])
expect(result).to be_persisted expect(execute_service).to be_persisted
end end
end end
it 'skips creating pipeline for refs without .gitlab-ci.yml' do it 'skips creating pipeline for refs without .gitlab-ci.yml' do
stub_ci_pipeline_yaml_file(nil) stub_ci_pipeline_yaml_file(nil)
result = execute(ref: 'refs/heads/master',
before: '00000000',
after: project.commit.id,
commits: [{ message: 'Message' }])
expect(result).not_to be_persisted expect(execute_service).not_to be_persisted
expect(Ci::Pipeline.count).to eq(0) expect(Ci::Pipeline.count).to eq(0)
end end
it 'fails commits if yaml is invalid' do shared_examples 'a failed pipeline' do
message = 'message' it 'creates failed pipeline' do
allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } stub_ci_pipeline_yaml_file(ci_yaml)
stub_ci_pipeline_yaml_file('invalid: file: file')
commits = [{ message: message }] pipeline = execute_service(message: message)
pipeline = execute(ref: 'refs/heads/master',
before: '00000000', expect(pipeline).to be_persisted
after: project.commit.id, expect(pipeline.builds.any?).to be false
commits: commits) expect(pipeline.status).to eq('failed')
expect(pipeline.yaml_errors).not_to be_nil
expect(pipeline).to be_persisted end
expect(pipeline.builds.any?).to be false end
expect(pipeline.status).to eq('failed')
expect(pipeline.yaml_errors).not_to be_nil context 'when yaml is invalid' do
let(:ci_yaml) { 'invalid: file: fiile' }
let(:message) { 'Message' }
it_behaves_like 'a failed pipeline'
context 'when receive git commit' do
before do
allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
end
it_behaves_like 'a failed pipeline'
end
end end
context 'when commit contains a [ci skip] directive' do context 'when commit contains a [ci skip] directive' do
...@@ -97,11 +165,7 @@ describe Ci::CreatePipelineService, services: true do ...@@ -97,11 +165,7 @@ describe Ci::CreatePipelineService, services: true do
ci_messages.each do |ci_message| ci_messages.each do |ci_message|
it "skips builds creation if the commit message is #{ci_message}" do it "skips builds creation if the commit message is #{ci_message}" do
commits = [{ message: ci_message }] pipeline = execute_service(message: ci_message)
pipeline = execute(ref: 'refs/heads/master',
before: '00000000',
after: project.commit.id,
commits: commits)
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
expect(pipeline.builds.any?).to be false expect(pipeline.builds.any?).to be false
...@@ -109,58 +173,34 @@ describe Ci::CreatePipelineService, services: true do ...@@ -109,58 +173,34 @@ describe Ci::CreatePipelineService, services: true do
end end
end end
it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do shared_examples 'creating a pipeline' do
allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } it 'does not skip pipeline creation' do
allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { commit_message }
commits = [{ message: "some message" }] pipeline = execute_service(message: commit_message)
pipeline = execute(ref: 'refs/heads/master',
before: '00000000',
after: project.commit.id,
commits: commits)
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
expect(pipeline.builds.first.name).to eq("rspec") expect(pipeline.builds.first.name).to eq("rspec")
end
end end
it "does not skip builds creation if the commit message is nil" do context 'when commit message does not contain [ci skip] nor [skip ci]' do
allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { nil } let(:commit_message) { 'some message' }
commits = [{ message: nil }]
pipeline = execute(ref: 'refs/heads/master',
before: '00000000',
after: project.commit.id,
commits: commits)
expect(pipeline).to be_persisted it_behaves_like 'creating a pipeline'
expect(pipeline.builds.first.name).to eq("rspec")
end end
it "fails builds creation if there is [ci skip] tag in commit message and yaml is invalid" do context 'when commit message is nil' do
stub_ci_pipeline_yaml_file('invalid: file: fiile') let(:commit_message) { nil }
commits = [{ message: message }]
pipeline = execute(ref: 'refs/heads/master',
before: '00000000',
after: project.commit.id,
commits: commits)
expect(pipeline).to be_persisted it_behaves_like 'creating a pipeline'
expect(pipeline.builds.any?).to be false
expect(pipeline.status).to eq("failed")
expect(pipeline.yaml_errors).not_to be_nil
end end
end
it "creates commit with failed status if yaml is invalid" do context 'when there is [ci skip] tag in commit message and yaml is invalid' do
stub_ci_pipeline_yaml_file('invalid: file') let(:ci_yaml) { 'invalid: file: fiile' }
commits = [{ message: "some message" }]
pipeline = execute(ref: 'refs/heads/master', it_behaves_like 'a failed pipeline'
before: '00000000', end
after: project.commit.id,
commits: commits)
expect(pipeline).to be_persisted
expect(pipeline.status).to eq("failed")
expect(pipeline.builds.any?).to be false
end end
context 'when there are no jobs for this pipeline' do context 'when there are no jobs for this pipeline' do
...@@ -170,10 +210,7 @@ describe Ci::CreatePipelineService, services: true do ...@@ -170,10 +210,7 @@ describe Ci::CreatePipelineService, services: true do
end end
it 'does not create a new pipeline' do it 'does not create a new pipeline' do
result = execute(ref: 'refs/heads/master', result = execute_service
before: '00000000',
after: project.commit.id,
commits: [{ message: 'some msg' }])
expect(result).not_to be_persisted expect(result).not_to be_persisted
expect(Ci::Build.all).to be_empty expect(Ci::Build.all).to be_empty
...@@ -188,10 +225,7 @@ describe Ci::CreatePipelineService, services: true do ...@@ -188,10 +225,7 @@ describe Ci::CreatePipelineService, services: true do
end end
it 'does not create a new pipeline' do it 'does not create a new pipeline' do
result = execute(ref: 'refs/heads/master', result = execute_service
before: '00000000',
after: project.commit.id,
commits: [{ message: 'some msg' }])
expect(result).to be_persisted expect(result).to be_persisted
expect(result.manual_actions).not_to be_empty expect(result.manual_actions).not_to be_empty
...@@ -205,10 +239,7 @@ describe Ci::CreatePipelineService, services: true do ...@@ -205,10 +239,7 @@ describe Ci::CreatePipelineService, services: true do
end end
it 'creates the environment' do it 'creates the environment' do
result = execute(ref: 'refs/heads/master', result = execute_service
before: '00000000',
after: project.commit.id,
commits: [{ message: 'some msg' }])
expect(result).to be_persisted expect(result).to be_persisted
expect(Environment.find_by(name: "review/master")).not_to be_nil expect(Environment.find_by(name: "review/master")).not_to be_nil
......
...@@ -16,20 +16,21 @@ describe Ci::RetryBuildService, :services do ...@@ -16,20 +16,21 @@ describe Ci::RetryBuildService, :services do
%i[id status user token coverage trace runner artifacts_expire_at %i[id status user token coverage trace runner artifacts_expire_at
artifacts_file artifacts_metadata artifacts_size created_at artifacts_file artifacts_metadata artifacts_size created_at
updated_at started_at finished_at queued_at erased_by updated_at started_at finished_at queued_at erased_by
erased_at].freeze erased_at auto_canceled_by].freeze
IGNORE_ACCESSORS = IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags %i[type lock_version target_url base_tags
commit_id deployments erased_by_id last_deployment project_id commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id runner_id tag_taggings taggings tags trigger_request_id
user_id].freeze user_id auto_canceled_by_id].freeze
shared_examples 'build duplication' do shared_examples 'build duplication' do
let(:build) do let(:build) do
create(:ci_build, :failed, :artifacts_expired, :erased, create(:ci_build, :failed, :artifacts_expired, :erased,
:queued, :coverage, :tags, :allowed_to_fail, :on_tag, :queued, :coverage, :tags, :allowed_to_fail, :on_tag,
:teardown_environment, :triggered, :trace, :teardown_environment, :triggered, :trace,
description: 'some build', pipeline: pipeline) description: 'some build', pipeline: pipeline,
auto_canceled_by: create(:ci_empty_pipeline))
end end
describe 'clone accessors' do describe 'clone accessors' do
......
...@@ -9,7 +9,7 @@ describe 'projects/builds/show', :view do ...@@ -9,7 +9,7 @@ describe 'projects/builds/show', :view do
end end
before do before do
assign(:build, build) assign(:build, build.present)
assign(:project, project) assign(:project, project)
allow(view).to receive(:can?).and_return(true) allow(view).to receive(:can?).and_return(true)
......
...@@ -5,7 +5,13 @@ describe 'projects/pipelines/show' do ...@@ -5,7 +5,13 @@ describe 'projects/pipelines/show' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, user: user) }
let(:pipeline) do
create(:ci_empty_pipeline,
project: project,
sha: project.commit.id,
user: user)
end
before do before do
controller.prepend_view_path('app/views/projects') controller.prepend_view_path('app/views/projects')
...@@ -21,7 +27,7 @@ describe 'projects/pipelines/show' do ...@@ -21,7 +27,7 @@ describe 'projects/pipelines/show' do
create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3) create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3)
assign(:project, project) assign(:project, project)
assign(:pipeline, pipeline) assign(:pipeline, pipeline.present(current_user: user))
assign(:commit, project.commit) assign(:commit, project.commit)
allow(view).to receive(:can?).and_return(true) allow(view).to receive(:can?).and_return(true)
......
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