diff options
36 files changed, 502 insertions, 267 deletions
diff --git a/CHANGELOG b/CHANGELOG index 9f3a9cad83f..93a47b87d63 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,12 +36,15 @@ v 8.10.0 (unreleased) - Support U2F devices in Firefox. !5177 - Fix issue, preventing users w/o push access to sort tags !5105 (redetection) - Add Spring EmojiOne updates. + - Added Rake task for tracking deployments !5320 - Fix fetching LFS objects for private CI projects - Add the new 2016 Emoji! Adds 72 new emoji including bacon, facepalm, and selfie. !5237 - Add syntax for multiline blockquote using `>>>` fence !3954 - Fix viewing notification settings when a project is pending deletion - Updated compare dropdown menus to use GL dropdown + - Redirects back to issue after clicking login link - Eager load award emoji on notes + - Allow to define manual actions/builds on Pipelines and Environments - Fix pagination when sorting by columns with lots of ties (like priority) - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020 - Updated project header design diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index a0334207c68..a3b72ec9574 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,7 +1,7 @@ .pipelines { .stage { - max-width: 80px; - width: 80px; + max-width: 90px; + width: 90px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -30,13 +30,17 @@ } .table.builds { - min-width: 1100px; + min-width: 1200px; tr { th { - padding: 16px; + padding: 16px 8px; border: none; } + + td { + padding: 10px 8px; + } } tbody { @@ -53,9 +57,8 @@ .branch-commit { .branch-name { - margin-left: 8px; font-weight: bold; - max-width: 180px; + max-width: 150px; overflow: hidden; display: inline-block; white-space: nowrap; @@ -64,10 +67,15 @@ } svg { - margin: 0 6px; height: 14px; width: auto; vertical-align: middle; + fill: $table-text-gray; + } + + .fa { + font-size: 12px; + color: $table-text-gray; } .commit-id { @@ -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, .finished-at { color: $table-text-gray; @@ -107,21 +131,19 @@ .fa { font-size: 12px; + margin-right: 4px; } svg { - height: 12px; - width: auto; + width: 12px; + height: auto; vertical-align: middle; - } - - .fa, - svg { - margin-right: 5px; + margin-right: 4px; } } .pipeline-actions { + min-width: 140px; .btn { margin: 0; diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index d7513d75f01..553b62741a5 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,6 +1,6 @@ class Projects::BuildsController < Projects::ApplicationController before_action :build, except: [:index, :cancel_all] - before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry] + before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play] before_action :authorize_update_build!, except: [:index, :show, :status, :raw] layout 'project' @@ -49,14 +49,19 @@ class Projects::BuildsController < Projects::ApplicationController end def retry - unless @build.retryable? - return render_404 - end + return render_404 unless @build.retryable? build = Ci::Build.retry(@build, current_user) redirect_to build_path(build) end + def play + return render_404 unless @build.playable? + + build = @build.play(current_user) + redirect_to build_path(build) + end + def cancel @build.cancel redirect_to build_path(@build) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 65dfe4f0190..20492c54729 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -18,6 +18,7 @@ module Ci scope :latest_successful_with_artifacts, ->() do with_artifacts.success.order(id: :desc) end + scope :manual_actions, ->() { where(when: :manual) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader @@ -94,6 +95,29 @@ module Ci end end + def manual? + self.when == 'manual' + end + + def other_actions + pipeline.manual_actions.where.not(id: self) + end + + def playable? + project.builds_enabled? && commands.present? && manual? + end + + def play(current_user = nil) + # Try to queue a current build + if self.queue + self.update(user: current_user) + self + else + # Otherwise we need to create a duplicate + Ci::Build.retry(self, current_user) + end + end + def retryable? project.builds_enabled? && commands.present? && complete? end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index f5b4124d1ee..fab91bdae5a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -77,6 +77,10 @@ module Ci !tag? end + def manual_actions + builds.latest.manual_actions + end + def retryable? builds.latest.any? do |build| build.failed? && build.retryable? diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 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/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/models/deployment.rb b/app/models/deployment.rb index 520026c18dd..1a7cd60817e 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -32,4 +32,8 @@ class Deployment < ActiveRecord::Base def keep_around_commit project.repository.keep_around(self.sha) end + + def manual_actions + deployable.try(:other_actions) + end end diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 3b21f0acb96..4946f7076fd 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -15,7 +15,7 @@ module Ci status == 'success' when 'on_failure' status == 'failed' - when 'always' + when 'always', 'manual' %w(success failed).include?(status) end end @@ -47,6 +47,10 @@ module Ci user: user, project: @pipeline.project) + # TODO: The proper implementation for this is in + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5295 + build_attrs[:status] = 'skipped' if build_attrs[:when] == 'manual' + ## # We do not persist new builds here. # Those will be persisted when @pipeline is saved. diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index e1b42b2cfa5..9264289987d 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -39,6 +39,8 @@ %span.label.label-danger allowed to fail - if defined?(retried) && retried %span.label.label-warning retried + - if build.manual? + %span.label.label-info manual - if defined?(runner) && runner @@ -79,6 +81,11 @@ - if build.active? = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do = icon('remove', class: 'cred') - - elsif defined?(allow_retry) && allow_retry && build.retryable? - = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do - = icon('repeat') + - elsif defined?(allow_retry) && allow_retry + - if build.retryable? + = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do + = icon('repeat') + - elsif build.playable? + = link_to play_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do + = icon('play') + diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 0557d384e33..996c9073770 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -10,12 +10,13 @@ = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do %span ##{pipeline.id} - 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" - = custom_icon("icon_commit") + .icon-container + = custom_icon("icon_commit") = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace" - - if pipeline.tag? - %span.label.label-primary tag - - elsif pipeline.latest? + - if pipeline.latest? %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest - if pipeline.triggered? %span.label.label-primary triggered @@ -57,18 +58,31 @@ %td.pipeline-actions .controls.hidden-xs.pull-right - artifacts = pipeline.builds.latest.select { |b| b.artifacts? } - - if artifacts.present? - .inline - .btn-group - %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} - = icon("download") - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - - artifacts.each do |build| - %li - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do - = icon("download") - %span Download '#{build.name}' artifacts + - actions = pipeline.manual_actions + - if artifacts.present? || actions.any? + .btn-group.inline + - if actions.any? + .btn-group + %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + = icon("play") + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - actions.each do |build| + %li + = link_to play_namespace_project_build_path(@project.namespace, @project, build), method: :post, rel: 'nofollow' do + = icon("play") + %span= build.name.humanize + - if artifacts.present? + .btn-group + %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} + = icon("download") + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - artifacts.each do |build| + %li + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do + = icon("download") + %span Download '#{build.name}' artifacts - if can?(current_user, :update_pipeline, @project) .cancel-retry-btns.inline diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml new file mode 100644 index 00000000000..65d68aa2985 --- /dev/null +++ b/app/views/projects/deployments/_actions.haml @@ -0,0 +1,22 @@ +- if can?(current_user, :create_deployment, deployment) && deployment.deployable + .pull-right + - actions = deployment.manual_actions + - if actions.present? + .btn-group.inline + .btn-group + %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + = icon("play") + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - actions.each do |action| + %li + = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do + = icon("play") + %span= action.name.humanize + + - if local_assigns.fetch(:allow_rollback, false) + = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do + - if deployment.last? + Retry + - else + Rollback diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index d08dd92f1f6..baf02f1e6a0 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -7,17 +7,11 @@ %td - if deployment.deployable - = link_to namespace_project_build_path(@project.namespace, @project, deployment.deployable) do + = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do = "#{deployment.deployable.name} (##{deployment.deployable.id})" %td #{time_ago_with_tooltip(deployment.created_at)} %td - - if can?(current_user, :create_deployment, deployment) && deployment.deployable - .pull-right - = link_to retry_namespace_project_build_path(@project.namespace, @project, deployment.deployable), method: :post, class: 'btn btn-build' do - - if deployment.last? - Retry - - else - Rollback + = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index eafa246d05f..e2453395602 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -15,3 +15,6 @@ %td - if last_deployment #{time_ago_with_tooltip(last_deployment.created_at)} + + %td + = render 'projects/deployments/actions', deployment: last_deployment diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 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/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/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 56d302fab82..74538a9723e 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -14,9 +14,9 @@ .disabled-comment.text-center .disabled-comment-text.inline Please - = link_to "register",new_user_session_path + = link_to "register", new_session_path(:user, redirect_to_referer: 'yes') or - = link_to "login",new_user_session_path + = link_to "login", new_session_path(:user, redirect_to_referer: 'yes') to post a comment :javascript 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/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/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb index 51ff451eb4c..124704cb451 100644 --- a/db/fixtures/development/14_builds.rb +++ b/db/fixtures/development/14_builds.rb @@ -1,13 +1,34 @@ class Gitlab::Seeder::Builds + STAGES = %w[build notify_build test notify_test deploy notify_deploy] + def initialize(project) @project = project end def seed! - ci_commits.each do |ci_commit| + pipelines.each do |pipeline| begin - build_create!(ci_commit, name: 'test build 1') - build_create!(ci_commit, status: 'success', name: 'test build 2') + build_create!(pipeline, name: 'build:linux', stage: 'build') + build_create!(pipeline, name: 'build:osx', stage: 'build') + + build_create!(pipeline, name: 'slack post build', stage: 'notify_build') + + build_create!(pipeline, name: 'rspec:linux', stage: 'test') + build_create!(pipeline, name: 'rspec:windows', stage: 'test') + build_create!(pipeline, name: 'rspec:windows', stage: 'test') + build_create!(pipeline, name: 'rspec:osx', stage: 'test') + build_create!(pipeline, name: 'spinach:linux', stage: 'test') + build_create!(pipeline, name: 'spinach:osx', stage: 'test') + build_create!(pipeline, name: 'cucumber:linux', stage: 'test') + build_create!(pipeline, name: 'cucumber:osx', stage: 'test') + + build_create!(pipeline, name: 'slack post test', stage: 'notify_test') + + build_create!(pipeline, name: 'staging', stage: 'deploy', environment: 'staging') + build_create!(pipeline, name: 'production', stage: 'deploy', environment: 'production', when: 'manual') + + commit_status_create!(pipeline, name: 'jenkins') + print '.' rescue ActiveRecord::RecordInvalid print 'F' @@ -15,8 +36,8 @@ class Gitlab::Seeder::Builds end end - def ci_commits - commits = @project.repository.commits('master', nil, 5) + def pipelines + commits = @project.repository.commits('master', limit: 5) commits_sha = commits.map { |commit| commit.raw.id } commits_sha.map do |sha| @project.ensure_pipeline(sha, 'master') @@ -25,11 +46,11 @@ class Gitlab::Seeder::Builds [] end - def build_create!(ci_commit, opts = {}) - attributes = build_attributes_for(ci_commit).merge(opts) + def build_create!(pipeline, opts = {}) + attributes = build_attributes_for(pipeline, opts) build = Ci::Build.new(attributes) - if %w(success failed).include?(build.status) + if opts[:name].start_with?('build') artifacts_cache_file(artifacts_archive_path) do |file| build.artifacts_file = file end @@ -40,19 +61,28 @@ class Gitlab::Seeder::Builds end build.save! + build.update(status: build_status) if %w(running success failed).include?(build.status) # We need to set build trace after saving a build (id required) build.trace = FFaker::Lorem.paragraphs(6).join("\n\n") end end + + def commit_status_create!(pipeline, opts = {}) + attributes = commit_status_attributes_for(pipeline, opts) + GenericCommitStatus.create(attributes) + end + + def commit_status_attributes_for(pipeline, opts) + { name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]), + ref: 'master', user: build_user, project: @project, pipeline: pipeline, + created_at: Time.now, updated_at: Time.now + }.merge(opts) + end - def build_attributes_for(ci_commit) - { name: 'test build', commands: "$ build command", - stage: 'test', stage_idx: 1, ref: 'master', - user_id: build_user, gl_project_id: @project.id, - status: build_status, commit_id: ci_commit.id, - created_at: Time.now, updated_at: Time.now } + def build_attributes_for(pipeline, opts) + commit_status_attributes_for(pipeline, opts).merge(commands: '$ build command') end def build_user @@ -63,13 +93,16 @@ class Gitlab::Seeder::Builds Ci::Build::AVAILABLE_STATUSES.sample end + def stage_index(stage) + STAGES.index(stage) || 0 + end + def artifacts_archive_path Rails.root + 'spec/fixtures/ci_build_artifacts.zip' end def artifacts_metadata_path Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' - end def artifacts_cache_file(file_path) diff --git a/doc/api/todos.md b/doc/api/todos.md index 23f6e35f2a4..937c71de386 100644 --- a/doc/api/todos.md +++ b/doc/api/todos.md @@ -277,8 +277,7 @@ Example Response: ## Mark all todos as done -Marks all pending todos for the current user as done. All todos marked as done -are returned in the response. +Marks all pending todos for the current user as done. It returns the number of marked todos. ``` DELETE /todos @@ -291,154 +290,7 @@ curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.c Example Response: ```json -[ - { - "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" - }, -] +3 ``` [ce-3188]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3188 diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5f77888f631..31b4fd2669e 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -485,6 +485,7 @@ failure. 1. `on_failure` - execute build only when at least one build from prior stages fails. 1. `always` - execute build regardless of the status of builds from prior stages. +1. `manual` - execute build manually. For example: @@ -516,6 +517,7 @@ deploy_job: stage: deploy script: - make deploy + when: manual cleanup_job: stage: cleanup @@ -527,7 +529,20 @@ cleanup_job: The above script will: 1. Execute `cleanup_build_job` only when `build_job` fails -2. Always execute `cleanup_job` as the last step in pipeline. +2. Always execute `cleanup_job` as the last step in pipeline +3. Allow you to manually execute `deploy_job` from GitLab + +#### Manual actions + +>**Note:** +Introduced in GitLab 8.10. + +Manual actions are special type of jobs that are not executed automatically in pipeline. +They need to be explicitly started by the user. +Manual actions can be started from pipelines, builds, environments and deployments views. +You can execute the same manual action multiple times. + +Example usage of manual actions is deployment, ex. promote a staging environment to production. ### environment diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index d9dce2af480..315cb56a089 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -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 rake assets:precompile on the production machine. If you suspect that assets 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 +``` diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 35efe4f2e4a..66b853eb342 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -15,7 +15,8 @@ module API # GET /projects/:id/repository/branches get ":id/repository/branches" do 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 # Get a single branch @@ -28,7 +29,8 @@ module API get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do @branch = user_project.repository.branches.find { |item| item.name == params[:branch] } not_found!("Branch") unless @branch - present @branch, with: Entities::RepoObject, project: user_project + + present @branch, with: Entities::RepoBranch, project: user_project end # Protect a single branch @@ -60,7 +62,7 @@ module API developers_can_merge: developers_can_merge || false) end - present @branch, with: Entities::RepoObject, project: user_project + present @branch, with: Entities::RepoBranch, project: user_project end # Unprotect a single branch @@ -79,7 +81,7 @@ module API protected_branch = user_project.protected_branches.find_by(name: @branch.name) protected_branch.destroy if protected_branch - present @branch, with: Entities::RepoObject, project: user_project + present @branch, with: Entities::RepoBranch, project: user_project end # Create branch @@ -97,7 +99,7 @@ module API if result[:status] == :success present result[:branch], - with: Entities::RepoObject, + with: Entities::RepoBranch, project: user_project else render_api_error!(result[:message], 400) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index d6fed1a1eed..d7e74582459 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -114,33 +114,23 @@ module API end end - class RepoObject < Grape::Entity + class RepoBranch < Grape::Entity expose :name - expose :commit do |repo_obj, options| - if repo_obj.respond_to?(:commit) - repo_obj.commit - elsif options[:project] - options[:project].repository.commit(repo_obj.target) - end + expose :commit do |repo_branch, options| + options[:project].repository.commit(repo_branch.target) end - expose :protected do |repo_obj, options| - if options[:project] - options[:project].protected_branch? repo_obj.name - end + expose :protected do |repo_branch, options| + options[:project].protected_branch? repo_branch.name end - expose :developers_can_push do |repo_obj, options| - if options[:project] - options[:project].developers_can_push_to_protected_branch? repo_obj.name - end + expose :developers_can_push do |repo_branch, options| + options[:project].developers_can_push_to_protected_branch? repo_branch.name end - expose :developers_can_merge do |repo_obj, options| - if options[:project] - options[:project].developers_can_merge_to_protected_branch? repo_obj.name - end + expose :developers_can_merge do |repo_branch, options| + options[:project].developers_can_merge_to_protected_branch? repo_branch.name end end @@ -437,27 +427,14 @@ module API end class RepoTag < Grape::Entity - expose :name - expose :message do |repo_obj, _options| - if repo_obj.respond_to?(:message) - repo_obj.message - else - nil - end - end + expose :name, :message - expose :commit do |repo_obj, options| - if repo_obj.respond_to?(:commit) - repo_obj.commit - elsif options[:project] - options[:project].repository.commit(repo_obj.target) - end + expose :commit do |repo_tag, options| + options[:project].repository.commit(repo_tag.target) end - expose :release, using: Entities::Release do |repo_obj, options| - if options[:project] - options[:project].releases.find_by(tag: repo_obj.name) - end + expose :release, using: Entities::Release do |repo_tag, options| + options[:project].releases.find_by(tag: repo_tag.name) end end diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 2a6bfa98ca4..26c24c3baff 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -75,7 +75,7 @@ module API todos = find_todos todos.each(&:done) - present paginate(Kaminari.paginate_array(todos)), with: Entities::Todo, current_user: current_user + todos.length end end end 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/lib/tasks/gitlab/track_deployment.rake b/lib/tasks/gitlab/track_deployment.rake new file mode 100644 index 00000000000..84aa2e8507a --- /dev/null +++ b/lib/tasks/gitlab/track_deployment.rake @@ -0,0 +1,9 @@ +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 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..9c018be14b7 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -13,6 +13,7 @@ feature 'Environments', feature: true do describe 'when showing environments' do given!(:environment) { } given!(:deployment) { } + given!(:manual) { } before do visit namespace_project_environments_path(project.namespace, project) @@ -43,6 +44,24 @@ feature 'Environments', feature: true do scenario 'does show deployment SHA' do expect(page).to have_link(deployment.short_sha) end + + context 'with build and manual actions' do + given(:pipeline) { create(:ci_pipeline, project: project) } + given(:build) { create(:ci_build, pipeline: pipeline) } + given(:deployment) { create(:deployment, environment: environment, deployable: build) } + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } + + scenario 'does show a play button' do + expect(page).to have_link(manual.name.humanize) + end + + scenario 'does allow to play manual action' do + expect(manual).to be_skipped + expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } + expect(page).to have_content(manual.name) + expect(manual.reload).to be_pending + end + end end end @@ -54,6 +73,7 @@ feature 'Environments', feature: true do describe 'when showing the environment' do given(:environment) { create(:environment, project: project) } given!(:deployment) { } + given!(:manual) { } before do visit namespace_project_environment_path(project.namespace, project, environment) @@ -77,7 +97,8 @@ feature 'Environments', feature: true do end context 'with build' do - given(:build) { create(:ci_build, project: project) } + given(:pipeline) { create(:ci_pipeline, project: project) } + given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } scenario 'does show build name' do @@ -87,6 +108,21 @@ feature 'Environments', feature: true do scenario 'does show retry button' do expect(page).to have_link('Retry') end + + context 'with manual action' do + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } + + scenario 'does show a play button' do + expect(page).to have_link(manual.name.humanize) + end + + scenario 'does allow to play manual action' do + expect(manual).to be_skipped + expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } + expect(page).to have_content(manual.name) + expect(manual.reload).to be_pending + end + end end end end diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index e7ee0aaea3c..7f861db1969 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -62,6 +62,20 @@ describe "Pipelines" do end end + context 'with manual actions' do + let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_link('Manual build') } + + context 'when playing' do + before { click_link('Manual build') } + + it { expect(manual.reload).to be_pending } + end + end + context 'for generic statuses' do context 'when running' do let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } @@ -117,6 +131,7 @@ describe "Pipelines" do @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') + @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') end @@ -131,6 +146,7 @@ describe "Pipelines" do expect(page).to have_content(@external.id) expect(page).to have_content('Retry failed') expect(page).to have_content('Cancel running') + expect(page).to have_link('Play') end context 'retrying builds' do @@ -154,6 +170,12 @@ describe "Pipelines" do it { expect(page).to have_selector('.ci-canceled') } end end + + context 'playing manual build' do + before { click_link('Play') } + + it { expect(@manual.reload).to be_pending } + end end describe 'POST /:project/pipelines' do diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 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 diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 355cb8fdfff..bd120e84c8f 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -713,4 +713,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..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 @@ -416,4 +478,28 @@ describe Ci::Pipeline, models: true do end end end + + describe '#manual_actions' do + subject { pipeline.manual_actions } + + it 'when none defined' do + is_expected.to be_empty + end + + context 'when action defined' do + let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') } + + it 'returns one action' do + is_expected.to contain_exactly(manual) + end + + context 'there are multiple of the same name' do + let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') } + + it 'returns latest one' do + is_expected.to contain_exactly(manual2) + end + end + end + end end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 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/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 92a4fa216cd..3ccd0af652f 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -134,8 +134,7 @@ describe API::Todos, api: true do delete api('/todos', john_doe) expect(response.status).to eq(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect(response.body).to eq('3') expect(pending_1.reload).to be_done expect(pending_2.reload).to be_done expect(pending_3.reload).to be_done |