Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-26 15:09:30 +00:00
parent 142890d5bb
commit 2eaa60e455
77 changed files with 1337 additions and 213 deletions

View File

@ -465,7 +465,7 @@ group :ed25519 do
end
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 13.5.0-rc2'
gem 'gitaly', '~> 13.6.1'
gem 'grpc', '~> 1.30.2'

View File

@ -420,7 +420,7 @@ GEM
rails (>= 3.2.0)
git (1.7.0)
rchardet (~> 1.8)
gitaly (13.5.0.pre.rc2)
gitaly (13.6.1)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-chronic (0.10.5)
@ -1345,7 +1345,7 @@ DEPENDENCIES
gettext (~> 3.3)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 13.5.0.pre.rc2)
gitaly (~> 13.6.1)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-fog-azure-rm (~> 1.0)

View File

@ -157,10 +157,10 @@ export default {
"
/>
</div>
<div :class="classNameMapCellLeft" class="diff-td diff-line-num old_line">
<div v-if="inline" :class="classNameMapCellLeft" class="diff-td diff-line-num old_line">
<a
v-if="line.left.old_line"
:data-linenumber="line.left.old_line"
v-if="line.left.new_line"
:data-linenumber="line.left.new_line"
:href="line.lineHrefOld"
@click="setHighlightedRow(line.lineCode)"
>
@ -179,21 +179,14 @@ export default {
</template>
<template v-else>
<div data-testid="leftEmptyCell" class="diff-td diff-line-num old_line empty-cell"></div>
<div class="diff-td diff-line-num old_line empty-cell"></div>
<div v-if="inline" class="diff-td diff-line-num old_line empty-cell"></div>
<div class="diff-td line-coverage left-side empty-cell"></div>
<div class="diff-td line_content with-coverage parallel left-side empty-cell"></div>
</template>
</div>
<div
v-if="!inline || (line.right && Boolean(line.right.type))"
class="diff-grid-right right-side"
>
<div v-if="!inline" class="diff-grid-right right-side">
<template v-if="line.right">
<div
:class="classNameMapCellRight"
data-testid="rightLineNumber"
class="diff-td diff-line-num new_line"
>
<div :class="classNameMapCellRight" class="diff-td diff-line-num new_line">
<span
v-if="shouldRenderCommentButton"
v-gl-tooltip
@ -231,15 +224,6 @@ export default {
"
/>
</div>
<div :class="classNameMapCellRight" class="diff-td diff-line-num new_line">
<a
v-if="line.right.new_line"
:data-linenumber="line.right.new_line"
:href="line.lineHrefNew"
@click="setHighlightedRow(line.lineCode)"
>
</a>
</div>
<div
v-gl-tooltip.hover
:title="coverageState.text"

View File

@ -47,7 +47,7 @@ export const parallelizeDiffLines = (diffLines, inline) => {
for (let i = 0, diffLinesLength = diffLines.length, index = 0; i < diffLinesLength; i += 1) {
const line = diffLines[i];
if (isRemoved(line)) {
if (isRemoved(line) || inline) {
lines.push({
[LINE_POSITION_LEFT]: line,
[LINE_POSITION_RIGHT]: null,
@ -59,7 +59,7 @@ export const parallelizeDiffLines = (diffLines, inline) => {
}
index += 1;
} else if (isAdded(line)) {
if (freeRightIndex !== null && !inline) {
if (freeRightIndex !== null) {
// If an old line came before this without a line on the right, this
// line can be put to the right of it.
lines[freeRightIndex].right = line;

View File

@ -51,35 +51,6 @@
text-align: center;
}
.fa-spin {
-webkit-animation: fa-spin 2s infinite linear;
animation: fa-spin 2s infinite linear;
}
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
.fa-inverse {
color: $white;
}
@ -97,10 +68,6 @@
content: '\f071';
}
.fa-spinner::before {
content: '\f110';
}
.fa-caret-right::before {
content: '\f0da';
}

View File

@ -597,10 +597,6 @@ table.code {
.diff-grid-right {
display: grid;
grid-template-columns: 50px 8px 1fr;
.diff-td:nth-child(2) {
display: none;
}
}
.diff-grid-comments {
@ -631,20 +627,6 @@ table.code {
.diff-grid-left,
.diff-grid-right {
grid-template-columns: 50px 50px 8px 1fr;
.diff-td:nth-child(2) {
display: block;
}
}
.diff-grid-left .old:nth-child(1) [data-linenumber],
.diff-grid-right .new:nth-child(2) [data-linenumber] {
display: inline;
}
.diff-grid-left .old:nth-child(2) [data-linenumber],
.diff-grid-right .new:nth-child(1) [data-linenumber] {
display: none;
}
}
}

View File

@ -143,15 +143,9 @@
.fa {
position: absolute;
&.fa-spinner {
font-size: 16px;
margin-top: -3px;
}
}
.fa-chevron-down,
.fa-spinner {
.fa-chevron-down {
position: absolute;
top: 11px;
right: 8px;

View File

@ -4,22 +4,22 @@
* @usage
* ### Active and Inactive text should be provided as data attributes:
* <button type="button" class="project-feature-toggle" data-enabled-text="Enabled" data-disabled-text="Disabled">
* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
* <span class="gl-spinner loading-icon hidden" aria-label="Loading"></span>
* </button>
* ### Checked should have `is-checked` class
* <button type="button" class="project-feature-toggle is-checked" data-enabled-text="Enabled" data-disabled-text="Disabled">
* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
* <span class="gl-spinner loading-icon hidden" aria-label="Loading"></span>
* </button>
* ### Disabled should have `is-disabled` class
* <button type="button" class="project-feature-toggle is-disabled" data-enabled-text="Enabled" data-disabled-text="Disabled" disabled="true">
* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
* <span class="gl-spinner loading-icon hidden" aria-label="Loading"></span>
* </button>
* ### Loading should have `is-loading` and an icon with `loading-icon` class
* <button type="button" class="project-feature-toggle is-loading" data-enabled-text="Enabled" data-disabled-text="Disabled">
* <i class="fa fa-spinner fa-spin loading-icon"></i>
* <span class="gl-spinner loading-icon" aria-label="Loading"></span>
* </button>
*/
.project-feature-toggle {

View File

@ -255,10 +255,6 @@ $colors: (
}
}
.btn-success .fa-spinner {
color: var(--white, $white);
}
.editor-wrap {
&.is-loading {
.editor {

View File

@ -174,12 +174,6 @@
}
.commit-actions {
@include media-breakpoint-up(sm) {
.fa-spinner {
font-size: 12px;
}
}
.ci-status-icon svg {
vertical-align: text-bottom;
}

View File

@ -190,8 +190,7 @@ $note-form-margin-left: 72px;
border: 1px solid darken($gray-100, 25%);
}
.note-headline-light,
.fa-spinner {
.note-headline-light {
margin-left: 3px;
}
}
@ -249,16 +248,6 @@ $note-form-margin-left: 72px;
.note-emoji-button {
position: relative;
line-height: 1;
.fa-spinner {
display: none;
}
&.is-loading {
.fa-spinner {
display: inline-block;
}
}
}
}
@ -407,8 +396,6 @@ $note-form-margin-left: 72px;
.discussion-body .diff-file {
.file-title {
cursor: default;
line-height: 42px;
padding: 0 $gl-padding;
border-top: 1px solid $border-color;
border-radius: 0;

View File

@ -19,6 +19,10 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
feature_category :static_site_editor
def index
render_404
end
def show
service_response = ::StaticSiteEditor::ConfigService.new(
container: project,

View File

@ -6,8 +6,6 @@ class RegistrationsController < Devise::RegistrationsController
include RecaptchaHelper
include InvisibleCaptchaOnSignup
BLOCKED_PENDING_APPROVAL_STATE = 'blocked_pending_approval'.freeze
layout 'devise'
prepend_before_action :check_captcha, only: :create
@ -167,12 +165,18 @@ class RegistrationsController < Devise::RegistrationsController
end
def set_user_state
return unless Gitlab::CurrentSettings.require_admin_approval_after_user_signup
return unless set_blocked_pending_approval?
resource.state = BLOCKED_PENDING_APPROVAL_STATE
resource.state = User::BLOCKED_PENDING_APPROVAL_STATE
end
def set_blocked_pending_approval?
Gitlab::CurrentSettings.require_admin_approval_after_user_signup
end
def set_invite_params
@invite_email = ActionController::Base.helpers.sanitize(params[:invite_email])
end
end
RegistrationsController.prepend_if_ee('EE::RegistrationsController')

View File

@ -0,0 +1,126 @@
fragment PageInfo on PageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
fragment RelatedTreeBaseEpic on Epic {
id
iid
title
webPath
relativePosition
userPermissions {
__typename
adminEpic
createEpic
}
descendantCounts {
__typename
openedEpics
closedEpics
openedIssues
closedIssues
}
healthStatus {
__typename
issuesAtRisk
issuesOnTrack
issuesNeedingAttention
}
}
fragment EpicNode on Epic {
...RelatedTreeBaseEpic
state
reference(full: true)
relationPath
createdAt
closedAt
hasChildren
hasIssues
group {
__typename
fullPath
}
}
query childItems(
$fullPath: ID!
$iid: ID
$pageSize: Int = 100
$epicEndCursor: String = ""
$issueEndCursor: String = ""
) {
group(fullPath: $fullPath) {
__typename
id
path
fullPath
epic(iid: $iid) {
__typename
...RelatedTreeBaseEpic
children(first: $pageSize, after: $epicEndCursor) {
__typename
edges {
__typename
node {
__typename
...EpicNode
}
}
pageInfo {
__typename
...PageInfo
}
}
issues(first: $pageSize, after: $issueEndCursor) {
__typename
edges {
__typename
node {
__typename
iid
epicIssueId
title
closedAt
state
createdAt
confidential
dueDate
weight
webPath
reference(full: true)
relationPath
relativePosition
assignees {
__typename
edges {
__typename
node {
__typename
webUrl
name
username
avatarUrl
}
}
}
milestone {
__typename
title
startDate
dueDate
}
healthStatus
}
}
pageInfo {
__typename
...PageInfo
}
}
}
}
}

View File

@ -0,0 +1,20 @@
query epicDetails($fullPath: ID!, $iid: ID!) {
group(fullPath: $fullPath) {
__typename
epic(iid: $iid) {
__typename
participants {
__typename
edges {
__typename
node {
__typename
name
avatarUrl
webUrl
}
}
}
}
}
}

View File

@ -109,6 +109,10 @@ module Resolvers
[args[:iid], args[:iids]].any? ? 0 : 0.01
end
def offset_pagination(relation)
::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(relation)
end
override :object
def object
super.tap do |obj|

View File

@ -16,7 +16,7 @@ module Resolvers
filter_params = issue_filters(args[:filters]).merge(board_id: list.board.id, id: list.id)
service = ::Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params)
Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(service.execute)
offset_pagination(service.execute)
end
# https://gitlab.com/gitlab-org/gitlab/-/issues/235681

View File

@ -27,7 +27,7 @@ module Resolvers
List.preload_preferences_for_user(lists, context[:current_user])
end
Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(lists)
offset_pagination(lists)
end
private

View File

@ -24,7 +24,7 @@ module Resolvers
if non_stable_cursor_sort?(args[:sort])
# Certain complex sorts are not supported by the stable cursor pagination yet.
# In these cases, we use offset pagination, so we return the correct connection.
Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(issues)
offset_pagination(issues)
else
issues
end

View File

@ -64,6 +64,8 @@ class Project < ApplicationRecord
SORTING_PREFERENCE_FIELD = :projects_sort
MAX_BUILD_TIMEOUT = 1.month
GL_REPOSITORY_TYPES = [Gitlab::GlRepository::PROJECT, Gitlab::GlRepository::WIKI, Gitlab::GlRepository::DESIGN].freeze
cache_markdown_field :description, pipeline: :description
default_value_for :packages_enabled, true
@ -164,6 +166,7 @@ class Project < ApplicationRecord
has_one :bamboo_service
has_one :teamcity_service
has_one :pushover_service
has_one :jenkins_service
has_one :jira_service
has_one :redmine_service
has_one :youtrack_service
@ -2275,7 +2278,9 @@ class Project < ApplicationRecord
end
def git_transfer_in_progress?
repo_reference_count > 0 || wiki_reference_count > 0
GL_REPOSITORY_TYPES.any? do |type|
reference_counter(type: type).value > 0
end
end
def storage_version=(value)
@ -2608,14 +2613,6 @@ class Project < ApplicationRecord
end
end
def repo_reference_count
reference_counter.value
end
def wiki_reference_count
reference_counter(type: Gitlab::GlRepository::WIKI).value
end
def check_repository_absence!
return if skip_disk_validation

View File

@ -0,0 +1,91 @@
# frozen_string_literal: true
class JenkinsService < CiService
prop_accessor :jenkins_url, :project_name, :username, :password
before_update :reset_password
validates :jenkins_url, presence: true, addressable_url: true, if: :activated?
validates :project_name, presence: true, if: :activated?
validates :username, presence: true, if: ->(service) { service.activated? && service.password_touched? && service.password.present? }
default_value_for :push_events, true
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
after_save :compose_service_hook, if: :activated?
def reset_password
# don't reset the password if a new one is provided
if (jenkins_url_changed? || username.blank?) && !password_touched?
self.password = nil
end
end
def compose_service_hook
hook = service_hook || build_service_hook
hook.url = hook_url
hook.save
end
def execute(data)
return if project.disabled_services.include?(to_param)
return unless supported_events.include?(data[:object_kind])
service_hook.execute(data, "#{data[:object_kind]}_hook")
end
def test(data)
begin
result = execute(data)
return { success: false, result: result[:message] } if result[:http_status] != 200
rescue StandardError => error
return { success: false, result: error }
end
{ success: true, result: result[:message] }
end
def hook_url
url = URI.parse(jenkins_url)
url.path = File.join(url.path || '/', "project/#{project_name}")
url.user = ERB::Util.url_encode(username) unless username.blank?
url.password = ERB::Util.url_encode(password) unless password.blank?
url.to_s
end
def self.supported_events
%w(push merge_request tag_push)
end
def title
'Jenkins CI'
end
def description
'An extendable open source continuous integration server'
end
def help
"You must have installed the Git Plugin and GitLab Plugin in Jenkins. [More information](#{Gitlab::Routing.url_helpers.help_page_url('integration/jenkins')})"
end
def self.to_param
'jenkins'
end
def fields
[
{
type: 'text', name: 'jenkins_url',
placeholder: 'Jenkins URL like http://jenkins.example.com'
},
{
type: 'text', name: 'project_name', placeholder: 'Project Name',
help: 'The URL-friendly project name. Example: my_project_name'
},
{ type: 'text', name: 'username' },
{ type: 'password', name: 'password' }
]
end
end

View File

@ -17,6 +17,10 @@ class Service < ApplicationRecord
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
].freeze
PROJECT_SPECIFIC_SERVICE_NAMES = %w[
jenkins
].freeze
# Fake services to help with local development.
DEV_SERVICE_NAMES = %w[
mock_ci mock_deployment mock_monitoring
@ -212,7 +216,7 @@ class Service < ApplicationRecord
end
def self.project_specific_services_names
[]
PROJECT_SPECIFIC_SERVICE_NAMES
end
def self.available_services_types(include_project_specific: true, include_dev: true)

View File

@ -30,6 +30,8 @@ class User < ApplicationRecord
INSTANCE_ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
BLOCKED_PENDING_APPROVAL_STATE = 'blocked_pending_approval'.freeze
add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
add_authentication_token_field :feed_token
add_authentication_token_field :static_object_token

View File

@ -0,0 +1,5 @@
---
title: Fix incorrect line height in file header
merge_request: 48117
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Track MAU for SSE edit
merge_request: 48377
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Move Jenkins to Core
merge_request: 37797
author: Ben Bodenmiller (@bbodenmiller)
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add metric for dead Sidekiq jobs
merge_request: 48361
author:
type: changed

View File

@ -0,0 +1,6 @@
---
title: Consider design repositories when determining if there is a git transfer in
progress
merge_request: 48304
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Update gitaly gem to 13.6.1
merge_request: 48601
author:
type: other

View File

@ -46,6 +46,8 @@ Sidekiq.configure_server do |config|
config.client_middleware(&Gitlab::SidekiqMiddleware.client_configurator)
config.death_handlers << Gitlab::SidekiqDeathHandler.method(:handler)
config.on :startup do
# Clear any connections that might have been obtained before starting
# Sidekiq (e.g. in an initializer).

View File

@ -34,6 +34,7 @@ scope format: false do
scope constraints: { id: /[^\0]+?/ } do
scope controller: :static_site_editor do
get '/sse/:id(/*vueroute)', action: :show, as: :show_sse
get '/sse', as: :root_sse, action: :index
end
end
end

View File

@ -308,6 +308,8 @@
- 2
- - service_desk_email_receiver
- 1
- - set_user_status_based_on_user_cap_setting
- 1
- - status_page_publish
- 1
- - sync_seat_link_request

View File

@ -142,6 +142,7 @@ configuration option in `gitlab.yml`. These metrics are served from the
| `sidekiq_jobs_queue_duration_seconds` | Histogram | 12.5 | Duration in seconds that a Sidekiq job was queued before being executed | `queue`, `boundary`, `external_dependencies`, `feature_category`, `urgency` |
| `sidekiq_jobs_failed_total` | Counter | 12.2 | Sidekiq jobs failed | `queue`, `boundary`, `external_dependencies`, `feature_category`, `urgency` |
| `sidekiq_jobs_retried_total` | Counter | 12.2 | Sidekiq jobs retried | `queue`, `boundary`, `external_dependencies`, `feature_category`, `urgency` |
| `sidekiq_jobs_dead_total` | Counter | 13.7 | Sidekiq dead jobs (jobs that have run out of retries) | `queue`, `boundary`, `external_dependencies`, `feature_category`, `urgency` |
| `sidekiq_redis_requests_total` | Counter | 13.1 | Redis requests during a Sidekiq job execution | `queue`, `boundary`, `external_dependencies`, `feature_category`, `job_status`, `urgency` |
| `sidekiq_elasticsearch_requests_total` | Counter | 13.1 | Elasticsearch requests during a Sidekiq job execution | `queue`, `boundary`, `external_dependencies`, `feature_category`, `job_status`, `urgency` |
| `sidekiq_running_jobs` | Gauge | 12.2 | Number of Sidekiq jobs running | `queue`, `boundary`, `external_dependencies`, `feature_category`, `urgency` |

View File

@ -86,6 +86,20 @@ However, there are some cases where we have to use the offset
pagination connection, `OffsetActiveRecordRelationConnection`, such as when
sorting by label priority in issues, due to the complexity of the sort.
If you return a relation from a resolver that is not suitable for keyset
pagination (due to the sort order for example), then you can use the
`BaseResolver#offset_pagination` method to wrap the relation in the correct
connection type. For example:
```ruby
def resolve(**args)
result = Finder.new(object, current_user, args).execute
result = offset_pagination(result) if needs_offset?(args[:sort])
result
end
```
### Keyset pagination
The keyset pagination implementation is a subclass of `GraphQL::Pagination::ActiveRecordRelationConnection`,
@ -225,7 +239,7 @@ instead of an `ActiveRecord::Relation`:
if non_stable_cursor_sort?(args[:sort])
# Certain complex sorts are not supported by the stable cursor pagination yet.
# In these cases, we use offset pagination, so we return the correct connection.
Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(issues)
offset_pagination(issues)
else
issues
end

View File

@ -421,7 +421,7 @@ Snowplow Micro is a Docker-based solution for testing frontend and backend event
1. Send a test Snowplow event from the Rails console:
```ruby
Gitlab::Tracking.self_describing_event('iglu:com.gitlab/pageview_context/jsonschema/1-0-0', { page_type: 'MY_TYPE' }, context: nil )
Gitlab::Tracking.self_describing_event('iglu:com.gitlab/pageview_context/jsonschema/1-0-0', data: { page_type: 'MY_TYPE' }, context: nil)
```
### Snowplow Mini

View File

@ -23,23 +23,12 @@ alternative authentication methods to your users.
### Remove Service Integration entries from the database
The `JenkinsService` and `GithubService` classes are only available in the Enterprise Edition codebase,
The `GithubService` class is only available in the Enterprise Edition codebase,
so if you downgrade to the Community Edition, the following error displays:
```plaintext
Completed 500 Internal Server Error in 497ms (ActiveRecord: 32.2ms)
ActionView::Template::Error (The single-table inheritance mechanism failed to locate the subclass: 'JenkinsService'. This
error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this
column if you didn't intend it to be used for storing the inheritance class or overwrite Service.inheritance_column to
use another column for that information.)
```
or
```plaintext
Completed 500 Internal Server Error in 497ms (ActiveRecord: 32.2ms)
ActionView::Template::Error (The single-table inheritance mechanism failed to locate the subclass: 'GithubService'. This
error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this
column if you didn't intend it to be used for storing the inheritance class or overwrite Service.inheritance_column to
@ -48,22 +37,23 @@ use another column for that information.)
All services are created automatically for every project you have, so in order
to avoid getting this error, you need to remove all instances of the
`JenkinsService` and `GithubService` from your database:
`GithubService` from your database:
**Omnibus Installation**
```shell
sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'GithubService']).delete_all"
sudo gitlab-rails runner "Service.where(type: ['GithubService']).delete_all"
```
**Source Installation**
```shell
bundle exec rails runner "Service.where(type: ['JenkinsService', 'GithubService']).delete_all" production
bundle exec rails runner "Service.where(type: ['GithubService']).delete_all" production
```
NOTE: **Note:**
If you are running `GitLab =< v13.0` you need to also remove `JenkinsDeprecatedService` records.
If you are running `GitLab =< v13.0` you need to also remove `JenkinsDeprecatedService` records
and if you are running `GitLab =< v13.6` you need to also remove `JenkinsService` records.
### Variables environment scopes

View File

@ -43,7 +43,7 @@ GitLab also provides features to improve the security of your own application. F
GitLab can be integrated with the following external service for continuous integration:
- [Jenkins](jenkins.md) CI. **(STARTER)**
- [Jenkins](jenkins.md) CI.
## Feature enhancements

View File

@ -4,7 +4,7 @@ group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Jenkins CI service **(STARTER)**
# Jenkins CI service
NOTE: **Note:**
This documentation focuses only on how to **configure** a Jenkins *integration* with
@ -73,11 +73,11 @@ Create a personal access token to authorize Jenkins' access to GitLab.
1. Click **Access Tokens** in the sidebar.
1. Create a personal access token with the **API** scope checkbox checked. For more details, see
[Personal access tokens](../user/profile/personal_access_tokens.md).
1. Record the personal access token's value, because it's required in [Configure the Jenkins server](#configure-the-jenkins-server).
1. Record the personal access token's value, because it's required in [Configure the Jenkins server](#configure-the-jenkins-server) section.
## Configure the Jenkins server
Install and configure the Jenkins plugins. Both plugins must be installed and configured to
Install and configure the Jenkins plugin. The plugin must be installed and configured to
authorize the connection to GitLab.
1. On the Jenkins server, go to **Manage Jenkins > Manage Plugins**.
@ -137,6 +137,8 @@ Set up the Jenkins project you intend to run your build on.
Configure the GitLab integration with Jenkins.
### Option 1: Jenkins integration (recommended)
1. Create a new GitLab project or choose an existing one.
1. Go to **Settings > Integrations**, then select **Jenkins CI**.
1. Turn on the **Active** toggle.
@ -154,6 +156,14 @@ Configure the GitLab integration with Jenkins.
authentication.
1. Click **Test settings and save changes**. GitLab tests the connection to Jenkins.
### Option 2: Webhook
1. In the configuration of your Jenkins job, in the GitLab configuration section, click **Advanced**.
1. Click the **Generate** button under the **Secret Token** field.
1. Copy the resulting token, and save the job configuration.
1. In GitLab, create a webhook for your project, enter the trigger URL (e.g. `https://JENKINS_URL/project/YOUR_JOB`) and paste the token in the **Secret Token** field.
1. After you add the webhook, click the **Test** button, and it should succeed.
## Troubleshooting
### Error in merge requests - "Could not connect to the CI server"

View File

@ -459,6 +459,32 @@ module API
desc: 'Colorize messages'
}
],
'jenkins' => [
{
required: true,
name: :jenkins_url,
type: String,
desc: 'Jenkins root URL like https://jenkins.example.com'
},
{
required: true,
name: :project_name,
type: String,
desc: 'The URL-friendly project name. Example: my_project_name'
},
{
required: false,
name: :username,
type: String,
desc: 'A user with access to the Jenkins server, if applicable'
},
{
required: false,
name: :password,
type: String,
desc: 'The password of the user'
}
],
'jira' => [
{
required: true,
@ -767,6 +793,7 @@ module API
::HangoutsChatService,
::HipchatService,
::IrkerService,
::JenkinsService,
::JiraService,
::MattermostSlashCommandsService,
::SlackSlashCommandsService,

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module API
module Helpers
module SSEHelpers
def request_from_sse?(project)
return false if request.referer.blank?
uri = URI.parse(request.referer)
uri.path.starts_with?(::Gitlab::Routing.url_helpers.project_root_sse_path(project))
rescue URI::InvalidURIError
false
end
end
end
end

View File

@ -11,6 +11,7 @@ module API
feature_category :code_review
helpers Helpers::MergeRequestsHelpers
helpers Helpers::SSEHelpers
# EE::API::MergeRequests would override the following helpers
helpers do
@ -216,6 +217,8 @@ module API
handle_merge_request_errors!(merge_request)
Gitlab::UsageDataCounters::EditorUniqueCounter.track_sse_edit_action(author: current_user) if request_from_sse?(user_project)
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Gitlab
module SidekiqDeathHandler
class << self
include ::Gitlab::SidekiqMiddleware::MetricsHelper
def handler(job, _exception)
labels = create_labels(job['class'].constantize, job['queue'])
counter.increment(labels)
end
def counter
@counter ||= ::Gitlab::Metrics.counter(:sidekiq_jobs_dead_total, 'Sidekiq dead jobs')
end
end
end
end

View File

@ -2,7 +2,9 @@
module Gitlab
module SidekiqMiddleware
class ClientMetrics < SidekiqMiddleware::Metrics
class ClientMetrics
include ::Gitlab::SidekiqMiddleware::MetricsHelper
ENQUEUED = :sidekiq_enqueued_jobs_total
def initialize

View File

@ -2,7 +2,7 @@
module Gitlab
module SidekiqMiddleware
class Metrics
module MetricsHelper
TRUE_LABEL = "yes"
FALSE_LABEL = "no"

View File

@ -2,7 +2,9 @@
module Gitlab
module SidekiqMiddleware
class ServerMetrics < SidekiqMiddleware::Metrics
class ServerMetrics
include ::Gitlab::SidekiqMiddleware::MetricsHelper
# SIDEKIQ_LATENCY_BUCKETS are latency histogram buckets better suited to Sidekiq
# timeframes than the DEFAULT_BUCKET definition. Defined in seconds.
SIDEKIQ_LATENCY_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60, 300, 600].freeze

View File

@ -14,8 +14,8 @@ module Gitlab
Gitlab::Tracking.event(category, action.to_s, **args)
end
def track_self_describing_event(schema_url, event_data_json, **args)
Gitlab::Tracking.self_describing_event(schema_url, event_data_json, **args)
def track_self_describing_event(schema_url, data:, **args)
Gitlab::Tracking.self_describing_event(schema_url, data: data, **args)
end
end
@ -29,8 +29,8 @@ module Gitlab
product_analytics.event(category, action, label: label, property: property, value: value, context: context)
end
def self_describing_event(schema_url, event_data_json, context: nil)
snowplow.self_describing_event(schema_url, event_data_json, context: context)
def self_describing_event(schema_url, data:, context: nil)
snowplow.self_describing_event(schema_url, data: data, context: context)
end
def snowplow_options(group)

View File

@ -15,10 +15,10 @@ module Gitlab
tracker.track_struct_event(category, action, label, property, value, context, (Time.now.to_f * 1000).to_i)
end
def self_describing_event(schema_url, event_data_json, context: nil)
def self_describing_event(schema_url, data:, context: nil)
return unless enabled?
event_json = SnowplowTracker::SelfDescribingJson.new(schema_url, event_data_json)
event_json = SnowplowTracker::SelfDescribingJson.new(schema_url, data)
tracker.track_self_describing_event(event_json, context, (Time.now.to_f * 1000).to_i)
end

View File

@ -770,6 +770,7 @@ module Gitlab
action_monthly_active_users_web_ide_edit: redis_usage_data { counter.count_web_ide_edit_actions(**date_range) },
action_monthly_active_users_sfe_edit: redis_usage_data { counter.count_sfe_edit_actions(**date_range) },
action_monthly_active_users_snippet_editor_edit: redis_usage_data { counter.count_snippet_editor_edit_actions(**date_range) },
action_monthly_active_users_sse_edit: redis_usage_data { counter.count_sse_edit_actions(**date_range) },
action_monthly_active_users_ide_edit: redis_usage_data { counter.count_edit_using_editor(**date_range) }
}
end

View File

@ -6,6 +6,7 @@ module Gitlab
EDIT_BY_SNIPPET_EDITOR = 'g_edit_by_snippet_ide'
EDIT_BY_SFE = 'g_edit_by_sfe'
EDIT_BY_WEB_IDE = 'g_edit_by_web_ide'
EDIT_BY_SSE = 'g_edit_by_sse'
EDIT_CATEGORY = 'ide_edit'
class << self
@ -38,6 +39,14 @@ module Gitlab
count_unique(events, date_from, date_to)
end
def track_sse_edit_action(author:, time: Time.zone.now)
track_unique_action(EDIT_BY_SSE, author, time)
end
def count_sse_edit_actions(date_from:, date_to:)
count_unique(EDIT_BY_SSE, date_from, date_to)
end
private
def track_unique_action(action, author, time)

View File

@ -118,6 +118,12 @@
expiry: 29
aggregation: daily
feature_flag: track_editor_edit_actions
- name: g_edit_by_sse
category: ide_edit
redis_slot: edit
expiry: 29
aggregation: daily
feature_flag: track_editor_edit_actions
- name: g_edit_by_snippet_ide
category: ide_edit
redis_slot: edit

View File

@ -307,14 +307,12 @@ module QA
module Services
autoload :Jira, 'qa/page/project/settings/services/jira'
autoload :Jenkins, 'qa/page/project/settings/services/jenkins'
autoload :Prometheus, 'qa/page/project/settings/services/prometheus'
end
autoload :Operations, 'qa/page/project/settings/operations'
autoload :Incidents, 'qa/page/project/settings/incidents'
autoload :Integrations, 'qa/page/project/settings/integrations'
module Services
autoload :Prometheus, 'qa/page/project/settings/services/prometheus'
end
end
module SubMenus

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
module QA
module Page
module Project
module Settings
module Services
class Jenkins < QA::Page::Base
view 'app/assets/javascripts/integrations/edit/components/dynamic_field.vue' do
element :jenkins_url_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern
element :project_name_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern
element :username_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern
element :password_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/integrations/edit/components/integration_form.vue' do
element :save_changes_button
end
def setup_service_with(jenkins_url:, project_name:)
set_jenkins_url(jenkins_url)
set_project_name(project_name)
set_username('admin')
set_password('password')
click_save_changes_button
end
private
def set_jenkins_url(jenkins_url)
fill_element(:jenkins_url_field, jenkins_url)
end
def set_project_name(project_name)
fill_element(:project_name_field, project_name)
end
def set_username(username)
fill_element(:username_field, username)
end
def set_password(password)
fill_element(:password_field, password)
end
def click_save_changes_button
click_element :save_changes_button
end
end
end
end
end
end
end

View File

@ -0,0 +1,156 @@
# frozen_string_literal: true
require 'securerandom'
module QA
RSpec.describe 'Create', :requires_admin, :skip_live_env, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/195179', type: :flaky } do
describe 'Jenkins integration' do
let(:project_name) { "project_with_jenkins_#{SecureRandom.hex(4)}" }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = project_name
project.initialize_with_readme = true
project.auto_devops_enabled = false
end
end
before do
jenkins_server = run_jenkins_server
Vendor::Jenkins::Page::Base.host = jenkins_server.host_address
Runtime::Env.personal_access_token ||= fabricate_personal_access_token
allow_requests_to_local_networks
setup_jenkins
end
it 'integrates and displays build status for MR pipeline in GitLab', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/719' do
login_to_gitlab
setup_project_integration_with_jenkins
expect(page).to have_text("Jenkins CI activated.")
QA::Support::Retrier.retry_on_exception do
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.branch_name = 'master'
push.new_branch = false
push.file_name = "file_#{SecureRandom.hex(4)}.txt"
end
Vendor::Jenkins::Page::LastJobConsole.perform do |job_console|
job_console.job_name = project_name
job_console.visit!
Support::Waiter.wait_until(sleep_interval: 2, reload_page: page) do
job_console.has_successful_build? && job_console.no_failed_status_update?
end
end
project.visit!
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |show|
expect(show).to have_build('jenkins', status: :success, wait: 15)
end
end
end
after do
remove_jenkins_server
end
def setup_jenkins
Vendor::Jenkins::Page::Login.perform do |login_page|
login_page.visit!
login_page.login
end
token_description = "token-#{SecureRandom.hex(8)}"
Vendor::Jenkins::Page::NewCredentials.perform do |new_credentials|
new_credentials.visit_and_set_gitlab_api_token(Runtime::Env.personal_access_token, token_description)
end
Vendor::Jenkins::Page::Configure.perform do |configure|
configure.visit_and_setup_gitlab_connection(patch_host_name(Runtime::Scenario.gitlab_address, 'gitlab'), token_description) do
configure.click_test_connection
expect(configure).to have_success
end
end
Vendor::Jenkins::Page::NewJob.perform do |new_job|
new_job.visit_and_create_new_job_with_name(project_name)
end
Vendor::Jenkins::Page::ConfigureJob.perform do |configure_job|
configure_job.job_name = project_name
configure_job.configure(scm_url: patch_host_name(project.repository_http_location.git_uri, 'gitlab'))
end
end
def run_jenkins_server
Service::DockerRun::Jenkins.new.tap do |runner|
runner.pull
runner.register!
end
end
def remove_jenkins_server
Service::DockerRun::Jenkins.new.remove!
end
def fabricate_personal_access_token
login_to_gitlab
token = Resource::PersonalAccessToken.fabricate!.access_token
Page::Main::Menu.perform(&:sign_out)
token
end
def login_to_gitlab
Flow::Login.sign_in
end
def patch_host_name(host_name, container_name)
return host_name unless host_name.include?('localhost')
ip_address = `docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' #{container_name}`.strip
host_name.gsub('localhost', ip_address)
end
def setup_project_integration_with_jenkins
project.visit!
Page::Project::Menu.perform(&:click_project)
Page::Project::Menu.perform(&:go_to_integrations_settings)
Page::Project::Settings::Integrations.perform(&:click_jenkins_ci_link)
QA::Page::Project::Settings::Services::Jenkins.perform do |jenkins|
jenkins.setup_service_with(jenkins_url: patch_host_name(Vendor::Jenkins::Page::Base.host, 'jenkins-server'),
project_name: project_name)
end
end
def allow_requests_to_local_networks
Page::Main::Menu.perform(&:sign_out_if_signed_in)
Flow::Login.sign_in_as_admin
Page::Main::Menu.perform(&:go_to_admin_area)
Page::Admin::Menu.perform(&:go_to_network_settings)
Page::Admin::Settings::Network.perform do |network|
network.expand_outbound_requests do |outbound_requests|
outbound_requests.allow_requests_to_local_network_from_services
end
end
Page::Main::Menu.perform(&:sign_out)
end
end
end
end

View File

@ -56,7 +56,7 @@ function update_tests_mapping() {
}
function crystalball_rspec_data_exists() {
compgen -G "crystalball/rspec*.yml" > /dev/null;
compgen -G "crystalball/rspec*.yml" >/dev/null
}
function rspec_simple_job() {
@ -117,7 +117,13 @@ function rspec_paralellized_job() {
export MEMORY_TEST_PATH="tmp/memory_test/${report_name}_memory.csv"
knapsack rspec "-Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}"
local rspec_args="-Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}"
if [[ -n $RSPEC_MATCHING_TESTS_ENABLED ]]; then
tooling/bin/parallel_rspec --rspec_args "${rspec_args}" --filter tmp/matching_tests.txt
else
tooling/bin/parallel_rspec --rspec_args "${rspec_args}"
fi
date
}

View File

@ -7,6 +7,21 @@ RSpec.describe Projects::StaticSiteEditorController do
let_it_be(:user) { create(:user) }
let(:data) { { key: 'value' } }
describe 'GET index' do
let(:default_params) do
{
namespace_id: project.namespace,
project_id: project
}
end
it 'responds with 404 page' do
get :index, params: default_params
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'GET show' do
render_views

View File

@ -19,15 +19,16 @@ FactoryBot.define do
create(:jira_import_state, :finished, project: projects[1], label: jira_label, imported_issues_count: 3)
create(:jira_import_state, :scheduled, project: projects[1], label: jira_label)
create(:prometheus_service, project: projects[1])
create(:service, project: projects[1], type: 'JenkinsService', active: true)
create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
create(:service, project: projects[1], type: 'SlackService', active: true)
create(:service, project: projects[2], type: 'SlackService', active: true)
create(:service, project: projects[2], type: 'MattermostService', active: false)
create(:service, group: group, project: nil, type: 'MattermostService', active: true)
create(:service, :template, type: 'MattermostService', active: true)
matermost_instance = create(:service, :instance, type: 'MattermostService', active: true)
create(:service, project: projects[1], type: 'MattermostService', active: true, inherit_from_id: matermost_instance.id)
create(:service, group: group, project: nil, type: 'SlackService', active: true, inherit_from_id: matermost_instance.id)
mattermost_instance = create(:service, :instance, type: 'MattermostService', active: true)
create(:service, project: projects[1], type: 'MattermostService', active: true, inherit_from_id: mattermost_instance.id)
create(:service, group: group, project: nil, type: 'SlackService', active: true, inherit_from_id: mattermost_instance.id)
create(:service, project: projects[2], type: 'CustomIssueTrackerService', active: true)
create(:project_error_tracking_setting, project: projects[0])
create(:project_error_tracking_setting, project: projects[1], enabled: false)

View File

@ -111,7 +111,6 @@ RSpec.describe 'Merge request > User resolves diff notes and threads', :js do
it 'shows resolved thread when toggled' do
find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click
expect(page.find(".line-holder-placeholder")).to be_visible
expect(page.find(".timeline-content #note_#{note.id}")).to be_visible
end

View File

@ -97,18 +97,18 @@ describe('DiffRow', () => {
${'right'}
`('$side side', ({ side }) => {
it(`renders empty cells if ${side} is unavailable`, () => {
const wrapper = createWrapper({ props: { line: testLines[2] } });
const wrapper = createWrapper({ props: { line: testLines[2], inline: false } });
expect(wrapper.find(`[data-testid="${side}LineNumber"]`).exists()).toBe(false);
expect(wrapper.find(`[data-testid="${side}EmptyCell"]`).exists()).toBe(true);
});
it('renders comment button', () => {
const wrapper = createWrapper({ props: { line: testLines[3] } });
const wrapper = createWrapper({ props: { line: testLines[3], inline: false } });
expect(wrapper.find(`[data-testid="${side}CommentButton"]`).exists()).toBe(true);
});
it('renders avatars', () => {
const wrapper = createWrapper({ props: { line: testLines[0] } });
const wrapper = createWrapper({ props: { line: testLines[0], inline: false } });
expect(wrapper.find(`[data-testid="${side}Discussions"]`).exists()).toBe(true);
});
});

View File

@ -1119,25 +1119,14 @@ describe('DiffsStoreUtils', () => {
);
});
/**
* What's going on here?
*
* The inline version of parallelizeDiffLines simply keeps the difflines
* in the same order they are received as opposed to shuffling them
* to be "side by side".
*
* This keeps the underlying data structure the same which simplifies
* the components, but keeps the changes grouped together as users
* expect when viewing changes inline.
*/
it('converts inline diff lines to inline diff lines with a parallel structure', () => {
it('converts inline diff lines', () => {
const file = getDiffFileMock();
const files = utils.parallelizeDiffLines(file.highlighted_diff_lines, true);
expect(files[5].left).toEqual(file.parallel_diff_lines[5].left);
expect(files[5].right).toBeNull();
expect(files[6].left).toBeNull();
expect(files[6].right).toEqual(file.parallel_diff_lines[5].right);
expect(files[6].left).toEqual(file.parallel_diff_lines[5].right);
expect(files[6].right).toBeNull();
});
});
});

View File

@ -273,4 +273,12 @@ RSpec.describe Resolvers::BaseResolver do
end
end
end
describe '#offset_pagination' do
let(:instance) { resolver_instance(resolver) }
it 'is sugar for OffsetActiveRecordRelationConnection.new' do
expect(instance.offset_pagination(User.none)).to be_a(::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection)
end
end
end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Helpers::SSEHelpers do
include Gitlab::Routing
let_it_be(:project) { create(:project) }
subject { Class.new.include(described_class).new }
describe '#request_from_sse?' do
before do
allow(subject).to receive(:request).and_return(request)
end
context 'when referer is nil' do
let(:request) { double(referer: nil)}
it 'returns false' do
expect(URI).not_to receive(:parse)
expect(subject.request_from_sse?(project)).to eq false
end
end
context 'when referer is not from SSE' do
let(:request) { double(referer: 'https://gitlab.com')}
it 'returns false' do
expect(URI).to receive(:parse).and_call_original
expect(subject.request_from_sse?(project)).to eq false
end
end
context 'when referer is from SSE' do
let(:request) { double(referer: project_show_sse_path(project, 'master/README.md'))}
it 'returns true' do
expect(URI).to receive(:parse).and_call_original
expect(subject.request_from_sse?(project)).to eq true
end
end
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::SidekiqDeathHandler, :clean_gitlab_redis_queues do
describe '.handler' do
context 'when the job class has worker attributes' do
let(:test_worker) do
Class.new do
include WorkerAttributes
urgency :low
worker_has_external_dependencies!
worker_resource_boundary :cpu
feature_category :users
end
end
before do
stub_const('TestWorker', test_worker)
end
it 'uses the attributes from the worker' do
expect(described_class.counter)
.to receive(:increment)
.with(queue: 'test_queue', worker: 'TestWorker',
urgency: 'low', external_dependencies: 'yes',
feature_category: 'users', boundary: 'cpu')
described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil)
end
end
context 'when the job class does not have worker attributes' do
before do
stub_const('TestWorker', Class.new)
end
it 'uses blank attributes' do
expect(described_class.counter)
.to receive(:increment)
.with(queue: 'test_queue', worker: 'TestWorker',
urgency: '', external_dependencies: 'no',
feature_category: '', boundary: '')
described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil)
end
end
end
end

View File

@ -46,7 +46,7 @@ RSpec.describe Gitlab::Tracking::Destinations::Snowplow do
it 'sends event to tracker' do
allow(tracker).to receive(:track_self_describing_event).and_call_original
subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', { foo: 'bar' })
subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' })
expect(tracker).to have_received(:track_self_describing_event) do |event, context, timestamp|
expect(event.to_json[:schema]).to eq('iglu:com.gitlab/foo/jsonschema/1-0-0')
@ -71,7 +71,7 @@ RSpec.describe Gitlab::Tracking::Destinations::Snowplow do
it 'does not send event to tracker' do
expect_any_instance_of(SnowplowTracker::Tracker).not_to receive(:track_self_describing_event)
subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', { foo: 'bar' })
subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' })
end
end
end

View File

@ -62,9 +62,9 @@ RSpec.describe Gitlab::Tracking do
it 'delegates to snowplow destination' do
expect_any_instance_of(Gitlab::Tracking::Destinations::Snowplow)
.to receive(:self_describing_event)
.with('iglu:com.gitlab/foo/jsonschema/1-0-0', { foo: 'bar' }, context: nil)
.with('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' }, context: nil)
described_class.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', { foo: 'bar' })
described_class.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' })
end
end
end

View File

@ -74,6 +74,18 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red
end
end
context 'for SSE edit actions' do
it_behaves_like 'tracks and counts action' do
def track_action(params)
described_class.track_sse_edit_action(**params)
end
def count_unique(params)
described_class.count_sse_edit_actions(**params)
end
end
end
it 'can return the count of actions per user deduplicated ' do
described_class.track_web_ide_edit_action(author: user1)
described_class.track_snippet_editor_edit_action(author: user1)

View File

@ -456,6 +456,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:projects]).to eq(4)
expect(count_data[:projects_asana_active]).to eq(0)
expect(count_data[:projects_prometheus_active]).to eq(1)
expect(count_data[:projects_jenkins_active]).to eq(1)
expect(count_data[:projects_jira_active]).to eq(4)
expect(count_data[:projects_jira_server_active]).to eq(2)
expect(count_data[:projects_jira_cloud_active]).to eq(2)
@ -1122,6 +1123,12 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
counter.track_web_ide_edit_action(author: user3, time: time - 3.days)
counter.track_snippet_editor_edit_action(author: user3)
counter.track_sse_edit_action(author: user1)
counter.track_sse_edit_action(author: user1)
counter.track_sse_edit_action(author: user2)
counter.track_sse_edit_action(author: user3)
counter.track_sse_edit_action(author: user2, time: time - 3.days)
end
it 'returns the distinct count of user actions within the specified time period' do
@ -1134,7 +1141,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
action_monthly_active_users_web_ide_edit: 2,
action_monthly_active_users_sfe_edit: 2,
action_monthly_active_users_snippet_editor_edit: 2,
action_monthly_active_users_ide_edit: 3
action_monthly_active_users_ide_edit: 3,
action_monthly_active_users_sse_edit: 3
}
)
end

View File

@ -0,0 +1,255 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe JenkinsService do
let(:project) { create(:project) }
let(:jenkins_url) { 'http://jenkins.example.com/' }
let(:jenkins_hook_url) { jenkins_url + 'project/my_project' }
let(:jenkins_username) { 'u$er name%2520' }
let(:jenkins_password) { 'pas$ word' }
let(:jenkins_params) do
{
active: true,
project: project,
properties: {
password: jenkins_password,
username: jenkins_username,
jenkins_url: jenkins_url,
project_name: 'my_project'
}
}
end
let(:jenkins_authorization) { "Basic " + ::Base64.strict_encode64(jenkins_username + ':' + jenkins_password) }
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe 'username validation' do
before do
@jenkins_service = described_class.create!(
active: active,
project: project,
properties: {
jenkins_url: 'http://jenkins.example.com/',
password: 'password',
username: 'username',
project_name: 'my_project'
}
)
end
subject { @jenkins_service }
context 'when the service is active' do
let(:active) { true }
context 'when password was not touched' do
before do
allow(subject).to receive(:password_touched?).and_return(false)
end
it { is_expected.not_to validate_presence_of :username }
end
context 'when password was touched' do
before do
allow(subject).to receive(:password_touched?).and_return(true)
end
it { is_expected.to validate_presence_of :username }
end
context 'when password is blank' do
it 'does not validate the username' do
expect(subject).not_to validate_presence_of :username
subject.password = ''
subject.save!
end
end
end
context 'when the service is inactive' do
let(:active) { false }
it { is_expected.not_to validate_presence_of :username }
end
end
describe '#hook_url' do
let(:username) { nil }
let(:password) { nil }
let(:jenkins_service) do
described_class.new(
project: project,
properties: {
jenkins_url: jenkins_url,
project_name: 'my_project',
username: username,
password: password
}
)
end
subject { jenkins_service.hook_url }
context 'when the jenkins_url has no relative path' do
let(:jenkins_url) { 'http://jenkins.example.com/' }
it { is_expected.to eq('http://jenkins.example.com/project/my_project') }
end
context 'when the jenkins_url has relative path' do
let(:jenkins_url) { 'http://organization.example.com/jenkins' }
it { is_expected.to eq('http://organization.example.com/jenkins/project/my_project') }
end
context 'userinfo is missing and username and password are set' do
let(:jenkins_url) { 'http://organization.example.com/jenkins' }
let(:username) { 'u$ername' }
let(:password) { 'pas$ word' }
it { is_expected.to eq('http://u%24ername:pas%24%20word@organization.example.com/jenkins/project/my_project') }
end
context 'userinfo is provided and username and password are set' do
let(:jenkins_url) { 'http://u:p@organization.example.com/jenkins' }
let(:username) { 'username' }
let(:password) { 'password' }
it { is_expected.to eq('http://username:password@organization.example.com/jenkins/project/my_project') }
end
context 'userinfo is provided username and password are not set' do
let(:jenkins_url) { 'http://u:p@organization.example.com/jenkins' }
it { is_expected.to eq('http://u:p@organization.example.com/jenkins/project/my_project') }
end
end
describe '#test' do
it 'returns the right status' do
user = create(:user, username: 'username')
project = create(:project, name: 'project')
push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
jenkins_service = described_class.create!(jenkins_params)
stub_request(:post, jenkins_hook_url).with(headers: { 'Authorization' => jenkins_authorization })
result = jenkins_service.test(push_sample_data)
expect(result).to eq({ success: true, result: '' })
end
end
describe '#execute' do
let(:user) { create(:user, username: 'username') }
let(:namespace) { create(:group, :private) }
let(:project) { create(:project, :private, name: 'project', namespace: namespace) }
let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:jenkins_service) { described_class.create!(jenkins_params) }
before do
stub_request(:post, jenkins_hook_url)
end
it 'invokes the Jenkins API' do
jenkins_service.execute(push_sample_data)
expect(a_request(:post, jenkins_hook_url)).to have_been_made.once
end
it 'adds default web hook headers to the request' do
jenkins_service.execute(push_sample_data)
expect(
a_request(:post, jenkins_hook_url)
.with(headers: { 'X-Gitlab-Event' => 'Push Hook', 'Authorization' => jenkins_authorization })
).to have_been_made.once
end
it 'request url contains properly serialized username and password' do
jenkins_service.execute(push_sample_data)
expect(
a_request(:post, 'http://jenkins.example.com/project/my_project')
.with(headers: { 'Authorization' => jenkins_authorization })
).to have_been_made.once
end
end
describe 'Stored password invalidation' do
let(:project) { create(:project) }
context 'when a password was previously set' do
before do
@jenkins_service = described_class.create!(
project: project,
properties: {
jenkins_url: 'http://jenkins.example.com/',
username: 'jenkins',
password: 'password'
}
)
end
it 'resets password if url changed' do
@jenkins_service.jenkins_url = 'http://jenkins-edited.example.com/'
@jenkins_service.save!
expect(@jenkins_service.password).to be_nil
end
it 'resets password if username is blank' do
@jenkins_service.username = ''
@jenkins_service.save!
expect(@jenkins_service.password).to be_nil
end
it 'does not reset password if username changed' do
@jenkins_service.username = 'some_name'
@jenkins_service.save!
expect(@jenkins_service.password).to eq('password')
end
it 'does not reset password if new url is set together with password, even if it\'s the same password' do
@jenkins_service.jenkins_url = 'http://jenkins_edited.example.com/'
@jenkins_service.password = 'password'
@jenkins_service.save!
expect(@jenkins_service.password).to eq('password')
expect(@jenkins_service.jenkins_url).to eq('http://jenkins_edited.example.com/')
end
it 'resets password if url changed, even if setter called multiple times' do
@jenkins_service.jenkins_url = 'http://jenkins1.example.com/'
@jenkins_service.jenkins_url = 'http://jenkins1.example.com/'
@jenkins_service.save!
expect(@jenkins_service.password).to be_nil
end
end
context 'when no password was previously set' do
before do
@jenkins_service = described_class.create!(
project: create(:project),
properties: {
jenkins_url: 'http://jenkins.example.com/',
username: 'jenkins'
}
)
end
it 'saves password if new url is set together with password' do
@jenkins_service.jenkins_url = 'http://jenkins_edited.example.com/'
@jenkins_service.password = 'password'
@jenkins_service.save!
expect(@jenkins_service.password).to eq('password')
expect(@jenkins_service.jenkins_url).to eq('http://jenkins_edited.example.com/')
end
end
end
end

View File

@ -4293,29 +4293,33 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#git_transfer_in_progress?' do
using RSpec::Parameterized::TableSyntax
let(:project) { build(:project) }
subject { project.git_transfer_in_progress? }
it 'returns false when repo_reference_count and wiki_reference_count are 0' do
allow(project).to receive(:repo_reference_count) { 0 }
allow(project).to receive(:wiki_reference_count) { 0 }
expect(subject).to be_falsey
where(:project_reference_counter, :wiki_reference_counter, :design_reference_counter, :result) do
0 | 0 | 0 | false
2 | 0 | 0 | true
0 | 2 | 0 | true
0 | 0 | 2 | true
end
it 'returns true when repo_reference_count is > 0' do
allow(project).to receive(:repo_reference_count) { 2 }
allow(project).to receive(:wiki_reference_count) { 0 }
with_them do
before do
allow(project).to receive(:reference_counter).with(type: Gitlab::GlRepository::PROJECT) do
double(:project_reference_counter, value: project_reference_counter)
end
allow(project).to receive(:reference_counter).with(type: Gitlab::GlRepository::WIKI) do
double(:wiki_reference_counter, value: wiki_reference_counter)
end
allow(project).to receive(:reference_counter).with(type: Gitlab::GlRepository::DESIGN) do
double(:design_reference_counter, value: design_reference_counter)
end
end
expect(subject).to be_truthy
end
it 'returns true when wiki_reference_count is > 0' do
allow(project).to receive(:repo_reference_count) { 0 }
allow(project).to receive(:wiki_reference_count) { 2 }
expect(subject).to be_truthy
specify { expect(subject).to be result }
end
end

View File

@ -916,5 +916,11 @@ RSpec.describe Service do
described_class.available_services_names(include_dev: false)
end
it { expect(described_class.available_services_names).to include('jenkins') }
end
describe '.project_specific_services_names' do
it { expect(described_class.project_specific_services_names).to include('jenkins') }
end
end

View File

@ -1888,6 +1888,54 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:created)
end
end
describe 'SSE counter' do
let(:headers) { {} }
let(:params) do
{
title: 'Test merge_request',
source_branch: 'feature_conflict',
target_branch: 'master',
author_id: user.id,
milestone_id: milestone.id,
squash: true
}
end
subject { post api("/projects/#{project.id}/merge_requests", user), params: params, headers: headers }
it 'does not increase the SSE counter by default' do
expect(Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_sse_edit_action)
subject
expect(response).to have_gitlab_http_status(:created)
end
context 'when referer is not the SSE' do
let(:headers) { { 'HTTP_REFERER' => 'https://gitlab.com' } }
it 'does not increase the SSE counter by default' do
expect(Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_sse_edit_action)
subject
expect(response).to have_gitlab_http_status(:created)
end
end
context 'when referer is the SSE' do
let(:headers) { { 'HTTP_REFERER' => project_show_sse_url(project, 'master/README.md') } }
it 'increases the SSE counter by default' do
expect(Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_sse_edit_action).with(author: user)
subject
expect(response).to have_gitlab_http_status(:created)
end
end
end
end
describe 'PUT /projects/:id/merge_reuests/:merge_request_iid' do

View File

@ -28,7 +28,7 @@ RSpec.describe Projects::HashedStorage::MigrateRepositoryService do
end
it 'fails when a git operation is in progress' do
allow(project).to receive(:repo_reference_count) { 1 }
allow(project).to receive(:git_transfer_in_progress?) { true }
expect { service.execute }.to raise_error(Projects::HashedStorage::RepositoryInUseError)
end

View File

@ -28,7 +28,7 @@ RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab
end
it 'fails when a git operation is in progress' do
allow(project).to receive(:repo_reference_count) { 1 }
allow(project).to receive(:git_transfer_in_progress?) { true }
expect { service.execute }.to raise_error(Projects::HashedStorage::RepositoryInUseError)
end

View File

@ -85,6 +85,7 @@ module UsageDataHelpers
projects
projects_imported_from_github
projects_asana_active
projects_jenkins_active
projects_jira_active
projects_jira_server_active
projects_jira_cloud_active

View File

@ -34,8 +34,7 @@ Service.available_services_names.each do |service|
let(:licensed_features) do
{
'github' => :github_project_service_integration,
'jenkins' => :jenkins_integration
'github' => :github_project_service_integration
}
end

View File

@ -0,0 +1,82 @@
# frozen_string_literal: true
require_relative '../../../../tooling/lib/tooling/parallel_rspec_runner'
RSpec.describe Tooling::ParallelRSpecRunner do # rubocop:disable RSpec/FilePath
describe '#run' do
let(:allocator) { instance_double(Knapsack::Allocator) }
let(:rspec_args) { '--seed 123' }
let(:filter_tests_file) { 'tests.txt' }
let(:node_tests) { %w[01_spec.rb 03_spec.rb 05_spec.rb] }
let(:filter_tests) { '01_spec.rb 02_spec.rb 03_spec.rb' }
let(:test_dir) { 'spec' }
before do
allow(Knapsack.logger).to receive(:info)
allow(allocator).to receive(:node_tests).and_return(node_tests)
allow(allocator).to receive(:test_dir).and_return(test_dir)
allow(File).to receive(:exist?).with(filter_tests_file).and_return(true)
allow(File).to receive(:read).and_call_original
allow(File).to receive(:read).with(filter_tests_file).and_return(filter_tests)
allow(subject).to receive(:exec)
end
subject { described_class.new(allocator: allocator, filter_tests_file: filter_tests_file, rspec_args: rspec_args) }
shared_examples 'runs node tests' do
it 'runs rspec with tests allocated for this node' do
expect_command(%w[bundle exec rspec --seed 123 --default-path spec -- 01_spec.rb 03_spec.rb 05_spec.rb])
subject.run
end
end
context 'given filter tests' do
it 'reads filter tests file for list of tests' do
expect(File).to receive(:read).with(filter_tests_file)
subject.run
end
it 'runs rspec filter tests that are allocated for this node' do
expect_command(%w[bundle exec rspec --seed 123 --default-path spec -- 01_spec.rb 03_spec.rb])
subject.run
end
end
context 'with empty filter tests file' do
let(:filter_tests) { '' }
it_behaves_like 'runs node tests'
end
context 'without filter_tests_file option' do
let(:filter_tests_file) { nil }
it_behaves_like 'runs node tests'
end
context 'if filter_tests_file does not exist' do
before do
allow(File).to receive(:exist?).with(filter_tests_file).and_return(false)
end
it_behaves_like 'runs node tests'
end
context 'without rspec args' do
let(:rspec_args) { nil }
it 'runs rspec with without extra arguments' do
expect_command(%w[bundle exec rspec --default-path spec -- 01_spec.rb 03_spec.rb])
subject.run
end
end
def expect_command(cmd)
expect(subject).to receive(:exec).with(*cmd)
end
end
end

19
tooling/bin/parallel_rspec Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'optparse'
require_relative '../lib/tooling/parallel_rspec_runner'
options = {}
OptionParser.new do |opts|
opts.on("--rspec_args rspec_args", String, "Optional rspec arguments") do |value|
options[:rspec_args] = value
end
opts.on("--filter filter_tests_file", String, "Optional filename containing tests to be filtered") do |value|
options[:filter_tests_file] = value
end
end.parse!
Tooling::ParallelRSpecRunner.run(options)

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
require 'knapsack'
# A custom parallel rspec runner based on Knapsack runner
# which takes in additional option for a file containing
# list of test files.
#
# When executing RSpec in CI, the list of tests allocated by Knapsack
# will be compared with the test files listed in the file.
#
# Only the test files allocated by Knapsack and listed in the file
# would be executed in the CI node.
module Tooling
class ParallelRSpecRunner
def self.run(rspec_args: nil, filter_tests_file: nil)
new(rspec_args: rspec_args, filter_tests_file: filter_tests_file).run
end
def initialize(allocator: knapsack_allocator, filter_tests_file: nil, rspec_args: nil)
@allocator = allocator
@filter_tests_file = filter_tests_file
@rspec_args = rspec_args&.split(' ') || []
end
def run
Knapsack.logger.info
Knapsack.logger.info 'Knapsack node specs:'
Knapsack.logger.info node_tests
Knapsack.logger.info
Knapsack.logger.info 'Filter specs:'
Knapsack.logger.info filter_tests
Knapsack.logger.info
Knapsack.logger.info 'Running specs:'
Knapsack.logger.info tests_to_run
Knapsack.logger.info
exec(*rspec_command)
end
private
attr_reader :allocator, :filter_tests_file, :rspec_args
def rspec_command
%w[bundle exec rspec].tap do |cmd|
cmd.push(*rspec_args)
cmd.push('--default-path', allocator.test_dir)
cmd.push('--')
cmd.push(*tests_to_run)
end
end
def tests_to_run
return node_tests if filter_tests.empty?
node_tests & filter_tests
end
def node_tests
allocator.node_tests
end
def filter_tests
filter_tests_file ? tests_from_file(filter_tests_file) : []
end
def tests_from_file(filter_tests_file)
return [] unless File.exist?(filter_tests_file)
File.read(filter_tests_file).split(' ')
end
def knapsack_allocator
Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).allocator
end
end
end