diff --git a/CHANGELOG b/CHANGELOG
index bf252612232404bab3358d567526bff396484d7a..428d41178d4c4f0a75986f852792a1df535796a6 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index d7513d75f014491e537789b28df6fb385ae02090..553b62741a5de091ad536aa467686d2ac1794014 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,6 +1,6 @@
 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 @@ class Projects::BuildsController < Projects::ApplicationController
   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)
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index ffac3a22efc09eab3cd818fa82233bed8b236cc0..49a123d488b5040879be05c5a4a88845dac08f59 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -15,6 +15,7 @@ module Ci
     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 @@ module Ci
       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
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index a65a826536de8f4fdc8a43a5e25b238a664035c1..aca8607f4e87d7d2a668ee9a02b3e3e6046e3c8e 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -69,6 +69,10 @@ module Ci
       !tag?
     end
 
+    def manual_actions
+      builds.latest.manual_actions
+    end
+
     def retryable?
       builds.latest.any? do |build|
         build.failed? && build.retryable?
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index e437e3417a8388a602c6870cc731ea0a6428a717..535db26240a7985ce568c15d3238e78e1daac355 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -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
diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb
index 3ef91caad473004a1d618739246eb15ec0fa209f..44c6b30f2788a89a4425950f6b0d70eaafc6f7b5 100644
--- a/app/models/concerns/statuseable.rb
+++ b/app/models/concerns/statuseable.rb
@@ -16,10 +16,10 @@ module Statuseable
 
       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)"
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 520026c18dd5d96c5dedb739d3187bbaa60d779f..1a7cd60817e7505fdc40728091efb6f53a843283 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -32,4 +32,8 @@ class Deployment < ActiveRecord::Base
   def keep_around_commit
     project.repository.keep_around(self.sha)
   end
+
+  def manual_actions
+    deployable.try(:other_actions)
+  end
 end
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index 3b21f0acb966c9f1d9715a50891ad09f76ff34b4..4946f7076fdd515ee1781bccd9bf2af28336ca1d 100644
--- a/app/services/ci/create_builds_service.rb
+++ b/app/services/ci/create_builds_service.rb
@@ -15,7 +15,7 @@ module Ci
           status == 'success'
         when 'on_failure'
           status == 'failed'
-        when 'always'
+        when 'always', 'manual'
           %w(success failed).include?(status)
         end
       end
@@ -47,6 +47,10 @@ module Ci
                            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.
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index e1b42b2cfa5c3ce09063ed137d752e551a0cfa3a..9264289987d039906ae5791a458497b0425a9847 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -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')
+
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 7ae699832f64c59ef7447d8c45d7ef3f25d616a1..996c90737701bb79a706cd2fb1a1b3914bbce76e 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -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
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
new file mode 100644
index 0000000000000000000000000000000000000000..65d68aa298589e1b63b017186d073adb3278259e
--- /dev/null
+++ b/app/views/projects/deployments/_actions.haml
@@ -0,0 +1,22 @@
+- 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
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index d08dd92f1f6baad77777ec22eb494b2fdc9d6b3f..baf02f1e6a013ca74f5af737d0f86195b6c80f5b 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -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
diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
index eafa246d05fd7ddf0b95c0f3780426f65f000168..e2453395602c1f6fe82a61a73c2268e491c3942f 100644
--- a/app/views/projects/environments/_environment.html.haml
+++ b/app/views/projects/environments/_environment.html.haml
@@ -15,3 +15,6 @@
   %td
     - if last_deployment
       #{time_ago_with_tooltip(last_deployment.created_at)}
+
+  %td
+    = render 'projects/deployments/actions', deployment: last_deployment
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 303d7c23d01b7203af1486e64078880389ba936f..a6dd34653abd84e4b0db61e0ae5aa38388ae7364 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -28,4 +28,5 @@
           %th Environment
           %th Last deployment
           %th Date
+          %th
         = render @environments
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index b17aba2431fd30db3b4e0f3a2f13aa42844e7ca3..b8b1ce52a91617fde8cfcb74f99c67d897b10230 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -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
diff --git a/config/initializers/relative_naming_ci_namespace.rb b/config/initializers/relative_naming_ci_namespace.rb
new file mode 100644
index 0000000000000000000000000000000000000000..59abe1b9b91984a2b68058eff8f7da8d34299a93
--- /dev/null
+++ b/config/initializers/relative_naming_ci_namespace.rb
@@ -0,0 +1,16 @@
+# 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
diff --git a/config/routes.rb b/config/routes.rb
index 3160fd767b84a43baa42b8b6a8e5c30faff18f50..be651d8903f0ca743c3bd91fd816f658df679d19 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -750,6 +750,7 @@ Rails.application.routes.draw do
             get :status
             post :cancel
             post :retry
+            post :play
             post :erase
             get :trace
             get :raw
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb
index 51ff451eb4c0280570dc6657b4a21bffd84bc6fe..124704cb45157574822bef247b469bbbb72c6235 100644
--- a/db/fixtures/development/14_builds.rb
+++ b/db/fixtures/development/14_builds.rb
@@ -1,13 +1,34 @@
 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 @@ class Gitlab::Seeder::Builds
     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 @@ class Gitlab::Seeder::Builds
     []
   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 @@ class Gitlab::Seeder::Builds
     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 @@ class Gitlab::Seeder::Builds
     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)
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 5f77888f6313789a3b336a8faff8552ceb967c3a..31b4fd2669e5de58b1a908aa57650a1c3813760e 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -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
 
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index a48dc542b14c35e87bfa1809bb70ba872d78c2ea..41449d720b355abda492b531d1ab4cc8e49b6807 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -194,8 +194,8 @@ module Ci
         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])
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 5fb671df570cc4f02c35c0d4ec97981ece3b2a40..fb1118895017596934b397e9b7c5178c69c5b56e 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -43,6 +43,11 @@ FactoryGirl.define do
       status 'pending'
     end
 
+    trait :manual do
+      status 'skipped'
+      self.when 'manual'
+    end
+
     trait :allowed_to_fail do
       allow_failure true
     end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index 7fb28f4174b01be2c13a32657d016700ecb6ecf0..9c018be14b7400a273554ed657bc4bd3e7c721e2 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -13,6 +13,7 @@ feature 'Environments', feature: true do
   describe 'when showing environments' do
     given!(:environment) { }
     given!(:deployment) { }
+    given!(:manual) { }
 
     before do
       visit namespace_project_environments_path(project.namespace, project)
@@ -43,6 +44,24 @@ feature 'Environments', feature: true do
         scenario 'does show deployment SHA' do
           expect(page).to have_link(deployment.short_sha)
         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
 
@@ -54,6 +73,7 @@ feature 'Environments', feature: true do
   describe 'when showing the environment' do
     given(:environment) { create(:environment, project: project) }
     given!(:deployment) { }
+    given!(:manual) { }
 
     before do
       visit namespace_project_environment_path(project.namespace, project, environment)
@@ -77,7 +97,8 @@ feature 'Environments', feature: true do
       end
 
       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) }
 
         scenario 'does show build name' do
@@ -87,6 +108,21 @@ feature 'Environments', feature: true do
         scenario 'does show retry button' do
           expect(page).to have_link('Retry')
         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
diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb
index e7ee0aaea3c6cf7feabecca37be9f54132f91acd..7f861db196981a1651e80e5d6a3b9b2222b00c85 100644
--- a/spec/features/pipelines_spec.rb
+++ b/spec/features/pipelines_spec.rb
@@ -62,6 +62,20 @@ describe "Pipelines" do
       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 'when running' do
         let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') }
@@ -117,6 +131,7 @@ describe "Pipelines" do
       @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
       @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
       @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')
     end
 
@@ -131,6 +146,7 @@ describe "Pipelines" do
       expect(page).to have_content(@external.id)
       expect(page).to have_content('Retry failed')
       expect(page).to have_content('Cancel running')
+      expect(page).to have_link('Play')
     end
 
     context 'retrying builds' do
@@ -154,6 +170,12 @@ describe "Pipelines" do
         it { expect(page).to have_selector('.ci-canceled') }
       end
     end
+
+    context 'playing manual build' do
+      before { click_link('Play') }
+
+      it { expect(@manual.reload).to be_pending }
+    end
   end
 
   describe 'POST /:project/pipelines' do
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index ad6587b4c25e28ba387d496b1940120655b605ae..d20fd4ab7dd8cac769315a3598136e692448dd4c 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -1141,7 +1141,7 @@ EOT
         config = YAML.dump({ rspec: { script: "test", when: 1 } })
         expect do
           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
 
       it "returns errors if job artifacts:name is not an a string" do
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 481416319dd9cd01ff8607045578839223679c09..4846c87a1007bee999773102f7ab5aef9276a156 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -670,4 +670,55 @@ describe Ci::Build, models: true do
       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
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 10db79bd15fd49d06de9474bb1962b520ea81cab..c29e4811385a7a754f92dcebce99b637fbf84d7c 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -260,6 +260,68 @@ describe Ci::Pipeline, models: true do
           expect(pipeline.reload.status).to eq('canceled')
         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
 
     context 'when no builds created' do
@@ -416,4 +478,28 @@ describe Ci::Pipeline, models: true do
       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
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index b273018707f491a8d2cf3c53a201ac78f998e164..7df3df4bb9e652016122d0e6ed99b605a15ca28e 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -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(:commit).to(:project) }
   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(:sha) }