Commit e9e750f5 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'manual-actions' into 'master'

Add support for manual CI actions

## What does this MR do?
This implements a `when: manual` which allows a jobs to be marked as manual actions.
Manual actions have to be explicitly executed by developers.

## What are the relevant issue numbers?
This is to solve: https://gitlab.com/gitlab-org/gitlab-ce/issues/17010

See merge request !5297
parent 263a9db7
...@@ -40,6 +40,7 @@ v 8.10.0 (unreleased) ...@@ -40,6 +40,7 @@ v 8.10.0 (unreleased)
- Fix viewing notification settings when a project is pending deletion - Fix viewing notification settings when a project is pending deletion
- Updated compare dropdown menus to use GL dropdown - Updated compare dropdown menus to use GL dropdown
- Eager load award emoji on notes - Eager load award emoji on notes
- Allow to define manual actions/builds on Pipelines and Environments
- Fix pagination when sorting by columns with lots of ties (like priority) - Fix pagination when sorting by columns with lots of ties (like priority)
- The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020 - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020
- Updated project header design - Updated project header design
......
class Projects::BuildsController < Projects::ApplicationController class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all] before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry] before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play]
before_action :authorize_update_build!, except: [:index, :show, :status, :raw] before_action :authorize_update_build!, except: [:index, :show, :status, :raw]
layout 'project' layout 'project'
...@@ -49,14 +49,19 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -49,14 +49,19 @@ class Projects::BuildsController < Projects::ApplicationController
end end
def retry def retry
unless @build.retryable? return render_404 unless @build.retryable?
return render_404
end
build = Ci::Build.retry(@build, current_user) build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build) redirect_to build_path(build)
end end
def play
return render_404 unless @build.playable?
build = @build.play(current_user)
redirect_to build_path(build)
end
def cancel def cancel
@build.cancel @build.cancel
redirect_to build_path(@build) redirect_to build_path(@build)
......
...@@ -15,6 +15,7 @@ module Ci ...@@ -15,6 +15,7 @@ module Ci
scope :with_artifacts, ->() { where.not(artifacts_file: nil) } scope :with_artifacts, ->() { where.not(artifacts_file: nil) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual) }
mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader
...@@ -91,6 +92,29 @@ module Ci ...@@ -91,6 +92,29 @@ module Ci
end end
end end
def manual?
self.when == 'manual'
end
def other_actions
pipeline.manual_actions.where.not(id: self)
end
def playable?
project.builds_enabled? && commands.present? && manual?
end
def play(current_user = nil)
# Try to queue a current build
if self.queue
self.update(user: current_user)
self
else
# Otherwise we need to create a duplicate
Ci::Build.retry(self, current_user)
end
end
def retryable? def retryable?
project.builds_enabled? && commands.present? && complete? project.builds_enabled? && commands.present? && complete?
end end
......
...@@ -69,6 +69,10 @@ module Ci ...@@ -69,6 +69,10 @@ module Ci
!tag? !tag?
end end
def manual_actions
builds.latest.manual_actions
end
def retryable? def retryable?
builds.latest.any? do |build| builds.latest.any? do |build|
build.failed? && build.retryable? build.failed? && build.retryable?
......
...@@ -22,6 +22,10 @@ class CommitStatus < ActiveRecord::Base ...@@ -22,6 +22,10 @@ class CommitStatus < ActiveRecord::Base
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
event :queue do
transition skipped: :pending
end
event :run do event :run do
transition pending: :running transition pending: :running
end end
......
...@@ -16,10 +16,10 @@ module Statuseable ...@@ -16,10 +16,10 @@ module Statuseable
deduce_status = "(CASE deduce_status = "(CASE
WHEN (#{builds})=0 THEN NULL WHEN (#{builds})=0 THEN NULL
WHEN (#{builds})=(#{success})+(#{ignored}) THEN 'success'
WHEN (#{builds})=(#{pending}) THEN 'pending'
WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored}) THEN 'canceled'
WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{pending})+(#{skipped}) THEN 'pending'
WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
WHEN (#{running})+(#{pending})>0 THEN 'running' WHEN (#{running})+(#{pending})>0 THEN 'running'
ELSE 'failed' ELSE 'failed'
END)" END)"
......
...@@ -32,4 +32,8 @@ class Deployment < ActiveRecord::Base ...@@ -32,4 +32,8 @@ class Deployment < ActiveRecord::Base
def keep_around_commit def keep_around_commit
project.repository.keep_around(self.sha) project.repository.keep_around(self.sha)
end end
def manual_actions
deployable.try(:other_actions)
end
end end
...@@ -15,7 +15,7 @@ module Ci ...@@ -15,7 +15,7 @@ module Ci
status == 'success' status == 'success'
when 'on_failure' when 'on_failure'
status == 'failed' status == 'failed'
when 'always' when 'always', 'manual'
%w(success failed).include?(status) %w(success failed).include?(status)
end end
end end
...@@ -47,6 +47,10 @@ module Ci ...@@ -47,6 +47,10 @@ module Ci
user: user, user: user,
project: @pipeline.project) project: @pipeline.project)
# TODO: The proper implementation for this is in
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5295
build_attrs[:status] = 'skipped' if build_attrs[:when] == 'manual'
## ##
# We do not persist new builds here. # We do not persist new builds here.
# Those will be persisted when @pipeline is saved. # Those will be persisted when @pipeline is saved.
......
...@@ -39,6 +39,8 @@ ...@@ -39,6 +39,8 @@
%span.label.label-danger allowed to fail %span.label.label-danger allowed to fail
- if defined?(retried) && retried - if defined?(retried) && retried
%span.label.label-warning retried %span.label.label-warning retried
- if build.manual?
%span.label.label-info manual
- if defined?(runner) && runner - if defined?(runner) && runner
...@@ -79,6 +81,11 @@ ...@@ -79,6 +81,11 @@
- 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
= icon('remove', class: 'cred') = icon('remove', class: 'cred')
- elsif defined?(allow_retry) && allow_retry && build.retryable? - elsif defined?(allow_retry) && allow_retry
= 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 - if build.retryable?
= icon('repeat') = 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
= icon('repeat')
- elsif build.playable?
= 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
= icon('play')
...@@ -58,18 +58,31 @@ ...@@ -58,18 +58,31 @@
%td.pipeline-actions %td.pipeline-actions
.controls.hidden-xs.pull-right .controls.hidden-xs.pull-right
- artifacts = pipeline.builds.latest.select { |b| b.artifacts? } - artifacts = pipeline.builds.latest.select { |b| b.artifacts? }
- if artifacts.present? - actions = pipeline.manual_actions
.inline - if artifacts.present? || actions.any?
.btn-group .btn-group.inline
%a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} - if actions.any?
= icon("download") .btn-group
%b.caret %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
%ul.dropdown-menu.dropdown-menu-align-right = icon("play")
- artifacts.each do |build| %b.caret
%li %ul.dropdown-menu.dropdown-menu-align-right
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do - actions.each do |build|
= icon("download") %li
%span Download '#{build.name}' artifacts = link_to play_namespace_project_build_path(@project.namespace, @project, build), method: :post, rel: 'nofollow' do
= icon("play")
%span= build.name.humanize
- if artifacts.present?
.btn-group
%a.dropdown-toggle.btn.btn-default.build-artifacts{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 Download '#{build.name}' artifacts
- if can?(current_user, :update_pipeline, @project) - if can?(current_user, :update_pipeline, @project)
.cancel-retry-btns.inline .cancel-retry-btns.inline
......
- if can?(current_user, :create_deployment, deployment) && deployment.deployable
.pull-right
- actions = deployment.manual_actions
- if actions.present?
.btn-group.inline
.btn-group
%a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= icon("play")
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action|
%li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= icon("play")
%span= action.name.humanize
- if local_assigns.fetch(:allow_rollback, false)
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
- if deployment.last?
Retry
- else
Rollback
...@@ -7,17 +7,11 @@ ...@@ -7,17 +7,11 @@
%td %td
- if deployment.deployable - if deployment.deployable
= link_to namespace_project_build_path(@project.namespace, @project, deployment.deployable) do = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do
= "#{deployment.deployable.name} (##{deployment.deployable.id})" = "#{deployment.deployable.name} (##{deployment.deployable.id})"
%td %td
#{time_ago_with_tooltip(deployment.created_at)} #{time_ago_with_tooltip(deployment.created_at)}
%td %td
- if can?(current_user, :create_deployment, deployment) && deployment.deployable = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true
.pull-right
= link_to retry_namespace_project_build_path(@project.namespace, @project, deployment.deployable), method: :post, class: 'btn btn-build' do
- if deployment.last?
Retry
- else
Rollback
...@@ -15,3 +15,6 @@ ...@@ -15,3 +15,6 @@
%td %td
- if last_deployment - if last_deployment
#{time_ago_with_tooltip(last_deployment.created_at)} #{time_ago_with_tooltip(last_deployment.created_at)}
%td
= render 'projects/deployments/actions', deployment: last_deployment
...@@ -28,4 +28,5 @@ ...@@ -28,4 +28,5 @@
%th Environment %th Environment
%th Last deployment %th Last deployment
%th Date %th Date
%th
= render @environments = render @environments
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
.col-md-9 .col-md-9
%h3.page-title= @environment.name.titleize %h3.page-title= @environment.name.capitalize
.col-md-3 .col-md-3
.nav-controls .nav-controls
......
# Description: https://coderwall.com/p/heed_q/rails-routing-and-namespaced-models
#
# This allows us to use CI ActiveRecord objects in all routes and use it:
# - [project.namespace, project, build]
#
# instead of:
# - namespace_project_build_path(project.namespace, project, build)
#
# Without that, Ci:: namespace is used for resolving routes:
# - namespace_project_ci_build_path(project.namespace, project, build)
module Ci
def self.use_relative_model_naming?
true
end
end
...@@ -750,6 +750,7 @@ Rails.application.routes.draw do ...@@ -750,6 +750,7 @@ Rails.application.routes.draw do
get :status get :status
post :cancel post :cancel
post :retry post :retry
post :play
post :erase post :erase
get :trace get :trace
get :raw get :raw
......
class Gitlab::Seeder::Builds class Gitlab::Seeder::Builds
STAGES = %w[build notify_build test notify_test deploy notify_deploy]
def initialize(project) def initialize(project)
@project = project @project = project
end end
def seed! def seed!
ci_commits.each do |ci_commit| pipelines.each do |pipeline|
begin begin
build_create!(ci_commit, name: 'test build 1') build_create!(pipeline, name: 'build:linux', stage: 'build')
build_create!(ci_commit, status: 'success', name: 'test build 2') build_create!(pipeline, name: 'build:osx', stage: 'build')
build_create!(pipeline, name: 'slack post build', stage: 'notify_build')
build_create!(pipeline, name: 'rspec:linux', stage: 'test')
build_create!(pipeline, name: 'rspec:windows', stage: 'test')
build_create!(pipeline, name: 'rspec:windows', stage: 'test')
build_create!(pipeline, name: 'rspec:osx', stage: 'test')
build_create!(pipeline, name: 'spinach:linux', stage: 'test')
build_create!(pipeline, name: 'spinach:osx', stage: 'test')
build_create!(pipeline, name: 'cucumber:linux', stage: 'test')
build_create!(pipeline, name: 'cucumber:osx', stage: 'test')
build_create!(pipeline, name: 'slack post test', stage: 'notify_test')
build_create!(pipeline, name: 'staging', stage: 'deploy', environment: 'staging')
build_create!(pipeline, name: 'production', stage: 'deploy', environment: 'production', when: 'manual')
commit_status_create!(pipeline, name: 'jenkins')
print '.' print '.'
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
print 'F' print 'F'
...@@ -15,8 +36,8 @@ class Gitlab::Seeder::Builds ...@@ -15,8 +36,8 @@ class Gitlab::Seeder::Builds
end end
end end
def ci_commits def pipelines
commits = @project.repository.commits('master', nil, 5) commits = @project.repository.commits('master', limit: 5)
commits_sha = commits.map { |commit| commit.raw.id } commits_sha = commits.map { |commit| commit.raw.id }
commits_sha.map do |sha| commits_sha.map do |sha|
@project.ensure_pipeline(sha, 'master') @project.ensure_pipeline(sha, 'master')
...@@ -25,11 +46,11 @@ class Gitlab::Seeder::Builds ...@@ -25,11 +46,11 @@ class Gitlab::Seeder::Builds
[] []
end end
def build_create!(ci_commit, opts = {}) def build_create!(pipeline, opts = {})
attributes = build_attributes_for(ci_commit).merge(opts) attributes = build_attributes_for(pipeline, opts)
build = Ci::Build.new(attributes) build = Ci::Build.new(attributes)
if %w(success failed).include?(build.status) if opts[:name].start_with?('build')
artifacts_cache_file(artifacts_archive_path) do |file| artifacts_cache_file(artifacts_archive_path) do |file|
build.artifacts_file = file build.artifacts_file = file
end end
...@@ -40,19 +61,28 @@ class Gitlab::Seeder::Builds ...@@ -40,19 +61,28 @@ class Gitlab::Seeder::Builds
end end
build.save! build.save!
build.update(status: build_status)
if %w(running success failed).include?(build.status) if %w(running success failed).include?(build.status)
# We need to set build trace after saving a build (id required) # We need to set build trace after saving a build (id required)
build.trace = FFaker::Lorem.paragraphs(6).join("\n\n") build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
end end
end end
def commit_status_create!(pipeline, opts = {})
attributes = commit_status_attributes_for(pipeline, opts)
GenericCommitStatus.create(attributes)
end
def commit_status_attributes_for(pipeline, opts)
{ name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]),
ref: 'master', user: build_user, project: @project, pipeline: pipeline,
created_at: Time.now, updated_at: Time.now
}.merge(opts)
end
def build_attributes_for(ci_commit) def build_attributes_for(pipeline, opts)
{ name: 'test build', commands: "$ build command", commit_status_attributes_for(pipeline, opts).merge(commands: '$ build command')
stage: 'test', stage_idx: 1, ref: 'master',
user_id: build_user, gl_project_id: @project.id,
status: build_status, commit_id: ci_commit.id,
created_at: Time.now, updated_at: Time.now }
end end
def build_user def build_user
...@@ -63,13 +93,16 @@ class Gitlab::Seeder::Builds ...@@ -63,13 +93,16 @@ class Gitlab::Seeder::Builds
Ci::Build::AVAILABLE_STATUSES.sample Ci::Build::AVAILABLE_STATUSES.sample
end end
def stage_index(stage)
STAGES.index(stage) || 0
end
def artifacts_archive_path def artifacts_archive_path
Rails.root + 'spec/fixtures/ci_build_artifacts.zip' Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
end end
def artifacts_metadata_path def artifacts_metadata_path
Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
end end
def artifacts_cache_file(file_path) def artifacts_cache_file(file_path)
......
...@@ -485,6 +485,7 @@ failure. ...@@ -485,6 +485,7 @@ failure.
1. `on_failure` - execute build only when at least one build from prior stages 1. `on_failure` - execute build only when at least one build from prior stages
fails. fails.
1. `always` - execute build regardless of the status of builds from prior stages. 1. `always` - execute build regardless of the status of builds from prior stages.
1. `manual` - execute build manually.
For example: For example:
...@@ -516,6 +517,7 @@ deploy_job: ...@@ -516,6 +517,7 @@ deploy_job:
stage: deploy stage: deploy
script: script:
- make deploy - make deploy
when: manual
cleanup_job: cleanup_job:
stage: cleanup stage: cleanup
...@@ -527,7 +529,20 @@ cleanup_job: ...@@ -527,7 +529,20 @@ cleanup_job:
The above script will: The above script will:
1. Execute `cleanup_build_job` only when `build_job` fails 1. Execute `cleanup_build_job` only when `build_job` fails
2. Always execute `cleanup_job` as the last step in pipeline. 2. Always execute `cleanup_job` as the last step in pipeline
3. Allow you to manually execute `deploy_job` from GitLab
#### Manual actions
>**Note:**
Introduced in GitLab 8.10.
Manual actions are special type of jobs that are not executed automatically in pipeline.
They need to be explicitly started by the user.
Manual actions can be started from pipelines, builds, environments and deployments views.
You can execute the same manual action multiple times.
Example usage of manual actions is deployment, ex. promote a staging environment to production.
### environment ### environment
......
...@@ -194,8 +194,8 @@ module Ci ...@@ -194,8 +194,8 @@ module Ci
raise ValidationError, "#{name} job: allow_failure parameter should be an boolean" raise ValidationError, "#{name} job: allow_failure parameter should be an boolean"
end end
if job[:when] && !job[:when].in?(%w[on_success on_failure always]) if job[:when] && !job[:when].in?(%w[on_success on_failure always manual])
raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always" raise ValidationError, "#{name} job: when parameter should be on_success, on_failure, always or manual"
end end
if job[:environment] && !validate_environment(job[:environment]) if job[:environment] && !validate_environment(job[:environment])
......
...@@ -43,6 +43,11 @@ FactoryGirl.define do ...@@ -43,6 +43,11 @@ FactoryGirl.define do
status 'pending' status 'pending'
end end
trait :manual do
status 'skipped'
self.when 'manual'
end
trait :allowed_to_fail do trait :allowed_to_fail do
allow_failure true allow_failure true
end end
......
...@@ -13,6 +13,7 @@ feature 'Environments', feature: true do ...@@ -13,6 +13,7 @@ feature 'Environments', feature: true do
describe 'when showing environments' do describe 'when showing environments' do
given!(:environment) { } given!(:environment) { }
given!(:deployment) { } given!(:deployment) { }
given!(:manual) { }
before do before do
visit namespace_project_environments_path(project.namespace, project) visit namespace_project_environments_path(project.namespace, project)
...@@ -43,6 +44,24 @@ feature 'Environments', feature: true do ...@@ -43,6 +44,24 @@ feature 'Environments', feature: true do
scenario 'does show deployment SHA' do scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha) expect(page).to have_link(deployment.short_sha)
end end
context 'with build and manual actions' do
given(:pipeline) { create(:ci_pipeline, project: project) }
given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
scenario 'does show a play button' do
expect(page).to have_link(manual.name.humanize)
end
scenario 'does allow to play manual action' do
expect(manual).to be_skipped
expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
end
end end
end end
...@@ -54,6 +73,7 @@ feature 'Environments', feature: true do ...@@ -54,6 +73,7 @@ feature 'Environments', feature: true do
describe 'when showing the environment' do describe 'when showing the environment' do
given(:environment) { create(:environment, project: project) } given(:environment) { create(:environment, project: project) }
given!(:deployment) { } given!(:deployment) { }
given!(:manual) { }
before do before do
visit namespace_project_environment_path(project.namespace, project, environment) visit namespace_project_environment_path(project.namespace, project, environment)
...@@ -77,7 +97,8 @@ feature 'Environments', feature: true do ...@@ -77,7 +97,8 @@ feature 'Environments', feature: true do
end end
context 'with build' do context 'with build' do
given(:build) { create(:ci_build, project: project) } given(:pipeline) { create(:ci_pipeline, project: project) }
given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) } given(:deployment) { create(:deployment, environment: environment, deployable: build) }
scenario 'does show build name' do scenario 'does show build name' do
...@@ -87,6 +108,21 @@ feature 'Environments', feature: true do ...@@ -87,6 +108,21 @@ feature 'Environments', feature: true do
scenario 'does show retry button' do scenario 'does show retry button' do
expect(page).to have_link('Retry') expect(page).to have_link('Retry')
end end
context 'with manual action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
scenario 'does show a play button' do
expect(page).to have_link(manual.name.humanize)
end
scenario 'does allow to play manual action' do
expect(manual).to be_skipped
expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
end
end end
end end
end end
......
...@@ -62,6 +62,20 @@ describe "Pipelines" do ...@@ -62,6 +62,20 @@ describe "Pipelines" do
end end
end end
context 'with manual actions' do
let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') }
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_link('Manual build') }
context 'when playing' do
before { click_link('Manual build') }
it { expect(manual.reload).to be_pending }
end
end
context 'for generic statuses' do context 'for generic statuses' do
context 'when running' do context 'when running' do
let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') }
...@@ -117,6 +131,7 @@ describe "Pipelines" do ...@@ -117,6 +131,7 @@ describe "Pipelines" do
@success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
@failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
@running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
@manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build')
@external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
end end
...@@ -131,6 +146,7 @@ describe "Pipelines" do ...@@ -131,6 +146,7 @@ describe "Pipelines" do
expect(page).to have_content(@external.id) expect(page).to have_content(@external.id)
expect(page).to have_content('Retry failed') expect(page).to have_content('Retry failed')
expect(page).to have_content('Cancel running') expect(page).to have_content('Cancel running')
expect(page).to have_link('Play')
end end
context 'retrying builds' do context 'retrying builds' do
...@@ -154,6 +170,12 @@ describe "Pipelines" do ...@@ -154,6 +170,12 @@ describe "Pipelines" do
it { expect(page).to have_selector('.ci-canceled') } it { expect(page).to have_selector('.ci-canceled') }
end end
end end
context 'playing manual build' do
before { click_link('Play') }
it { expect(@manual.reload).to be_pending }
end
end end
describe 'POST /:project/pipelines' do describe 'POST /:project/pipelines' do
......
...@@ -1141,7 +1141,7 @@ EOT ...@@ -1141,7 +1141,7 @@ EOT
config = YAML.dump({ rspec: { script: "test", when: 1 } }) config = YAML.dump({ rspec: { script: "test", when: 1 } })
expect do expect do
GitlabCiYamlProcessor.new(config, path) GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure, always or manual")
end end
it "returns errors if job artifacts:name is not an a string" do it "returns errors if job artifacts:name is not an a string" do
......
...@@ -670,4 +670,55 @@ describe Ci::Build, models: true do ...@@ -670,4 +670,55 @@ describe Ci::Build, models: true do
end end
end end
end end
describe '#manual?' do
before do
build.update(when: value)
end
subject { build.manual? }
context 'when is set to manual' do
let(:value) { 'manual' }
it { is_expected.to be_truthy }
end
context 'when set to something else' do
let(:value) { 'something else' }
it { is_expected.to be_falsey }
end
end
describe '#other_actions' do
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') }
subject { build.other_actions }
it 'returns other actions' do
is_expected.to contain_exactly(other_build)
end
end
describe '#play' do
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
subject { build.play }
it 'enques a build' do
is_expected.to be_pending
is_expected.to eq(build)
end
context 'for success build' do
before { build.queue }
it 'creates a new build' do
is_expected.to be_pending
is_expected.not_to eq(build)
end
end
end
end end
...@@ -260,6 +260,68 @@ describe Ci::Pipeline, models: true do ...@@ -260,6 +260,68 @@ describe Ci::Pipeline, models: true do
expect(pipeline.reload.status).to eq('canceled') expect(pipeline.reload.status).to eq('canceled')
end end
end end
context 'when listing manual actions' do
let(:yaml) do
{
stages: ["build", "test", "test_failure", "deploy", "cleanup"],
build: {
stage: "build",
script: "BUILD",
},
test: {
stage: "test",
script: "TEST",
},
test_failure: {
stage: "test_failure",
script: "ON test failure",
when: "on_failure",
},
deploy: {
stage: "deploy",
script: "PUBLISH",
},
production: {
stage: "deploy",
script: "PUBLISH",
when: "manual",
},
cleanup: {
stage: "cleanup",
script: "TIDY UP",
when: "always",
},
clear_cache: {
stage: "cleanup",
script: "CLEAR CACHE",
when: "manual",
}
}
end
it 'returns only for skipped builds' do
# currently all builds are created
expect(create_builds).to be_truthy
expect(manual_actions).to be_empty
# succeed stage build
pipeline.builds.running_or_pending.each(&:success)
expect(manual_actions).to be_empty
# succeed stage test
pipeline.builds.running_or_pending.each(&:success)
expect(manual_actions).to be_one # production
# succeed stage deploy
pipeline.builds.running_or_pending.each(&:success)
expect(manual_actions).to be_many # production and clear cache
end
def manual_actions
pipeline.manual_actions
end
end
end end
context 'when no builds created' do context 'when no builds created' do
...@@ -416,4 +478,28 @@ describe Ci::Pipeline, models: true do ...@@ -416,4 +478,28 @@ describe Ci::Pipeline, models: true do
end end
end end
end end
describe '#manual_actions' do
subject { pipeline.manual_actions }
it 'when none defined' do
is_expected.to be_empty
end
context 'when action defined' do
let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
it 'returns one action' do
is_expected.to contain_exactly(manual)
end
context 'there are multiple of the same name' do
let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
it 'returns latest one' do
is_expected.to contain_exactly(manual2)
end
end
end
end
end end
...@@ -11,6 +11,7 @@ describe Deployment, models: true do ...@@ -11,6 +11,7 @@ describe Deployment, models: true do
it { is_expected.to delegate_method(:name).to(:environment).with_prefix } it { is_expected.to delegate_method(:name).to(:environment).with_prefix }
it { is_expected.to delegate_method(:commit).to(:project) } it { is_expected.to delegate_method(:commit).to(:project) }
it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) } it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) }
it { is_expected.to delegate_method(:manual_actions).to(:deployable).as(:try) }
it { is_expected.to validate_presence_of(:ref) } it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) } it { is_expected.to validate_presence_of(:sha) }
......
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