Commit d53b4d5b authored by Yorick Peterse's avatar Yorick Peterse

Merge branch 'master' into 8-8-stable

parents a08e3c67 f26389a0
......@@ -21,7 +21,7 @@ AllCops:
- 'lib/email_validator.rb'
- 'lib/gitlab/upgrader.rb'
- 'lib/gitlab/seeder.rb'
- 'lib/templates/**/*'
- 'generator_templates/**/*'
##################### Style ##################################
......
......@@ -6,8 +6,10 @@ v 8.8.0 (unreleased)
- Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen)
- Use a case-insensitive comparison in sanitizing URI schemes
- Toggle sign-up confirmation emails in application settings
- Make it possible to prevent tagged runner from picking untagged jobs
- Project#open_branches has been cleaned up and no longer loads entire records into memory.
- Escape HTML in commit titles in system note messages
- Fix creation of Ci::Commit object which can lead to pending, failed in some scenarios
- Improve multiple branch push performance by memoizing permission checking
- Log to application.log when an admin starts and stops impersonating a user
- Changing the confidentiality of an issue now creates a new system note (Alex Moore-Niemi)
......
@import "framework/variables";
table.code {
width: 100%;
font-family: monospace;
border: none;
border-collapse: separate;
margin: 0;
padding: 0;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
td {
line-height: $code_line_height;
font-family: monospace;
font-size: $code_font_size;
}
td.diff-line-num {
margin: 0;
padding: 0;
border: none;
background: $background-color;
color: rgba(0, 0, 0, 0.3);
padding: 0 5px;
border-right: 1px solid $border-color;
text-align: right;
min-width: 35px;
max-width: 50px;
width: 35px;
}
td.line_content {
display: block;
margin: 0;
padding: 0 0.5em;
border: none;
white-space: pre;
}
}
@import "highlight/white";
.pipeline-stage {
overflow: hidden;
text-overflow: ellipsis;
}
......@@ -9,24 +9,19 @@ class Admin::RunnersController < Admin::ApplicationController
end
def show
@builds = @runner.builds.order('id DESC').first(30)
@projects =
if params[:search].present?
::Project.search(params[:search])
else
Project.all
end
@projects = @projects.where.not(id: @runner.projects.select(:id)) if @runner.projects.any?
@projects = @projects.page(params[:page]).per(30)
assign_builds_and_projects
end
def update
@runner.update_attributes(runner_params)
if @runner.update_attributes(runner_params)
respond_to do |format|
format.js
format.html { redirect_to admin_runner_path(@runner) }
end
else
assign_builds_and_projects
render 'show'
end
end
def destroy
......@@ -60,4 +55,16 @@ class Admin::RunnersController < Admin::ApplicationController
def runner_params
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end
def assign_builds_and_projects
@builds = runner.builds.order('id DESC').first(30)
@projects =
if params[:search].present?
::Project.search(params[:search])
else
Project.all
end
@projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any?
@projects = @projects.page(params[:page]).per(30)
end
end
class Projects::PipelinesController < Projects::ApplicationController
before_action :pipeline, except: [:index, :new, :create]
before_action :commit, only: [:show]
before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
def index
@scope = params[:scope]
all_pipelines = project.ci_commits
@pipelines_count = all_pipelines.count
@running_or_pending_count = all_pipelines.running_or_pending.count
@pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope)
@pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30)
end
def new
@pipeline = project.ci_commits.new(ref: @project.default_branch)
end
def create
@pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute
unless @pipeline.persisted?
render 'new'
return
end
redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline)
end
def show
end
def retry
pipeline.retry_failed
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
def cancel
pipeline.cancel_running
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
private
def create_params
params.require(:pipeline).permit(:ref)
end
def pipeline
@pipeline ||= project.ci_commits.find_by!(id: params[:id])
end
def commit
@commit ||= @pipeline.commit_data
end
end
......@@ -20,7 +20,7 @@ class Projects::RunnersController < Projects::ApplicationController
if @runner.update_attributes(runner_params)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
render 'edit'
end
end
......
class PipelinesFinder
attr_reader :project
def initialize(project)
@project = project
end
def execute(pipelines, scope)
case scope
when 'running'
pipelines.running_or_pending
when 'branches'
from_ids(pipelines, ids_for_ref(pipelines, branches))
when 'tags'
from_ids(pipelines, ids_for_ref(pipelines, tags))
else
pipelines
end
end
private
def ids_for_ref(pipelines, refs)
pipelines.where(ref: refs).group(:ref).select('max(id)')
end
def from_ids(pipelines, ids)
pipelines.unscoped.where(id: ids)
end
def branches
project.repository.branches.map(&:name)
end
def tags
project.repository.tags.map(&:name)
end
end
......@@ -36,7 +36,7 @@ class TodosFinder
private
def action_id?
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i)
action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i)
end
def action_id
......
......@@ -38,19 +38,30 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
def render_ci_status(ci_commit, tooltip_placement: 'auto left')
# TODO: split this method into
# - render_commit_status
# - render_pipeline_status
link_to ci_icon_for_status(ci_commit.status),
ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
title: "Build #{ci_label_for_status(ci_commit.status)}",
data: { toggle: 'tooltip', placement: tooltip_placement }
def render_commit_status(commit, tooltip_placement: 'auto left')
project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit)
render_status_with_link('commit', commit.status, path, tooltip_placement)
end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
project = pipeline.project
path = namespace_project_pipeline_path(project.namespace, project, pipeline)
render_status_with_link('pipeline', pipeline.status, path, tooltip_placement)
end
def no_runners_for_project?(project)
project.runners.blank? &&
Ci::Runner.shared.blank?
end
private
def render_status_with_link(type, status, path, tooltip_placement)
link_to ci_icon_for_status(status),
path,
class: "ci-status-link ci-status-icon-#{status.dasherize}",
title: "#{type.titleize}: #{ci_label_for_status(status)}",
data: { toggle: 'tooltip', placement: tooltip_placement }
end
end
......@@ -32,12 +32,6 @@ module EmailsHelper
nil
end
def color_email_diff(diffcontent)
formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github')
lexer = Rouge::Lexers::Diff
raw formatter.format(lexer.lex(diffcontent))
end
def password_reset_token_valid_time
valid_hours = Devise.reset_password_within / 60 / 60
if valid_hours >= 24
......
......@@ -11,6 +11,7 @@ module TodosHelper
case todo.action
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your'
end
end
......@@ -28,8 +29,11 @@ module TodosHelper
namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
todo.target, anchor: anchor)
else
polymorphic_path([todo.project.namespace.becomes(Namespace),
todo.project, todo.target], anchor: anchor)
path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
path.unshift(:builds) if todo.build_failed?
polymorphic_path(path, anchor: anchor)
end
end
......
......@@ -65,7 +65,8 @@ module Emails
# used in notify layout
@target_url = @message.target_url
@project = Project.find project_id
@project = Project.find(project_id)
@diff_notes_disabled = true
add_project_headers
headers['X-GitLab-Author'] = @message.author_username
......
......@@ -10,6 +10,8 @@ class Notify < BaseMailer
include Emails::Builds
add_template_helper MergeRequestsHelper
add_template_helper DiffHelper
add_template_helper BlobHelper
add_template_helper EmailsHelper
def test_email(recipient_email, subject, body)
......
......@@ -205,6 +205,7 @@ class Ability
:read_commit_status,
:read_build,
:read_container_image,
:read_pipeline,
]
end
......@@ -216,6 +217,8 @@ class Ability
:update_commit_status,
:create_build,
:update_build,
:create_pipeline,
:update_pipeline,
:create_merge_request,
:create_wiki,
:push_code,
......@@ -248,6 +251,7 @@ class Ability
:admin_commit_status,
:admin_build,
:admin_container_image,
:admin_pipeline
]
end
......@@ -290,6 +294,7 @@ class Ability
unless project.builds_enabled
rules += named_abilities('build')
rules += named_abilities('pipeline')
end
unless project.container_registry_enabled
......
......@@ -53,6 +53,7 @@ module Ci
new_build.stage_idx = build.stage_idx
new_build.trigger_request = build.trigger_request
new_build.save
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
new_build
end
end
......@@ -290,9 +291,15 @@ module Ci
end
def can_be_served?(runner)
return false unless has_tags? || runner.run_untagged?
(tag_list - runner.tag_list).empty?
end
def has_tags?
tag_list.any?
end
def any_runners_online?
project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
end
......
......@@ -8,8 +8,6 @@ module Ci
has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
delegate :stages, to: :statuses
validates_presence_of :sha
validates_presence_of :status
validate :valid_commit_sha
......@@ -22,7 +20,8 @@ module Ci
end
def self.stages
CommitStatus.where(commit: all).stages
# We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
CommitStatus.where(commit: pluck(:id)).stages
end
def project_id
......@@ -67,6 +66,25 @@ module Ci
end
end
def cancel_running
builds.running_or_pending.each(&:cancel)
end
def retry_failed
builds.latest.failed.select(&:retryable?).each(&:retry)
end
def latest?
return false unless ref
commit = project.commit(ref)
return false unless commit
commit.sha == sha
end
def triggered?
trigger_requests.any?
end
def create_builds(user, trigger_request = nil)
return unless config_processor
config_processor.stages.any? do |stage|
......
......@@ -4,7 +4,7 @@ module Ci
LAST_CONTACT_TIME = 5.minutes.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active]
FORM_EDITABLE = %i[description tag_list active run_untagged]
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
......@@ -26,6 +26,8 @@ module Ci
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end
validate :tag_constraints
acts_as_taggable
# Searches for runners matching the given query.
......@@ -96,5 +98,18 @@ module Ci
def short_sha
token[0...8] if token
end
def has_tags?
tag_list.any?
end
private
def tag_constraints
unless has_tags? || run_untagged?
errors.add(:tags_list,
'can not be empty when runner is not allowed to pick untagged jobs')
end
end
end
end
......@@ -14,7 +14,8 @@ class CommitStatus < ActiveRecord::Base
alias_attribute :author, :user
scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) }
scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
state_machine :status, initial: :pending do
......@@ -45,6 +46,10 @@ class CommitStatus < ActiveRecord::Base
after_transition [:pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
end
after_transition any => :failed do |commit_status|
MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.commit.project, nil).execute(commit_status)
end
end
delegate :sha, :short_sha, to: :commit
......@@ -54,13 +59,15 @@ class CommitStatus < ActiveRecord::Base
end
def self.stages
order_by = 'max(stage_idx)'
group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact
# We group by stage name, but order stages by theirs' index
unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
end
def self.stages_status
all.stages.inject({}) do |h, stage|
h[stage] = all.where(stage: stage).status
# We execute subquery for each stage to calculate a stage status
statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql)
statuses.inject({}) do |h, k|
h[k.first] = k.last
h
end
end
......
class Todo < ActiveRecord::Base
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
belongs_to :author, class_name: "User"
belongs_to :note
......@@ -28,6 +29,10 @@ class Todo < ActiveRecord::Base
state :done
end
def build_failed?
action == BUILD_FAILED
end
def body
if note.present?
note.note
......
module Ci
class CreatePipelineService < BaseService
def execute
pipeline = project.ci_commits.new(params)
unless ref_names.include?(params[:ref])
pipeline.errors.add(:base, 'Reference not found')
return pipeline
end
unless commit
pipeline.errors.add(:base, 'Commit not found')
return pipeline
end
unless can?(current_user, :create_pipeline, project)
pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline')
return pipeline
end
begin
Ci::Commit.transaction do
pipeline.sha = commit.id
unless pipeline.config_processor
pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
raise ActiveRecord::Rollback
end
pipeline.save!
pipeline.create_builds(current_user)
end
rescue
pipeline.errors.add(:base, 'The pipeline could not be created. Please try again.')
end
pipeline
end
private
def ref_names
@ref_names ||= project.repository.ref_names
end
def commit
@commit ||= project.commit(params[:ref])
end
end
end
......@@ -18,9 +18,7 @@ class CreateCommitBuildsService
return false
end
commit = project.ci_commit(sha, ref)
unless commit
commit = project.ci_commits.new(sha: sha, ref: ref, before_sha: before_sha, tag: tag)
commit = Ci::Commit.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
# Skip creating ci_commit when no gitlab-ci.yml is found
unless commit.ci_yaml_file
......@@ -29,7 +27,6 @@ class CreateCommitBuildsService
# Create a new ci_commit
commit.save!
end
# Skip creating builds for commits that have [ci skip]
unless commit.skip_ci?
......
module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails
def execute(commit_status)
each_merge_request(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request)
end
end
# Closes any pending build failed todos for the parent MRs when a build is retried
def close(commit_status)
each_merge_request(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request)
end
end
end
end
......@@ -38,5 +38,30 @@ module MergeRequests
def filter_params
super(:merge_request)
end
def merge_request_from(commit_status)
branches = commit_status.ref
# This is for ref-less builds
branches ||= @project.repository.branch_names_contains(commit_status.sha)
return [] if branches.blank?
merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
merge_requests.uniq.select(&:source_project)
end
def each_merge_request(commit_status)
merge_request_from(commit_status).each do |merge_request|
ci_commit = merge_request.ci_commit
next unless ci_commit
next unless ci_commit.sha == commit_status.sha
yield merge_request, ci_commit
end
end
end
end
......@@ -20,15 +20,9 @@ module MergeRequests
# Triggers the automatic merge of merge_request once the build succeeds
def trigger(commit_status)
merge_requests = merge_request_from(commit_status)
merge_requests.each do |merge_request|
each_merge_request(commit_status) do |merge_request, ci_commit|
next unless merge_request.merge_when_build_succeeds?
next unless merge_request.mergeable?
ci_commit = merge_request.ci_commit
next unless ci_commit
next unless ci_commit.sha == commit_status.sha
next unless ci_commit.success?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
......@@ -47,20 +41,5 @@ module MergeRequests
end
end
private
def merge_request_from(commit_status)
branches = commit_status.ref
# This is for ref-less builds
branches ||= @project.repository.branch_names_contains(commit_status.sha)
return [] if branches.blank?
merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a
merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a
merge_requests.uniq.select(&:source_project)
end
end
end
......@@ -12,6 +12,7 @@ module MergeRequests
close_merge_requests
reload_merge_requests
reset_merge_when_build_succeeds
mark_pending_todos_done
# Leave a system note if a branch was deleted/added
if branch_added? || branch_removed?
......@@ -80,6 +81,12 @@ module MergeRequests
merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds)
end
def mark_pending_todos_done
merge_requests_for_source_branch.each do |merge_request|
todo_service.merge_request_push(merge_request, @current_user)
end
end
def find_new_commits
if branch_added?
@commits = []
......
......@@ -80,6 +80,30 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
end
# When a build fails on the HEAD of a merge request we should:
#
# * create a todo for that user to fix it
#
def merge_request_build_failed(merge_request)
create_build_failed_todo(merge_request)
end
# When a new commit is pushed to a merge request we should:
#
# * mark all pending todos related to the merge request for that user as done
#
def merge_request_push(merge_request, current_user)
mark_pending_todos_as_done(merge_request, current_user)
end
# When a build is retried to a merge request we should:
#
# * mark all pending todos related to the merge request for the author as done
#
def merge_request_build_retried(merge_request)
mark_pending_todos_as_done(merge_request, merge_request.author)
end
# When create a note we should:
#
# * mark all pending todos related to the noteable for the note author as done
......@@ -145,6 +169,12 @@ class TodoService
create_todos(mentioned_users, attributes)
end
def create_build_failed_todo(merge_request)
author = merge_request.author
attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED)
create_todos(author, attributes)
end
def attributes_for_target(target)
attributes = {
project_id: target.project.id,
......
......@@ -9,8 +9,6 @@
%span.runner-state.runner-state-specific
Specific
- if @runner.shared?
.bs-callout.bs-callout-success
%h4 This runner will process builds from ALL UNASSIGNED projects
......
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
.todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
.todo-title.title
- unless todo.build_failed?
%span.author-name
- if todo.author
= link_to_author(todo)
......
......@@ -39,6 +39,13 @@
Commits
- if project_nav_tab? :builds
= nav_link(controller: :pipelines) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
= icon('ship fw')
%span
Pipelines
%span.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count)
= nav_link(controller: %w(builds)) do
= link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
= icon('cubes fw')
......
......@@ -4,6 +4,7 @@
%title
GitLab
= stylesheet_link_tag 'notify'
= yield :head
%body
%div.content
= yield
......
= content_for :head do
= stylesheet_link_tag 'mailers/repository_push_email'
%h3
#{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name}
at #{link_to(@message.project_name_with_namespace, namespace_project_url(@message.project_namespace, @message.project))}
......@@ -43,26 +46,38 @@
= diff.new_path
- unless @message.disable_diffs?
- diff_files = @message.diffs
- if @message.compare_timeout
%h5 The diff was not included because it is too large.
- else
%h4 Changes:
- @message.diffs.each_with_index do |diff, i|
- diff_files.each_with_index do |diff_file, i|
%li{id: "diff-#{i}"}
%a{href: @message.target_url + "#diff-#{i}"}
- if diff.deleted_file
%strong
= diff.old_path
%a{href: @message.target_url + "#diff-#{i}"}<
- if diff_file.deleted_file
%strong<
= diff_file.old_path
deleted
- elsif diff.renamed_file
%strong
= diff.old_path
- elsif diff_file.renamed_file
%strong<
= diff_file.old_path
&rarr;
%strong
= diff.new_path
%strong<
= diff_file.new_path
- else
%strong<
= diff_file.new_path
- if diff_file.too_large?
The diff for this file was not included because it is too large.
- else
%strong
= diff.new_path
%hr
= color_email_diff(diff.diff)
- diff_commit = diff_file.deleted_file ? @message.diff_refs.first : @message.diff_refs.last
- blob = @message.project.repository.blob_for_diff(diff_commit, diff_file)
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
%table.code.white
- diff_file.highlighted_diff_lines.each do |line|
= render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: nil, plain: true}
- else
No preview for this file type
%br
- if @message.compare_timeout
%h5 Huge diff. To prevent performance issues changes are hidden
......@@ -25,24 +25,28 @@
- else
\- #{diff.new_path}
- unless @message.disable_diffs?
- if @message.compare_timeout
\
\
The diff was not included because it is too large.
- else
\
\
Changes:
- @message.diffs.each do |diff|
- @message.diffs.each do |diff_file|
\
\=====================================
- if diff.deleted_file
#{diff.old_path} deleted
- elsif diff.renamed_file
#{diff.old_path}#{diff.new_path}
- if diff_file.deleted_file
#{diff_file.old_path} deleted
- elsif diff_file.renamed_file
#{diff_file.old_path}#{diff_file.new_path}
- else
= diff.new_path
= diff_file.new_path
\=====================================
!= diff.diff
- if @message.compare_timeout
\
\
Huge diff. To prevent performance issues it was hidden
- if diff_file.too_large?
The diff for this file was not included because it is too large.
- else
!= diff_file.diff.diff
- if @message.target_url
\
\
......
......@@ -13,7 +13,9 @@
%strong ##{build.id}
- if build.stuck?
%i.fa.fa-warning.text-warning
= icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
- if defined?(retried) && retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
- if defined?(commit_sha) && commit_sha
%td
......@@ -40,7 +42,7 @@
%td
= build.name
%td
.pull-right
.label-container
- if build.tags.any?
- build.tags.each do |tag|
......@@ -55,10 +57,14 @@
%td.duration
- if build.duration
= icon("clock-o")
&nbsp;
#{duration_in_words(build.finished_at, build.started_at)}
%td.timestamp
- if build.finished_at
= icon("calendar")
&nbsp;
%span #{time_ago_with_tooltip(build.finished_at)}
- if defined?(coverage) && coverage
......@@ -70,11 +76,11 @@
.pull-right
- if can?(current_user, :read_build, build) && build.artifacts?
= link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do
%i.fa.fa-download
= icon('download')
- if can?(current_user, :update_build, build)
- 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
%i.fa.fa-remove.cred
= 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
%i.fa.fa-refresh
= icon('refresh')
- status = commit.status
%tr.commit
%td.commit-link
= link_to namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status)
%strong ##{commit.id}
%td
%div.branch-commit
- if commit.ref
= link_to commit.ref, namespace_project_commits_path(@project.namespace, @project, commit.ref), class: "monospace"
&middot;
= link_to commit.short_sha, namespace_project_commit_path(@project.namespace, @project, commit.sha), class: "commit-id monospace"
&nbsp;
- if commit.latest?
%span.label.label-success latest
- if commit.tag?
%span.label.label-primary tag
- if commit.triggered?
%span.label.label-primary triggered
- if commit.yaml_errors.present?
%span.label.label-danger.has-tooltip{ title: "#{commit.yaml_errors}" } yaml invalid
- if commit.builds.any?(&:stuck?)
%span.label.label-warning stuck
%p
%span
- if commit_data = commit.commit_data
= link_to_gfm commit_data.title, namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
- stages_status = commit.statuses.stages_status
- stages.each do |stage|
%td
- if status = stages_status[stage]
- tooltip = "#{stage.titleize}: #{status}"
%span.has-tooltip{ title: "#{tooltip}", class: "ci-status-icon-#{status}" }
= ci_icon_for_status(status)
%td
- if commit.started_at && commit.finished_at
%p
= icon("clock-o")
&nbsp;
#{duration_in_words(commit.finished_at, commit.started_at)}
- if commit.finished_at
%p
= icon("calendar")
&nbsp;
#{time_ago_with_tooltip(commit.finished_at)}
%td
.controls.hidden-xs.pull-right
- artifacts = commit.builds.latest.select { |b| b.artifacts? }
- if artifacts.present?
.dropdown.inline.build-artifacts
%button.dropdown-toggle.btn{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 #{build.name}
- if can?(current_user, :update_pipeline, @project)
&nbsp;
- if commit.retryable? && commit.builds.failed.any?
= link_to retry_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do
= icon("repeat")
&nbsp;
- if commit.active?
= link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
= icon("remove")
- @ci_commits.each do |ci_commit|
= render "ci_commit", ci_commit: ci_commit
= render "ci_commit", ci_commit: ci_commit, pipeline_details: true
.row-content-block.build-content.middle-block
.pull-right
- if can?(current_user, :update_build, @project)
- if can?(current_user, :update_pipeline, @project)
- if ci_commit.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post
= link_to "Retry failed", retry_namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), class: 'btn btn-grouped btn-primary', method: :post
- if ci_commit.builds.running_or_pending.any?
= link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
= link_to "Cancel running", cancel_namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
.oneline
.oneline.clearfix
- if defined?(pipeline_details) && pipeline_details
Pipeline
= link_to "##{ci_commit.id}", namespace_project_pipeline_path(@project.namespace, @project, ci_commit.id), class: "monospace"
with
= pluralize ci_commit.statuses.count(:id), "build"
- if ci_commit.ref
for
%span.label.label-info
= ci_commit.ref
= link_to ci_commit.ref, namespace_project_commits_path(@project.namespace, @project, ci_commit.ref), class: "monospace"
- if defined?(link_to_commit) && link_to_commit
for commit
= link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace"
......@@ -34,38 +37,5 @@
.table-holder
%table.table.builds
%thead
%tr
%th Status
%th Build ID
%th Stage
%th Name
%th Tags
%th Duration
%th Finished at
- if @project.build_coverage_enabled?
%th Coverage
%th
- builds = ci_commit.statuses.latest.ordered
= render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true
- if ci_commit.retried.any?
.row-content-block.second-block
Retried builds
.table-holder
%table.table.builds
%thead
%tr
%th Status
%th Build ID
%th Ref
%th Stage
%th Name
%th Tags
%th Duration
%th Finished at
- if @project.build_coverage_enabled?
%th Coverage
%th
= render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false
- ci_commit.statuses.stages.each do |stage|
= render 'projects/commit/ci_stage', stage: stage, statuses: ci_commit.statuses.where(stage: stage)
%tr
%th{colspan: 10}
%strong
- status = statuses.latest.status
%span{class: "ci-status-link ci-status-icon-#{status}"}
= ci_icon_for_status(status)
- if stage
&nbsp;
= stage.titleize.pluralize
= render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
= render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
%tr
%td{colspan: 10}
&nbsp;
.pull-right.commit-action-buttons
%div
- if @notes_count > 0
- if defined?(@notes_count) && @notes_count > 0
%span.btn.disabled.btn-grouped
%i.fa.fa-comment
= @notes_count
......@@ -23,11 +23,6 @@
%p
.commit-info-row
- if @commit.status
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do
= ci_icon_for_status(@commit.status)
build:
= ci_label_for_status(@commit.status)
%span.light Authored by
%strong
= commit_author_link(@commit, avatar: true, size: 24)
......@@ -51,6 +46,17 @@
%span.commit-info.branches
%i.fa.fa-spinner.fa-spin
- if @commit.status
.commit-info-row
Builds for
= pluralize(@commit.ci_commits.count, 'pipeline')
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do
= ci_icon_for_status(@commit.status)
= ci_label_for_status(@commit.status)
- if @commit.ci_commits.duration
in
= time_interval_in_words @commit.ci_commits.duration
.commit-box.content-block
%h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line
......
......@@ -17,7 +17,7 @@
.pull-right
- if commit.status
= render_ci_status(commit)
= render_commit_status(commit)
= clipboard_button(clipboard_text: commit.id)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
......
......@@ -41,7 +41,7 @@
.diff-content.diff-wrap-lines
- # Skip all non non-supported blobs
- return unless blob.respond_to?('text?')
- return unless blob.respond_to?(:text?)
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
- elsif blob_text_viewable?(blob) && !project.repository.diffable?(blob)
......
......@@ -12,6 +12,9 @@
- else
%strong ##{generic_commit_status.id}
- if defined?(retried) && retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.')
- if defined?(commit_sha) && commit_sha
%td
= link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
......@@ -42,13 +45,19 @@
- generic_commit_status.tags.each do |tag|
%span.label.label-primary
= tag
- if defined?(retried) && retried
%span.label.label-warning retried
%td.duration
- if generic_commit_status.duration
= icon("clock-o")
&nbsp;
#{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
%td.timestamp
- if generic_commit_status.finished_at
= icon("calendar")
&nbsp;
%span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
- if defined?(coverage) && coverage
......
......@@ -7,7 +7,7 @@
%li
%span.merge-request-ci-status
- if merge_request.ci_commit
= render_ci_status(merge_request.ci_commit)
= render_pipeline_status(merge_request.ci_commit)
- elsif has_any_ci
= icon('blank fw')
%span.merge-request-id
......
......@@ -8,7 +8,7 @@
- ci_commit = @project.ci_commit(sha, branch) if sha
- if ci_commit
%span.related-branch-ci-status
= render_ci_status(ci_commit)
= render_pipeline_status(ci_commit)
%span.related-branch-info
%strong
= link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do
......
......@@ -13,7 +13,7 @@
- if merge_request.ci_commit
%li
= render_ci_status(merge_request.ci_commit)
= render_pipeline_status(merge_request.ci_commit)
- if merge_request.open? && merge_request.broken?
%li
......
- header_title project_title(@project, "Pipelines", project_pipelines_path(@project))
%p
.commit-info-row
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace"
with
= pluralize @pipeline.statuses.count(:id), "build"
- if @pipeline.ref
for
= link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
- if @pipeline.duration
in
= time_interval_in_words @pipeline.duration
.pull-right
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
= ci_icon_for_status(@pipeline.status)
= ci_label_for_status(@pipeline.status)
- if @commit
.commit-info-row
%span.light Authored by
%strong
= commit_author_link(@commit, avatar: true, size: 24)
#{time_ago_with_tooltip(@commit.authored_date)}
.commit-info-row
%span.light Commit
= link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace"
= clipboard_button(clipboard_text: @pipeline.sha)
- if @commit
.commit-box.content-block
%h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line
- if @commit.description.present?
%pre.commit-description
= preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
- page_title "Pipelines"
= render "header_title"
.top-area
%ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to project_pipelines_path(@project) do
All
%span.badge.js-totalbuilds-count
= number_with_delimiter(@pipelines_count)
%li{class: ('active' if @scope == 'running')}
= link_to project_pipelines_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@running_or_pending_count)
%li{class: ('active' if @scope == 'branches')}
= link_to project_pipelines_path(@project, scope: :branches) do
Branches
%li{class: ('active' if @scope == 'tags')}
= link_to project_pipelines_path(@project, scope: :tags) do
Tags
.nav-controls
- if can? current_user, :create_pipeline, @project
= link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do
= icon('plus')
New pipeline
- unless @repository.gitlab_ci_yml
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do
= icon('wrench')
%span CI Lint
.row-content-block
- if @scope == 'running'
Running pipelines for this project
- elsif @scope.nil?
Pipelines for this project
- else
#{@scope.titleize} for this project
%ul.content-list
- stages = @pipelines.stages
- if @pipelines.blank?
%li
.nothing-here-block No pipelines to show
- else
.table-holder
%table.table.builds
%tbody
%th ID
%th Commit
- stages.each do |stage|
%th
%span.pipeline-stage.has-tooltip{ title: "#{stage.titleize}" }
= stage.titleize.pluralize
%th
%th
= render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
= paginate @pipelines, theme: 'gitlab'
- page_title "New Pipeline"
= render "header_title"
%h3.page-title
New Pipeline
%hr
= form_for @pipeline, as: :pipeline, url: namespace_project_pipelines_path(@project.namespace, @project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
.form-group
= f.label :ref, 'Create for', class: 'control-label'
.col-sm-10
= f.text_field :ref, required: true, tabindex: 2, class: 'form-control'
.help-block Existing branch name, tag
.form-actions
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', namespace_project_pipelines_path(@project.namespace, @project), class: 'btn btn-cancel'
:javascript
var availableRefs = #{@project.repository.ref_names.to_json};
new NewBranchForm($('.js-new-pipeline-form'), availableRefs)
- page_title "Pipeline"
= render "header_title"
.prepend-top-default
- if @commit
= render "projects/pipelines/info"
%div.block-connector
= render "projects/commit/ci_commit", ci_commit: @pipeline
= form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f|
= form_errors(runner)
.form-group
= label :active, "Active", class: 'control-label'
.col-sm-10
.checkbox
= f.check_box :active
%span.light Paused runners don't accept new builds
.form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10
.checkbox
= f.check_box :run_untagged
%span.light Indicates whether this runner can pick jobs without tags
.form-group
= label_tag :token, class: 'control-label' do
Token
......
......@@ -5,7 +5,7 @@
- if @runners.include?(runner)
= link_to runner.short_sha, runner_path(runner)
%small
=link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
= link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
%i.fa.fa-edit.btn
- else
= runner.short_sha
......
- page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners"
%h4 Runner ##{@runner.id}
%hr
= render 'form', runner: @runner, runner_form_url: runner_path(@runner)
......@@ -17,50 +17,39 @@
%th Property Name
%th Value
%tr
%td
Tags
%td Active
%td= @runner.active? ? 'Yes' : 'No'
%tr
%td Can run untagged jobs
%td= @runner.run_untagged? ? 'Yes' : 'No'
%tr
%td Tags
%td
- @runner.tag_list.each do |tag|
%span.label.label-primary
= tag
%tr
%td
Name
%td
= @runner.name
%td Name
%td= @runner.name
%tr
%td
Version
%td
= @runner.version
%td Version
%td= @runner.version
%tr
%td
Revision
%td
= @runner.revision
%td Revision
%td= @runner.revision
%tr
%td
Platform
%td
= @runner.platform
%td Platform
%td= @runner.platform
%tr
%td
Architecture
%td
= @runner.architecture
%td Architecture
%td= @runner.architecture
%tr
%td
Description
%td
= @runner.description
%td Description
%td= @runner.description
%tr
%td
Last contact
%td Last contact
%td
- if @runner.contacted_at
#{time_ago_in_words(@runner.contacted_at)} ago
- else
Never
......@@ -17,7 +17,7 @@
= project.main_language
- if project.commit.try(:status)
%span
= render_ci_status(project.commit)
= render_commit_status(project.commit)
- if forks
%span
= icon('code-fork')
......
......@@ -27,15 +27,18 @@ class EmailsOnPushWorker
:push
end
diff_refs = nil
compare = nil
reverse_compare = false
if action == :push
compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha)
diff_refs = [project.merge_base_commit(before_sha, after_sha), project.commit(after_sha)]
return false if compare.same
if compare.commits.empty?
compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha)
diff_refs = [project.merge_base_commit(after_sha, before_sha), project.commit(before_sha)]
reverse_compare = true
......@@ -53,6 +56,7 @@ class EmailsOnPushWorker
action: action,
compare: compare,
reverse_compare: reverse_compare,
diff_refs: diff_refs,
send_from_committer_email: send_from_committer_email,
disable_diffs: disable_diffs
)
......
......@@ -26,6 +26,8 @@ module Gitlab
#{config.root}/app/models/members
#{config.root}/app/models/project_services))
config.generators.templates.push("#{config.root}/generator_templates")
# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named.
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
......@@ -78,6 +80,7 @@ module Gitlab
config.assets.precompile << "*.png"
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/repository_push_email.css"
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
......
......@@ -666,6 +666,13 @@ Rails.application.routes.draw do
resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :triggers, only: [:index, :create, :destroy]
resources :pipelines, only: [:index, :new, :create, :show] do
member do
post :cancel
post :retry
end
end
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
......
class AddRunUntaggedToCiRunner < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
add_column_with_default(:ci_runners, :run_untagged, :boolean,
default: true, allow_null: false)
end
def down
remove_column(:ci_runners, :run_untagged)
end
end
......@@ -269,6 +269,7 @@ ActiveRecord::Schema.define(version: 20160509201028) do
t.string "revision"
t.string "platform"
t.string "architecture"
t.boolean "run_untagged", default: true, null: false
end
add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
......
......@@ -125,7 +125,13 @@ shared runners will only run the jobs they are equipped to run.
For instance, at GitLab we have runners tagged with "rails" if they contain
the appropriate dependencies to run Rails test suites.
### Be Careful with Sensitive Information
### Prevent runner with tags from picking jobs without tags
You can configure a runner to prevent it from picking jobs with tags when
the runnner does not have tags assigned. This setting is available on each
runner in *Project Settings* > *Runners*.
### Be careful with sensitive information
If you can run a build on a runner, you can get access to any code it runs
and get the token of the runner. With shared runners, this means that anyone
......
......@@ -128,7 +128,7 @@ builds, including deploy builds. This can be an array or a multi-line string.
### after_script
>**Note:**
Introduced in GitLab 8.7 and GitLab Runner v1.2.
Introduced in GitLab 8.7 and requires Gitlab Runner v1.2 (not yet released)
`after_script` is used to define the command that will be run after for all
builds. This has to be an array or a multi-line string.
......
......@@ -13,7 +13,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
end
step 'I should see "Shop" project CI status' do
expect(page).to have_link "Build skipped"
expect(page).to have_link "Commit: skipped"
end
step 'I should see last push widget' do
......
......@@ -173,7 +173,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I see commit ci info' do
expect(page).to have_content "build: pending"
expect(page).to have_content "Builds for 1 pipeline pending"
end
step 'I click status link' do
......@@ -181,7 +181,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I see builds list' do
expect(page).to have_content "build: pending"
expect(page).to have_content "Builds for 1 pipeline pending"
expect(page).to have_content "1 build"
end
......
......@@ -525,7 +525,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see merge request "Bug NS-05" with CI status' do
page.within ".mr-list" do
expect(page).to have_link "Build pending"
expect(page).to have_link "Pipeline: pending"
end
end
......
......@@ -408,6 +408,7 @@ module API
class RunnerDetails < Runner
expose :tag_list
expose :run_untagged
expose :version, :revision, :platform, :architecture
expose :contacted_at
expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? }
......
......@@ -49,7 +49,7 @@ module API
runner = get_runner(params[:id])
authenticate_update_runner!(runner)
attrs = attributes_for_keys [:description, :active, :tag_list]
attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged]
if runner.update(attrs)
present runner, with: Entities::RunnerDetails, current_user: current_user
else
......
......@@ -28,20 +28,20 @@ module Ci
post "register" do
required_attributes! [:token]
attributes = { description: params[:description],
tag_list: params[:tag_list] }
unless params[:run_untagged].nil?
attributes[:run_untagged] = params[:run_untagged]
end
runner =
if runner_registration_token_valid?
# Create shared runner. Requires admin access
Ci::Runner.create(
description: params[:description],
tag_list: params[:tag_list],
is_shared: true
)
Ci::Runner.create(attributes.merge(is_shared: true))
elsif project = Project.find_by(runners_token: params[:token])
# Create a specific runner for project.
project.runners.create(
description: params[:description],
tag_list: params[:tag_list]
)
project.runners.create(attributes)
end
return forbidden! unless runner
......
......@@ -47,7 +47,7 @@ module Gitlab
first['count'].
to_i
# Update in batches of 5% with an upper limit of 5000 rows.
# Update in batches of 5%
batch_size = ((total / 100.0) * 5.0).ceil
while processed < total
......
......@@ -5,6 +5,7 @@ module Gitlab
attr_reader :author_id, :ref, :action
include Gitlab::Routing.url_helpers
include DiffHelper
delegate :namespace, :name_with_namespace, to: :project, prefix: :project
delegate :name, to: :author, prefix: :author
......@@ -36,7 +37,7 @@ module Gitlab
end
def diffs
@diffs ||= (compare.diffs if compare)
@diffs ||= (safe_diff_files(compare.diffs, diff_refs) if compare)
end
def diffs_count
......@@ -47,6 +48,10 @@ module Gitlab
@opts[:compare]
end
def diff_refs
@opts[:diff_refs]
end
def compare_timeout
diffs.overflow? if diffs
end
......
......@@ -18,5 +18,9 @@ FactoryGirl.define do
commit_id RepoHelpers.sample_commit.id
target_type "Commit"
end
trait :build_failed do
action { Todo::BUILD_FAILED }
end
end
end
require 'spec_helper'
describe "Pipelines" do
include GitlabRoutingHelper
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
before do
login_as(user)
project.team << [user, :developer]
end
describe 'GET /:project/pipelines' do
let!(:pipeline) { create(:ci_commit, project: project, ref: 'master', status: 'running') }
[:all, :running, :branches].each do |scope|
context "displaying #{scope}" do
let(:project) { create(:project) }
before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) }
it { expect(page).to have_content(pipeline.short_sha) }
end
end
context 'cancelable pipeline' do
let!(:running) { create(:ci_build, :running, commit: pipeline, stage: 'test', commands: 'test') }
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_link('Cancel') }
it { expect(page).to have_selector('.ci-running') }
context 'when canceling' do
before { click_link('Cancel') }
it { expect(page).to_not have_link('Cancel') }
it { expect(page).to have_selector('.ci-canceled') }
end
end
context 'retryable pipelines' do
let!(:failed) { create(:ci_build, :failed, commit: pipeline, stage: 'test', commands: 'test') }
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_link('Retry') }
it { expect(page).to have_selector('.ci-failed') }
context 'when retrying' do
before { click_link('Retry') }
it { expect(page).to_not have_link('Retry') }
it { expect(page).to have_selector('.ci-pending') }
end
end
context 'downloadable pipelines' do
context 'with artifacts' do
let!(:with_artifacts) { create(:ci_build, :artifacts, :success, commit: pipeline, name: 'rspec tests', stage: 'test') }
before { visit namespace_project_pipelines_path(project.namespace, project) }
it { expect(page).to have_selector('.build-artifacts') }
it { expect(page).to have_link(with_artifacts.name) }
end
context 'without artifacts' do
let!(:without_artifacts) { create(:ci_build, :success, commit: pipeline, name: 'rspec', stage: 'test') }
it { expect(page).to_not have_selector('.build-artifacts') }
end
end
end
describe 'GET /:project/pipelines/:id' do
let(:pipeline) { create(:ci_commit, project: project, ref: 'master') }
before do
@success = create(:ci_build, :success, commit: pipeline, stage: 'build', name: 'build')
@failed = create(:ci_build, :failed, commit: pipeline, stage: 'test', name: 'test', commands: 'test')
@running = create(:ci_build, :running, commit: pipeline, stage: 'deploy', name: 'deploy')
@external = create(:generic_commit_status, status: 'success', commit: pipeline, name: 'jenkins', stage: 'external')
end
before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
it 'showing a list of builds' do
expect(page).to have_content('Tests')
expect(page).to have_content(@success.id)
expect(page).to have_content('Deploy')
expect(page).to have_content(@failed.id)
expect(page).to have_content(@running.id)
expect(page).to have_content(@external.id)
expect(page).to have_content('Retry failed')
expect(page).to have_content('Cancel running')
end
context 'retrying builds' do
it { expect(page).to_not have_content('retried') }
context 'when retrying' do
before { click_on 'Retry failed' }
it { expect(page).to_not have_content('Retry failed') }
it { expect(page).to have_content('retried') }
end
end
context 'canceling builds' do
it { expect(page).to_not have_selector('.ci-canceled') }
context 'when canceling' do
before { click_on 'Cancel running' }
it { expect(page).to_not have_content('Cancel running') }
it { expect(page).to have_selector('.ci-canceled') }
end
end
end
describe 'POST /:project/pipelines' do
let(:project) { create(:project) }
before { visit new_namespace_project_pipeline_path(project.namespace, project) }
context 'for valid commit' do
before { fill_in('Create for', with: 'master') }
context 'with gitlab-ci.yml' do
before { stub_ci_commit_to_return_yaml_file }
it { expect{ click_on 'Create pipeline' }.to change{ Ci::Commit.count }.by(1) }
end
context 'without gitlab-ci.yml' do
before { click_on 'Create pipeline' }
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
end
end
context 'for invalid commit' do
before do
fill_in('Create for', with: 'invalid reference')
click_on 'Create pipeline'
end
it { expect(page).to have_content('Reference not found') }
end
end
end
......@@ -110,4 +110,37 @@ describe "Runners" do
expect(page).to have_content(@specific_runner.platform)
end
end
feature 'configuring runners ability to picking untagged jobs' do
given(:project) { create(:empty_project) }
given(:runner) { create(:ci_runner) }
background do
project.team << [user, :master]
project.runners << runner
end
scenario 'user checks default configuration' do
visit namespace_project_runner_path(project.namespace, project, runner)
expect(page).to have_content 'Can run untagged jobs Yes'
end
context 'when runner has tags' do
before { runner.update_attribute(:tag_list, ['tag']) }
scenario 'user wants to prevent runner from running untagged job' do
visit runners_path(project)
page.within('.activated-specific-runners') do
first('small > a').click
end
uncheck 'runner_run_untagged'
click_button 'Save changes'
expect(page).to have_content 'Can run untagged jobs No'
expect(runner.reload.run_untagged?).to eq false
end
end
end
end
......@@ -57,7 +57,7 @@ describe Gitlab::Email::Message::RepositoryPush do
describe '#diffs' do
subject { message.diffs }
it { is_expected.to all(be_an_instance_of Gitlab::Git::Diff) }
it { is_expected.to all(be_an_instance_of Gitlab::Diff::File) }
end
describe '#diffs_count' do
......
......@@ -693,8 +693,9 @@ describe Notify do
let(:commits) { Commit.decorate(compare.commits, nil) }
let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) }
let(:send_from_committer_email) { false }
let(:diff_refs) { [project.merge_base_commit(sample_image_commit.id, sample_commit.id), project.commit(sample_commit.id)] }
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) }
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) }
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
......@@ -715,15 +716,15 @@ describe Notify do
is_expected.to have_body_text /Change some files/
end
it 'includes diffs' do
is_expected.to have_body_text /def archive_formats_regex/
it 'includes diffs with character-level highlighting' do
is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
end
it 'contains a link to the diff' do
is_expected.to have_body_text /#{diff_path}/
end
it 'doesn not contain the misleading footer' do
it 'does not contain the misleading footer' do
is_expected.not_to have_body_text /you are a member of/
end
......@@ -797,8 +798,9 @@ describe Notify do
let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
let(:commits) { Commit.decorate(compare.commits, nil) }
let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) }
let(:diff_refs) { [project.merge_base_commit(sample_commit.parent_id, sample_commit.id), project.commit(sample_commit.id)] }
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) }
subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) }
it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like "a user cannot unsubscribe through footer link"
......@@ -819,8 +821,8 @@ describe Notify do
is_expected.to have_body_text /Change some files/
end
it 'includes diffs' do
is_expected.to have_body_text /def archive_formats_regex/
it 'includes diffs with character-level highlighting' do
is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
end
it 'contains a link to the diff' do
......
......@@ -259,11 +259,11 @@ describe Ci::Build, models: true do
end
describe '#can_be_served?' do
let(:runner) { FactoryGirl.create :ci_runner }
let(:runner) { create(:ci_runner) }
before { build.project.runners << runner }
context 'runner without tags' do
context 'when runner does not have tags' do
it 'can handle builds without tags' do
expect(build.can_be_served?(runner)).to be_truthy
end
......@@ -274,23 +274,51 @@ describe Ci::Build, models: true do
end
end
context 'runner with tags' do
context 'when runner has tags' do
before { runner.tag_list = ['bb', 'cc'] }
it 'can handle builds without tags' do
expect(build.can_be_served?(runner)).to be_truthy
end
shared_examples 'tagged build picker' do
it 'can handle build with matching tags' do
build.tag_list = ['bb']
expect(build.can_be_served?(runner)).to be_truthy
end
it 'cannot handle build with not matching tags' do
it 'cannot handle build without matching tags' do
build.tag_list = ['aa']
expect(build.can_be_served?(runner)).to be_falsey
end
end
context 'when runner can pick untagged jobs' do
it 'can handle builds without tags' do
expect(build.can_be_served?(runner)).to be_truthy
end
it_behaves_like 'tagged build picker'
end
context 'when runner can not pick untagged jobs' do
before { runner.run_untagged = false }
it 'can not handle builds without tags' do
expect(build.can_be_served?(runner)).to be_falsey
end
it_behaves_like 'tagged build picker'
end
end
end
describe '#has_tags?' do
context 'when build has tags' do
subject { create(:ci_build, tag_list: ['tag']) }
it { is_expected.to have_tags }
end
context 'when build does not have tags' do
subject { create(:ci_build, tag_list: []) }
it { is_expected.to_not have_tags }
end
end
describe '#any_runners_online?' do
......
......@@ -10,7 +10,6 @@ describe Ci::Commit, models: true do
it { is_expected.to have_many(:builds) }
it { is_expected.to validate_presence_of :sha }
it { is_expected.to validate_presence_of :status }
it { is_expected.to delegate_method(:stages).to(:statuses) }
it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email }
......
require 'spec_helper'
describe Ci::Runner, models: true do
describe 'validation' do
context 'when runner is not allowed to pick untagged jobs' do
context 'when runner does not have tags' do
it 'is not valid' do
runner = build(:ci_runner, tag_list: [], run_untagged: false)
expect(runner).to be_invalid
end
end
context 'when runner has tags' do
it 'is valid' do
runner = build(:ci_runner, tag_list: ['tag'], run_untagged: false)
expect(runner).to be_valid
end
end
end
end
describe '#display_name' do
it 'should return the description if it has a value' do
runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
......@@ -114,7 +132,19 @@ describe Ci::Runner, models: true do
end
end
describe '#search' do
describe '#has_tags?' do
context 'when runner has tags' do
subject { create(:ci_runner, tag_list: ['tag']) }
it { is_expected.to have_tags }
end
context 'when runner does not have tags' do
subject { create(:ci_runner, tag_list: []) }
it { is_expected.to_not have_tags }
end
end
describe '.search' do
let(:runner) { create(:ci_runner, token: '123abc') }
it 'returns runners with a matching token' do
......
......@@ -184,21 +184,24 @@ describe API::Runners, api: true do
description = shared_runner.description
active = shared_runner.active
put api("/runners/#{shared_runner.id}", admin), description: "#{description}_updated", active: !active,
tag_list: ['ruby2.1', 'pgsql', 'mysql']
update_runner(shared_runner.id, admin, description: "#{description}_updated",
active: !active,
tag_list: ['ruby2.1', 'pgsql', 'mysql'],
run_untagged: 'false')
shared_runner.reload
expect(response.status).to eq(200)
expect(shared_runner.description).to eq("#{description}_updated")
expect(shared_runner.active).to eq(!active)
expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
expect(shared_runner.run_untagged?).to be false
end
end
context 'when runner is not shared' do
it 'should update runner' do
description = specific_runner.description
put api("/runners/#{specific_runner.id}", admin), description: 'test'
update_runner(specific_runner.id, admin, description: 'test')
specific_runner.reload
expect(response.status).to eq(200)
......@@ -208,10 +211,14 @@ describe API::Runners, api: true do
end
it 'should return 404 if runner does not exists' do
put api('/runners/9999', admin), description: 'test'
update_runner(9999, admin, description: 'test')
expect(response.status).to eq(404)
end
def update_runner(id, user, args)
put api("/runners/#{id}", user), args
end
end
context 'authorized user' do
......
......@@ -128,6 +128,38 @@ describe Ci::API::API do
end
end
end
context 'when build has no tags' do
before do
commit = create(:ci_commit, project: project)
create(:ci_build, commit: commit, tags: [])
end
context 'when runner is allowed to pick untagged builds' do
before { runner.update_column(:run_untagged, true) }
it 'picks build' do
register_builds
expect(response).to have_http_status 201
end
end
context 'when runner is not allowed to pick untagged builds' do
before { runner.update_column(:run_untagged, false) }
it 'does not pick build' do
register_builds
expect(response).to have_http_status 404
end
end
def register_builds
post ci_api("/builds/register"), token: runner.token,
info: { platform: :darwin }
end
end
end
describe "PUT /builds/:id" do
......
......@@ -12,44 +12,85 @@ describe Ci::API::API do
end
describe "POST /runners/register" do
describe "should create a runner if token provided" do
context 'when runner token is provided' do
before { post ci_api("/runners/register"), token: registration_token }
it { expect(response.status).to eq(201) }
it 'creates runner with default values' do
expect(response).to have_http_status 201
expect(Ci::Runner.first.run_untagged).to be true
end
end
describe "should create a runner with description" do
before { post ci_api("/runners/register"), token: registration_token, description: "server.hostname" }
context 'when runner description is provided' do
before do
post ci_api("/runners/register"), token: registration_token,
description: "server.hostname"
end
it { expect(response.status).to eq(201) }
it { expect(Ci::Runner.first.description).to eq("server.hostname") }
it 'creates runner' do
expect(response).to have_http_status 201
expect(Ci::Runner.first.description).to eq("server.hostname")
end
end
describe "should create a runner with tags" do
before { post ci_api("/runners/register"), token: registration_token, tag_list: "tag1, tag2" }
context 'when runner tags are provided' do
before do
post ci_api("/runners/register"), token: registration_token,
tag_list: "tag1, tag2"
end
it { expect(response.status).to eq(201) }
it { expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) }
it 'creates runner' do
expect(response).to have_http_status 201
expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"])
end
end
describe "should create a runner if project token provided" do
context 'when option for running untagged jobs is provided' do
context 'when tags are provided' do
it 'creates runner' do
post ci_api("/runners/register"), token: registration_token,
run_untagged: false,
tag_list: ['tag']
expect(response).to have_http_status 201
expect(Ci::Runner.first.run_untagged).to be false
end
end
context 'when tags are not provided' do
it 'does not create runner' do
post ci_api("/runners/register"), token: registration_token,
run_untagged: false
expect(response).to have_http_status 404
end
end
end
context 'when project token is provided' do
let(:project) { FactoryGirl.create(:empty_project) }
before { post ci_api("/runners/register"), token: project.runners_token }
it { expect(response.status).to eq(201) }
it { expect(project.runners.size).to eq(1) }
it 'creates runner' do
expect(response).to have_http_status 201
expect(project.runners.size).to eq(1)
end
end
it "should return 403 error if token is invalid" do
context 'when token is invalid' do
it 'returns 403 error' do
post ci_api("/runners/register"), token: 'invalid'
expect(response.status).to eq(403)
expect(response).to have_http_status 403
end
end
it "should return 400 error if no token" do
context 'when no token provided' do
it 'returns 400 error' do
post ci_api("/runners/register")
expect(response.status).to eq(400)
expect(response).to have_http_status 400
end
end
%w(name version revision platform architecture).each do |param|
......@@ -60,7 +101,7 @@ describe Ci::API::API do
it do
post ci_api("/runners/register"), token: registration_token, info: { param => value }
expect(response.status).to eq(201)
expect(response).to have_http_status 201
is_expected.to eq(value)
end
end
......@@ -71,7 +112,7 @@ describe Ci::API::API do
let!(:runner) { FactoryGirl.create(:ci_runner) }
before { delete ci_api("/runners/delete"), token: runner.token }
it { expect(response.status).to eq(200) }
it { expect(response).to have_http_status 200 }
it { expect(Ci::Runner.count).to eq(0) }
end
end
require 'spec_helper'
# Write specs in this file.
describe MergeRequests::AddTodoWhenBuildFailsService do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { create(:project) }
let(:sha) { '1234567890abcdef1234567890abcdef12345678' }
let(:ci_commit) { create(:ci_commit_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) }
let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user, commit_message: 'Awesome message') }
let(:todo_service) { TodoService.new }
let(:merge_request) do
create(:merge_request, merge_user: user, source_branch: 'master',
target_branch: 'feature', source_project: project, target_project: project,
state: 'opened')
end
before do
allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
allow(service).to receive(:todo_service).and_return(todo_service)
end
describe '#execute' do
context 'commit status with ref' do
let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, commit: ci_commit) }
it 'notifies the todo service' do
expect(todo_service).to receive(:merge_request_build_failed).with(merge_request)
service.execute(commit_status)
end
end
context 'commit status with non-HEAD ref' do
let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) }
it 'does not notify the todo service' do
expect(todo_service).not_to receive(:merge_request_build_failed)
service.execute(commit_status)
end
end
context 'commit status without ref' do
let(:commit_status) { create(:generic_commit_status) }
it 'does not notify the todo service' do
expect(todo_service).not_to receive(:merge_request_build_failed)
service.execute(commit_status)
end
end
end
describe '#close' do
context 'commit status with ref' do
let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, commit: ci_commit) }
it 'notifies the todo service' do
expect(todo_service).to receive(:merge_request_build_retried).with(merge_request)
service.close(commit_status)
end
end
context 'commit status with non-HEAD ref' do
let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) }
it 'does not notify the todo service' do
expect(todo_service).not_to receive(:merge_request_build_retried)
service.close(commit_status)
end
end
context 'commit status without ref' do
let(:commit_status) { create(:generic_commit_status) }
it 'does not notify the todo service' do
expect(todo_service).not_to receive(:merge_request_build_retried)
service.close(commit_status)
end
end
end
end
......@@ -27,6 +27,20 @@ describe MergeRequests::RefreshService, services: true do
target_branch: 'feature',
target_project: @project)
@build_failed_todo = create(:todo,
:build_failed,
user: @user,
project: @project,
target: @merge_request,
author: @user)
@fork_build_failed_todo = create(:todo,
:build_failed,
user: @user,
project: @project,
target: @merge_request,
author: @user)
@commits = @merge_request.commits
@oldrev = @commits.last.id
......@@ -51,6 +65,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.merge_when_build_succeeds).to be_falsey}
it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
it { expect(@build_failed_todo).to be_done }
it { expect(@fork_build_failed_todo).to be_done }
end
context 'push to origin repo target branch' do
......@@ -63,6 +79,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
end
context 'manual merge of source branch' do
......@@ -82,6 +100,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.diffs.size).to be > 0 }
it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push to fork repo source branch' do
......@@ -101,6 +121,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_open }
it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') }
it { expect(@fork_merge_request).to be_open }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push to fork repo target branch' do
......@@ -113,6 +135,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
it { expect(@fork_merge_request).to be_open }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push to origin repo target branch after fork project was removed' do
......@@ -126,6 +150,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push new branch that exists in a merge request' do
......@@ -153,6 +179,8 @@ describe MergeRequests::RefreshService, services: true do
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
@build_failed_todo.reload
@fork_build_failed_todo.reload
end
end
end
......@@ -305,6 +305,25 @@ describe TodoService, services: true do
expect(second_todo.reload).to be_done
end
end
describe '#merge_request_build_failed' do
it 'creates a pending todo for the merge request author' do
service.merge_request_build_failed(mr_unassigned)
should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED)
end
end
describe '#merge_request_push' do
it 'marks related pending todos to the target for the user as done' do
first_todo = create(:todo, :build_failed, user: author, project: project, target: mr_assigned, author: john_doe)
second_todo = create(:todo, :build_failed, user: john_doe, project: project, target: mr_assigned, author: john_doe)
service.merge_request_push(mr_assigned, author)
expect(first_todo.reload).to be_done
expect(second_todo.reload).not_to be_done
end
end
end
def should_create_todo(attributes = {})
......
......@@ -48,6 +48,22 @@ describe PostReceive do
PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
end
context "gitlab-ci.yml" do
subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) }
context "creates a Ci::Commit for every change" do
before { stub_ci_commit_to_return_yaml_file }
it { expect{ subject }.to change{ Ci::Commit.count }.by(2) }
end
context "does not create a Ci::Commit" do
before { stub_ci_commit_yaml_file(nil) }
it { expect{ subject }.to_not change{ Ci::Commit.count } }
end
end
end
context "webhook" do
......
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