Read about our upcoming Code of Conduct on this issue

Commit 61e7453e authored by Rémy Coutable's avatar Rémy Coutable
Browse files

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
......@@ -43,6 +43,7 @@ v 8.10.0 (unreleased)
- Fix viewing notification settings when a project is pending deletion
- Updated compare dropdown menus to use GL dropdown
- 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)
- The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020
- Updated project header design
......
class Projects::BuildsController < Projects::ApplicationController
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]
layout 'project'
......@@ -49,14 +49,19 @@ def trace
end
def retry
unless @build.retryable?
return render_404
end
return render_404 unless @build.retryable?
build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build)
end
def play
return render_404 unless @build.playable?
build = @build.play(current_user)
redirect_to build_path(build)
end
def cancel
@build.cancel
redirect_to build_path(@build)
......
......@@ -15,6 +15,7 @@ class Build < CommitStatus
scope :with_artifacts, ->() { where.not(artifacts_file: nil) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual) }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
......@@ -91,6 +92,29 @@ def retry(build, user = nil)
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?
project.builds_enabled? && commands.present? && complete?
end
......
......@@ -69,6 +69,10 @@ def branch?
!tag?
end
def manual_actions
builds.latest.manual_actions
end
def retryable?
builds.latest.any? do |build|
build.failed? && build.retryable?
......
......@@ -22,6 +22,10 @@ class CommitStatus < ActiveRecord::Base
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
state_machine :status, initial: :pending do
event :queue do
transition skipped: :pending
end
event :run do
transition pending: :running
end
......
......@@ -16,10 +16,10 @@ def status_sql
deduce_status = "(CASE
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})=(#{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'
ELSE 'failed'
END)"
......
......@@ -32,4 +32,8 @@ def last?
def keep_around_commit
project.repository.keep_around(self.sha)
end
def manual_actions
deployable.try(:other_actions)
end
end
......@@ -15,7 +15,7 @@ def execute(stage, user, status, trigger_request = nil)
status == 'success'
when 'on_failure'
status == 'failed'
when 'always'
when 'always', 'manual'
%w(success failed).include?(status)
end
end
......@@ -47,6 +47,10 @@ def execute(stage, user, status, trigger_request = nil)
user: user,
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.
# Those will be persisted when @pipeline is saved.
......
......@@ -39,6 +39,8 @@
%span.label.label-danger allowed to fail
- if defined?(retried) && retried
%span.label.label-warning retried
- if build.manual?
%span.label.label-info manual
- if defined?(runner) && runner
......@@ -79,6 +81,11 @@
- 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
= icon('remove', class: 'cred')
- 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
= icon('repeat')
- elsif defined?(allow_retry) && allow_retry
- if 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
= 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 @@
%td.pipeline-actions
.controls.hidden-xs.pull-right
- artifacts = pipeline.builds.latest.select { |b| b.artifacts? }
- if artifacts.present?
.inline
.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
- actions = pipeline.manual_actions
- if artifacts.present? || actions.any?
.btn-group.inline
- if actions.any?
.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 |build|
%li
= 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)
.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 @@
%td
- 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})"
%td
#{time_ago_with_tooltip(deployment.created_at)}
%td
- if can?(current_user, :create_deployment, deployment) && deployment.deployable
.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
= render 'projects/deployments/actions', deployment: deployment, allow_rollback: true
......@@ -15,3 +15,6 @@
%td
- if last_deployment
#{time_ago_with_tooltip(last_deployment.created_at)}
%td
= render 'projects/deployments/actions', deployment: last_deployment
......@@ -28,4 +28,5 @@
%th Environment
%th Last deployment
%th Date
%th
= render @environments
......@@ -5,7 +5,7 @@
%div{ class: container_class }
.top-area
.col-md-9
%h3.page-title= @environment.name.titleize
%h3.page-title= @environment.name.capitalize
.col-md-3
.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 @@
get :status
post :cancel
post :retry
post :play
post :erase
get :trace
get :raw
......
class Gitlab::Seeder::Builds
STAGES = %w[build notify_build test notify_test deploy notify_deploy]
def initialize(project)
@project = project
end
def seed!
ci_commits.each do |ci_commit|
pipelines.each do |pipeline|
begin
build_create!(ci_commit, name: 'test build 1')
build_create!(ci_commit, status: 'success', name: 'test build 2')
build_create!(pipeline, name: 'build:linux', stage: 'build')
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 '.'
rescue ActiveRecord::RecordInvalid
print 'F'
......@@ -15,8 +36,8 @@ def seed!
end
end
def ci_commits
commits = @project.repository.commits('master', nil, 5)
def pipelines
commits = @project.repository.commits('master', limit: 5)
commits_sha = commits.map { |commit| commit.raw.id }
commits_sha.map do |sha|
@project.ensure_pipeline(sha, 'master')
......@@ -25,11 +46,11 @@ def ci_commits
[]
end
def build_create!(ci_commit, opts = {})
attributes = build_attributes_for(ci_commit).merge(opts)
def build_create!(pipeline, opts = {})
attributes = build_attributes_for(pipeline, opts)
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|
build.artifacts_file = file
end
......@@ -40,19 +61,28 @@ def build_create!(ci_commit, opts = {})
end
build.save!
build.update(status: build_status)
if %w(running success failed).include?(build.status)
# We need to set build trace after saving a build (id required)
build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
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)
{ name: 'test build', 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 }
def build_attributes_for(pipeline, opts)
commit_status_attributes_for(pipeline, opts).merge(commands: '$ build command')
end
def build_user
......@@ -63,13 +93,16 @@ def build_status
Ci::Build::AVAILABLE_STATUSES.sample
end
def stage_index(stage)
STAGES.index(stage) || 0
end
def artifacts_archive_path
Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
end
def artifacts_metadata_path
Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
end
def artifacts_cache_file(file_path)
......
......@@ -485,6 +485,7 @@ failure.
1. `on_failure` - execute build only when at least one build from prior stages
fails.
1. `always` - execute build regardless of the status of builds from prior stages.
1. `manual` - execute build manually.
For example:
......@@ -516,6 +517,7 @@ deploy_job:
stage: deploy
script:
- make deploy
when: manual
cleanup_job:
stage: cleanup
......@@ -527,7 +529,20 @@ cleanup_job:
The above script will:
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
......
......@@ -194,8 +194,8 @@ def validate_job_types!(name, job)
raise ValidationError, "#{name} job: allow_failure parameter should be an boolean"
end
if job[:when] && !job[:when].in?(%w[on_success on_failure always])
raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or 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, always or manual"
end
if job[:environment] && !validate_environment(job[:environment])
......
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