From 7d0fe1f04ed285e7e5cf825a305114b3e981c2f8 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 16 Jul 2016 18:39:58 +0200 Subject: [PATCH 01/14] Add implementation of manual actions --- app/controllers/projects/builds_controller.rb | 9 +++++ app/models/ci/build.rb | 24 +++++++++++++ app/models/ci/pipeline.rb | 4 +++ app/models/deployment.rb | 4 +++ app/views/projects/ci/builds/_build.html.haml | 13 +++++-- .../projects/ci/pipelines/_pipeline.html.haml | 34 +++++++++++++------ .../deployments/_deployment.html.haml | 2 ++ .../projects/deployments/_playable.html.haml | 12 +++++++ .../environments/_environment.html.haml | 4 +++ config/routes.rb | 1 + lib/ci/gitlab_ci_yaml_processor.rb | 4 +-- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- 12 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 app/views/projects/deployments/_playable.html.haml diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index d7513d75f01..597cfa93712 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -57,6 +57,15 @@ class Projects::BuildsController < Projects::ApplicationController redirect_to build_path(build) end + def play + unless @build.playable? + return render_404 + end + + 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 ffac3a22efc..81feca0088f 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).without_created } 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 playable_actions + pipeline.playable_actions + 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 + 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 b468434866b..5aefe1d1694 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -65,6 +65,10 @@ module Ci !tag? end + def playable_actions + builds.manual_actions.latest + end + def retryable? builds.latest.any? do |build| build.failed? && build.retryable? diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 520026c18dd..44dfb67552e 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 playable_actions + deployable.try(:playable_actions) + end end diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index e1b42b2cfa5..efedafefc38 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: 'Retry', 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 0557d384e33..1285fce4930 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -57,18 +57,30 @@ %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| + - playable = pipeline.playable_actions + - if artifacts.present? || playable.any? + .btn-group.inline + - if playable.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 download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do - = icon("download") - %span Download '#{build.name}' artifacts + = link_to play_namespace_project_build_path(@project.namespace, @project, build), rel: 'nofollow' do + = icon("play") + %span= playable.name.titleize + - 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/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index d08dd92f1f6..e430d195ea6 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -21,3 +21,5 @@ Retry - else Rollback + + = render 'playable', deployment: deployment diff --git a/app/views/projects/deployments/_playable.html.haml b/app/views/projects/deployments/_playable.html.haml new file mode 100644 index 00000000000..02e4fb7974e --- /dev/null +++ b/app/views/projects/deployments/_playable.html.haml @@ -0,0 +1,12 @@ +- playable = deployment.playable_actions +- if playable.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= playable.name.titleize diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index eafa246d05f..8e517196e27 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -15,3 +15,7 @@ %td - if last_deployment #{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 diff --git a/config/routes.rb b/config/routes.rb index 3160fd767b8..be651d8903f 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/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index a48dc542b14..41449d720b3 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/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index ad6587b4c25..d20fd4ab7dd 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 From 3248c9fb56e5d5cb8980cb37effec1ee2f4f664a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sat, 16 Jul 2016 23:06:34 +0200 Subject: [PATCH 02/14] Rename playable_actions to manual_actions --- app/models/ci/build.rb | 4 ---- app/models/ci/pipeline.rb | 4 ++-- app/models/deployment.rb | 4 ++-- app/views/projects/ci/pipelines/_pipeline.html.haml | 6 +++--- app/views/projects/deployments/_playable.html.haml | 6 +++--- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 81feca0088f..a05b3f43f97 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -96,10 +96,6 @@ module Ci self.when == 'manual' end - def playable_actions - pipeline.playable_actions - end - def playable? project.builds_enabled? && commands.present? && manual? end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5aefe1d1694..de9c74194ac 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -65,8 +65,8 @@ module Ci !tag? end - def playable_actions - builds.manual_actions.latest + def manual_actions + builds.latest.manual_actions end def retryable? diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 44dfb67552e..aa1f7e462d2 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -33,7 +33,7 @@ class Deployment < ActiveRecord::Base project.repository.keep_around(self.sha) end - def playable_actions - deployable.try(:playable_actions) + def manual_actions + deployable.try(:manual_actions) end end diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 1285fce4930..b9adc920ea6 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -57,8 +57,8 @@ %td.pipeline-actions .controls.hidden-xs.pull-right - artifacts = pipeline.builds.latest.select { |b| b.artifacts? } - - playable = pipeline.playable_actions - - if artifacts.present? || playable.any? + - actions = pipeline.manual_actions + - if artifacts.present? || actions.any? .btn-group.inline - if playable.any? .btn-group @@ -69,7 +69,7 @@ %li = link_to play_namespace_project_build_path(@project.namespace, @project, build), rel: 'nofollow' do = icon("play") - %span= playable.name.titleize + %span= actions.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/_playable.html.haml b/app/views/projects/deployments/_playable.html.haml index 02e4fb7974e..4bff21ce7a5 100644 --- a/app/views/projects/deployments/_playable.html.haml +++ b/app/views/projects/deployments/_playable.html.haml @@ -1,5 +1,5 @@ -- playable = deployment.playable_actions -- if playable.any? +- actions = deployment.manual_actions +- if actions.any? .btn-group.inline .btn-group %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} @@ -9,4 +9,4 @@ %li = link_to play_namespace_project_build_path(@project.namespace, @project, build), rel: 'nofollow' do = icon("play") - %span= playable.name.titleize + %span= actions.name.titleize From e00da96c88314df79600e7848ae6fe7e4a29af2e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 17 Jul 2016 01:48:51 +0200 Subject: [PATCH 03/14] 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 From 4faaa62aad6b71c681fc67c5ae1e3b47cfcf18b2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 17 Jul 2016 02:03:59 +0200 Subject: [PATCH 04/14] Update build fixtures to include manual actions --- db/fixtures/development/14_builds.rb | 63 +++++++++++++++++++++------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb index 51ff451eb4c..44f8a61d608 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) From 5b119a9dc85943b3f60868a347a814e767c9840c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 17 Jul 2016 15:59:07 +0200 Subject: [PATCH 05/14] Fix rubocop offenses --- app/models/ci/build.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 248ecd71962..c9566560461 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -107,8 +107,8 @@ module Ci def play(current_user = nil) # Try to queue a current build if self.queue - self.update(user: current_user) - self + self.update(user: current_user) + self else # Otherwise we need to create a duplicate Ci::Build.retry(self, current_user) From 640924e4a23dcfe1e8d6b616e822bfc7fce56d8f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Jul 2016 14:48:36 +0200 Subject: [PATCH 06/14] Mark builds with manual actions as skipped --- app/models/concerns/statuseable.rb | 6 +++--- app/services/ci/create_builds_service.rb | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb index 3ef91caad47..44c6b30f278 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/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 3b21f0acb96..e4eb75598a2 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,8 @@ module Ci user: user, project: @pipeline.project) + build_attrs[:status] = 'skipped' if build_attrs[:when] == 'manual' + ## # We do not persist new builds here. # Those will be persisted when @pipeline is saved. From 20438213d5f16907f0b0ea2cd2722b6ccfdc7556 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Jul 2016 14:56:22 +0200 Subject: [PATCH 07/14] Use `capitalize` instead of `titleize` for manual actions --- app/views/projects/ci/pipelines/_pipeline.html.haml | 2 +- app/views/projects/deployments/_actions.haml | 2 +- app/views/projects/environments/show.html.haml | 2 +- spec/features/environments_spec.rb | 8 ++++---- spec/features/pipelines_spec.rb | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 397f26012d4..797e52d1122 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -70,7 +70,7 @@ %li = link_to play_namespace_project_build_path(@project.namespace, @project, build), method: :post, rel: 'nofollow' do = icon("play") - %span= build.name.titleize + %span= build.name.capitalize - 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 index 9a8d4a63007..8dd9e70ac67 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -12,7 +12,7 @@ %li = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do = icon("play") - %span= action.name.titleize + %span= action.name.capitalize - if defined?(allow_rollback) && allow_rollback = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index b17aba2431f..b8b1ce52a91 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/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 491eec70752..d62b3772fdb 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -52,12 +52,12 @@ feature 'Environments', feature: true 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) + expect(page).to have_link(manual.name.capitalize) 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{ click_link(manual.name.capitalize) }.to change{ Ci::Pipeline.count }.by(0) expect(page).to have_content(manual.name) expect(manual.reload).to be_pending end @@ -113,12 +113,12 @@ feature 'Environments', feature: true 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) + expect(page).to have_link(manual.name.capitalize) 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{ click_link(manual.name.capitalize) }.to change{ Ci::Pipeline.count }.by(0) expect(page).to have_content(manual.name) expect(manual.reload).to be_pending end diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index 55cecf3d587..7f861db1969 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -67,10 +67,10 @@ describe "Pipelines" do before { visit namespace_project_pipelines_path(project.namespace, project) } - it { expect(page).to have_link('Manual Build') } + it { expect(page).to have_link('Manual build') } context 'when playing' do - before { click_link('Manual Build') } + before { click_link('Manual build') } it { expect(manual.reload).to be_pending } end From 60583bf9db5be7f83863602672e1689853609687 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Jul 2016 14:57:24 +0200 Subject: [PATCH 08/14] Make manual actions to work with master code --- app/models/ci/build.rb | 2 +- app/models/commit_status.rb | 4 +++ spec/models/ci/pipeline_spec.rb | 62 +++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index c9566560461..49a123d488b 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).relevant } + scope :manual_actions, ->() { where(when: :manual) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index e437e3417a8..535db26240a 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/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 887f36db519..c29e4811385 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 From 410e638bbffeed2f374fcd00f5d002b04451f533 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Jul 2016 14:59:01 +0200 Subject: [PATCH 09/14] Add notice implementation --- app/services/ci/create_builds_service.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index e4eb75598a2..4946f7076fd 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -47,6 +47,8 @@ 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' ## From f08eb7bbb74451363843f2b5b4d31828cb0be16b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Jul 2016 15:02:26 +0200 Subject: [PATCH 10/14] Remove unused create_pipeline_service_spec.rb --- .../ci/create_pipeline_service_spec.rb | 213 ------------------ 1 file changed, 213 deletions(-) delete mode 100644 spec/services/ci/create_pipeline_service_spec.rb diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb deleted file mode 100644 index 89eca2cd768..00000000000 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ /dev/null @@ -1,213 +0,0 @@ -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 From f12faae8b841189fb3f8693fc4880fdb9688b5d2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Jul 2016 18:42:03 +0200 Subject: [PATCH 11/14] Improve code design --- app/controllers/projects/builds_controller.rb | 8 ++------ db/fixtures/development/14_builds.rb | 2 +- doc/ci/yaml/README.md | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index d64d1e52e2c..553b62741a5 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -49,18 +49,14 @@ 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 - unless @build.playable? - return render_404 - end + return render_404 unless @build.playable? build = @build.play(current_user) redirect_to build_path(build) diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb index 44f8a61d608..124704cb451 100644 --- a/db/fixtures/development/14_builds.rb +++ b/db/fixtures/development/14_builds.rb @@ -1,5 +1,5 @@ class Gitlab::Seeder::Builds - STAGES = %w(build notify_build test notify_test deploy notify_deploy) + STAGES = %w[build notify_build test notify_test deploy notify_deploy] def initialize(project) @project = project diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index b421bbdec3f..143d2f175ef 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -530,7 +530,7 @@ 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 -3. Allow you to manually execute `deploy_job` form GitLab +3. Allow you to manually execute `deploy_job` from GitLab #### Manual actions From d92054dd660fba766282ad382ca6f541b5e1bad6 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Jul 2016 18:42:57 +0200 Subject: [PATCH 12/14] Use `humanize` --- app/views/projects/ci/pipelines/_pipeline.html.haml | 2 +- app/views/projects/deployments/_actions.haml | 2 +- spec/features/environments_spec.rb | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 797e52d1122..f26d93e265b 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -70,7 +70,7 @@ %li = link_to play_namespace_project_build_path(@project.namespace, @project, build), method: :post, rel: 'nofollow' do = icon("play") - %span= build.name.capitalize + %span= build.name.humanize - 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 index 8dd9e70ac67..213d74972f1 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -12,7 +12,7 @@ %li = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do = icon("play") - %span= action.name.capitalize + %span= action.name.humanize - if defined?(allow_rollback) && allow_rollback = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index d62b3772fdb..9c018be14b7 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -52,12 +52,12 @@ feature 'Environments', feature: true 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.capitalize) + 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.capitalize) }.to change{ Ci::Pipeline.count }.by(0) + 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 @@ -113,12 +113,12 @@ feature 'Environments', feature: true 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.capitalize) + 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.capitalize) }.to change{ Ci::Pipeline.count }.by(0) + 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 From 7dbb1952fdca3c1a2e14ee0fc139d78ef98db708 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Mon, 18 Jul 2016 19:00:55 +0200 Subject: [PATCH 13/14] Use local_assigns --- app/views/projects/deployments/_actions.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index 213d74972f1..65d68aa2985 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -14,7 +14,7 @@ = icon("play") %span= action.name.humanize - - if defined?(allow_rollback) && allow_rollback + - 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 From 72229c375fb3efe9dd28c0bf98f0df7696d99fc2 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 19 Jul 2016 14:43:45 +0200 Subject: [PATCH 14/14] Add CHANGELOG entry [ci skip] --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index fcfc2d7aa30..1e4411064d1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -37,6 +37,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