Commit ed265429 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 082c051b2db8
......@@ -54,6 +54,7 @@ Click on the service links to see further configuration instructions and details
| [Prometheus](prometheus.md) | Monitor the performance of your deployed apps |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
| [Redmine](redmine.md) | Redmine issue tracker |
| [Unify Circuit](unify_circuit.md) | Receive events notifications in Unify Circuit |
| [YouTrack](youtrack.md) | YouTrack issue tracker |
## Push hooks limit
......
# Unify Circuit service
The Unify Circuit service sends notifications from GitLab to the conversation for which the webhook was created.
## On Unify Circuit
1. Open the conversation in which you want to see the notifications.
1. From the conversation menu, select **Configure Webhooks**.
1. Click **ADD WEBHOOK** and fill in the name of the bot that will post the messages. Optionally define avatar.
1. Click **SAVE** and copy the **Webhook URL** of your webhook.
For more information, see the [Unify Circuit documentation for configuring incoming webhooks](https://www.circuit.com/unifyportalfaqdetail?articleId=164448).
## On GitLab
When you have the **Webhook URL** for your Unify Circuit conversation webhook, you can set up the GitLab service.
1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) in your project's settings, i.e. **Project > Settings > Integrations**.
1. Select the **Unify Circuit** project service to configure it.
1. Check the **Active** checkbox to turn on the service.
1. Check the checkboxes corresponding to the GitLab events you want to receive in Unify Circuit.
1. Paste the **Webhook URL** that you copied from the Unify Circuit configuration step.
1. Configure the remaining options and click `Save changes`.
Your Unify Circuit conversation will now start receiving GitLab event notifications as configured.
![Unify Circuit configuration](img/unify_circuit_configuration.png)
......@@ -69,7 +69,7 @@ For example, to that on merge requests there is always a passing job even though
```yaml
enable_merge:
only: merge_requests
only: [merge_requests]
script:
- echo true
```
......
......@@ -132,6 +132,12 @@ def self.chat_notification_events
type: Boolean,
desc: 'Enable notifications for note_events'
},
{
required: false,
name: :confidential_note_events,
type: Boolean,
desc: 'Enable notifications for confidential_note_events'
},
{
required: false,
name: :tag_push_events,
......@@ -696,7 +702,16 @@ def self.services
type: String,
desc: 'The password of the user'
}
]
],
'unify-circuit' => [
{
required: true,
name: :webhook,
type: String,
desc: 'The Unify Circuit webhook. e.g. https://circuit.com/rest/v2/webhooks/incoming/…'
},
chat_notification_events
].flatten
}
end
......
......@@ -2,6 +2,8 @@
module Gitaly
class Server
SHA_VERSION_REGEX = /\A\d+\.\d+\.\d+-\d+-g([a-f0-9]{8})\z/.freeze
class << self
def all
Gitlab.config.repositories.storages.keys.map { |s| Gitaly::Server.new(s) }
......@@ -30,9 +32,10 @@ def git_binary_version
info.git_version
end
def up_to_date?
server_version == Gitlab::GitalyClient.expected_server_version
def expected_version?
server_version == Gitlab::GitalyClient.expected_server_version || matches_sha?
end
alias_method :up_to_date?, :expected_version?
def read_writeable?
readable? && writeable?
......@@ -62,6 +65,13 @@ def storage_status
@storage_status ||= info.storage_statuses.find { |s| s.storage_name == storage }
end
def matches_sha?
match = server_version.match(SHA_VERSION_REGEX)
return false unless match
Gitlab::GitalyClient.expected_server_version.start_with?(match[1])
end
def info
@info ||=
begin
......
.auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.7.0"
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.8.0"
review:
extends: .auto-deploy
......
......@@ -155,6 +155,7 @@ def remove_concurrent_index_by_name(table_name, index_name, options = {})
# column - The name of the column to create the foreign key on.
# on_delete - The action to perform when associated data is removed,
# defaults to "CASCADE".
# name - The name of the foreign key.
#
# rubocop:disable Gitlab/RailsLogger
def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade, name: nil)
......@@ -164,25 +165,31 @@ def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade, nam
raise 'add_concurrent_foreign_key can not be run inside a transaction'
end
on_delete = 'SET NULL' if on_delete == :nullify
options = {
column: column,
on_delete: on_delete,
name: name.presence || concurrent_foreign_key_name(source, column)
}
key_name = name || concurrent_foreign_key_name(source, column)
unless foreign_key_exists?(source, target, column: column)
Rails.logger.warn "Foreign key not created because it exists already " \
if foreign_key_exists?(source, target, options)
warning_message = "Foreign key not created because it exists already " \
"(this may be due to an aborted migration or similar): " \
"source: #{source}, target: #{target}, column: #{column}"
"source: #{source}, target: #{target}, column: #{options[:column]}, "\
"name: #{options[:name]}, on_delete: #{options[:on_delete]}"
Rails.logger.warn warning_message
else
# Using NOT VALID allows us to create a key without immediately
# validating it. This means we keep the ALTER TABLE lock only for a
# short period of time. The key _is_ enforced for any newly created
# data.
execute <<-EOF.strip_heredoc
ALTER TABLE #{source}
ADD CONSTRAINT #{key_name}
FOREIGN KEY (#{column})
ADD CONSTRAINT #{options[:name]}
FOREIGN KEY (#{options[:column]})
REFERENCES #{target} (id)
#{on_delete ? "ON DELETE #{on_delete.upcase}" : ''}
#{on_delete_statement(options[:on_delete])}
NOT VALID;
EOF
end
......@@ -193,18 +200,15 @@ def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade, nam
#
# Note this is a no-op in case the constraint is VALID already
disable_statement_timeout do
execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};")
execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{options[:name]};")
end
end
# rubocop:enable Gitlab/RailsLogger
def foreign_key_exists?(source, target = nil, column: nil)
foreign_keys(source).any? do |key|
if column
key.options[:column].to_s == column.to_s
else
key.to_table.to_s == target.to_s
end
def foreign_key_exists?(source, target = nil, **options)
foreign_keys(source).any? do |foreign_key|
tables_match?(target.to_s, foreign_key.to_table.to_s) &&
options_match?(foreign_key.options, options)
end
end
......@@ -1050,6 +1054,21 @@ def postgres_exists_by_name?(table, name)
private
def tables_match?(target_table, foreign_key_table)
target_table.blank? || foreign_key_table == target_table
end
def options_match?(foreign_key_options, options)
options.all? { |k, v| foreign_key_options[k].to_s == v.to_s }
end
def on_delete_statement(on_delete)
return '' if on_delete.blank?
return 'ON DELETE SET NULL' if on_delete == :nullify
"ON DELETE #{on_delete.upcase}"
end
def create_column_from(table, old, new, type: nil)
old_col = column_for(table, old)
new_type = type || old_col.type
......
......@@ -34,12 +34,18 @@ def issue_latest_event(issue_id:)
end
def list_issues(**keyword_args)
issues = get_issues(keyword_args)
response = get_issues(keyword_args)
issues = response[:issues]
pagination = response[:pagination]
validate_size(issues)
handle_mapping_exceptions do
map_to_errors(issues)
{
issues: map_to_errors(issues),
pagination: pagination
}
end
end
......@@ -83,36 +89,40 @@ def http_get(url, params = {})
end
def get_issues(**keyword_args)
http_get(
response = http_get(
issues_api_url,
query: list_issue_sentry_query(keyword_args)
)
{
issues: response[:body],
pagination: Sentry::PaginationParser.parse(response[:headers])
}
end
def list_issue_sentry_query(issue_status:, limit:, sort: nil, search_term: '')
def list_issue_sentry_query(issue_status:, limit:, sort: nil, search_term: '', cursor: nil)
unless SENTRY_API_SORT_VALUE_MAP.key?(sort)
raise BadRequestError, 'Invalid value for sort param'
end
query_params = {
{
query: "is:#{issue_status} #{search_term}".strip,
limit: limit,
sort: SENTRY_API_SORT_VALUE_MAP[sort]
}
query_params.compact
sort: SENTRY_API_SORT_VALUE_MAP[sort],
cursor: cursor
}.compact
end
def get_issue(issue_id:)
http_get(issue_api_url(issue_id))
http_get(issue_api_url(issue_id))[:body]
end
def get_issue_latest_event(issue_id:)
http_get(issue_latest_event_api_url(issue_id))
http_get(issue_latest_event_api_url(issue_id))[:body]
end
def get_projects
http_get(projects_api_url)
http_get(projects_api_url)[:body]
end
def handle_request_exceptions
......@@ -138,7 +148,7 @@ def handle_response(response)
raise_error "Sentry response status code: #{response.code}"
end
response.parsed_response
{ body: response.parsed_response, headers: response.headers }
end
def raise_error(message)
......
# frozen_string_literal: true
module Sentry
module PaginationParser
PATTERN = /rel=\"(?<direction>\w+)\";\sresults=\"(?<results>\w+)\";\scursor=\"(?<cursor>.+)\"/.freeze
def self.parse(headers)
links = headers['link'].to_s.split(',')
links.map { |link| parse_link(link) }.compact.to_h
end
def self.parse_link(link)
match = link.match(PATTERN)
return unless match
return if match['results'] != "true"
[match['direction'], { 'cursor' => match['cursor'] }]
end
private_class_method :parse_link
end
end
......@@ -3361,6 +3361,9 @@ msgstr ""
msgid "Clear"
msgstr ""
msgid "Clear chart filters"
msgstr ""
msgid "Clear input"
msgstr ""
......@@ -20934,6 +20937,9 @@ msgstr ""
msgid "is not an email you own"
msgstr ""
msgid "is too long (%{current_value}). The maximum size is %{max_size}."
msgstr ""
msgid "is too long (maximum is 100 entries)"
msgstr ""
......
......@@ -2,8 +2,8 @@ source 'https://rubygems.org'
gem 'gitlab-qa'
gem 'activesupport', '5.2.3' # This should stay in sync with the root's Gemfile
gem 'capybara', '~> 2.16.1'
gem 'capybara-screenshot', '~> 1.0.18'
gem 'capybara', '~> 3.29.0'
gem 'capybara-screenshot', '~> 1.0.23'
gem 'rake', '~> 12.3.0'
gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.12'
......
......@@ -6,8 +6,8 @@ GEM
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
airborne (0.2.13)
activesupport
rack
......@@ -15,15 +15,16 @@ GEM
rest-client (>= 1.7.3, < 3.0)
rspec (~> 3.1)
byebug (9.1.0)
capybara (2.16.1)
capybara (3.29.0)
addressable
mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
capybara-screenshot (1.0.18)
capybara (>= 1.0, < 3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (~> 1.5)
xpath (~> 3.2)
capybara-screenshot (1.0.23)
capybara (>= 1.0, < 4)
launchy
childprocess (3.0.0)
coderay (1.1.2)
......@@ -50,7 +51,7 @@ GEM
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_mime (1.0.0)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.11.3)
netrc (0.11.0)
......@@ -65,11 +66,12 @@ GEM
pry-byebug (3.5.1)
byebug (~> 9.1)
pry (~> 0.10)
public_suffix (3.0.1)
rack (2.0.6)
rack-test (0.8.2)
public_suffix (4.0.1)
rack (2.0.7)
rack-test (0.8.3)
rack (>= 1.0, < 3)
rake (12.3.3)
rake (12.3.0)
regexp_parser (1.6.0)
rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
......@@ -103,8 +105,8 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.4)
xpath (2.1.0)
nokogiri (~> 1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
PLATFORMS
ruby
......@@ -112,8 +114,8 @@ PLATFORMS
DEPENDENCIES
activesupport (= 5.2.3)
airborne (~> 0.2.13)
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
capybara (~> 3.29.0)
capybara-screenshot (~> 1.0.23)
debase (~> 0.2.4.1)
faker (~> 1.6, >= 1.6.6)
gitlab-qa
......
......@@ -141,6 +141,10 @@ def has_no_text?(text)
page.has_no_text? text
end
def has_normalized_ws_text?(text, wait: Capybara.default_max_wait_time)
page.has_text?(text.gsub(/\s+/, " "), wait: wait)
end
def finished_loading?
has_no_css?('.fa-spinner', wait: Capybara.default_max_wait_time)
end
......
......@@ -14,11 +14,7 @@ class Index < QA::Page::Base
def click_on_latest_pipeline
css = '.js-pipeline-url-link'
link = wait(reload: false) do
first(css)
end
link.click
first(css, wait: 60).click
end
def wait_for_latest_pipeline_success
......
......@@ -132,6 +132,10 @@ def self.configure!
config.default_max_wait_time = CAPYBARA_MAX_WAIT_TIME
# https://github.com/mattheworiordan/capybara-screenshot/issues/164
config.save_path = ::File.expand_path('../../tmp', __dir__)
# Cabybara 3 does not normalize text by default, so older tests
# fail because of unexpected line breaks and other white space
config.default_normalize_ws = true
end
end
......
......@@ -52,16 +52,16 @@ module QA
Page::Project::Show.perform(&:create_new_file!)
Page::File::Form.perform do |form|
form.select_template template[:file_name], template[:name]
end
expect(page).to have_content(content[0..100])
expect(form).to have_normalized_ws_text(content[0..100])
Page::File::Form.perform(&:commit_changes)
form.commit_changes
expect(page).to have_content('The file has been successfully created.')
expect(page).to have_content(template[:file_name])
expect(page).to have_content('Add new file')
expect(page).to have_content(content[0..100])
expect(form).to have_content('The file has been successfully created.')
expect(form).to have_content(template[:file_name])
expect(form).to have_content('Add new file')
expect(form).to have_normalized_ws_text(content[0..100])
end
end
end
end
......
......@@ -54,15 +54,15 @@ module QA
ide.create_new_file_from_template template[:file_name], template[:name]
expect(ide.has_file?(template[:file_name])).to be_truthy
end
expect(page).to have_button('Undo')
expect(page).to have_content(content[0..100])
expect(ide).to have_button('Undo')
expect(ide).to have_normalized_ws_text(content[0..100])
Page::Project::WebIDE::Edit.perform(&:commit_changes)
ide.commit_changes
expect(page).to have_content(template[:file_name])
expect(page).to have_content(content[0..100])
expect(ide).to have_content(template[:file_name])
expect(ide).to have_normalized_ws_text(content[0..100])
end
end
end
end
......
......@@ -50,8 +50,6 @@
let(:external_url) { 'http://example.com' }
context 'no data' do
let(:params) { project_params(format: :json) }
let(:permitted_params) do
ActionController::Parameters.new({}).permit!
end
......@@ -72,11 +70,13 @@
end
end
context 'with a search_term and sort params' do
let(:params) { project_params(format: :json, search_term: 'something', sort: 'last_seen') }
context 'with extra params' do
let(:cursor) { '1572959139000:0:0' }
let(:search_term) { 'something' }
let(:sort) { 'last_seen' }
let(:params) { project_params(format: :json, search_term: search_term, sort: sort, cursor: cursor) }
let(:permitted_params) do
ActionController::Parameters.new(search_term: 'something', sort: 'last_seen').permit!
ActionController::Parameters.new(search_term: search_term, sort: sort, cursor: cursor).permit!
end
before do
......@@ -88,7 +88,7 @@
context 'service result is successful' do
before do
expect(list_issues_service).to receive(:execute)
.and_return(status: :success, issues: [error])
.and_return(status: :success, issues: [error], pagination: {})
expect(list_issues_service).to receive(:external_url)
.and_return(external_url)
end
......@@ -100,13 +100,16 @@
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/index')
expect(json_response['external_url']).to eq(external_url)
expect(json_response['errors']).to eq([error].as_json)
expect(json_response).to eq(
'errors' => [error].as_json,
'pagination' => {},
'external_url' => external_url
)
end
end
end
context 'without params' do
context 'without extra params' do
before do
expect(ErrorTracking::ListIssuesService)
.to receive(:new).with(project, user, {})
......@@ -116,7 +119,7 @@
context 'service result is successful' do
before do
expect(list_issues_service).to receive(:execute)
.and_return(status: :success, issues: [error])
.and_return(status: :success, issues: [error], pagination: {})
expect(list_issues_service).to receive(:external_url)
.and_return(external_url)
end
......@@ -128,8 +131,11 @@
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/index')
expect(json_response['external_url']).to eq(external_url)
expect(json_response['errors']).to eq([error].as_json)
expect(json_response).to eq(
'errors' => [error].as_json,
'pagination' => {},
'external_url' => external_url
)
end
end
......
......@@ -2,6 +2,7 @@
"type": "object",
"required": [
"external_url",
"pagination",
"errors"
],
"properties": {
......@@ -9,6 +10,9 @@
"errors": {
"type": "array",
"items": { "$ref": "error.json" }
},
"pagination": {
"type":