Commit d5381c65 authored by Ruben Davila's avatar Ruben Davila

Merge remote-tracking branch 'ce/master'

parents fd7df136 4bdcbc85
......@@ -48,3 +48,4 @@
/vendor/bundle/*
/builds/*
/shared/*
/.gitlab_workhorse_secret
......@@ -7,6 +7,7 @@ v 8.12.0 (unreleased)
- Prune events older than 12 months. (ritave)
- Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
- Filter tags by name !6121
- Give project selection dropdowns responsive width, make non-wrapping.
- Make push events have equal vertical spacing.
- Add two-factor recovery endpoint to internal API !5510
- Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
......@@ -19,6 +20,7 @@ v 8.12.0 (unreleased)
- Change merge_error column from string to text type
- Reduce contributions calendar data payload (ClemMakesApps)
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Move parsing of sidekiq ps into helper !6245 (pascalbetz)
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
- Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
- Fix blame table layout width
......@@ -30,6 +32,7 @@ v 8.12.0 (unreleased)
- Fix project visibility level fields on settings
- Add hover color to emoji icon (ClemMakesApps)
- Add textarea autoresize after comment (ClemMakesApps)
- Refresh todos count cache when an Issue/MR is deleted
- Fix branches page dropdown sort alignment (ClemMakesApps)
- Add white background for no readme container (ClemMakesApps)
- API: Expose issue confidentiality flag. (Robert Schilling)
......@@ -46,6 +49,8 @@ v 8.12.0 (unreleased)
- Use 'git update-ref' for safer web commits !6130
- Sort pipelines requested through the API
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
- Change pipeline duration to be jobs running time instead of simple wall time from start to end !6084
- Show queued time when showing a pipeline !6084
- Remove unused mixins (ClemMakesApps)
- Add search to all issue board lists
- Fix groups sort dropdown alignment (ClemMakesApps)
......@@ -66,6 +71,7 @@ v 8.12.0 (unreleased)
- Align add button on repository view (ClemMakesApps)
- Fix contributions calendar month label truncation (ClemMakesApps)
- Added tests for diff notes
- Add pipeline events to Slack integration !5525
- Add a button to download latest successful artifacts for branches and tags !5142
- Remove redundant pipeline tooltips (ClemMakesApps)
- Expire commit info views after one day, instead of two weeks, to allow for user email updates
......@@ -104,6 +110,7 @@ v 8.12.0 (unreleased)
- Show values of CI trigger variables only when clicked (Katarzyna Kobierska Ula Budziszewska)
- Use default clone protocol on "check out, review, and merge locally" help page URL
- API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska)
- Allow bulk update merge requests from merge requests index page
v 8.11.6 (unreleased)
......
......@@ -23,6 +23,7 @@
case 'projects:boards:show':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
new IssuableBulkActions();
......@@ -93,10 +94,6 @@
break;
case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver()
case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation();
Issuable.init();
break;
case 'dashboard:activity':
new Activities();
break;
......
......@@ -77,7 +77,7 @@
},
checkChanged: function() {
const $checkedIssues = $('.selected_issue:checked');
const $updateIssuesIds = $('#update_issues_ids');
const $updateIssuesIds = $('#update_issuable_ids');
const $issuesOtherFilters = $('.issues-other-filters');
const $issuesBulkUpdate = $('.issues_bulk_update');
......
......@@ -5,7 +5,7 @@
if (opts == null) {
opts = {};
}
this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issues-list .issue');
this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li');
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
this.bindEvents();
......@@ -106,7 +106,7 @@
state_event: this.form.find('input[name="update[state_event]"]').val(),
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issues_ids: this.form.find('input[name="update[issues_ids]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
add_label_ids: [],
remove_label_ids: []
......
......@@ -404,3 +404,18 @@
margin-bottom: $gl-padding;
}
}
.issuable-list {
li {
.issue-check {
float: left;
padding-right: $gl-padding;
margin-bottom: 10px;
min-width: 15px;
.selected_issue {
vertical-align: text-top;
}
}
}
}
......@@ -7,17 +7,6 @@
margin-bottom: 2px;
}
.issue-check {
float: left;
padding-right: 16px;
margin-bottom: 10px;
min-width: 15px;
.selected_issue {
vertical-align: text-top;
}
}
.issue-labels {
display: inline-block;
}
......
......@@ -736,9 +736,15 @@ a.allowed-to-merge, a.allowed-to-push {
}
}
.project-refs-form {
.dropdown-menu {
width: 300px;
.project-refs-form .dropdown-menu, .dropdown-menu-projects {
width: 300px;
@media (min-width: $screen-sm-min) {
width: 500px;
}
a {
white-space: normal;
}
}
......
......@@ -3,21 +3,54 @@ module IssuableActions
included do
before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
end
def destroy
issuable.destroy
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
TodoService.new.public_send(destroy_method, issuable, current_user)
name = issuable.class.name.titleize.downcase
flash[:notice] = "The #{name} was successfully deleted."
redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
end
def bulk_update
result = Issuable::BulkUpdateService.new(project, current_user, bulk_update_params).execute(resource_name)
quantity = result[:count]
render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" }
end
private
def authorize_destroy_issuable!
unless current_user.can?(:"destroy_#{issuable.to_ability_name}", issuable)
unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied!
end
end
def authorize_admin_issuable!
unless can?(current_user, :"admin_#{resource_name}", @project)
return access_denied!
end
end
def bulk_update_params
params.require(:update).permit(
:issuable_ids,
:assignee_id,
:milestone_id,
:state_event,
:subscription_event,
label_ids: [],
add_label_ids: [],
remove_label_ids: []
)
end
def resource_name
@resource_name ||= controller_name.singularize
end
end
......@@ -117,4 +117,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def ci?
@ci.present?
end
def verify_workhorse_api!
Gitlab::Workhorse.verify_api_request!(request.headers)
end
end
# This file should be identical in GitLab Community Edition and Enterprise Edition
class Projects::GitHttpController < Projects::GitHttpClientController
before_action :verify_workhorse_api!
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
......@@ -56,6 +58,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def render_ok
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.git_http_ok(repository, user)
end
......
......@@ -20,9 +20,6 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow modify issue
before_action :authorize_update_issue!, only: [:edit, :update]
# Allow issues bulk update
before_action :authorize_admin_issues!, only: [:bulk_update]
respond_to :html
def index
......@@ -174,16 +171,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
def bulk_update
result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
respond_to do |format|
format.json do
render json: { notice: "#{result[:count]} issues updated" }
end
end
end
protected
def issue
......@@ -243,17 +230,4 @@ class Projects::IssuesController < Projects::ApplicationController
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
)
end
def bulk_update_params
params.require(:update).permit(
:issues_ids,
:assignee_id,
:milestone_id,
:state_event,
:subscription_event,
label_ids: [],
add_label_ids: [],
remove_label_ids: []
)
end
end
......@@ -3,6 +3,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
before_action :require_lfs_enabled!
before_action :lfs_check_access!
before_action :verify_workhorse_api!, only: [:upload_authorize]
def download
lfs_object = LfsObject.find_by_oid(oid)
......@@ -15,14 +16,8 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
end
def upload_authorize
render(
json: {
StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload",
LfsOid: oid,
LfsSize: size,
},
content_type: 'application/json; charset=utf-8'
)
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.lfs_upload_ok(oid, size)
end
def upload_finalize
......
module SidekiqHelper
SIDEKIQ_PS_REGEXP = /\A
(?<pid>\d+)\s+
(?<cpu>[\d\.,]+)\s+
(?<mem>[\d\.,]+)\s+
(?<state>[DRSTWXZNLsl\+<]+)\s+
(?<start>.+)\s+
(?<command>sidekiq.*\])\s+
\z/x
def parse_sidekiq_ps(line)
match = line.match(SIDEKIQ_PS_REGEXP)
if match
match[1..6]
else
%w[? ? ? ? ? ?]
end
end
end
......@@ -34,4 +34,8 @@ module WorkhorseHelper
headers.store(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
head :ok
end
def set_workhorse_internal_api_content_type
headers['Content-Type'] = Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
end
end
......@@ -257,8 +257,17 @@ module Ci
]
end
def queued_duration
return unless started_at
seconds = (started_at - created_at).to_i
seconds unless seconds.zero?
end
def update_duration
self.duration = calculate_duration
return unless started_at
self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
end
def execute_hooks
......
class SlackService < Service
prop_accessor :webhook, :username, :channel
boolean_accessor :notify_only_broken_builds
boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated?
def initialize_properties
......@@ -10,6 +10,7 @@ class SlackService < Service
if properties.nil?
self.properties = {}
self.notify_only_broken_builds = true
self.notify_only_broken_pipelines = true
end
end
......@@ -38,13 +39,15 @@ class SlackService < Service
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'text', name: 'channel', placeholder: "#general" },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
default_fields + build_event_channels
end
def supported_events
%w(push issue confidential_issue merge_request note tag_push build wiki_page)
%w[push issue confidential_issue merge_request note tag_push
build pipeline wiki_page]
end
def execute(data)
......@@ -62,32 +65,22 @@ class SlackService < Service
# 'close' action. Ignore update events for now to prevent duplicate
# messages from arriving.
message = \
case object_kind
when "push", "tag_push"
PushMessage.new(data)
when "issue"
IssueMessage.new(data) unless is_update?(data)
when "merge_request"
MergeMessage.new(data) unless is_update?(data)
when "note"
NoteMessage.new(data)
when "build"
BuildMessage.new(data) if should_build_be_notified?(data)
when "wiki_page"
WikiPageMessage.new(data)
end
opt = {}
event_channel = get_channel_field(object_kind) || channel
opt[:channel] = event_channel if event_channel
opt[:username] = username if username
message = get_message(object_kind, data)
if message
opt = {}
event_channel = get_channel_field(object_kind) || channel
opt[:channel] = event_channel if event_channel
opt[:username] = username if username
notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
true
else
false
end
end
......@@ -105,6 +98,25 @@ class SlackService < Service
private
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
PushMessage.new(data)
when "issue"
IssueMessage.new(data) unless is_update?(data)
when "merge_request"
MergeMessage.new(data) unless is_update?(data)
when "note"
NoteMessage.new(data)
when "build"
BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline"
PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
WikiPageMessage.new(data)
end
end
def get_channel_field(event)
field_name = event_channel_name(event)
self.public_send(field_name)
......@@ -142,6 +154,17 @@ class SlackService < Service
false
end
end
def should_pipeline_be_notified?(data)
case data[:object_attributes][:status]
when 'success'
!notify_only_broken_pipelines?
when 'failed'
true
else
false
end
end
end
require "slack_service/issue_message"
......@@ -149,4 +172,5 @@ require "slack_service/push_message"
require "slack_service/merge_message"
require "slack_service/note_message"
require "slack_service/build_message"
require "slack_service/pipeline_message"
require "slack_service/wiki_page_message"
......@@ -9,7 +9,7 @@ class SlackService
attr_reader :user_name
attr_reader :duration
def initialize(params, commit = true)
def initialize(params)
@sha = params[:sha]
@ref_type = params[:tag] ? 'tag' : 'branch'
@ref = params[:ref]
......@@ -36,7 +36,7 @@ class SlackService
def message
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
end
end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
......
class SlackService
class PipelineMessage < BaseMessage
attr_reader :sha, :ref_type, :ref, :status, :project_name, :project_url,
:user_name, :duration, :pipeline_id
def initialize(data)
pipeline_attributes = data[:object_attributes]
@sha = pipeline_attributes[:sha]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status]
@duration = pipeline_attributes[:duration]
@pipeline_id = pipeline_attributes[:id]
@project_name = data[:project][:path_with_namespace]
@project_url = data[:project][:web_url]
@user_name = data[:commit] && data[:commit][:author_name]
end
def pretext
''
end
def fallback
format(message)
end
def attachments
[{ text: format(message), color: attachment_color }]
end
private
def message
"#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
end
def humanized_status
case status
when 'success'
'passed'
else
status
end
end
def attachment_color
if status == 'success'
'good'
else
'danger'
end
end
def branch_url
"#{project_url}/commits/#{ref}"
end
def branch_link
"[#{ref}](#{branch_url})"
end
def project_link
"[#{project_name}](#{project_url})"
end
def pipeline_url
"#{project_url}/pipelines/#{pipeline_id}"
end
def pipeline_link
"[#{Commit.truncate_sha(sha)}](#{pipeline_url})"
end
end
end
module Issuable
class BulkUpdateService < IssuableBaseService
def execute(type)
model_class = type.classify.constantize
update_class = type.classify.pluralize.constantize::UpdateService
ids = params.delete(:issuable_ids).split(",")
items = model_class.where(id: ids)
%i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key|
params.delete(key) unless params[key].present?
end
items.each do |issuable|
next unless can?(current_user, :"update_#{type}", issuable)
update_class.new(issuable.project, current_user, params).execute(issuable)
end
{
count: items.count,
success: !items.count.zero?
}
end
end
end
module Issues
class BulkUpdateService < BaseService
def execute
issues_ids = params.delete(:issues_ids).split(",")
issue_params = params
%i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key|
issue_params.delete(key) unless issue_params[key].present?
end
issues = Issue.where(id: issues_ids)
issues.each do |issue|
next unless can?(current_user, :update_issue, issue)
Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
end
{
count: issues.count,
success: !issues.count.zero?
}
end
end
end
......@@ -31,6 +31,14 @@ class TodoService
mark_pending_todos_as_done(issue, current_user)
end
# When we destroy an issue we should:
#
# * refresh the todos count cache for the current user
#
def destroy_issue(issue, current_user)
destroy_issuable(issue, current_user)
end
# When we reassign an issue we should:
#
# * create a pending todo for new assignee if issue is assigned
......@@ -64,6 +72,14 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
end
# When we destroy a merge request we should:
#
# * refresh the todos count cache for the current user
#
def destroy_merge_request(merge_request, current_user)
destroy_issuable(merge_request, current_user)
end
# When we reassign a merge request we should:
#
# * creates a pending todo for new assignee if merge request is assigned
......@@ -200,6 +216,10 @@ class TodoService
create_mention_todos(issuable.project, issuable, author)
end
def destroy_issuable(issuable, user)
user.update_todos_count_cache
end
def toggling_tasks?(issuable)
issuable.previous_changes.include?('description') &&
issuable.tasks? && issuable.updated_tasks.any?
......
......@@ -28,14 +28,10 @@
%th COMMAND
%tbody
- @sidekiq_processes.each do |process|
- next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/)
- data = process.strip.split(' ')
%tr
%td= gitlab_config.user
- 5.times do
%td= data.shift
%td= data.join(' ')
- parse_sidekiq_ps(process).each do |value|
%td= value
.clearfix
%p
%i.fa.fa-exclamation-circle
......
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
- if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
- if @bulk_edit
.issue-check
= check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
= check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
.issue-title.title
%span.issue-title-text
......
%ul.content-list.issues-list
%ul.content-list.issues-list.issuable-list
= render @issues
- if @issues.blank?
%li
......
- @no_container = true
- @bulk_edit = can?(current_user, :admin_issue, @project)
- page_title "Issues"
- new_issue_email = @project.new_issue_address(current_user)
= render "projects/issues/head"
......
%li{ class: mr_css_classes(merge_request) }
- if @bulk_edit
.issue-check
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
.merge-request-title.title
%span.merge-request-title-text
= link_to merge_request.title, merge_request_path(merge_request)
......
%ul.content-list.mr-list
%ul.content-list.mr-list.issuable-list
= render @merge_requests
- if @merge_requests.blank?
%li
......
- @no_container = true
- @bulk_edit = can?(current_user, :admin_merge_request, @project)
- page_title "Merge Requests"
= render "projects/issues/head"
= render 'projects/last_push'
......
......@@ -10,6 +10,8 @@
- if @pipeline.duration
in
= time_interval_in_words(@pipeline.duration)
- if @pipeline.queued_duration
= "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
.pull-right
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
......
- boards_page = controller.controller_name == 'boards'
.issues-filters
.issues-details-filters.row-content-block.second-block
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:issue_search].present?
= hidden_field_tag :issue_search, params[:issue_search]
- if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
- if @bulk_edit
.check-all-holder
= check_box_tag "check_all_issues", nil, false,
class: "check_all_issues left"
......@@ -42,7 +44,7 @@
%a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search])} Reset filters
.pull-right
- if controller.controller_name == 'boards'
- if boards_page
#js-boards-seach.issue-boards-search
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
......@@ -57,9 +59,9 @@
- else
= render 'shared/sort_dropdown', type: local_assigns[:type]
- if controller.controller_name == 'issues'
- if @bulk_edit
.issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update' do
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul
......@@ -82,10 +84,10 @@
%li
%a{href: "#", data: {id: "unsubscribe"}} Unsubscribe
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
= button_tag "Update issues", class: "btn update_selected_issues btn-save"
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
- if !@labels.nil?
.row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) }
......
begin
Gitlab::Workhorse.secret
rescue
Gitlab::Workhorse.write_secret
end
# Try a second time. If it does not work this will raise.
Gitlab::Workhorse.secret
......@@ -797,6 +797,7 @@ Rails.application.routes.draw do
get :branch_to
get :update_branches
get :diff_for_path
post :bulk_update
end
resources :approvers, only: :destroy
......
# CI Examples
A collection of `.gitlab-ci.yml` files is maintained at the [GitLab CI Yml project][gitlab-ci-templates].
If your favorite programming language or framework are missing we would love your help by sending a merge request
with a `.gitlab-ci.yml`.
Apart from those, here is an collection of tutorials and guides on setting up your CI pipeline:
- [Testing a PHP application](php.md)
- [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
- [Test a Clojure application](test-clojure-application.md)
- [Test a Scala application](test-scala-application.md)
- [Using `dpl` as deployment tool](deployment/README.md)
- Help your favorite programming language and GitLab by sending a merge request
with a guide for that language.
## Outside the documentation
- [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
- [A collection of useful .gitlab-ci.yml templates](https://gitlab.com/gitlab-org/gitlab-ci-yml)
[gitlab-ci-templates][https://gitlab.com/gitlab-org/gitlab-ci-yml]
......@@ -403,7 +403,7 @@ If you are not using Linux you may have to run `gmake` instead of
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
cd gitlab-workhorse
sudo -u git -H git checkout v0.7.11
sudo -u git -H git checkout v0.8.0
sudo -u git -H make
### Initialize Database and Activate Advanced Features
......
......@@ -82,7 +82,7 @@ GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
sudo -u git -H git checkout v0.7.11
sudo -u git -H git checkout v0.8.0
sudo -u git -H make
```
......
......@@ -46,6 +46,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
page.check('Issue')
page.check('Merge request')
page.check('Build')
page.check('Pipeline')
click_on 'Save'
end
......
......@@ -31,7 +31,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I click link "Closed"' do
click_link "Closed"
page.within('.issues-state-filters') do
click_link "Closed"
end
end
step 'I should see merge request "Wiki Feature"' do
......
......@@ -101,6 +101,7 @@ module Ci
# POST /builds/:id/artifacts/authorize
post ":id/artifacts/authorize" do
require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers)
not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id])
not_found! unless build
......@@ -113,7 +114,8 @@ module Ci
end
status 200
{ TempPath: ArtifactUploader.artifacts_upload_path }
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
Gitlab::Workhorse.artifact_upload_ok
end
# Upload artifacts to build - Runners only
......
module Gitlab
module Ci
# # Introduction - total running time
#
# The problem this module is trying to solve is finding the total running
# time amongst all the jobs, excluding retries and pending (queue) time.
# We could reduce this problem down to finding the union of periods.
#
# So each job would be represented as a `Period`, which consists of
# `Period#first` as when the job started and `Period#last` as when the
# job was finished. A simple example here would be:
#
# * A (1, 3)
# * B (2, 4)
# * C (6, 7)
#
# Here A begins from 1, and ends to 3. B begins from 2, and ends to 4.
# C begins from 6, and ends to 7. Visually it could be viewed as:
#
# 0 1 2 3 4 5 6 7
# AAAAAAA
# BBBBBBB
# CCCC
#
# The union of A, B, and C would be (1, 4) and (6, 7), therefore the
# total running time should be:
#
# (4 - 1) + (7 - 6) => 4
#
# # The Algorithm
#
# The algorithm used here for union would be described as follow.
# First we make sure that all periods are sorted by `Period#first`.
# Then we try to merge periods by iterating through the first period
# to the last period. The goal would be merging all overlapped periods
# so that in the end all the periods are discrete. When all periods
# are discrete, we're free to just sum all the periods to get real
# running time.
#
# Here we begin from A, and compare it to B. We could find that
# before A ends, B already started. That is `B.first <= A.last`
# that is `2 <= 3` which means A and B are overlapping!
#
# When we found that two periods are overlapping, we would need to merge
# them into a new period and disregard the old periods. To make a new
# period, we take `A.first` as the new first because remember? we sorted
# them, so `A.first` must be smaller or equal to `B.first`. And we take
# `[A.last, B.last].max` as the new last because we want whoever ended
# later. This could be broken into two cases:
#
# 0 1 2 3 4
# AAAAAAA
# BBBBBBB
#
# Or:
#
# 0 1 2 3 4
# AAAAAAAAAA
# BBBB
#
# So that we need to take whoever ends later. Back to our example,
# after merging and discard A and B it could be visually viewed as:
#
# 0 1 2 3 4 5 6 7
# DDDDDDDDDD
# CCCC
#
# Now we could go on and compare the newly created D and the old C.
# We could figure out that D and C are not overlapping by checking
# `C.first <= D.last` is `false`. Therefore we need to keep both C
# and D. The example would end here because there are no more jobs.
#
# After having the union of all periods, we just need to sum the length
# of all periods to get total time.
#
# (4 - 1) + (7 - 6) => 4
#
# That is 4 is the answer in the example.
module PipelineDuration
extend self
Period = Struct.new(:first, :last) do
def duration
last - first
end
end
def from_pipeline(pipeline)
status = %w[success failed running canceled]
builds = pipeline.builds.latest.
where(status: status).where.not(started_at: nil).order(:started_at)
from_builds(builds)
end
def from_builds(builds)
now = Time.now
periods = builds.map do |b|
Period.new(b.started_at, b.finished_at || now)
end
from_periods(periods)
end
# periods should be sorted by `first`
def from_periods(periods)
process_duration(process_periods(periods))
end
private
def process_periods(periods)
return periods if periods.empty?
periods.drop(1).inject([periods.first]) do |result, current|
previous = result.last
if overlap?(previous, current)
result[-1] = merge(previous, current)
result
else
result << current
end
end
end
def overlap?(previous, current)
current.first <= previous.last
end
def merge(previous, current)
Period.new(previous.first, [previous.last, current.last].max)
end
def process_duration(periods)
periods.sum(&:duration)
end
end
end
end
require 'base64'
require 'json'
require 'securerandom'
module Gitlab
class Workhorse
SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'
INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'
INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'
# Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
# bytes https://tools.ietf.org/html/rfc4868#section-2.6
SECRET_LENGTH = 32
class << self
def git_http_ok(repository, user)
{
'GL_ID' => Gitlab::GlId.gl_id(user),
'RepoPath' => repository.path_to_repo,
GL_ID: Gitlab::GlId.gl_id(user),
RepoPath: repository.path_to_repo,
}
end
def lfs_upload_ok(oid, size)
{
StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload",
LfsOid: oid,
LfsSize: size,
}
end
def artifact_upload_ok
{ TempPath: ArtifactUploader.artifacts_upload_path }
end
def send_git_blob(repository, blob)
params = {
'RepoPath' => repository.path_to_repo,
......@@ -81,6 +100,35 @@ module Gitlab
path.readable? ? path.read.chomp : 'unknown'
end
def secret
@secret ||= begin
bytes = Base64.strict_decode64(File.read(secret_path))
raise "#{secret_path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH
bytes
end
end
def write_secret
bytes = SecureRandom.random_bytes(SECRET_LENGTH)
File.open(secret_path, 'w:BINARY', 0600) do |f|
f.chmod(0600)
f.write(Base64.strict_encode64(bytes))
end
end
def verify_api_request!(request_headers)
JWT.decode(
request_headers[INTERNAL_API_REQUEST_HEADER],
secret,
true,
{ iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' },
)
end
def secret_path
Rails.root.join('.gitlab_workhorse_secret')
end
protected
def encode(hash)
......
......@@ -370,6 +370,12 @@ describe Projects::IssuesController do
expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
end
it 'delegates the update of the todos count cache to TodoService' do
expect_any_instance_of(TodoService).to receive(:destroy_issue).with(issue, owner).once
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
end
end
end
......
......@@ -451,6 +451,12 @@ describe Projects::MergeRequestsController do
expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
end
it 'delegates the update of the todos count cache to TodoService' do
expect_any_instance_of(TodoService).to receive(:destroy_merge_request).with(merge_request, owner).once
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
end
end
end
......
require 'rails_helper'
feature 'Issues > User uses slash commands', feature: true, js: true do
include SlashCommandsHelpers
include WaitForAjax
it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do
......@@ -17,14 +18,15 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
after do
wait_for_ajax
end
describe 'adding a due date from note' do
let(:issue) { create(:issue, project: project) }
it 'does not create a note, and sets the due date accordingly' do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/due 2016-08-28"
click_button 'Comment'
end
write_note("/due 2016-08-28")
expect(page).not_to have_content '/due 2016-08-28'
expect(page).to have_content 'Your commands have been executed!'
......@@ -41,10 +43,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
it 'does not create a note, and removes the due date accordingly' do
expect(issue.due_date).to eq Date.new(2016, 8, 28)
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/remove_due_date"
click_button 'Comment'
end
write_note("/remove_due_date")
expect(page).not_to have_content '/remove_due_date'
expect(page).to have_content 'Your commands have been executed!'
......
require 'rails_helper'
feature 'Multiple merge requests updating from merge_requests#index', feature: true do
include WaitForAjax
let!(:user) { create(:user)}
let!(:project) { create(:project) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
before do
project.team << [user, :master]
login_as(user)
end
context 'status', js: true do
describe 'close merge request' do
before do
visit namespace_project_merge_requests_path(project.namespace, project)
end
it 'closes merge request' do
change_status('Closed')
expect(page).to have_selector('.merge-request', count: 0)
end
end
describe 'reopen merge request' do
before do
merge_request.close
visit namespace_project_merge_requests_path(project.namespace, project, state: 'closed')
end
it 'reopens merge request' do
change_status('Open')
expect(page).to have_selector('.merge-request', count: 0)
end
end
end
context 'assignee', js: true do
describe 'set assignee' do
before do
visit namespace_project_merge_requests_path(project.namespace, project)
end
it "updates merge request with assignee" do
change_assignee(user.name)
page.within('.merge-request .controls') do
expect(find('.author_link')["title"]).to have_content(user.name)
end
end
end
describe 'remove assignee' do
before do
merge_request.assignee = user
merge_request.save
visit namespace_project_merge_requests_path(project.namespace, project)
end
it "removes assignee from the merge request" do
change_assignee('Unassigned')
expect(find('.merge-request .controls')).not_to have_css('.author_link')
end
end
end
context 'milestone', js: true do
let(:milestone) { create(:milestone, project: project) }
describe 'set milestone' do
before do
visit namespace_project_merge_requests_path(project.namespace, project)
end
it "updates merge request with milestone" do
change_milestone(milestone.title)
expect(find('.merge-request')).to have_content milestone.title
end
end
describe 'unset milestone' do
before do
merge_request.milestone = milestone
merge_request.save
visit namespace_project_merge_requests_path(project.namespace, project)
end
it "removes milestone from the merge request" do
change_milestone("No Milestone")
expect(find('.merge-request')).not_to have_content milestone.title
end
end
end
def change_status(text)
find('#check_all_issues').click
find('.js-issue-status').click
find('.dropdown-menu-status a', text: text).click
click_update_merge_requests_button
end
def change_assignee(text)
find('#check_all_issues').click
find('.js-update-assignee').click
wait_for_ajax
page.within '.dropdown-menu-user' do
click_link text
end
click_update_merge_requests_button
end
def change_milestone(text)
find('#check_all_issues').click
find('.issues_bulk_update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: text).click
click_update_merge_requests_button
end
def click_update_merge_requests_button
find('.update_selected_issues').click
wait_for_ajax
end
end
require 'rails_helper'
feature 'Merge Requests > User uses slash commands', feature: true, js: true do
include SlashCommandsHelpers
include WaitForAjax
let(:user) { create(:user) }
......@@ -20,11 +21,12 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
after do
wait_for_ajax
end
it 'does not recognize the command nor create a note' do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/due 2016-08-28"
click_button 'Comment'
end
write_note("/due 2016-08-28")
expect(page).not_to have_content '/due 2016-08-28'
end
......
require 'spec_helper'
describe SidekiqHelper do
describe 'parse_sidekiq_ps' do
it 'parses line with time' do
line = '55137 10,0 2,1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] '
parts = helper.parse_sidekiq_ps(line)
expect(parts).to eq(['55137', '10,0', '2,1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
end
it 'parses line with date' do
line = '55137 10,0 2,1 S+ Aug 4 sidekiq 4.1.4 gitlab [0 of 25 busy] '
parts = helper.parse_sidekiq_ps(line)
expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 4', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
end
it 'parses line with two digit date' do
line = '55137 10,0 2,1 S+ Aug 04 sidekiq 4.1.4 gitlab [0 of 25 busy] '
parts = helper.parse_sidekiq_ps(line)
expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 04', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
end
it 'parses line with dot as float separator' do
line = '55137 10.0 2.1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] '
parts = helper.parse_sidekiq_ps(line)
expect(parts).to eq(['55137', '10.0', '2.1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
end
it 'does fail gracefully on line not matching the format' do
line = '55137 10.0 2.1 S+ 2:30pm something'
parts = helper.parse_sidekiq_ps(line)
expect(parts).to eq(['?', '?', '?', '?', '?', '?'])
end
end
end
require 'spec_helper'
describe Gitlab::Ci::PipelineDuration do
let(:calculated_duration) { calculate(data) }
shared_examples 'calculating duration' do
it do
expect(calculated_duration).to eq(duration)
end
end
context 'test sample A' do
let(:data) do
[[0, 1],
[1, 2],
[3, 4],
[5, 6]]
end
let(:duration) { 4 }
it_behaves_like 'calculating duration'
end
context 'test sample B' do
let(:data) do
[[0, 1],
[1, 2],
[2, 3],
[3, 4],
[0, 4]]
end
let(:duration) { 4 }
it_behaves_like 'calculating duration'
end
context 'test sample C' do
let(:data) do
[[0, 4],
[2, 6],
[5, 7],
[8, 9]]
end
let(:duration) { 8 }
it_behaves_like 'calculating duration'
end
context 'test sample D' do
let(:data) do
[[0, 1],
[2, 3],
[4, 5],
[6, 7]]
end
let(:duration) { 4 }
it_behaves_like 'calculating duration'
end
context 'test sample E' do
let(:data) do
[[0, 1],
[3, 9],
[3, 4],
[3, 5],
[3, 8],
[4, 5],
[4, 7],
[5, 8]]
end
let(:duration) { 7 }
it_behaves_like 'calculating duration'
end
context 'test sample F' do
let(:data) do
[[1, 3],
[2, 4],
[2, 4],
[2, 4],
[5, 8]]
end
let(:duration) { 6 }
it_behaves_like 'calculating duration'
end
context 'test sample G' do
let(:data) do
[[1, 3],
[2, 4],
[6, 7]]
end
let(:duration) { 4 }
it_behaves_like 'calculating duration'
end
def calculate(data)
periods = data.shuffle.map do |(first, last)|
Gitlab::Ci::PipelineDuration::Period.new(first, last)
end
Gitlab::Ci::PipelineDuration.from_periods(periods.sort_by(&:first))
end
end
......@@ -4,7 +4,7 @@ describe Gitlab::Workhorse, lib: true do
let(:project) { create(:project) }
let(:subject) { Gitlab::Workhorse }
describe "#send_git_archive" do
describe ".send_git_archive" do
context "when the repository doesn't have an archive file path" do
before do
allow(project.repository).to receive(:archive_metadata).and_return(Hash.new)
......@@ -15,4 +15,88 @@ describe Gitlab::Workhorse, lib: true do
end
end
end
describe ".secret" do
subject { described_class.secret }
before do
described_class.instance_variable_set(:@secret, nil)
described_class.write_secret
end
it 'returns 32 bytes' do
expect(subject).to be_a(String)
expect(subject.length).to eq(32)
expect(subject.encoding).to eq(Encoding::ASCII_8BIT)
end
it 'raises an exception if the secret file cannot be read' do
File.delete(described_class.secret_path)
expect { subject }.to raise_exception(Errno::ENOENT)
end
it 'raises an exception if the secret file contains the wrong number of bytes' do
File.truncate(described_class.secret_path, 0)
expect { subject }.to raise_exception(RuntimeError)
end
end
describe ".write_secret" do
let(:secret_path) { described_class.secret_path }
before do
begin
File.delete(secret_path)
rescue Errno::ENOENT
end
described_class.write_secret
end
it 'uses mode 0600' do
expect(File.stat(secret_path).mode & 0777).to eq(0600)
end
it 'writes base64 data' do
bytes = Base64.strict_decode64(File.read(secret_path))
expect(bytes).not_to be_empty
end
end
describe '#verify_api_request!' do
let(:header_key) { described_class::INTERNAL_API_REQUEST_HEADER }
let(:payload) { { 'iss' => 'gitlab-workhorse' } }
it 'accepts a correct header' do
headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') }
expect { call_verify(headers) }.not_to raise_error
end
it 'raises an error when the header is not set' do
expect { call_verify({}) }.to raise_jwt_error
end
it 'raises an error when the header is not signed' do
headers = { header_key => JWT.encode(payload, nil, 'none') }
expect { call_verify(headers) }.to raise_jwt_error
end
it 'raises an error when the header is signed with the wrong key' do
headers = { header_key => JWT.encode(payload, 'wrongkey', 'HS256') }
expect { call_verify(headers) }.to raise_jwt_error
end
it 'raises an error when the issuer is incorrect' do
payload['iss'] = 'somebody else'
headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') }
expect { call_verify(headers) }.to raise_jwt_error
end
def raise_jwt_error
raise_error(JWT::DecodeError)
end
def call_verify(headers)
described_class.verify_api_request!(headers)
end
end
end
......@@ -124,21 +124,38 @@ describe Ci::Pipeline, models: true do
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
let(:build) { create :ci_build, name: 'build1', pipeline: pipeline }
let(:build) { create_build('build1', current, 10) }
let(:build_b) { create_build('build2', current, 20) }
let(:build_c) { create_build('build3', current + 50, 10) }
describe '#duration' do
before do
travel_to(current - 120) do
pipeline.update(created_at: current)
travel_to(current + 5) do
pipeline.run
pipeline.save
end
travel_to(current + 30) do
build.success
end
travel_to(current + 40) do
build_b.drop
end
travel_to(current) do
pipeline.succeed
travel_to(current + 70) do
build_c.success
end
pipeline.drop
end
it 'matches sum of builds duration' do
expect(pipeline.reload.duration).to eq(120)
pipeline.reload
expect(pipeline.duration).to eq(40)
end
end
......@@ -169,6 +186,14 @@ describe Ci::Pipeline, models: true do
expect(pipeline.reload.finished_at).to be_nil
end
end
def create_build(name, queued_at = current, started_from = 0)
create(:ci_build,
name: name,
pipeline: pipeline,
queued_at: queued_at,
started_at: queued_at + started_from)
end
end
describe '#branch?' do
......
......@@ -10,7 +10,7 @@ describe SlackService::BuildMessage do
tag: false,
project_name: 'project_name',
project_url: 'somewhere.com',
project_url: 'example.gitlab.com',
commit: {
status: status,
......@@ -20,42 +20,38 @@ describe SlackService::BuildMessage do
}
end
context 'succeeded' do
let(:message) { build_message }
context 'build succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
let(:duration) { 10 }
let(:message) { build_message('passed') }
it 'returns a message with information about succeeded build' do
message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 seconds'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
end
context 'failed' do
context 'build failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
let(:duration) { 10 }
it 'returns a message with information about failed build' do
message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 seconds'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
end
describe '#seconds_name' do
let(:status) { 'failed' }
let(:color) { 'danger' }
let(:duration) { 1 }
end
it 'returns seconds as singular when there is only one' do
message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 1 second'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
def build_message(status_text = status)
"<example.gitlab.com|project_name>:" \
" Commit <example.gitlab.com/commit/" \
"97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
" of <example.gitlab.com/commits/develop|develop> branch" \
" by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end
end
require 'spec_helper'
describe SlackService::PipelineMessage do
subject { SlackService::PipelineMessage.new(args) }
let(:args) do
{
object_attributes: {
id: 123,
sha: '97de212e80737a608d939f648d959671fb0a0142',
tag: false,
ref: 'develop',
status: status,
duration: duration
},
project: { path_with_namespace: 'project_name',
web_url: 'example.gitlab.com' },
commit: { author_name: 'hacker' }
}
end
let(:message) { build_message }
context 'pipeline succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
let(:duration) { 10 }
let(:message) { build_message('passed') }
it 'returns a message with information about succeeded build' do
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
end
context 'pipeline failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
let(:duration) { 10 }
it 'returns a message with information about failed build' do
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
end
def build_message(status_text = status)
"<example.gitlab.com|project_name>:" \
" Pipeline <example.gitlab.com/pipelines/123|97de212e>" \
" of <example.gitlab.com/commits/develop|develop> branch" \
" by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end
end
......@@ -21,6 +21,9 @@
require 'spec_helper'
describe SlackService, models: true do
let(:slack) { SlackService.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
......@@ -42,15 +45,14 @@ describe SlackService, models: true do
end
describe "Execute" do
let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
before do
allow(slack).to receive_messages(
......@@ -212,10 +214,8 @@ describe SlackService, models: true do
end
describe "Note events" do
let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project, creator_id: user.id) }
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
before do
allow(slack).to receive_messages(
......@@ -285,4 +285,63 @@ describe SlackService, models: true do
end
end
end
describe 'Pipeline events' do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:pipeline) do
create(:ci_pipeline,
project: project, status: status,
sha: project.commit.sha, ref: project.default_branch)
end
before do
allow(slack).to receive_messages(
project: project,
service_hook: true,
webhook: webhook_url
)
end
shared_examples 'call Slack API' do
before do
WebMock.stub_request(:post, webhook_url)
end
it 'calls Slack API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
end
context 'with failed pipeline' do
let(:status) { 'failed' }
it_behaves_like 'call Slack API'
end
context 'with succeeded pipeline' do
let(:status) { 'success' }
context 'with default to notify_only_broken_pipelines' do
it 'does not call Slack API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline)
result = slack.execute(data)
expect(result).to be_falsy
end
end
context 'with setting notify_only_broken_pipelines to false' do
before do
slack.notify_only_broken_pipelines = false
end
it_behaves_like 'call Slack API'
end
end
end
end
......@@ -230,7 +230,8 @@ describe Ci::API::API do
let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
let(:get_url) { ci_api("/builds/#{build.id}/artifacts") }
let(:headers) { { "GitLab-Workhorse" => "1.0" } }
let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { "GitLab-Workhorse" => "1.0", Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) }
before { build.run! }
......@@ -240,14 +241,22 @@ describe Ci::API::API do
it "using token as parameter" do
post authorize_url, { token: build.token }, headers
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response["TempPath"]).not_to be_nil
end
it "using token as header" do
post authorize_url, {}, headers_with_token
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response["TempPath"]).not_to be_nil
end
it "reject requests that did not go through gitlab-workhorse" do
headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
post authorize_url, { token: build.token }, headers
expect(response).to have_http_status(500)
end
end
context "fails to post too large artifact" do
......
require "spec_helper"
describe 'Git HTTP requests', lib: true do
include WorkhorseHelpers
let(:user) { create(:user) }
let(:project) { create(:project, path: 'project.git-project') }
......@@ -48,6 +50,7 @@ describe 'Git HTTP requests', lib: true do
expect(response).to have_http_status(200)
expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
end
......@@ -63,6 +66,7 @@ describe 'Git HTTP requests', lib: true do
it "downloads get status 200" do
download(path, {}) do |response|
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
......@@ -101,6 +105,14 @@ describe 'Git HTTP requests', lib: true do
end
end
end
context 'when the request is not from gitlab-workhorse' do
it 'raises an exception' do
expect do
get("/#{project.path_with_namespace}.git/info/refs?service=git-upload-pack")
end.to raise_error(JWT::DecodeError)
end
end
end
context "when the project is private" do
......@@ -258,11 +270,13 @@ describe 'Git HTTP requests', lib: true do
clone_get(path, env)
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it "uploads get status 200" do
upload(path, env) do |response|
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
end
......@@ -277,6 +291,7 @@ describe 'Git HTTP requests', lib: true do
clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it "uploads get status 401 (no project existence information leak)" do
......@@ -385,6 +400,7 @@ describe 'Git HTTP requests', lib: true do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it "uploads get status 401 (no project existence information leak)" do
......@@ -514,7 +530,7 @@ describe 'Git HTTP requests', lib: true do
end
def auth_env(user, password, spnego_request_token)
env = {}
env = workhorse_internal_api_request_header
if user && password
env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
elsif spnego_request_token
......
require 'spec_helper'
describe 'Git LFS API and storage' do
include WorkhorseHelpers
let(:user) { create(:user) }
let!(:lfs_object) { create(:lfs_object, :with_file) }
......@@ -715,6 +717,12 @@ describe 'Git LFS API and storage' do
project.team << [user, :developer]
end
context 'and the request bypassed workhorse' do
it 'raises an exception' do
expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError
end
end
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
......@@ -724,6 +732,10 @@ describe 'Git LFS API and storage' do
expect(response).to have_http_status(200)
end
it 'uses the gitlab-workhorse content type' do
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it 'responds with status 200, location of lfs store and object details' do
expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
expect(json_response['LfsOid']).to eq(sample_oid)
......@@ -863,8 +875,11 @@ describe 'Git LFS API and storage' do
end
end
def put_authorize
put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, headers
def put_authorize(verified: true)
authorize_headers = headers
authorize_headers.merge!(workhorse_internal_api_request_header) if verified
put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, authorize_headers
end
def put_finalize(lfs_tmp = lfs_tmp_file)
......
require 'spec_helper'
describe Issues::BulkUpdateService, services: true do
describe Issuable::BulkUpdateService, services: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project, namespace: user.namespace) }
def bulk_update(issues, extra_params = {})
bulk_update_params = extra_params
.reverse_merge(issues_ids: Array(issues).map(&:id).join(','))
.reverse_merge(issuable_ids: Array(issues).map(&:id).join(','))
Issues::BulkUpdateService.new(project, user, bulk_update_params).execute
Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute('issue')
end
describe 'close issues' do
......
......@@ -145,6 +145,14 @@ describe TodoService, services: true do
end
end
describe '#destroy_issue' do
it 'refresh the todos count cache for the user' do
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
service.destroy_issue(issue, john_doe)
end
end
describe '#reassigned_issue' do
it 'creates a pending todo for new assignee' do
unassigned_issue.update_attribute(:assignee, john_doe)
......@@ -424,6 +432,14 @@ describe TodoService, services: true do
end
end
describe '#destroy_merge_request' do
it 'refresh the todos count cache for the user' do
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
service.destroy_merge_request(mr_assigned, john_doe)
end
end
describe '#reassigned_merge_request' do
it 'creates a pending todo for new assignee' do
mr_unassigned.update_attribute(:assignee, john_doe)
......
......@@ -2,6 +2,9 @@
# It takes a `issuable_type`, and expect an `issuable`.
shared_examples 'issuable record that supports slash commands in its description and notes' do |issuable_type|
include SlashCommandsHelpers
include WaitForAjax
let(:master) { create(:user) }
let(:assignee) { create(:user, username: 'bob') }
let(:guest) { create(:user) }
......@@ -18,6 +21,11 @@ shared_examples 'issuable record that supports slash commands in its description
login_with(master)
end
after do
# Ensure all outstanding Ajax requests are complete to avoid database deadlocks
wait_for_ajax
end
describe "new #{issuable_type}" do
context 'with commands in the description' do
it "creates the #{issuable_type} and interpret commands accordingly" do
......@@ -44,10 +52,7 @@ shared_examples 'issuable record that supports slash commands in its description
context 'with a note containing commands' do
it 'creates a note without the commands and interpret the commands accordingly' do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
click_button 'Comment'
end
write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
expect(page).to have_content 'Awesome!'
expect(page).not_to have_content '/assign @bob'
......@@ -66,10 +71,7 @@ shared_examples 'issuable record that supports slash commands in its description
context 'with a note containing only commands' do
it 'does not create a note but interpret the commands accordingly' do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
click_button 'Comment'
end
write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
expect(page).not_to have_content '/assign @bob'
expect(page).not_to have_content '/label ~bug'
......@@ -92,10 +94,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "when current user can close #{issuable_type}" do
it "closes the #{issuable_type}" do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/close"
click_button 'Comment'
end
write_note("/close")
expect(page).not_to have_content '/close'
expect(page).to have_content 'Your commands have been executed!'
......@@ -112,10 +111,7 @@ shared_examples 'issuable record that supports slash commands in its description
end
it "does not close the #{issuable_type}" do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/close"
click_button 'Comment'
end
write_note("/close")
expect(page).not_to have_content '/close'
expect(page).not_to have_content 'Your commands have been executed!'
......@@ -133,10 +129,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "when current user can reopen #{issuable_type}" do
it "reopens the #{issuable_type}" do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/reopen"
click_button 'Comment'
end
write_note("/reopen")
expect(page).not_to have_content '/reopen'
expect(page).to have_content 'Your commands have been executed!'
......@@ -153,10 +146,7 @@ shared_examples 'issuable record that supports slash commands in its description
end
it "does not reopen the #{issuable_type}" do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/reopen"
click_button 'Comment'
end
write_note("/reopen")
expect(page).not_to have_content '/reopen'
expect(page).not_to have_content 'Your commands have been executed!'
......@@ -169,10 +159,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "with a note changing the #{issuable_type}'s title" do
context "when current user can change title of #{issuable_type}" do
it "reopens the #{issuable_type}" do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/title Awesome new title"
click_button 'Comment'
end
write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
expect(page).to have_content 'Your commands have been executed!'
......@@ -189,10 +176,7 @@ shared_examples 'issuable record that supports slash commands in its description
end
it "does not reopen the #{issuable_type}" do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/title Awesome new title"
click_button 'Comment'
end
write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
expect(page).not_to have_content 'Your commands have been executed!'
......@@ -204,10 +188,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "with a note marking the #{issuable_type} as todo" do
it "creates a new todo for the #{issuable_type}" do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/todo"
click_button 'Comment'
end
write_note("/todo")
expect(page).not_to have_content '/todo'
expect(page).to have_content 'Your commands have been executed!'
......@@ -238,10 +219,7 @@ shared_examples 'issuable record that supports slash commands in its description
expect(todo.author).to eq master
expect(todo.user).to eq master
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/done"
click_button 'Comment'
end
write_note("/done")
expect(page).not_to have_content '/done'
expect(page).to have_content 'Your commands have been executed!'
......@@ -254,10 +232,7 @@ shared_examples 'issuable record that supports slash commands in its description
it "creates a new todo for the #{issuable_type}" do
expect(issuable.subscribed?(master)).to be_falsy
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/subscribe"
click_button 'Comment'
end
write_note("/subscribe")
expect(page).not_to have_content '/subscribe'
expect(page).to have_content 'Your commands have been executed!'
......@@ -274,10 +249,7 @@ shared_examples 'issuable record that supports slash commands in its description
it "creates a new todo for the #{issuable_type}" do
expect(issuable.subscribed?(master)).to be_truthy
page.within('.js-main-target-form') do
fill_in 'note[note]', with: "/unsubscribe"
click_button 'Comment'
end
write_note("/unsubscribe")
expect(page).not_to have_content '/unsubscribe'
expect(page).to have_content 'Your commands have been executed!'
......
......@@ -75,6 +75,7 @@ module LoginHelpers
def logout
find(".header-user-dropdown-toggle").click
click_link "Sign out"
expect(page).to have_content('Signed out successfully')
end
# Logout without JavaScript driver
......
module SlashCommandsHelpers
def write_note(text)
Sidekiq::Testing.fake! do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: text
click_button 'Comment'
end
end
end
end
......@@ -13,4 +13,9 @@ module WorkhorseHelpers
]
end
end
def workhorse_internal_api_request_header
jwt_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256')
{ 'HTTP_' + Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER.upcase.tr('-', '_') => jwt_token }
end
end
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