Commit 87604ed6 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge branch 'master' into artifacts-from-ref-and-build-name-api

* master: (23 commits)
  Add CHANGELOG entry [ci skip]
  CHANGELOG item
  Added redirect_to_referer to login link on issues
  Simplify entities for branches and tags API
  Added Rake task for tracking deployments
  Return the number of marked todos
  Use local_assigns
  Use `humanize`
  Improve code design
  Remove unused create_pipeline_service_spec.rb
  Add notice implementation
  Make manual actions to work with master code
  Use `capitalize` instead of `titleize` for manual actions
  Mark builds with manual actions as skipped
  Fix rubocop offenses
  Update build fixtures to include manual actions
  Improve manual actions code and add model, service and feature tests
  Rename playable_actions to manual_actions
  Add implementation of manual actions
  Position commit icons closer to branch name/tag/sha; add min-width to pipeline actions cell
  ...
parents 08b9532e e57c0c89
...@@ -36,12 +36,15 @@ v 8.10.0 (unreleased) ...@@ -36,12 +36,15 @@ v 8.10.0 (unreleased)
- Support U2F devices in Firefox. !5177 - Support U2F devices in Firefox. !5177
- Fix issue, preventing users w/o push access to sort tags !5105 (redetection) - Fix issue, preventing users w/o push access to sort tags !5105 (redetection)
- Add Spring EmojiOne updates. - Add Spring EmojiOne updates.
- Added Rake task for tracking deployments !5320
- Fix fetching LFS objects for private CI projects - Fix fetching LFS objects for private CI projects
- Add the new 2016 Emoji! Adds 72 new emoji including bacon, facepalm, and selfie. !5237 - Add the new 2016 Emoji! Adds 72 new emoji including bacon, facepalm, and selfie. !5237
- Add syntax for multiline blockquote using `>>>` fence !3954 - Add syntax for multiline blockquote using `>>>` fence !3954
- Fix viewing notification settings when a project is pending deletion - Fix viewing notification settings when a project is pending deletion
- Updated compare dropdown menus to use GL dropdown - Updated compare dropdown menus to use GL dropdown
- Redirects back to issue after clicking login link
- Eager load award emoji on notes - Eager load award emoji on notes
- Allow to define manual actions/builds on Pipelines and Environments
- Fix pagination when sorting by columns with lots of ties (like priority) - Fix pagination when sorting by columns with lots of ties (like priority)
- The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020 - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020
- Updated project header design - Updated project header design
......
.pipelines { .pipelines {
.stage { .stage {
max-width: 80px; max-width: 90px;
width: 80px; width: 90px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
...@@ -30,13 +30,17 @@ ...@@ -30,13 +30,17 @@
} }
.table.builds { .table.builds {
min-width: 1100px; min-width: 1200px;
tr { tr {
th { th {
padding: 16px; padding: 16px 8px;
border: none; border: none;
} }
td {
padding: 10px 8px;
}
} }
tbody { tbody {
...@@ -53,9 +57,8 @@ ...@@ -53,9 +57,8 @@
.branch-commit { .branch-commit {
.branch-name { .branch-name {
margin-left: 8px;
font-weight: bold; font-weight: bold;
max-width: 180px; max-width: 150px;
overflow: hidden; overflow: hidden;
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
...@@ -64,10 +67,15 @@ ...@@ -64,10 +67,15 @@
} }
svg { svg {
margin: 0 6px;
height: 14px; height: 14px;
width: auto; width: auto;
vertical-align: middle; vertical-align: middle;
fill: $table-text-gray;
}
.fa {
font-size: 12px;
color: $table-text-gray;
} }
.commit-id { .commit-id {
...@@ -100,6 +108,22 @@ ...@@ -100,6 +108,22 @@
} }
} }
.icon-container {
display: inline-block;
text-align: right;
width: 20px;
.fa {
position: relative;
right: 3px;
}
svg {
position: relative;
right: 1px;
}
}
.duration, .duration,
.finished-at { .finished-at {
color: $table-text-gray; color: $table-text-gray;
...@@ -107,21 +131,19 @@ ...@@ -107,21 +131,19 @@
.fa { .fa {
font-size: 12px; font-size: 12px;
margin-right: 4px;
} }
svg { svg {
height: 12px; width: 12px;
width: auto; height: auto;
vertical-align: middle; vertical-align: middle;
} margin-right: 4px;
.fa,
svg {
margin-right: 5px;
} }
} }
.pipeline-actions { .pipeline-actions {
min-width: 140px;
.btn { .btn {
margin: 0; margin: 0;
......
class Projects::BuildsController < Projects::ApplicationController class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all] before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry] before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play]
before_action :authorize_update_build!, except: [:index, :show, :status, :raw] before_action :authorize_update_build!, except: [:index, :show, :status, :raw]
layout 'project' layout 'project'
...@@ -49,14 +49,19 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -49,14 +49,19 @@ class Projects::BuildsController < Projects::ApplicationController
end end
def retry def retry
unless @build.retryable? return render_404 unless @build.retryable?
return render_404
end
build = Ci::Build.retry(@build, current_user) build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build) redirect_to build_path(build)
end end
def play
return render_404 unless @build.playable?
build = @build.play(current_user)
redirect_to build_path(build)
end
def cancel def cancel
@build.cancel @build.cancel
redirect_to build_path(@build) redirect_to build_path(@build)
......
...@@ -18,6 +18,7 @@ module Ci ...@@ -18,6 +18,7 @@ module Ci
scope :latest_successful_with_artifacts, ->() do scope :latest_successful_with_artifacts, ->() do
with_artifacts.success.order(id: :desc) with_artifacts.success.order(id: :desc)
end end
scope :manual_actions, ->() { where(when: :manual) }
mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader
...@@ -94,6 +95,29 @@ module Ci ...@@ -94,6 +95,29 @@ module Ci
end end
end end
def manual?
self.when == 'manual'
end
def other_actions
pipeline.manual_actions.where.not(id: self)
end
def playable?
project.builds_enabled? && commands.present? && manual?
end
def play(current_user = nil)
# Try to queue a current build
if self.queue
self.update(user: current_user)
self
else
# Otherwise we need to create a duplicate
Ci::Build.retry(self, current_user)
end
end
def retryable? def retryable?
project.builds_enabled? && commands.present? && complete? project.builds_enabled? && commands.present? && complete?
end end
......
...@@ -77,6 +77,10 @@ module Ci ...@@ -77,6 +77,10 @@ module Ci
!tag? !tag?
end end
def manual_actions
builds.latest.manual_actions
end
def retryable? def retryable?
builds.latest.any? do |build| builds.latest.any? do |build|
build.failed? && build.retryable? build.failed? && build.retryable?
......
...@@ -22,6 +22,10 @@ class CommitStatus < ActiveRecord::Base ...@@ -22,6 +22,10 @@ class CommitStatus < ActiveRecord::Base
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
state_machine :status, initial: :pending do state_machine :status, initial: :pending do
event :queue do
transition skipped: :pending
end
event :run do event :run do
transition pending: :running transition pending: :running
end end
......
...@@ -16,10 +16,10 @@ module Statuseable ...@@ -16,10 +16,10 @@ module Statuseable
deduce_status = "(CASE deduce_status = "(CASE
WHEN (#{builds})=0 THEN NULL WHEN (#{builds})=0 THEN NULL
WHEN (#{builds})=(#{success})+(#{ignored}) THEN 'success'
WHEN (#{builds})=(#{pending}) THEN 'pending'
WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored}) THEN 'canceled'
WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{pending})+(#{skipped}) THEN 'pending'
WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
WHEN (#{running})+(#{pending})>0 THEN 'running' WHEN (#{running})+(#{pending})>0 THEN 'running'
ELSE 'failed' ELSE 'failed'
END)" END)"
......
...@@ -32,4 +32,8 @@ class Deployment < ActiveRecord::Base ...@@ -32,4 +32,8 @@ class Deployment < ActiveRecord::Base
def keep_around_commit def keep_around_commit
project.repository.keep_around(self.sha) project.repository.keep_around(self.sha)
end end
def manual_actions
deployable.try(:other_actions)
end
end end
...@@ -15,7 +15,7 @@ module Ci ...@@ -15,7 +15,7 @@ module Ci
status == 'success' status == 'success'
when 'on_failure' when 'on_failure'
status == 'failed' status == 'failed'
when 'always' when 'always', 'manual'
%w(success failed).include?(status) %w(success failed).include?(status)
end end
end end
...@@ -47,6 +47,10 @@ module Ci ...@@ -47,6 +47,10 @@ module Ci
user: user, user: user,
project: @pipeline.project) project: @pipeline.project)
# TODO: The proper implementation for this is in
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5295
build_attrs[:status] = 'skipped' if build_attrs[:when] == 'manual'
## ##
# We do not persist new builds here. # We do not persist new builds here.
# Those will be persisted when @pipeline is saved. # Those will be persisted when @pipeline is saved.
......
...@@ -39,6 +39,8 @@ ...@@ -39,6 +39,8 @@
%span.label.label-danger allowed to fail %span.label.label-danger allowed to fail
- if defined?(retried) && retried - if defined?(retried) && retried
%span.label.label-warning retried %span.label.label-warning retried
- if build.manual?
%span.label.label-info manual
- if defined?(runner) && runner - if defined?(runner) && runner
...@@ -79,6 +81,11 @@ ...@@ -79,6 +81,11 @@
- if build.active? - if build.active?
= link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
= icon('remove', class: 'cred') = icon('remove', class: 'cred')
- elsif defined?(allow_retry) && allow_retry && build.retryable? - elsif defined?(allow_retry) && allow_retry
- 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 = 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') = icon('repeat')
- elsif build.playable?
= link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
= icon('play')
...@@ -10,12 +10,13 @@ ...@@ -10,12 +10,13 @@
= link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do
%span ##{pipeline.id} %span ##{pipeline.id}
- if pipeline.ref - if pipeline.ref
.icon-container
= pipeline.tag? ? icon('tag') : icon('code-fork')
= link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name" = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name"
.icon-container
= custom_icon("icon_commit") = custom_icon("icon_commit")
= link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace" = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace"
- if pipeline.tag? - if pipeline.latest?
%span.label.label-primary tag
- elsif pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
- if pipeline.triggered? - if pipeline.triggered?
%span.label.label-primary triggered %span.label.label-primary triggered
...@@ -57,8 +58,21 @@ ...@@ -57,8 +58,21 @@
%td.pipeline-actions %td.pipeline-actions
.controls.hidden-xs.pull-right .controls.hidden-xs.pull-right
- artifacts = pipeline.builds.latest.select { |b| b.artifacts? } - artifacts = pipeline.builds.latest.select { |b| b.artifacts? }
- actions = pipeline.manual_actions
- if artifacts.present? || actions.any?
.btn-group.inline
- if actions.any?
.btn-group
%a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= icon("play")
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
= link_to play_namespace_project_build_path(@project.namespace, @project, build), method: :post, rel: 'nofollow' do
= icon("play")
%span= build.name.humanize
- if artifacts.present? - if artifacts.present?
.inline
.btn-group .btn-group
%a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
= icon("download") = icon("download")
......
- if can?(current_user, :create_deployment, deployment) && deployment.deployable
.pull-right
- actions = deployment.manual_actions
- if actions.present?
.btn-group.inline
.btn-group
%a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= icon("play")
%b.caret
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action|
%li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= icon("play")
%span= action.name.humanize
- if local_assigns.fetch(:allow_rollback, false)
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
- if deployment.last?
Retry
- else
Rollback
...@@ -7,17 +7,11 @@ ...@@ -7,17 +7,11 @@
%td %td
- if deployment.deployable - if deployment.deployable
= link_to namespace_project_build_path(@project.namespace, @project, deployment.deployable) do = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do
= "#{deployment.deployable.name} (##{deployment.deployable.id})" = "#{deployment.deployable.name} (##{deployment.deployable.id})"
%td %td
#{time_ago_with_tooltip(deployment.created_at)} #{time_ago_with_tooltip(deployment.created_at)}
%td %td
- if can?(current_user, :create_deployment, deployment) && deployment.deployable = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true
.pull-right
= link_to retry_namespace_project_build_path(@project.namespace, @project, deployment.deployable), method: :post, class: 'btn btn-build' do
- if deployment.last?
Retry
- else
Rollback
...@@ -15,3 +15,6 @@ ...@@ -15,3 +15,6 @@
%td %td
- if last_deployment - if last_deployment
#{time_ago_with_tooltip(last_deployment.created_at)} #{time_ago_with_tooltip(last_deployment.created_at)}
%td
= render 'projects/deployments/actions', deployment: last_deployment
...@@ -28,4 +28,5 @@ ...@@ -28,4 +28,5 @@
%th Environment %th Environment
%th Last deployment %th Last deployment
%th Date %th Date
%th
= render @environments = render @environments
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
.col-md-9 .col-md-9
%h3.page-title= @environment.name.titleize %h3.page-title= @environment.name.capitalize
.col-md-3 .col-md-3
.nav-controls .nav-controls
......
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
.disabled-comment.text-center .disabled-comment.text-center
.disabled-comment-text.inline .disabled-comment-text.inline
Please Please
= link_to "register",new_user_session_path = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
or or
= link_to "login",new_user_session_path = link_to "login", new_session_path(:user, redirect_to_referer: 'yes')
to post a comment to post a comment
:javascript :javascript
......
# Description: https://coderwall.com/p/heed_q/rails-routing-and-namespaced-models
#
# This allows us to use CI ActiveRecord objects in all routes and use it:
# - [project.namespace, project, build]
#
# instead of:
# - namespace_project_build_path(project.namespace, project, build)
#
# Without that, Ci:: namespace is used for resolving routes:
# - namespace_project_ci_build_path(project.namespace, project, build)
module Ci
def self.use_relative_model_naming?
true
end
end
...@@ -750,6 +750,7 @@ Rails.application.routes.draw do ...@@ -750,6 +750,7 @@ Rails.application.routes.draw do
get :status get :status
post :cancel post :cancel
post :retry post :retry
post :play
post :erase post :erase
get :trace get :trace
get :raw get :raw
......
class Gitlab::Seeder::Builds class Gitlab::Seeder::Builds
STAGES = %w[build notify_build test notify_test deploy notify_deploy]
def initialize(project) def initialize(project)
@project = project @project = project
end end
def seed! def seed!
ci_commits.each do |ci_commit| pipelines.each do |pipeline|
begin begin
build_create!(ci_commit, name: 'test build 1') build_create!(pipeline, name: 'build:linux', stage: 'build')
build_create!(ci_commit, status: 'success', name: 'test build 2') build_create!(pipeline, name: 'build:osx', stage: 'build')
build_create!(pipeline, name: 'slack post build', stage: 'notify_build')
build_create!(pipeline, name: 'rspec:linux', stage: 'test')
build_create!(pipeline, name: 'rspec:windows', stage: 'test')
build_create!(pipeline, name: 'rspec:windows', stage: 'test')
build_create!(pipeline, name: 'rspec:osx', stage: 'test')
build_create!(pipeline, name: 'spinach:linux', stage: 'test')
build_create!(pipeline, name: 'spinach:osx', stage: 'test')
build_create!(pipeline, name: 'cucumber:linux', stage: 'test')
build_create!(pipeline, name: 'cucumber:osx', stage: 'test')
build_create!(pipeline, name: 'slack post test', stage: 'notify_test')
build_create!(pipeline, name: 'staging', stage: 'deploy', environment: 'staging')
build_create!(pipeline, name: 'production', stage: 'deploy', environment: 'production', when: 'manual')
commit_status_create!(pipeline, name: 'jenkins')
print '.' print '.'
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
print 'F' print 'F'
...@@ -15,8 +36,8 @@ class Gitlab::Seeder::Builds ...@@ -15,8 +36,8 @@ class Gitlab::Seeder::Builds
end end
end end
def ci_commits def pipelines
commits = @project.repository.commits('master', nil, 5) commits = @project.repository.commits('master', limit: 5)
commits_sha = commits.map { |commit| commit.raw.id } commits_sha = commits.map { |commit| commit.raw.id }
commits_sha.map do |sha| commits_sha.map do |sha|
@project.ensure_pipeline(sha, 'master') @project.ensure_pipeline(sha, 'master')
...@@ -25,11 +46,11 @@ class Gitlab::Seeder::Builds ...@@ -25,11 +46,11 @@ class Gitlab::Seeder::Builds
[] []
end end
def build_create!(ci_commit, opts = {}) def build_create!(pipeline, opts = {})
attributes = build_attributes_for(ci_commit).merge(opts) attributes = build_attributes_for(pipeline, opts)
build = Ci::Build.new(attributes) build = Ci::Build.new(attributes)
if %w(success failed).include?(build.status) if opts[:name].start_with?('build')
artifacts_cache_file(artifacts_archive_path) do |file| artifacts_cache_file(artifacts_archive_path) do |file|
build.artifacts_file = file build.artifacts_file = file
end end
...@@ -40,6 +61,7 @@ class Gitlab::Seeder::Builds ...@@ -40,6 +61,7 @@ class Gitlab::Seeder::Builds
end end
build.save! build.save!
build.update(status: build_status)
if %w(running success failed).include?(build.status) if %w(running success failed).include?(build.status)
# We need to set build trace after saving a build (id required) # We need to set build trace after saving a build (id required)
...@@ -47,12 +69,20 @@ class Gitlab::Seeder::Builds ...@@ -47,12 +69,20 @@ class Gitlab::Seeder::Builds
end end
end end
def build_attributes_for(ci_commit) def commit_status_create!(pipeline, opts = {})
{ name: 'test build', commands: "$ build command", attributes = commit_status_attributes_for(pipeline, opts)
stage: 'test', stage_idx: 1, ref: 'master', GenericCommitStatus.create(attributes)
user_id: build_user, gl_project_id: @project.id, end
status: build_status, commit_id: ci_commit.id,
created_at: Time.now, updated_at: Time.now } 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(pipeline, opts)
commit_status_attributes_for(pipeline, opts).merge(commands: '$ build command')
end end
def build_user def build_user
...@@ -63,13 +93,16 @@ class Gitlab::Seeder::Builds ...@@ -63,13 +93,16 @@ class Gitlab::Seeder::Builds
Ci::Build::AVAILABLE_STATUSES.sample Ci::Build::AVAILABLE_STATUSES.sample
end end
def stage_index(stage)
STAGES.index(stage) || 0
end
def artifacts_archive_path def artifacts_archive_path
Rails.root + 'spec/fixtures/ci_build_artifacts.zip' Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
end end
def artifacts_metadata_path def artifacts_metadata_path
Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
end end
def artifacts_cache_file(file_path) def artifacts_cache_file(file_path)
......
...@@ -277,8 +277,7 @@ Example Response: ...@@ -277,8 +277,7 @@ Example Response:
## Mark all todos as done ## Mark all todos as done
Marks all pending todos for the current user as done. All todos marked as done Marks all pending todos for the current user as done. It returns the number of marked todos.
are returned in the response.
``` ```
DELETE /todos DELETE /todos
...@@ -291,154 +290,7 @@ curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.c ...@@ -291,154 +290,7 @@ curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.c
Example Response: Example Response:
```json ```json
[ 3
{
"id": 102,
"project": {
"id": 2,
"name": "Gitlab Ce",
"name_with_namespace": "Gitlab Org / Gitlab Ce",
"path": "gitlab-ce",
"path_with_namespace": "gitlab-org/gitlab-ce"
},
"author": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root"
},
"action_name": "marked",
"target_type": "MergeRequest",
"target": {
"id": 34,
"iid": 7,
"project_id": 2,
"title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
"description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
"state": "opened",
"created_at": "2016-06-17T07:49:24.419Z",
"updated_at": "2016-06-17T07:52:43.484Z",
"target_branch": "tutorials_git_tricks",
"source_branch": "DNSBL_docs",
"upvotes": 0,
"downvotes": 0,
"author": {
"name": "Maxie Medhurst",
"username": "craig_rutherford",
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford"
},
"assignee": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root"
},
"source_project_id": 2,
"target_project_id": 2,
"labels": [],
"work_in_progress": false,
"milestone": {
"id": 32,
"iid": 2,
"project_id": 2,
"title": "v1.0",
"description": "Assumenda placeat ea voluptatem voluptate qui.",
"state": "active",
"created_at": "2016-06-17T07:47:34.163Z",
"updated_at": "2016-06-17T07:47:34.163Z",
"due_date": null
},
"merge_when_build_succeeds": false,
"merge_status": "cannot_be_merged",
"subscribed": true,
"user_notes_count": 7
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
"body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
"state": "done",
"created_at": "2016-06-17T07:52:35.225Z"
},
{
"id": 98,
"project": {
"id": 2,
"name": "Gitlab Ce",
"name_with_namespace": "Gitlab Org / Gitlab Ce",
"path": "gitlab-ce",
"path_with_namespace": "gitlab-org/gitlab-ce"
},
"author": {
"name": "Maxie Medhurst",
"username": "craig_rutherford",
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford"
},
"action_name": "assigned",
"target_type": "MergeRequest",
"target": {
"id": 34,
"iid": 7,
"project_id": 2,
"title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
"description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
"state": "opened",
"created_at": "2016-06-17T07:49:24.419Z",
"updated_at": "2016-06-17T07:52:43.484Z",
"target_branch": "tutorials_git_tricks",
"source_branch": "DNSBL_docs",
"upvotes": 0,
"downvotes": 0,
"author": {
"name": "Maxie Medhurst",
"username": "craig_rutherford",
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/craig_rutherford"
},
"assignee": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/root"
},
"source_project_id": 2,
"target_project_id": 2,
"labels": [],
"work_in_progress": false,
"milestone": {
"id": 32,
"iid": 2,
"project_id": 2,
"title": "v1.0",
"description": "Assumenda placeat ea voluptatem voluptate qui.",
"state": "active",
"created_at": "2016-06-17T07:47:34.163Z",
"updated_at": "2016-06-17T07:47:34.163Z",
"due_date": null
},
"merge_when_build_succeeds": false,
"merge_status": "cannot_be_merged",
"subscribed": true,
"user_notes_count": 7
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
"body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
"state": "done",
"created_at": "2016-06-17T07:49:24.624Z"
},
]
``` ```
[ce-3188]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3188 [ce-3188]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3188
...@@ -485,6 +485,7 @@ failure. ...@@ -485,6 +485,7 @@ failure.
1. `on_failure` - execute build only when at least one build from prior stages 1. `on_failure` - execute build only when at least one build from prior stages
fails. fails.
1. `always` - execute build regardless of the status of builds from prior stages. 1. `always` - execute build regardless of the status of builds from prior stages.
1. `manual` - execute build manually.
For example: For example:
...@@ -516,6 +517,7 @@ deploy_job: ...@@ -516,6 +517,7 @@ deploy_job:
stage: deploy stage: deploy
script: script:
- make deploy - make deploy
when: manual
cleanup_job: cleanup_job:
stage: cleanup stage: cleanup
...@@ -527,7 +529,20 @@ cleanup_job: ...@@ -527,7 +529,20 @@ cleanup_job:
The above script will: The above script will:
1. Execute `cleanup_build_job` only when `build_job` fails 1. Execute `cleanup_build_job` only when `build_job` fails
2. Always execute `cleanup_job` as the last step in pipeline. 2. Always execute `cleanup_job` as the last step in pipeline
3. Allow you to manually execute `deploy_job` from GitLab
#### Manual actions
>**Note:**
Introduced in GitLab 8.10.
Manual actions are special type of jobs that are not executed automatically in pipeline.
They need to be explicitly started by the user.
Manual actions can be started from pipelines, builds, environments and deployments views.
You can execute the same manual action multiple times.
Example usage of manual actions is deployment, ex. promote a staging environment to production.
### environment ### environment
......
...@@ -167,3 +167,22 @@ of those assets. Unless you are modifying the JavaScript / CSS code on your ...@@ -167,3 +167,22 @@ of those assets. Unless you are modifying the JavaScript / CSS code on your
production machine after installing the package, there should be no reason to redo production machine after installing the package, there should be no reason to redo
rake assets:precompile on the production machine. If you suspect that assets rake assets:precompile on the production machine. If you suspect that assets
have been corrupted, you should reinstall the omnibus package. have been corrupted, you should reinstall the omnibus package.
## Tracking Deployments
GitLab provides a Rake task that lets you track deployments in GitLab
Performance Monitoring. This Rake task simply stores the current GitLab version
in the GitLab Performance Monitoring database.
For Omnibus-packages:
```
sudo gitlab-rake gitlab:track_deployment
```
For installations from source:
```
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production
```
...@@ -15,7 +15,8 @@ module API ...@@ -15,7 +15,8 @@ module API
# GET /projects/:id/repository/branches # GET /projects/:id/repository/branches
get ":id/repository/branches" do get ":id/repository/branches" do
branches = user_project.repository.branches.sort_by(&:name) branches = user_project.repository.branches.sort_by(&:name)
present branches, with: Entities::RepoObject, project: user_project
present branches, with: Entities::RepoBranch, project: user_project
end end
# Get a single branch # Get a single branch
...@@ -28,7 +29,8 @@ module API ...@@ -28,7 +29,8 @@ module API
get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
@branch = user_project.repository.branches.find { |item| item.name == params[:branch] } @branch = user_project.repository.branches.find { |item| item.name == params[:branch] }
not_found!("Branch") unless @branch not_found!("Branch") unless @branch
present @branch, with: Entities::RepoObject, project: user_project
present @branch, with: Entities::RepoBranch, project: user_project
end end
# Protect a single branch # Protect a single branch
...@@ -60,7 +62,7 @@ module API ...@@ -60,7 +62,7 @@ module API
developers_can_merge: developers_can_merge || false) developers_can_merge: developers_can_merge || false)
end end
present @branch, with: Entities::RepoObject, project: user_project present @branch, with: Entities::RepoBranch, project: user_project
end end
# Unprotect a single branch # Unprotect a single branch
...@@ -79,7 +81,7 @@ module API ...@@ -79,7 +81,7 @@ module API
protected_branch = user_project.protected_branches.find_by(name: @branch.name) protected_branch = user_project.protected_branches.find_by(name: @branch.name)
protected_branch.destroy if protected_branch protected_branch.destroy if protected_branch
present @branch, with: Entities::RepoObject, project: user_project present @branch, with: Entities::RepoBranch, project: user_project
end end
# Create branch # Create branch
...@@ -97,7 +99,7 @@ module API ...@@ -97,7 +99,7 @@ module API
if result[:status] == :success if result[:status] == :success
present result[:branch], present result[:branch],
with: Entities::RepoObject, with: Entities::RepoBranch,
project: user_project project: user_project
else else
render_api_error!(result[:message], 400) render_api_error!(result[:message], 400)
......
...@@ -114,33 +114,23 @@ module API ...@@ -114,33 +114,23 @@ module API
end end
end end
class RepoObject < Grape::Entity class RepoBranch < Grape::Entity
expose :name expose :name
expose :commit do |repo_obj, options| expose :commit do |repo_branch, options|
if repo_obj.respond_to?(:commit) options[:project].repository.commit(repo_branch.target)
repo_obj.commit
elsif options[:project]
options[:project].repository.commit(repo_obj.target)
end
end end
expose :protected do |repo_obj, options| expose :protected do |repo_branch, options|
if options[:project] options[:project].protected_branch? repo_branch.name
options[:project].protected_branch? repo_obj.name
end
end end
expose :developers_can_push do |repo_obj, options| expose :developers_can_push do |repo_branch, options|
if options[:project] options[:project].developers_can_push_to_protected_branch? repo_branch.name
options[:project].developers_can_push_to_protected_branch? repo_obj.name
end
end end
expose :developers_can_merge do |repo_obj, options| expose :developers_can_merge do |repo_branch, options|
if options[:project] options[:project].developers_can_merge_to_protected_branch? repo_branch.name
options[:project].developers_can_merge_to_protected_branch? repo_obj.name
end
end end
end end
...@@ -437,27 +427,14 @@ module API ...@@ -437,27 +427,14 @@ module API
end end
class RepoTag < Grape::Entity class RepoTag < Grape::Entity
expose :name expose :name, :message
expose :message do |repo_obj, _options|
if repo_obj.respond_to?(:message)
repo_obj.message
else
nil
end
end
expose :commit do |repo_obj, options| expose :commit do |repo_tag, options|
if repo_obj.respond_to?(:commit) options[:project].repository.commit(repo_tag.target)
repo_obj.commit
elsif options[:project]
options[:project].repository.commit(repo_obj.target)
end
end end
expose :release, using: Entities::Release do |repo_obj, options| expose :release, using: Entities::Release do |repo_tag, options|
if options[:project] options[:project].releases.find_by(tag: repo_tag.name)
options[:project].releases.find_by(tag: repo_obj.name)
end
end end
end end
......
...@@ -75,7 +75,7 @@ module API ...@@ -75,7 +75,7 @@ module API
todos = find_todos todos = find_todos
todos.each(&:done) todos.each(&:done)
present paginate(Kaminari.paginate_array(todos)), with: Entities::Todo, current_user: current_user todos.length
end end
end end
end end
......
...@@ -194,8 +194,8 @@ module Ci ...@@ -194,8 +194,8 @@ module Ci
raise ValidationError, "#{name} job: allow_failure parameter should be an boolean" raise ValidationError, "#{name} job: allow_failure parameter should be an boolean"
end end
if job[:when] && !job[:when].in?(%w[on_success on_failure always]) if job[:when] && !job[:when].in?(%w[on_success on_failure always manual])
raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always" raise ValidationError, "#{name} job: when parameter should be on_success, on_failure, always or manual"
end end
if job[:environment] && !validate_environment(job[:environment]) if job[:environment] && !validate_environment(job[:environment])
......
namespace :gitlab do
desc 'GitLab | Tracks a deployment in GitLab Performance Monitoring'
task track_deployment: :environment do
metric = Gitlab::Metrics::Metric.
new('deployments', version: Gitlab::VERSION)
Gitlab::Metrics.submit_metrics([metric.to_hash])
end
end
...@@ -43,6 +43,11 @@ FactoryGirl.define do ...@@ -43,6 +43,11 @@ FactoryGirl.define do
status 'pending' status 'pending'
end end
trait :manual do
status 'skipped'
self.when 'manual'
end
trait :allowed_to_fail do trait :allowed_to_fail do
allow_failure true allow_failure true
end end
......
...@@ -13,6 +13,7 @@ feature 'Environments', feature: true do ...@@ -13,6 +13,7 @@ feature 'Environments', feature: true do
describe 'when showing environments' do describe 'when showing environments' do
given!(:environment) { } given!(:environment) { }
given!(:deployment) { } given!(:deployment) { }
given!(:manual) { }
before do before do
visit namespace_project_environments_path(project.namespace, project) visit namespace_project_environments_path(project.namespace, project)
...@@ -43,6 +44,24 @@ feature 'Environments', feature: true do ...@@ -43,6 +44,24 @@ feature 'Environments', feature: true do
scenario 'does show deployment SHA' do scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha) expect(page).to have_link(deployment.short_sha)
end end
context 'with build and manual actions' do
given(:pipeline) { create(:ci_pipeline, project: project) }
given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
scenario 'does show a play button' do
expect(page).to have_link(manual.name.humanize)
end
scenario 'does allow to play manual action' do
expect(manual).to be_skipped
expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
end
end end
end end
...@@ -54,6 +73,7 @@ feature 'Environments', feature: true do ...@@ -54,6 +73,7 @@ feature 'Environments', feature: true do
describe 'when showing the environment' do describe 'when showing the environment' do
given(:environment) { create(:environment, project: project) } given(:environment) { create(:environment, project: project) }
given!(:deployment) { } given!(:deployment) { }
given!(:manual) { }
before do before do
visit namespace_project_environment_path(project.namespace, project, environment) visit namespace_project_environment_path(project.namespace, project, environment)
...@@ -77,7 +97,8 @@ feature 'Environments', feature: true do ...@@ -77,7 +97,8 @@ feature 'Environments', feature: true do
end end
context 'with build' do context 'with build' do
given(:build) { create(:ci_build, project: project) } given(:pipeline) { create(:ci_pipeline, project: project) }
given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) } given(:deployment) { create(:deployment, environment: environment, deployable: build) }
scenario 'does show build name' do scenario 'does show build name' do
...@@ -87,6 +108,21 @@ feature 'Environments', feature: true do ...@@ -87,6 +108,21 @@ feature 'Environments', feature: true do
scenario 'does show retry button' do scenario 'does show retry button' do
expect(page).to have_link('Retry') expect(page).to have_link('Retry')
end end
context 'with manual action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
scenario 'does show a play button' do
expect(page).to have_link(manual.name.humanize)
end
scenario 'does allow to play manual action' do
expect(manual).to be_skipped
expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
end
end end
end end
end end
......
...@@ -62,6 +62,20 @@ describe "Pipelines" do ...@@ -62,6 +62,20 @@ describe "Pipelines" do
end end
end end
context 'with manual actions' do
let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') }
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_link('Manual build') }
context 'when playing' do
before { click_link('Manual build') }
it { expect(manual.reload).to be_pending }
end
end
context 'for generic statuses' do context 'for generic statuses' do
context 'when running' do context 'when running' do
let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') }
...@@ -117,6 +131,7 @@ describe "Pipelines" do ...@@ -117,6 +131,7 @@ describe "Pipelines" do
@success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
@failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
@running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
@manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build')
@external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
end end
...@@ -131,6 +146,7 @@ describe "Pipelines" do ...@@ -131,6 +146,7 @@ describe "Pipelines" do
expect(page).to have_content(@external.id) expect(page).to have_content(@external.id)
expect(page).to have_content('Retry failed') expect(page).to have_content('Retry failed')
expect(page).to have_content('Cancel running') expect(page).to have_content('Cancel running')
expect(page).to have_link('Play')
end end
context 'retrying builds' do context 'retrying builds' do
...@@ -154,6 +170,12 @@ describe "Pipelines" do ...@@ -154,6 +170,12 @@ describe "Pipelines" do
it { expect(page).to have_selector('.ci-canceled') } it { expect(page).to have_selector('.ci-canceled') }
end end
end end
context 'playing manual build' do
before { click_link('Play') }
it { expect(@manual.reload).to be_pending }
end
end end
describe 'POST /:project/pipelines' do describe 'POST /:project/pipelines' do
......
...@@ -1141,7 +1141,7 @@ EOT ...@@ -1141,7 +1141,7 @@ EOT
config = YAML.dump({ rspec: { script: "test", when: 1 } }) config = YAML.dump({ rspec: { script: "test", when: 1 } })
expect do expect do
GitlabCiYamlProcessor.new(config, path) GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure, always or manual")
end end
it "returns errors if job artifacts:name is not an a string" do it "returns errors if job artifacts:name is not an a string" do
......
...@@ -713,4 +713,55 @@ describe Ci::Build, models: true do ...@@ -713,4 +713,55 @@ describe Ci::Build, models: true do
end end
end end
end end
describe '#manual?' do
before do
build.update(when: value)
end
subject { build.manual? }
context 'when is set to manual' do
let(:value) { 'manual' }
it { is_expected.to be_truthy }
end
context 'when set to something else' do
let(:value) { 'something else' }
it { is_expected.to be_falsey }
end
end
describe '#other_actions' do
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') }
subject { build.other_actions }
it 'returns other actions' do
is_expected.to contain_exactly(other_build)
end
end
describe '#play' do
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
subject { build.play }
it 'enques a build' do
is_expected.to be_pending
is_expected.to eq(build)
end
context 'for success build' do
before { build.queue }
it 'creates a new build' do
is_expected.to be_pending
is_expected.not_to eq(build)
end
end
end
end end
...@@ -260,6 +260,68 @@ describe Ci::Pipeline, models: true do ...@@ -260,6 +260,68 @@ describe Ci::Pipeline, models: true do
expect(pipeline.reload.status).to eq('canceled') expect(pipeline.reload.status).to eq('canceled')
end end
end end
context 'when listing manual actions' do
let(:yaml) do
{
stages: ["build", "test", "test_failure", "deploy", "cleanup"],
build: {
stage: "build",
script: "BUILD",
},
test: {
stage: "test",
script: "TEST",
},
test_failure: {
stage: "test_failure",
script: "ON test failure",
when: "on_failure",
},
deploy: {
stage: "deploy",
script: "PUBLISH",
},
production: {
stage: "deploy",
script: "PUBLISH",
when: "manual",
},
cleanup: {
stage: "cleanup",
script: "TIDY UP",
when: "always",
},
clear_cache: {
stage: "cleanup",
script: "CLEAR CACHE",
when: "manual",
}
}
end
it 'returns only for skipped builds' do
# currently all builds are created
expect(create_builds).to be_truthy
expect(manual_actions).to be_empty
# succeed stage build
pipeline.builds.running_or_pending.each(&:success)
expect(manual_actions).to be_empty
# succeed stage test
pipeline.builds.running_or_pending.each(&:success)
expect(manual_actions).to be_one # production
# succeed stage deploy
pipeline.builds.running_or_pending.each(&:success)
expect(manual_actions).to be_many # production and clear cache
end
def manual_actions
pipeline.manual_actions
end
end
end end
context 'when no builds created' do context 'when no builds created' do
...@@ -416,4 +478,28 @@ describe Ci::Pipeline, models: true do ...@@ -416,4 +478,28 @@ describe Ci::Pipeline, models: true do
end end
end end
end end
describe '#manual_actions' do
subject { pipeline.manual_actions }
it 'when none defined' do
is_expected.to be_empty
end
context 'when action defined' do
let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
it 'returns one action' do
is_expected.to contain_exactly(manual)
end
context 'there are multiple of the same name' do
let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
it 'returns latest one' do
is_expected.to contain_exactly(manual2)
end
end
end
end
end end
...@@ -11,6 +11,7 @@ describe Deployment, models: true do ...@@ -11,6 +11,7 @@ describe Deployment, models: true do
it { is_expected.to delegate_method(:name).to(:environment).with_prefix } it { is_expected.to delegate_method(:name).to(:environment).with_prefix }
it { is_expected.to delegate_method(:commit).to(:project) } it { is_expected.to delegate_method(:commit).to(:project) }
it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) } it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) }
it { is_expected.to delegate_method(:manual_actions).to(:deployable).as(:try) }
it { is_expected.to validate_presence_of(:ref) } it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) } it { is_expected.to validate_presence_of(:sha) }
......
...@@ -134,8 +134,7 @@ describe API::Todos, api: true do ...@@ -134,8 +134,7 @@ describe API::Todos, api: true do
delete api('/todos', john_doe) delete api('/todos', john_doe)
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(json_response).to be_an Array expect(response.body).to eq('3')
expect(json_response.length).to eq(3)
expect(pending_1.reload).to be_done expect(pending_1.reload).to be_done
expect(pending_2.reload).to be_done expect(pending_2.reload).to be_done
expect(pending_3.reload).to be_done expect(pending_3.reload).to be_done
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment