From e00da96c88314df79600e7848ae6fe7e4a29af2e Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski <ayufan@ayufan.eu>
Date: Sun, 17 Jul 2016 01:48:51 +0200
Subject: [PATCH] Improve manual actions code and add model, service and
 feature tests

Manual actions are accessible from:
- Pipelines
- Builds
- Environments
- Deployments
---
 app/controllers/projects/builds_controller.rb |   2 +-
 app/models/ci/build.rb                        |  14 +-
 app/models/deployment.rb                      |   2 +-
 app/views/projects/ci/builds/_build.html.haml |   2 +-
 .../projects/ci/pipelines/_pipeline.html.haml |  11 +-
 app/views/projects/deployments/_actions.haml  |  22 ++
 .../deployments/_deployment.html.haml         |  12 +-
 .../projects/deployments/_playable.html.haml  |  12 -
 .../environments/_environment.html.haml       |   3 +-
 .../projects/environments/index.html.haml     |   1 +
 .../relative_naming_ci_namespace.rb           |  16 ++
 doc/ci/yaml/README.md                         |  17 +-
 spec/factories/ci/builds.rb                   |   5 +
 spec/features/environments_spec.rb            |  38 +++-
 spec/features/pipelines_spec.rb               |  22 ++
 spec/models/build_spec.rb                     |  51 +++++
 spec/models/ci/pipeline_spec.rb               |  24 ++
 spec/models/deployment_spec.rb                |   1 +
 .../ci/create_pipeline_service_spec.rb        | 213 ++++++++++++++++++
 19 files changed, 429 insertions(+), 39 deletions(-)
 create mode 100644 app/views/projects/deployments/_actions.haml
 delete mode 100644 app/views/projects/deployments/_playable.html.haml
 create mode 100644 config/initializers/relative_naming_ci_namespace.rb
 create mode 100644 spec/services/ci/create_pipeline_service_spec.rb

diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 597cfa93712..d64d1e52e2c 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'
 
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index a05b3f43f97..248ecd71962 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -15,7 +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).without_created }
+    scope :manual_actions, ->() { where(when: :manual).relevant }
 
     mount_uploader :artifacts_file, ArtifactUploader
     mount_uploader :artifacts_metadata, ArtifactUploader
@@ -96,15 +96,19 @@ module Ci
       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)
-      if skipped?
-        # We can run skipped build
-        new_build.user = current_user
-        new_build.queue
+      # 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)
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index aa1f7e462d2..1a7cd60817e 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -34,6 +34,6 @@ class Deployment < ActiveRecord::Base
   end
 
   def manual_actions
-    deployable.try(:manual_actions)
+    deployable.try(:other_actions)
   end
 end
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index efedafefc38..9264289987d 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -86,6 +86,6 @@
             = 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: 'Retry', class: 'btn btn-build' do
+            = 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 b9adc920ea6..397f26012d4 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -60,16 +60,17 @@
       - actions = pipeline.manual_actions
       - if artifacts.present? || actions.any?
         .btn-group.inline
-          - if playable.any?
+          - 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
-                %li
-                  = link_to play_namespace_project_build_path(@project.namespace, @project, build), rel: 'nofollow' do
-                    = icon("play")
-                    %span= actions.name.titleize
+                - 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.titleize
           - if artifacts.present?
             .btn-group
               %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
new file mode 100644
index 00000000000..9a8d4a63007
--- /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.titleize
+
+    - if defined?(allow_rollback) && allow_rollback
+      = 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 e430d195ea6..baf02f1e6a0 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -7,19 +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 'playable', deployment: deployment
+    = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true
diff --git a/app/views/projects/deployments/_playable.html.haml b/app/views/projects/deployments/_playable.html.haml
deleted file mode 100644
index 4bff21ce7a5..00000000000
--- a/app/views/projects/deployments/_playable.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-- actions = deployment.manual_actions
-- if actions.any?
-  .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
-        %li
-          = link_to play_namespace_project_build_path(@project.namespace, @project, build), rel: 'nofollow' do
-            = icon("play")
-            %span= actions.name.titleize
diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
index 8e517196e27..e2453395602 100644
--- a/app/views/projects/environments/_environment.html.haml
+++ b/app/views/projects/environments/_environment.html.haml
@@ -17,5 +17,4 @@
       #{time_ago_with_tooltip(last_deployment.created_at)}
 
   %td
-    - if can?(current_user, :create_deployment, last_deployment) && last_deployment.deployable
-      = render 'projects/deployments/playable', deployment: last_deployment
+    = 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 303d7c23d01..a6dd34653ab 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/config/initializers/relative_naming_ci_namespace.rb b/config/initializers/relative_naming_ci_namespace.rb
new file mode 100644
index 00000000000..59abe1b9b91
--- /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/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 50fa263f693..b421bbdec3f 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` form 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/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 5fb671df570..fb111889501 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 7fb28f4174b..491eec70752 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.titleize)
+          end
+
+          scenario 'does allow to play manual action' do
+            expect(manual).to be_skipped
+            expect{ click_link(manual.name.titleize) }.to change{ Ci::Pipeline.count }.by(0)
+            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.titleize)
+          end
+
+          scenario 'does allow to play manual action' do
+            expect(manual).to be_skipped
+            expect{ click_link(manual.name.titleize) }.to change{ Ci::Pipeline.count }.by(0)
+            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 e7ee0aaea3c..55cecf3d587 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/models/build_spec.rb b/spec/models/build_spec.rb
index 481416319dd..4846c87a100 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 10db79bd15f..887f36db519 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -416,4 +416,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 b273018707f..7df3df4bb9e 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) }
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
new file mode 100644
index 00000000000..89eca2cd768
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -0,0 +1,213 @@
+require 'spec_helper'
+
+describe Ci::CreatePipelineService, services: true do
+  let(:project) { FactoryGirl.create(:project) }
+  let(:user) { create(:admin) }
+
+  before do
+    stub_ci_pipeline_to_return_yaml_file
+  end
+
+  describe '#execute' do
+    def execute(params)
+      described_class.new(project, user, params).execute
+    end
+
+    context 'valid params' do
+      let(:pipeline) do
+        execute(ref: 'refs/heads/master',
+                before: '00000000',
+                after: project.commit.id,
+                commits: [{ message: "Message" }])
+      end
+
+      it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
+      it { expect(pipeline).to be_valid }
+      it { expect(pipeline).to be_persisted }
+      it { expect(pipeline).to eq(project.pipelines.last) }
+      it { expect(pipeline).to have_attributes(user: user) }
+      it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
+    end
+
+    context "skip tag if there is no build for it" do
+      it "creates commit if there is appropriate job" do
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: "Message" }])
+        expect(result).to be_persisted
+      end
+
+      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"] } })
+        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
+      end
+    end
+
+    it 'skips creating pipeline for refs without .gitlab-ci.yml' do
+      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(Ci::Pipeline.count).to eq(0)
+    end
+
+    it 'fails commits if yaml is invalid' do
+      message = 'message'
+      allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
+      stub_ci_pipeline_yaml_file('invalid: file: file')
+      commits = [{ message: message }]
+      pipeline = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: commits)
+
+      expect(pipeline).to be_persisted
+      expect(pipeline.builds.any?).to be false
+      expect(pipeline.status).to eq('failed')
+      expect(pipeline.yaml_errors).not_to be_nil
+    end
+
+    context 'when commit contains a [ci skip] directive' do
+      let(:message) { "some message[ci skip]" }
+      let(:messageFlip) { "some message[skip ci]" }
+      let(:capMessage) { "some message[CI SKIP]" }
+      let(:capMessageFlip) { "some message[SKIP CI]" }
+
+      before do
+        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
+      end
+
+      it "skips builds creation if there is [ci skip] tag in commit message" do
+        commits = [{ message: message }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "skips builds creation if there is [skip ci] tag in commit message" do
+        commits = [{ message: messageFlip }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "skips builds creation if there is [CI SKIP] tag in commit message" do
+        commits = [{ message: capMessage }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "skips builds creation if there is [SKIP CI] tag in commit message" do
+        commits = [{ message: capMessageFlip }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("skipped")
+      end
+
+      it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
+        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
+
+        commits = [{ message: "some message" }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.first.name).to eq("rspec")
+      end
+
+      it "fails builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
+        stub_ci_pipeline_yaml_file('invalid: file: fiile')
+        commits = [{ message: message }]
+        pipeline = execute(ref: 'refs/heads/master',
+                           before: '00000000',
+                           after: project.commit.id,
+                           commits: commits)
+
+        expect(pipeline).to be_persisted
+        expect(pipeline.builds.any?).to be false
+        expect(pipeline.status).to eq("failed")
+        expect(pipeline.yaml_errors).not_to be_nil
+      end
+    end
+
+    it "creates commit with failed status if yaml is invalid" do
+      stub_ci_pipeline_yaml_file('invalid: file')
+      commits = [{ message: "some message" }]
+      pipeline = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         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
+
+    context 'when there are no jobs for this pipeline' do
+      before do
+        config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
+        stub_ci_pipeline_yaml_file(config)
+      end
+
+      it 'does not create a new pipeline' do
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: 'some msg' }])
+
+        expect(result).not_to be_persisted
+        expect(Ci::Build.all).to be_empty
+        expect(Ci::Pipeline.count).to eq(0)
+      end
+    end
+
+    context 'with manual actions' do
+      before do
+        config = YAML.dump({ deploy: { script: 'ls', when: 'manual' } })
+        stub_ci_pipeline_yaml_file(config)
+      end
+
+      it 'does not create a new pipeline' do
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: 'some msg' }])
+
+        expect(result).to be_persisted
+        expect(result.manual_actions).not_to be_empty
+      end
+    end
+  end
+end
-- 
GitLab