Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
142890d5bb
commit
2eaa60e455
2
Gemfile
2
Gemfile
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -255,10 +255,6 @@ $colors: (
|
|||
}
|
||||
}
|
||||
|
||||
.btn-success .fa-spinner {
|
||||
color: var(--white, $white);
|
||||
}
|
||||
|
||||
.editor-wrap {
|
||||
&.is-loading {
|
||||
.editor {
|
||||
|
|
|
|||
|
|
@ -174,12 +174,6 @@
|
|||
}
|
||||
|
||||
.commit-actions {
|
||||
@include media-breakpoint-up(sm) {
|
||||
.fa-spinner {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon svg {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix incorrect line height in file header
|
||||
merge_request: 48117
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Track MAU for SSE edit
|
||||
merge_request: 48377
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move Jenkins to Core
|
||||
merge_request: 37797
|
||||
author: Ben Bodenmiller (@bbodenmiller)
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add metric for dead Sidekiq jobs
|
||||
merge_request: 48361
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Consider design repositories when determining if there is a git transfer in
|
||||
progress
|
||||
merge_request: 48304
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update gitaly gem to 13.6.1
|
||||
merge_request: 48601
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Gitlab
|
||||
module SidekiqMiddleware
|
||||
class Metrics
|
||||
module MetricsHelper
|
||||
TRUE_LABEL = "yes"
|
||||
FALSE_LABEL = "no"
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
6
qa/qa.rb
6
qa/qa.rb
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue