Commit ef31adeb authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖

Add latest changes from gitlab-org/[email protected]

parent 7e019504
Pipeline #129993052 passed with stages
in 89 minutes and 57 seconds
......@@ -301,7 +301,7 @@ gem 'sentry-raven', '~> 2.9'
gem 'premailer-rails', '~> 1.10.3'
# LabKit: Tracing and Correlation
gem 'gitlab-labkit', '0.11.0'
gem 'gitlab-labkit', '0.12.0'
# I18n
gem 'ruby_parser', '~> 3.8', require: false
......
......@@ -380,7 +380,7 @@ GEM
rake (> 10, < 14)
ruby-statistics (>= 2.1)
thor (>= 0.19, < 2)
gitlab-labkit (0.11.0)
gitlab-labkit (0.12.0)
actionpack (>= 5.0.0, < 6.1.0)
activesupport (>= 5.0.0, < 6.1.0)
grpc (~> 1.19)
......@@ -1232,7 +1232,7 @@ DEPENDENCIES
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-derailed_benchmarks
gitlab-labkit (= 0.11.0)
gitlab-labkit (= 0.12.0)
gitlab-license (~> 1.0)
gitlab-mail_room (~> 0.0.3)
gitlab-markup (~> 1.7.0)
......
import $ from 'jquery';
import initSnippet from '~/snippet/snippet_bundle';
import initForm from '~/pages/projects/init_form';
document.addEventListener('DOMContentLoaded', () => {
initSnippet();
initForm($('.snippet-form'));
});
import '~/snippet/snippet_edit';
import $ from 'jquery';
import initSnippet from '~/snippet/snippet_bundle';
import initForm from '~/pages/projects/init_form';
document.addEventListener('DOMContentLoaded', () => {
initSnippet();
initForm($('.snippet-form'));
});
import '~/snippet/snippet_edit';
import initSnippet from '~/snippet/snippet_bundle';
import form from '../form';
document.addEventListener('DOMContentLoaded', () => {
initSnippet();
form();
});
import '~/snippet/snippet_edit';
import initSnippet from '~/snippet/snippet_bundle';
import form from '../form';
document.addEventListener('DOMContentLoaded', () => {
initSnippet();
form();
});
import '~/snippet/snippet_edit';
import $ from 'jquery';
import initSnippet from '~/snippet/snippet_bundle';
import ZenMode from '~/zen_mode';
import GLForm from '~/gl_form';
document.addEventListener('DOMContentLoaded', () => {
const form = document.querySelector('.snippet-form');
const personalSnippetOptions = {
members: false,
issues: false,
mergeRequests: false,
epics: false,
milestones: false,
labels: false,
snippets: false,
};
const projectSnippetOptions = {};
const options =
form.dataset.snippetType === 'project' ? projectSnippetOptions : personalSnippetOptions;
initSnippet();
new ZenMode(); // eslint-disable-line no-new
new GLForm($(form), options); // eslint-disable-line no-new
});
......@@ -37,7 +37,7 @@ module Authenticates2FAForAdminMode
# Remove any lingering user data from login
session.delete(:otp_user_id)
user.save!
user.save! unless Gitlab::Database.read_only?
# The admin user has successfully passed 2fa, enable admin mode ignoring password
enable_admin_mode
......
......@@ -64,7 +64,9 @@ class Admin::SessionsController < ApplicationController
end
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
valid_otp_attempt = user.validate_and_consume_otp!(user_params[:otp_attempt])
return valid_otp_attempt if Gitlab::Database.read_only?
valid_otp_attempt || user.invalidate_otp_backup_code!(user_params[:otp_attempt])
end
end
......@@ -92,8 +92,8 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
# Deprecated: https://gitlab.com/gitlab-org/gitlab/issues/37735
def find_merge_request_diff_compare
@merge_request_diff =
if diff_id = params[:diff_id].presence
@merge_request.merge_request_diffs.viewable.find_by(id: diff_id)
if params[:diff_id].present?
@merge_request.merge_request_diffs.viewable.find_by(id: params[:diff_id])
else
@merge_request.merge_request_diff
end
......
# frozen_string_literal: true
module Resolvers
module Projects
class JiraImportsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
alias_method :project, :object
def resolve(**args)
return JiraImportData.none unless project&.import_data.present?
authorize!(project)
project.import_data.becomes(JiraImportData).projects
end
def authorized_resource?(project)
Ability.allowed?(context[:current_user], :admin_project, project)
end
end
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
# Authorization is at project level for owners or admins,
# so it is added directly to the Resolvers::JiraImportsResolver
class JiraImportType < BaseObject
graphql_name 'JiraImport'
field :scheduled_at, Types::TimeType, null: true,
description: 'Timestamp of when the Jira import was created/started'
field :scheduled_by, Types::UserType, null: true,
description: 'User that started the Jira import'
field :jira_project_key, GraphQL::STRING_TYPE, null: false,
description: 'Project key for the imported Jira project',
method: :key
def scheduled_at
DateTime.parse(object.scheduled_at)
end
def scheduled_by
::Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.scheduled_by['user_id']).find
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
......@@ -90,8 +90,9 @@ module Types
end
field :import_status, GraphQL::STRING_TYPE, null: true,
description: 'Status of project import background job of the project'
description: 'Status of import background job of the project'
field :jira_import_status, GraphQL::STRING_TYPE, null: true,
description: 'Status of Jira import background job of the project'
field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if merge requests of the project can only be merged with successful jobs'
field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true,
......@@ -192,6 +193,12 @@ module Types
null: true,
description: 'A single board of the project',
resolver: Resolvers::BoardsResolver.single
field :jira_imports,
Types::JiraImportType.connection_type,
null: true,
description: 'Jira imports into the project',
resolver: Resolvers::Projects::JiraImportsResolver
end
end
......
......@@ -231,14 +231,6 @@ module Ci
end
end
after_transition created: :pending do |pipeline|
next if Feature.enabled?(:ci_drop_bridge_on_downstream_errors, pipeline.project, default_enabled: true)
next unless pipeline.bridge_triggered?
next if pipeline.bridge_waiting?
pipeline.update_bridge_status!
end
after_transition any => [:success, :failed] do |pipeline|
pipeline.run_after_commit do
if Feature.enabled?(:ci_pipeline_fixed_notifications)
......@@ -761,15 +753,6 @@ module Ci
end
end
def update_bridge_status!
raise ArgumentError unless bridge_triggered?
raise BridgeStatusError unless source_bridge.active?
source_bridge.success!
rescue => e
Gitlab::ErrorTracking.track_exception(e, pipeline_id: id)
end
def bridge_triggered?
source_bridge.present?
end
......
......@@ -39,4 +39,8 @@ class JiraImportData < ProjectImportData
data['jira'].delete(FORCE_IMPORT_KEY)
end
def current_project
projects.last
end
end
......@@ -857,6 +857,12 @@ class Project < ApplicationRecord
import_state&.status || 'none'
end
def jira_import_status
return import_status if jira_force_import?
import_data&.becomes(JiraImportData)&.projects.blank? ? 'none' : 'finished'
end
def human_import_status_name
import_state&.human_status_name || 'none'
end
......
......@@ -73,7 +73,7 @@ class BambooService < CiService
end
def calculate_reactive_cache(sha, ref)
response = get_path("rest/api/latest/result/byChangeset/#{sha}")
response = try_get_path("rest/api/latest/result/byChangeset/#{sha}")
{ build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
......@@ -81,7 +81,7 @@ class BambooService < CiService
private
def get_build_result(response)
return if response.code != 200
return if response&.code != 200
# May be nil if no result, a single result hash, or an array if multiple results for a given changeset.
result = response.dig('results', 'results', 'result')
......@@ -107,7 +107,7 @@ class BambooService < CiService
end
def read_commit_status(response)
return :error unless response.code == 200 || response.code == 404
return :error unless response && (response.code == 200 || response.code == 404)
result = get_build_result(response)
status =
......@@ -130,24 +130,31 @@ class BambooService < CiService
end
end
def try_get_path(path, query_params = {})
params = build_get_params(query_params)
params[:extra_log_info] = { project_id: project_id }
Gitlab::HTTP.try_get(build_url(path), params)
end
def get_path(path, query_params = {})
Gitlab::HTTP.get(build_url(path), build_get_params(query_params))
end
def build_url(path)
Gitlab::Utils.append_path(bamboo_url, path)
end
def get_path(path, query_params = {})
url = build_url(path)
def build_get_params(query_params)
params = { verify: false, query: query_params }
return params if username.blank? && password.blank?
if username.blank? && password.blank?
Gitlab::HTTP.get(url, verify: false, query: query_params)
else
query_params[:os_authType] = 'basic'
Gitlab::HTTP.get(url,
verify: false,
query: query_params,
basic_auth: {
username: username,
password: password
})
end
query_params[:os_authType] = 'basic'
params[:basic_auth] = basic_auth
params
end
def basic_auth
{ username: username, password: password }
end
end
......@@ -1715,6 +1715,23 @@ class User < ApplicationRecord
super
end
# This is copied from Devise::Models::TwoFactorAuthenticatable#consume_otp!
#
# An OTP cannot be used more than once in a given timestep
# Storing timestep of last valid OTP is sufficient to satisfy this requirement
#
# See:
# <https://github.com/tinfoil/devise-two-factor/blob/master/lib/devise_two_factor/models/two_factor_authenticatable.rb#L66>
#
def consume_otp!
if self.consumed_timestep != current_otp_timestep
self.consumed_timestep = current_otp_timestep
return Gitlab::Database.read_only? ? true : save(validate: false)
end
false
end
private
def default_private_profile_to_false
......
......@@ -34,8 +34,6 @@ module Ci
end
downstream_pipeline.tap do |pipeline|
next if Feature.disabled?(:ci_drop_bridge_on_downstream_errors, project, default_enabled: true)
update_bridge_status!(@bridge, pipeline)
end
end
......
......@@ -3,7 +3,8 @@
.snippet-form-holder
= form_for @snippet, url: url,
html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" } do |f|
html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" },
data: { "snippet-type": @snippet.project_id ? 'project' : 'personal'} do |f|
= form_errors(@snippet)
.form-group
......
......@@ -556,6 +556,13 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: jira_importer:jira_import_import_issue
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: jira_importer:jira_import_stage_finish_import
:feature_category: :importers
:has_external_dependencies:
......
# frozen_string_literal: true
module Gitlab
module JiraImport
class ImportIssueWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
include NotifyUponDeath
include Gitlab::JiraImport::QueueOptions
include Gitlab::Import::DatabaseHelpers
def perform(project_id, jira_issue_id, issue_attributes, waiter_key)
issue_id = insert_and_return_id(issue_attributes, Issue)
cache_issue_mapping(issue_id, jira_issue_id, project_id)
rescue => ex
# Todo: Record jira issue id(or better jira issue key),
# so that we can report the list of failed to import issues to the user
# see https://gitlab.com/gitlab-org/gitlab/-/issues/211653
#
# It's possible the project has been deleted since scheduling this
# job. In this case we'll just skip creating the issue.
Gitlab::ErrorTracking.track_exception(ex, project_id: project_id)
JiraImport.increment_issue_failures(project_id)
ensure
# ensure we notify job waiter that the job has finished
JobWaiter.notify(waiter_key, jid) if waiter_key
end
private
def cache_issue_mapping(issue_id, jira_issue_id, project_id)
cache_key = JiraImport.jira_issue_cache_key(project_id, jira_issue_id)
Gitlab::Cache::Import::Caching.write(cache_key, issue_id)
end
end
end
end
......@@ -11,6 +11,7 @@ module Gitlab
def import(project)
project.after_import
ensure
JiraImport.cache_cleanup(project.id)
project.import_data.becomes(JiraImportData).finish_import!
project.import_data.save!
end
......
......@@ -9,12 +9,19 @@ module Gitlab
private
def import(project)
# fake issues import workers for now
# new job waiter will have zero jobs_remaining by default, so it will just pass on to next stage
jobs_waiter = JobWaiter.new
jobs_waiter = Gitlab::JiraImport::IssuesImporter.new(project).execute
project.import_state.refresh_jid_expiration
Gitlab::JiraImport::AdvanceStageWorker.perform_async(project.id, { jobs_waiter.key => jobs_waiter.jobs_remaining }, :attachments)
Gitlab::JiraImport::AdvanceStageWorker.perform_async(
project.id,
{ jobs_waiter.key => jobs_waiter.jobs_remaining },
next_stage(project)
)
end
def next_stage(project)
Gitlab::JiraImport.get_issues_next_start_at(project.id) < 0 ? :attachments : :issues
end
end
end
......
......@@ -26,6 +26,7 @@ module Gitlab
def start_import
return false unless project
return false if Feature.disabled?(:jira_issue_import, project)
return false unless project.jira_force_import?
return true if start(project.import_state)
Gitlab::Import::Logger.info(
......
......@@ -13,6 +13,11 @@ class ReactiveCachingWorker # rubocop:disable Scalability/IdempotentWorker
urgency :high
worker_resource_boundary :cpu
def self.context_for_arguments(arguments)
class_name, *_other_args = arguments
Gitlab::ApplicationContext.new(related_class: class_name)
end
def perform(class_name, id, *args)
klass = begin
class_name.constantize
......
---
title: Allow querying of Jira imports and their status via GraphQL
merge_request: 27587
author:
type: added
......@@ -62,6 +62,11 @@ input AdminSidekiqQueuesDeleteJobsInput {
"""
queueName: String!
"""
Delete jobs matching related_class in the context metadata
"""
relatedClass: String
"""
Delete jobs matching root_namespace in the context metadata
"""
......@@ -4093,6 +4098,58 @@ Represents untyped JSON
"""
scalar JSON
type JiraImport {
"""
Project key for the imported Jira project
"""
jiraProjectKey: String!
"""
Timestamp of when the Jira import was created/started
"""
scheduledAt: Time
"""
User that started the Jira import
"""
scheduledBy: User
}
"""
The connection type for JiraImport.
"""
type JiraImportConnection {
"""
A list of edges.
"""
edges: [JiraImportEdge]
"""
A list of nodes.
"""
nodes: [JiraImport]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type JiraImportEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: JiraImport
}
type Label {
"""
Background color of the label
......@@ -5749,7 +5806,7 @@ type Project {
id: ID!
"""
Status of project import background job of the project
Status of import background job of the project
"""
importStatus: String
......@@ -5938,6 +5995,36 @@ type Project {
"""
issuesEnabled: Boolean
"""
Status of Jira import background job of the project
"""
jiraImportStatus: String
"""
Jira imports into the project
"""
jiraImports(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): JiraImportConnection
"""
(deprecated) Enable jobs for this project. Use `builds_access_level` instead
"""
......
......@@ -181,6 +181,16 @@
},
"defaultValue": null
},
{
"name": "relatedClass",
"description": "Delete jobs matching related_class in the context metadata",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "queueName",
"description": "The name of the queue to delete jobs from",
......@@ -11611,6 +11621,177 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "JiraImport",
"description": null,
"fields": [
{
"name": "jiraProjectKey",
"description": "Project key for the imported Jira project",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scheduledAt",
"description": "Timestamp of when the Jira import was created/started",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scheduledBy",
"description": "User that started the Jira import",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "User",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}