Commit bffcdf9b authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 259c0cc0
......@@ -11,18 +11,19 @@ import {
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import DateTimePicker from './date_time_picker/date_time_picker.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import { getTimeDiff, isValidDate } from '../utils';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getTimeDiff, isValidDate, getAddMetricTrackingOptions } from '../utils';
export default {
components: {
......@@ -43,6 +44,7 @@ export default {
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
props: {
externalDashboardUrl: {
......@@ -298,6 +300,7 @@ export default {
onDateTimePickerApply(timeWindowUrlParams) {
return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
},
getAddMetricTrackingOptions,
},
addMetric: {
title: s__('Metrics|Add metric'),
......@@ -389,9 +392,10 @@ export default {
</gl-button>
<gl-button
v-if="addingMetricsAvailable"
ref="addMetricBtn"
v-gl-modal="$options.addMetric.modalId"
variant="outline-success"
class="mr-2 mt-1 js-add-metric-button"
class="mr-2 mt-1"
>
{{ $options.addMetric.title }}
</gl-button>
......@@ -411,6 +415,8 @@ export default {
<div slot="modal-footer">
<gl-button @click="hideAddMetricModal">{{ __('Cancel') }}</gl-button>
<gl-button
ref="submitCustomMetricsFormBtn"
v-track-event="getAddMetricTrackingOptions()"
:disabled="!formIsValid"
variant="success"
@click="submitCustomMetricsForm"
......
......@@ -115,6 +115,7 @@ export const generateLinkToChartOptions = chartLink => {
/**
* Tracks snowplow event when user downloads CSV of cluster metric
* @param {String} chart title that will be sent as a property for the event
* @return {Object} config object for event tracking
*/
export const downloadCSVOptions = title => {
const isCLusterHealthBoard = isClusterHealthBoard();
......@@ -129,6 +130,18 @@ export const downloadCSVOptions = title => {
return { category, action, label: 'Chart title', property: title };
};
/**
* Generate options for snowplow to track adding a new metric via the dashboard
* custom metric modal
* @return {Object} config object for event tracking
*/
export const getAddMetricTrackingOptions = () => ({
category: document.body.dataset.page,
action: 'click_button',
label: 'add_new_metric',
property: 'modal',
});
/**
* This function validates the graph data contains exactly 3 metrics plus
* value validations from graphDataValidatorForValues.
......
......@@ -15,7 +15,6 @@
}
.tree-controls {
display: flex;
text-align: right;
.btn {
......
......@@ -55,6 +55,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
render json: {
errors: serialize_errors(result[:issues]),
pagination: result[:pagination],
external_url: service.external_url
}
end
......@@ -111,7 +112,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
end
def list_issues_params
params.permit([:search_term, :sort])
params.permit(:search_term, :sort, :cursor)
end
def list_projects_params
......
......@@ -301,7 +301,8 @@ module ApplicationSettingsHelper
:snowplow_iglu_registry_url,
:push_event_hooks_limit,
:push_event_activities_limit,
:custom_http_clone_url_root
:custom_http_clone_url_root,
:snippet_size_limit
]
end
......
......@@ -226,6 +226,8 @@ class ApplicationSetting < ApplicationRecord
validates :push_event_activities_limit,
numericality: { greater_than_or_equal_to: 0 }
validates :snippet_size_limit, numericality: { only_integer: true, greater_than: 0 }
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
......
......@@ -26,7 +26,8 @@ module ApplicationSettingImplementation
'/users',
'/users/confirmation',
'/unsubscribes/',
'/import/github/personal_access_token'
'/import/github/personal_access_token',
'/admin/session'
].freeze
class_methods do
......@@ -139,7 +140,8 @@ module ApplicationSettingImplementation
snowplow_app_id: nil,
snowplow_iglu_registry_url: nil,
custom_http_clone_url_root: nil,
productivity_analytics_start_date: Time.now
productivity_analytics_start_date: Time.now,
snippet_size_limit: 50.megabytes
}
end
......
......@@ -104,7 +104,7 @@ module ErrorTracking
def calculate_reactive_cache(request, opts)
case request
when 'list_issues'
{ issues: sentry_client.list_issues(**opts.symbolize_keys) }
sentry_client.list_issues(**opts.symbolize_keys)
when 'issue_details'
{
issue: sentry_client.issue_details(**opts.symbolize_keys)
......
......@@ -170,6 +170,7 @@ class Project < ApplicationRecord
has_one :microsoft_teams_service
has_one :packagist_service
has_one :hangouts_chat_service
has_one :unify_circuit_service
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
......
# frozen_string_literal: true
class UnifyCircuitService < ChatNotificationService
def title
'Unify Circuit'
end
def description
'Receive event notifications in Unify Circuit'
end
def self.to_param
'unify_circuit'
end
def help
'This service sends notifications about projects events to a Unify Circuit conversation.<br />
To set up this service:
<ol>
<li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications.</li>
</ol>'
end
def event_field(event)
end
def default_channel_placeholder
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. https://circuit.com/rest/v2/webhooks/incoming/…", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: BRANCH_CHOICES }
]
end
private
def notify(message, opts)
response = Gitlab::HTTP.post(webhook, body: {
subject: message.project_name,
text: message.pretext,
markdown: true
}.to_json)
response if response.success?
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
......@@ -289,6 +289,7 @@ class Service < ApplicationRecord
slack
teamcity
microsoft_teams
unify_circuit
]
if Rails.env.development?
......
......@@ -46,6 +46,18 @@ class Snippet < ApplicationRecord
length: { maximum: 255 }
validates :content, presence: true
validates :content,
length: {
maximum: ->(_) { Gitlab::CurrentSettings.snippet_size_limit },
message: -> (_, data) do
current_value = ActiveSupport::NumberHelper.number_to_human_size(data[:value].size)
max_size = ActiveSupport::NumberHelper.number_to_human_size(Gitlab::CurrentSettings.snippet_size_limit)
_("is too long (%{current_value}). The maximum size is %{max_size}.") % { current_value: current_value, max_size: max_size }
end
},
if: :content_changed?
validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
# Scopes
......
......@@ -6,37 +6,24 @@ module ErrorTracking
DEFAULT_LIMIT = 20
DEFAULT_SORT = 'last_seen'
def execute
return error('Error Tracking is not enabled') unless enabled?
return error('Access denied', :unauthorized) unless can_read?
result = project_error_tracking_setting.list_sentry_issues(
issue_status: issue_status,
limit: limit,
search_term: search_term,
sort: sort
)
# our results are not yet ready
unless result
return error('Not ready. Try again later', :no_content)
end
if result[:error].present?
return error(result[:error], http_status_for(result[:error_type]))
end
success(issues: result[:issues])
end
def external_url
project_error_tracking_setting&.sentry_external_url
end
private
def fetch
project_error_tracking_setting.list_sentry_issues(
issue_status: issue_status,
limit: limit,
search_term: params[:search_term].presence,
sort: sort,
cursor: params[:cursor].presence
)
end
def parse_response(response)
{ issues: response[:issues] }
response.slice(:issues, :pagination)
end
def issue_status
......@@ -47,18 +34,6 @@ module ErrorTracking
params[:limit] || DEFAULT_LIMIT
end
def search_term
params[:search_term].presence
end
def enabled?
project_error_tracking_setting&.enabled?
end
def can_read?
can?(current_user, :read_sentry_issue, project)
end
def sort
params[:sort] || DEFAULT_SORT
end
......
......@@ -15,7 +15,7 @@
%li.breadcrumb-item
= link_to truncate(title, length: 40), browse_project_job_artifacts_path(@project, @build, path)
.tree-controls
.tree-controls<
= link_to download_project_job_artifacts_path(@project, @build),
rel: 'nofollow', download: '', class: 'btn btn-default download' do
= sprite_icon('download')
......
......@@ -17,21 +17,19 @@
- else
= link_to title, project_tree_path(@project, tree_join(@ref, path))
.tree-controls
.tree-controls<
= render 'projects/find_file_link'
-# only show normal/blame view links for text files
- if blob.readable_text?
- if blame
= link_to 'Normal view', project_blob_path(@project, @id),
class: 'btn'
- else
= link_to 'Blame', project_blame_path(@project, @id),
class: 'btn js-blob-blame-link' unless blob.empty?
.btn-group{ role: "group" }<
-# only show normal/blame view links for text files
- if blob.readable_text?
- if blame
= link_to 'Normal view', project_blob_path(@project, @id),
class: 'btn'
- else
= link_to 'Blame', project_blame_path(@project, @id),
class: 'btn js-blob-blame-link' unless blob.empty?
= link_to 'History', project_commits_path(@project, @id),
class: 'btn'
= link_to 'History', project_commits_path(@project, @id),
class: 'btn'
= link_to 'Permalink', project_blob_path(@project,
tree_join(@commit.sha, @path)), class: 'btn js-data-file-blob-permalink-url'
= link_to 'Permalink', project_blob_path(@project,
tree_join(@commit.sha, @path)), class: 'btn js-data-file-blob-permalink-url'
......@@ -7,7 +7,7 @@
- show_menu = can_create_issue || can_create_project_snippet || can_push_code || create_mr_from_new_fork || merge_project
- if show_menu
.project-action-button.dropdown.inline
.project-action-button.dropdown.inline<
%a.btn.dropdown-toggle.has-tooltip.qa-create-new-dropdown{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...'), 'data-display' => 'static' }
= icon('plus')
= icon("caret-down")
......
......@@ -13,7 +13,7 @@
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
.tree-controls.d-none.d-sm-none.d-md-block
.tree-controls.d-none.d-sm-none.d-md-block<
- if @merge_request.present?
.control
= link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'btn'
......
......@@ -75,7 +75,7 @@
= link_to new_project_tag_path(@project) do
#{ _('New tag') }
.tree-controls
.tree-controls<
= render_if_exists 'projects/tree/lock_link'
- if vue_file_list_enabled?
#js-tree-history-link.d-inline-block{ data: { history_link: project_commits_path(@project, @ref) } }
......@@ -85,20 +85,19 @@
= render 'projects/find_file_link'
- if can_create_mr_from_fork
= succeed " " do
- if can_collaborate || current_user&.already_forked?(@project)
- if vue_file_list_enabled?
#js-tree-web-ide-link.d-inline-block
- else
= link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
= _('Web IDE')
- if can_collaborate || current_user&.already_forked?(@project)
- if vue_file_list_enabled?
#js-tree-web-ide-link.d-inline-block
- else
= link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
= link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
= _('Web IDE')
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
- else
= link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
= _('Web IDE')
= render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
- if show_xcode_link?(@project)
.project-action-button.project-xcode.inline
.project-action-button.project-xcode.inline<
= render "projects/buttons/xcode_link"
= render 'projects/buttons/download', project: @project, ref: @ref
---
title: Track adding metric via monitoring dashboard
merge_request: 20818
author:
type: added
---
title: Remove whitespaces between tree-controls elements
merge_request: 20952
author:
type: other
---
title: Add admin mode controller path to Rack::Attack defaults
merge_request: 20735
author: Diego Louzán
type: changed
---
title: Add Unify Circuit project integration service
merge_request: 19849
author: Fabio Huser
type: added
---
title: Add created_before/after filter to group/project audit events
merge_request: 20641
author:
type: added
---
title: Add limit for snippet content size
merge_request: 20346
author:
type: performance
---
title: Upgrade auto-deploy-image for helm default values file
merge_request: 20588
author:
type: changed
# frozen_string_literal: true
class AddSnippetSizeLimitToApplicationSettings < ActiveRecord::Migration[5.2]
DOWNTIME = false
def up
add_column :application_settings, :snippet_size_limit, :bigint, default: 50.megabytes, null: false
end
def down
remove_column :application_settings, :snippet_size_limit
end
end
# frozen_string_literal: true
class AddAdminModeProtectedPath < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
ADMIN_MODE_ENDPOINT = '/admin/session'
OLD_DEFAULT_PROTECTED_PATHS = [
'/users/password',
'/users/sign_in',
'/api/v3/session.json',
'/api/v3/session',
'/api/v4/session.json',
'/api/v4/session',
'/users',
'/users/confirmation',
'/unsubscribes/',
'/import/github/personal_access_token'
]
NEW_DEFAULT_PROTECTED_PATHS = OLD_DEFAULT_PROTECTED_PATHS.dup << ADMIN_MODE_ENDPOINT
class ApplicationSetting < ActiveRecord::Base
self.table_name = 'application_settings'
end
def up
change_column_default :application_settings, :protected_paths, NEW_DEFAULT_PROTECTED_PATHS
# schema allows nulls for protected_paths
ApplicationSetting.where.not(protected_paths: nil).each do |application_setting|
unless application_setting.protected_paths.include?(ADMIN_MODE_ENDPOINT)
updated_protected_paths = application_setting.protected_paths << ADMIN_MODE_ENDPOINT
application_setting.update(protected_paths: updated_protected_paths)
end
end
end
def down
change_column_default :application_settings, :protected_paths, OLD_DEFAULT_PROTECTED_PATHS
# schema allows nulls for protected_paths
ApplicationSetting.where.not(protected_paths: nil).each do |application_setting|
if application_setting.protected_paths.include?(ADMIN_MODE_ENDPOINT)
updated_protected_paths = application_setting.protected_paths - [ADMIN_MODE_ENDPOINT]
application_setting.update(protected_paths: updated_protected_paths)
end
end
end
end
......@@ -328,7 +328,7 @@ ActiveRecord::Schema.define(version: 2019_11_25_140458) do
t.boolean "throttle_protected_paths_enabled", default: false, null: false
t.integer "throttle_protected_paths_requests_per_period", default: 10, null: false
t.integer "throttle_protected_paths_period_in_seconds", default: 60, null: false
t.string "protected_paths", limit: 255, default: ["/users/password", "/users/sign_in", "/api/v3/session.json", "/api/v3/session", "/api/v4/session.json", "/api/v4/session", "/users", "/users/confirmation", "/unsubscribes/", "/import/github/personal_access_token"], array: true
t.string "protected_paths", limit: 255, default: ["/users/password", "/users/sign_in", "/api/v3/session.json", "/api/v3/session", "/api/v4/session.json", "/api/v4/session", "/users", "/users/confirmation", "/unsubscribes/", "/import/github/personal_access_token", "/admin/session"], array: true
t.boolean "throttle_incident_management_notification_enabled", default: false, null: false
t.integer "throttle_incident_management_notification_period_in_seconds", default: 3600
t.integer "throttle_incident_management_notification_per_period", default: 3600
......@@ -361,6 +361,7 @@ ActiveRecord::Schema.define(version: 2019_11_25_140458) do
t.string "encrypted_slack_app_secret_iv", limit: 255
t.text "encrypted_slack_app_verification_token"
t.string "encrypted_slack_app_verification_token_iv", limit: 255
t.bigint "snippet_size_limit", default: 52428800, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
......
......@@ -158,6 +158,10 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Shared Runners pipelines quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota-starter-only): Limit the usage of pipeline minutes for Shared Runners. **(STARTER ONLY)**
- [Enable/disable Auto DevOps](../topics/autodevops/index.md#enablingdisabling-auto-devops): Enable or disable Auto DevOps for your instance.
## Snippet settings
- [Setting snippet content size limit](snippets/index.md): Set a maximum size limit for snippets' content.
## Git configuration options
- [Custom Git hooks](custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough.
......
---
type: reference, howto
---
# Snippets settings **(CORE ONLY)**
Adjust the snippets' settings of your GitLab instance.
## Snippets content size limit
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31133) in GitLab 12.6.
You can set a content size max limit in snippets. This limit can prevent
abuses of the feature. The default content size limit is **52428800 Bytes** (50MB).
### How does it work?
The content size limit will be applied when a snippet is created or
updated. Nevertheless, in order not to break any existing snippet,
the limit will only be enforced in stored snippets when the content
is updated.
### Snippets size limit configuration
This setting is not available through the [Admin Area settings](../../user/admin_area/settings/index.md).
In order to configure this setting, use either the Rails console
or the [Application settings API](../../api/settings.md).
NOTE: **IMPORTANT:**
The value of the limit **must** be in Bytes.
#### Through the Rails console
The steps to configure this setting through the Rails console are:
1. Start the Rails console:
```bash
# For Omnibus installations
sudo gitlab-rails console
# For installations from source
sudo -u git -H bundle exec rails console production
```
1. Update the snippets maximum file size:
```ruby
ApplicationSetting.first.update!(snippet_size_limit: 50.megabytes)
```
To retrieve the current value, start the Rails console and run:
```ruby
Gitlab::CurrentSettings.snippet_size_limit
```
#### Through the API
The process to set the snippets size limit through the Application Settings API is
exactly the same as you would do to [update any other setting](../../api/settings.md#change-application-settings).
```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/application/settings?snippet_size_limit=52428800
```
You can also use the API to [retrieve the current value](../../api/settings.md#get-current-application-settings).
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/application/settings
```
......@@ -229,6 +229,51 @@ Get Campfire service settings for a project.
GET /projects/:id/services/campfire
```
## Unify Circuit
Unify Circuit RTC and collaboration tool.
### Create/Edit Unify Circuit service
Set Unify Circuit service for a project.
```
PUT /projects/:id/services/unify-circuit
```
Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `webhook` | string | true | The Unify Circuit webhook. For example, `https://circuit.com/rest/v2/webhooks/incoming/...`. |
| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
| `branches_to_be_notified` | string | all | Branches to send notifications for. Valid options are "all", "default", "protected", and "default_and_protected" |
| `push_events` | boolean | false | Enable notifications for push events |
| `issues_events` | boolean | false | Enable notifications for issue events |
| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
| `tag_push_events` | boolean | false | Enable notifications for tag push events |
| `note_events` | boolean | false | Enable notifications for note events |
| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
### Delete Unify Circuit service
Delete Unify Circuit service for a project.
```
DELETE /projects/:id/services/unify-circuit
```
### Get Unify Circuit service settings
Get Unify Circuit service settings for a project.
```
GET /projects/:id/services/unify-circuit
```
## Custom Issue Tracker
Custom issue tracker
......@@ -480,6 +525,7 @@ Parameters:
| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
| `tag_push_events` | boolean | false | Enable notifications for tag push events |
| `note_events` | boolean | false | Enable notifications for note events |
| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
......@@ -1088,6 +1134,7 @@ Parameters:
| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
| `tag_push_events` | boolean | false | Enable notifications for tag push events |
| `note_events` | boolean | false | Enable notifications for note events |
| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
| `push_channel` | string | false | The name of the channel to receive push events notifications |
......@@ -1095,6 +1142,7 @@ Parameters:
| `confidential_issue_channel` | string | false | The name of the channel to receive confidential issues events notifications |
| `merge_request_channel` | string | false | The name of the channel to receive merge request events notifications |
| `note_channel` | string | false | The name of the channel to receive note events notifications |
| `confidential_note_channel` | boolean | The name of the channel to receive confidential note events notifications |
| `tag_push_channel` | string | false | The name of the channel to receive tag push events notifications |
| `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications |
| `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications |
......
......@@ -350,3 +350,4 @@ are listed in the descriptions of the relevant settings.
| `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. |
| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
| `web_ide_clientside_preview_enabled` | boolean | no | Client side evaluation (allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation). |
| `snippet_size_limit` | integer | no | Max snippet content size in **bytes**. Default: 52428800 Bytes (50MB).|
......@@ -228,5 +228,5 @@ our very powerful [`only/except` rules system](../yaml/README.md#onlyexcept-basi
```yaml
my_job:
only: branches
only: [branches]
```
......@@ -30,7 +30,7 @@ Pipelines for merge requests have the following requirements and limitations:
## Configuring pipelines for merge requests
To configure pipelines for merge requests, add the `only: merge_requests` parameter to
To configure pipelines for merge requests, add the `only: [merge_requests]` parameter to
the jobs that you want to run only for merge requests.
Then, when developers create or update merge requests, a pipeline runs
......@@ -68,7 +68,7 @@ After the merge request is updated with new commits:
- The pipeline fetches the latest code from the source branch and run tests against it.
In the above example, the pipeline contains only a `test` job.
Since the `build` and `deploy` jobs don't have the `only: merge_requests` parameter,
Since the `build` and `deploy` jobs don't have the `only: [merge_requests]` parameter,
they will not run in the merge request.
Pipelines tagged with the **detached** badge indicate that they were triggered
......@@ -86,7 +86,7 @@ Read the [documentation on Merge Trains](pipelines_for_merged_results/merge_trai
## Excluding certain jobs
The behavior of the `only: merge_requests` parameter is such that _only_ jobs with
The behavior of the `only: [merge_requests]` parameter is such that _only_ jobs with
that parameter are run in the context of a merge request; no other jobs will be run.
However, you may want to reverse this behavior, having all of your jobs to run _except_
......
......@@ -780,7 +780,7 @@ it is possible to define a job to be created based on files modified
in a merge request.
In order to deduce the correct base SHA of the source branch, we recommend combining
this keyword with `only: merge_requests`. This way, file differences are correctly
this keyword with `only: [merge_requests]`. This way, file differences are correctly
calculated from any further commits, thus all changes in the merge requests are properly
tested in pipelines.
......@@ -802,7 +802,7 @@ either files in `service-one` directory or the `Dockerfile`, GitLab creates
and triggers the `docker build service one` job.
Note that if [pipelines for merge requests](../merge_request_pipelines/index.md) is
combined with `only: change`, but `only: merge_requests` is omitted, there could be
combined with `only: [change]`, but `only: [merge_requests]` is omitted, there could be
unwanted behavior.
For example:
......
......@@ -699,7 +699,7 @@ nicely on different mobile devices.
## Code blocks
- Always wrap code added to a sentence in inline code blocks (`` ` ``).
E.g., `.gitlab-ci.yml`, `git add .`, `CODEOWNERS`, `only: master`.
E.g., `.gitlab-ci.yml`, `git add .`, `CODEOWNERS`, `only: [master]`.
File names, commands, entries, and anything that refers to code should be added to code blocks.
To make things easier for the user, always add a full code block for things that can be
useful to copy and paste, as they can easily do it with the button on code blocks.
......
......@@ -165,6 +165,54 @@ can quickly spiral out of control.
There are some cases where this may be needed. If this is the case this should
be clearly mentioned in the merge request description.
## Batch process
**Summary:** Iterating a single process to external services (e.g. PostgreSQL, Redis, Object Storage, etc)
should be executed in a **batch-style** in order to reduce connection overheads.
For fetching rows from various tables in a batch-style, please see [Eager Loading](#eager-loading) section.
### Example: Delete multiple files from Object Storage
When you delete multiple files from object storage (e.g. GCS),
executing a single REST API call multiple times is a quite expensive
process. Ideally, this should be done in a batch-style, for example, S3 provides
[batch deletion API](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html),
so it'd be a good idea to consider such an approach.
The `FastDestroyAll` module might help this situation. It's a
small framework when you remove a bunch of database rows and its associated data
in a batch style.
## Timeout
**Summary:** You should set a reasonable timeout when the system invokes HTTP calls
to external services (e.g. Kubernetes), and it should be executed in Sidekiq, not
in Puma/Unicorn threads.
Often, GitLab needs to communicate with an external service such as Kubernetes
clusters. In this case, it's hard to estimate when the external service finishes
the requested process, for example, if it's a user-owned cluster that is inactive for some reason,
GitLab might wait for the response forever ([Example](https://gitlab.com/gitlab-org/gitlab/issues/31475)).
This could result in Puma/Unicorn timeout and should be avoided at all cost.
You should set a reasonable timeout, gracefully handle exceptions and surface the
errors in UI or logging internally.
Using [`ReactiveCaching`](https://docs.gitlab.com/ee/development/utilities.html#reactivecaching) is one of the best solutions to fetch external data.
## Keep database transaction minimal
**Summary:** You should avoid accessing to external services (e.g. Gitaly) during database
transactions, otherwise it leads to severe contention problems
as an open transaction basically blocks the release of a Postgres backend connection.
For keeping transaction as minimal as possible, please consider using `AfterCommitQueue`
module or `after_commit` AR hook.
Here is [an example](https://gitlab.com/gitlab-org/gitlab/issues/36154#note_247228859)
that one request to Gitaly instance during transaction triggered a P1 issue.
## Eager Loading
**Summary:** always eager load associations when retrieving more than one row.
......
......@@ -53,7 +53,8 @@ default['gitlab']['gitlab-rails']['rack_attack_protected_paths'] = [
'/users',
'/users/confirmation',
'/unsubscribes/',
'/import/github/personal_access_token'
'/import/github/personal_access_token',
'/admin/session'
]
```
......
......@@ -651,6 +651,8 @@ procfile exec` to replicate the environment where your application will run.
#### Workers
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30628) in GitLab 12.6, `.gitlab/auto-deploy-values.yaml` will be used by default for Helm upgrades.
Some web applications need to run extra deployments for "worker processes". For
example, it is common in a Rails application to have a separate worker process
to run background tasks like sending emails.
......@@ -672,13 +674,18 @@ need to:
- Set a CI variable `K8S_SECRET_REDIS_URL`, which the URL of this instance to
ensure it's passed into your deployments.
Once you have configured your worker to respond to health checks, you will
need to configure a CI variable `HELM_UPGRADE_EXTRA_ARGS` with the value
`--values helm-values.yaml`.
Once you have configured your worker to respond to health checks, run a Sidekiq
worker for your Rails application. For:
- GitLab 12.6 and later, either:
- Add a file named `.gitlab/auto-deploy-values.yaml` to your repository. It will
be automatically used if found.
- Add a file with a different name or path to the repository, and override the value of the
`HELM_UPGRADE_VALUES_FILE` variable with the path and name.
- GitLab 12.5 and earlier, run the worker with the `--values` parameter that specifies
a file in the repository.
Then you can, for example, run a Sidekiq worker for your Rails application
by adding a file named `helm-values.yaml` to your repository with the following
content:
In any case, the file must contain the following:
```yml
workers:
......@@ -983,6 +990,7 @@ applications.
| `CANARY_PRODUCTION_REPLICAS` | Number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md) in the production environment. Takes precedence over `CANARY_REPLICAS`. Defaults to 1. |
| `CANARY_REPLICAS` | Number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md). Defaults to 1. |
| `HELM_RELEASE_NAME` | From GitLab 12.1, allows the `helm` release name to be overridden. Can be used to assign unique release names when deploying multiple projects to a single namespace. |
| `HELM_UPGRADE_VALUES_FILE` | From GitLab 12.6, allows the `helm upgrade` values file to be overridden. Defaults to `.gitlab/auto-deploy-values.yaml`. |
| `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, allows extra arguments in `helm` commands when deploying the application. Note that using quotes will not prevent word splitting. **Tip:** you can use this variable to [customize the Auto Deploy Helm chart](#custom-helm-chart) by applying custom override values with `--values my-values.yaml`. |
| `INCREMENTAL_ROLLOUT_MODE` | From GitLab 11.4, if present, can be used to enable an [incremental rollout](#incremental-rollout-to-production-premium) of your application for the production environment. Set to `manual` for manual deployment jobs or `timed` for automatic rollout deployments with a 5 minute delay each one. |
| `K8S_SECRET_*` | From GitLab 11.7, any variable prefixed with [`K8S_SECRET_`](#application-secret-variables) will be made available by Auto DevOps as environment variables to the deployed application. |
......
......@@ -14,7 +14,8 @@ GitLab protects the following paths with Rack Attack by default:
'/users',
'/users/confirmation',
'/unsubscribes/',
'/import/github/personal_access_token'
'/import/github/personal_access_token',
'/admin/session'
```
GitLab responds with HTTP status code `429` to POST requests at protected paths
......
......@@ -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 @@ module API
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 @@ module API
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 @@ module Gitaly
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 @@ module Gitaly
@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 @@ module Gitlab
# 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 @@ module Gitlab
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 @@ module Gitlab
#
# 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 @@ into similar problems in the future (e.g. when new tables are created).
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 @@ module Sentry
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 @@ module Sentry
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 @@ module Sentry
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 @@ module QA
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 @@ module QA::Page
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 @@ module QA
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 @@ describe Projects::ErrorTrackingController do
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 @@ describe Projects::ErrorTrackingController do
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 @@ describe Projects::ErrorTrackingController do
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 @@ describe Projects::ErrorTrackingController do
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 @@ describe Projects::ErrorTrackingController do
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 @@ describe Projects::ErrorTrackingController do
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": "object"
}
},
"additionalProperties": false
......
......@@ -20,6 +20,7 @@ describe('pipeline graph action component', () => {
actionIcon: 'cancel',
},
sync: false,
attachToDocument: true,
});
});
......
import { mount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import pipelineTriggerer from '~/pipelines/components/pipeline_triggerer.vue';
describe('Pipelines Triggerer', () => {
let wrapper;
const expectComponentWithProps = (Component, props = {}) => {
const componentWrapper = wrapper.find(Component);
expect(componentWrapper.isVisible()).toBe(true);
expect(componentWrapper.props()).toEqual(expect.objectContaining(props));
};
const mockData = {
pipeline: {
user: {
......@@ -15,9 +22,10 @@ describe('Pipelines Triggerer', () => {
};
const createComponent = () => {
wrapper = mount(pipelineTriggerer, {
wrapper = shallowMount(pipelineTriggerer, {
propsData: mockData,
sync: false,
attachToDocument: true,
});
};
......@@ -33,14 +41,12 @@ describe('Pipelines Triggerer', () => {
expect(wrapper.contains('.table-section')).toBe(true);
});
it('should render triggerer information when triggerer is provided', () => {
const link = wrapper.find('.js-pipeline-url-user');
expect(link.attributes('href')).toEqual(mockData.pipeline.user.path);
expect(link.find('.js-user-avatar-image-toolip').text()).toEqual(mockData.pipeline.user.name);
expect(link.find('img.avatar').attributes('src')).toEqual(
`${mockData.pipeline.user.avatar_url}?width=26`,
);
it('should pass triggerer information when triggerer is provided', () => {
expectComponentWithProps(UserAvatarLink, {
linkHref: mockData.pipeline.user.path,
tooltipText: mockData.pipeline.user.name,
imgSrc: mockData.pipeline.user.avatar_url,
});
});
it('should render "API" when no triggerer is provided', () => {
......@@ -50,7 +56,7 @@ describe('Pipelines Triggerer', () => {
},
});
wrapper.vm.$nextTick(() => {
return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API');
});
});
......
......@@ -65,4 +65,26 @@ describe Gitaly::Server do
end
end
end
describe '#expected_version?' do
using RSpec::Parameterized::TableSyntax
where(:expected_version, :server_version, :result) do
'1.1.1' | '1.1.1' | true
'1.1.2' | '1.1.1' | false
'1.73.0' | '1.73.0-18-gf756ebe2' | false
'594c3ea3e0e5540e5915bd1c49713a0381459dd6' | '1.55.6-45-g594c3ea3' | true
'594c3ea3e0e5540e5915bd1c49713a0381459dd6' | '1.55.6-46-gabc123ff' | false
'594c3ea3e0e5540e5915bd1c49713a0381459dd6' | '1.55.6' | false
end
with_them do
it do
allow(Gitlab::GitalyClient).to receive(:expected_server_version).and_return(expected_version)
allow(server).to receive(:server_version).and_return(server_version)
expect(server.expected_version?).to eq(result)
end
end
end
end
......@@ -290,6 +290,7 @@ project:
- microsoft_teams_service
- mattermost_service
- hangouts_chat_service
- unify_circuit_service
- buildkite_service
- bamboo_service
- teamcity_service
......
......@@ -54,10 +54,20 @@ describe Sentry::Client do
end
end
shared_examples 'issues has correct return type' do |klass|
it "returns objects of type #{klass}" do
expect(subject[:issues]).to all( be_a(klass) )
end
end
shared_examples 'has correct length' do |length|
it { expect(subject.length).to eq(length) }
end
shared_examples 'issues has correct length' do |length|
it { expect(subject[:issues].length).to eq(length) }
end
# Requires sentry_api_request and subject to be defined
shared_examples 'calls sentry api' do
it 'calls sentry api' do
......@@ -95,26 +105,44 @@ describe Sentry::Client do
let(:issue_status) { 'unresolved' }
let(:limit) { 20 }
let(:search_term) { '' }
let(:cursor) { nil }
let(:sort) { 'last_seen' }
let(:sentry_api_response) { issues_sample_response }
let(:sentry_request_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' }
let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) }
subject { client.list_issues(issue_status: issue_status, limit: limit, search_term: search_term, sort: 'last_seen') }
subject { client.list_issues(issue_status: issue_status, limit: limit, search_term: search_term, sort: sort, cursor: cursor) }
it_behaves_like 'calls sentry api'
it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'has correct length', 1
it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'issues has correct length', 1
shared_examples 'has correct external_url' do
context 'external_url' do
it 'is constructed correctly' do
expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11')
expect(subject[:issues][0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11')
end
end
end
context 'when response has a pagination info' do
let(:headers) do
{
link: '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
}
end
let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response, headers: headers) }
it 'parses the pagination' do
expect(subject[:pagination]).to eq(
'previous' => { 'cursor' => '1573556671000:0:1' },
'next' => { 'cursor' => '1572959139000:0:0' }
)
end
end
context 'error object created from sentry response' do
using RSpec::Parameterized::TableSyntax
......@@ -137,7 +165,7 @@ describe Sentry::Client do
end
with_them do
it { expect(subject[0].public_send(error_object)).to eq(sentry_api_response[0].dig(*sentry_response)) }
it { expect(subject[:issues][0].public_send(error_object)).to eq(sentry_api_response[0].dig(*sentry_response)) }
end
it_behaves_like 'has correct external_url'
......@@ -210,8 +238,8 @@ describe Sentry::Client do
it_behaves_like 'calls sentry api'
it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'has correct length', 1
it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'issues has correct length', 1
it_behaves_like 'has correct external_url'
end
......@@ -240,13 +268,23 @@ describe Sentry::Client do
it_behaves_like 'maps exceptions'
context 'when search term is present' do
let(:search_term) { 'NoMethodError'}
let(:search_term) { 'NoMethodError' }
let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&query=is:unresolved NoMethodError" }
it_behaves_like 'calls sentry api'
it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'has correct length', 1
it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'issues has correct length', 1
end
context 'when cursor is present' do
let(:cursor) { '1572959139000:0:0' }
let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&cursor=#{cursor}&query=is:unresolved" }
it_behaves_like 'calls sentry api'
it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'issues has correct length', 1
end
end
......
# frozen_string_literal: true
require 'fast_spec_helper'
require 'support/helpers/fixture_helpers'
describe Sentry::PaginationParser do
include FixtureHelpers
describe '.parse' do
subject { described_class.parse(headers) }
context 'when headers do not have "link" param' do
let(:headers) { {} }
it 'returns empty hash' do
is_expected.to eq({})
end
end
context 'when headers.link has previous and next pages' do
let(:headers) do
{
'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
}
end
it 'returns info about both pages' do
is_expected.to eq(
'previous' => { 'cursor' => '1573556671000:0:1' },
'next' => { 'cursor' => '1572959139000:0:0' }
)
end
end
context 'when headers.link has only next page' do
let(:headers) do
{
'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="false"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
}
end
it 'returns only info about the next page' do
is_expected.to eq(
'next' => { 'cursor' => '1572959139000:0:0' }
)
end
end
context 'when headers.link has only previous page' do
let(:headers) do
{
'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="false"; cursor="1572959139000:0:0"'
}
end
it 'returns only info about the previous page' do
is_expected.to eq(
'previous' => { 'cursor' => '1573556671000:0:1' }
)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20191125114345_add_admin_mode_protected_path.rb')
describe AddAdminModeProtectedPath, :migration do
ADMIN_MODE_ENDPOINT = '/admin/session'
subject(:migration) { described_class.new }
let(:application_settings) { table(:application_settings) }
context 'no settings available' do
it 'makes no changes' do
expect { migrate! }.not_to change { application_settings.count }
end
end
context 'protected_paths is null' do
before do
application_settings.create!(protected_paths: nil)
end
it 'makes no changes' do
expect { migrate! }.not_to change { application_settings.first.protected_paths }
end
end
it 'appends admin mode endpoint' do
application_settings.create!(protected_paths: '{a,b,c}')
protected_paths_before = %w[a b c]
protected_paths_after = protected_paths_before.dup << ADMIN_MODE_ENDPOINT
expect { migrate! }.to change { application_settings.first.protected_paths }.from(protected_paths_before).to(protected_paths_after)
end
it 'new default includes admin mode endpoint' do
settings_before = application_settings.create!
expect(settings_before.protected_paths).not_to include(ADMIN_MODE_ENDPOINT)
migrate!
application_settings.reset_column_information
settings_after = application_settings.create!
expect(settings_after.protected_paths).to include(ADMIN_MODE_ENDPOINT)
end
end
......@@ -66,6 +66,8 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) }
it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) }
it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) }
context 'when snowplow is enabled' do
before do
setting.snowplow_enabled = true
......
......@@ -153,9 +153,9 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
it 'returns cached issues' do
expect(sentry_client).to receive(:list_issues).with(opts)
.and_return(issues)
.and_return(issues: issues, pagination: {})
expect(result).to eq(issues: issues)
expect(result).to eq(issues: issues, pagination: {})
end
end
......
# frozen_string_literal: true
require "spec_helper"
describe UnifyCircuitService do
it_behaves_like "chat service", "Unify Circuit" do
let(:client_arguments) { webhook_url }
let(:content_key) { :subject }
end
end
......@@ -33,6 +33,7 @@ describe Project do
it { is_expected.to have_one(:microsoft_teams_service) }
it { is_expected.to have_one(:mattermost_service) }
it { is_expected.to have_one(:hangouts_chat_service) }
it { is_expected.to have_one(:unify_circuit_service) }
it { is_expected.to have_one(:packagist_service) }
it { is_expected.to have_one(:pushover_service) }
it { is_expected.to have_one(:asana_service) }
......
......@@ -31,6 +31,62 @@ describe Snippet do
it { is_expected.to validate_presence_of(:content) }
it { is_expected.to validate_inclusion_of(:visibility_level).in_array(Gitlab::VisibilityLevel.values) }
it do
allow(Gitlab::CurrentSettings).to receive(:snippet_size_limit).and_return(1)
is_expected
.to validate_length_of(:content)
.is_at_most(Gitlab::CurrentSettings.snippet_size_limit)
.with_message("is too long (2 Bytes). The maximum size is 1 Byte.")
end
context 'content validations' do
context 'with existing snippets' do
let(:snippet) { create(:personal_snippet, content: 'This is a valid content at the time of creation') }
before do
expect(snippet).to be_valid
stub_application_setting(snippet_size_limit: 2)
end
it 'does not raise a validation error if the content is not changed' do
snippet.title = 'new title'
expect(snippet).to be_valid
end
it 'raises and error if the content is changed and the size is bigger than limit' do
snippet.content = snippet.content + "test"
expect(snippet).not_to be_valid
end
end
context 'with new snippets' do
let(:limit) { 15 }
before do
stub_application_setting(snippet_size_limit: limit)
end
it 'is valid when content is smaller than the limit' do
snippet = build(:personal_snippet, content: 'Valid Content')
expect(snippet).to be_valid
end
it 'raises error when content is bigger than setting limit' do
snippet = build(:personal_snippet, content: 'This is an invalid content')
aggregate_failures do
expect(snippet).not_to be_valid
expect(snippet.errors[:content]).to include("is too long (#{snippet.content.size} Bytes). The maximum size is #{limit} Bytes.")
end
end
end
end
end
describe '#to_reference' do
......
......@@ -36,6 +36,7 @@ describe API::Settings, 'Settings' do
expect(json_response['allow_local_requests_from_system_hooks']).to be(true)
expect(json_response).not_to have_key('performance_bar_allowed_group_path')
expect(json_response).not_to have_key('performance_bar_enabled')
expect(json_response['snippet_size_limit']).to eq(50.megabytes)
end
end
......@@ -85,7 +86,8 @@ describe API::Settings, 'Settings' do
allow_local_requests_from_web_hooks_and_services: true,
allow_local_requests_from_system_hooks: false,
push_event_hooks_limit: 2,
push_event_activities_limit: 2
push_event_activities_limit: 2,
snippet_size_limit: 5
}
expect(response).to have_gitlab_http_status(200)
......@@ -121,6 +123,7 @@ describe API::Settings, 'Settings' do
expect(json_response['allow_local_requests_from_system_hooks']).to eq(false)
expect(json_response['push_event_hooks_limit']).to eq(2)
expect(json_response['push_event_activities_limit']).to eq(2)
expect(json_response['snippet_size_limit']).to eq(5)
end
end
......
......@@ -5,13 +5,14 @@ require 'spec_helper'
describe ErrorTracking::ListIssuesService do
set(:user) { create(:user) }
set(:project) { create(:project) }
let(:params) { { search_term: 'something', sort: 'last_seen' } }
let(:params) { { search_term: 'something', sort: 'last_seen', cursor: 'some-cursor' } }
let(:list_sentry_issues_args) do
{
issue_status: 'unresolved',
limit: 20,
search_term: params[:search_term],
sort: params[:sort]
search_term: 'something',
sort: 'last_seen',
cursor: 'some-cursor'
}
end
......@@ -40,11 +41,11 @@ describe ErrorTracking::ListIssuesService do
expect(error_tracking_setting)
.to receive(:list_sentry_issues)
.with(list_sentry_issues_args)
.and_return(issues: issues)
.and_return(issues: issues, pagination: {})
end
it 'returns the issues' do
expect(result).to eq(status: :success, issues: issues)
expect(result).to eq(status: :success, pagination: {}, issues: issues)
end
end
......
......@@ -80,7 +80,7 @@ shared_examples_for "chat service" do |service_name|
it_behaves_like "triggered #{service_name} service"
it "specifies the webhook when it is configured" do
it "specifies the webhook when it is configured", if: defined?(client) do
expect(client).to receive(:new).with(client_arguments).and_return(double(:chat_service).as_null_object)
subject.execute(sample_data)
......
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