Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-12-07 15:07:11 +00:00
parent e799c1393a
commit 2a501f63df
97 changed files with 977 additions and 557 deletions

View File

@ -9,6 +9,3 @@ RSpec/LeakyConstantDeclaration:
- 'spec/models/concerns/batch_destroy_dependent_associations_spec.rb'
- 'spec/models/concerns/bulk_insert_safe_spec.rb'
- 'spec/models/concerns/bulk_insertable_associations_spec.rb'
- 'spec/models/concerns/triggerable_hooks_spec.rb'
- 'spec/models/repository_spec.rb'
- 'spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb'

View File

@ -110,7 +110,7 @@ const csvData = (metricHeaders, metricValues) => {
// "If double-quotes are used to enclose fields, then a double-quote
// appearing inside a field must be escaped by preceding it with
// another double quote."
// https://tools.ietf.org/html/rfc4180#page-2
// https://www.rfc-editor.org/rfc/rfc4180#page-2
const headers = metricHeaders.map((header) => `"${header.replace(/"/g, '""')}"`);
return {

View File

@ -55,16 +55,16 @@ export default {
</script>
<template>
<section>
<p v-if="relatedLinks.closing" class="gl-display-inline gl-m-0">
<p v-if="relatedLinks.closing" class="gl-display-inline gl-m-0 gl-font-sm!">
{{ closesText }}
<span v-safe-html="relatedLinks.closing"></span>
</p>
<p v-if="relatedLinks.mentioned" class="gl-display-inline gl-m-0">
<p v-if="relatedLinks.mentioned" class="gl-display-inline gl-m-0 gl-font-sm!">
<span v-if="relatedLinks.closing">&middot;</span>
{{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
<span v-safe-html="relatedLinks.mentioned"></span>
</p>
<p v-if="shouldShowAssignToMeLink" class="gl-display-inline gl-m-0">
<p v-if="shouldShowAssignToMeLink" class="gl-display-inline gl-m-0 gl-font-sm!">
<span>
<gl-link rel="nofollow" data-method="post" :href="relatedLinks.assignToMe">{{
assignIssueText

View File

@ -1,9 +1,9 @@
/** COLORS **/
.cgray { color: $gl-text-color; }
.clgray { color: $common-gray-light; }
.clgray { color: $gray-200; }
.cred { color: $red-500; }
.cgreen { color: $green-600; }
.cdark { color: $common-gray-dark; }
.cdark { color: $gray-800; }
.fwhite { fill: $white; }
.fgray { fill: $gray-500; }

View File

@ -252,7 +252,7 @@
z-index: 1;
&:hover .clear-search-icon {
color: $common-gray-dark;
color: $gray-800;
}
}
}

View File

@ -731,12 +731,6 @@ $commit-max-width-marker-color: rgba(0, 0, 0, 0);
$commit-message-text-area-bg: rgba(0, 0, 0, 0);
$commit-stat-summary-height: 36px;
/*
* Common
*/
$common-gray-light: $gray-200;
$common-gray-dark: $gray-800;
/*
* Files
*/
@ -785,11 +779,6 @@ $fade-mask-transition-curve: ease-in-out;
*/
$login-brand-holder-color: #888;
/*
* Projects
*/
$project-option-descr-color: #54565b;
/*
Stat Graph
*/

View File

@ -0,0 +1,209 @@
@import 'mixins_and_variables_and_functions';
@keyframes expandMaxHeight {
0% {
max-height: 0;
}
99% {
max-height: 100vh;
}
100% {
max-height: none;
}
}
@keyframes collapseMaxHeight {
0% {
max-height: 100vh;
}
100% {
max-height: 0;
}
}
.settings {
// border-top for each item except the top one
border-top: 1px solid var(--border-color, $border-color);
&:first-of-type {
margin-top: 10px;
padding-top: 0;
border: 0;
}
+ div .settings:first-of-type {
margin-top: 0;
border-top: 1px solid var(--border-color, $border-color);
}
&.animating {
overflow: hidden;
}
}
.settings-header {
position: relative;
padding: $gl-padding-24 110px 0 0;
h4 {
margin-top: 0;
}
.settings-title {
cursor: pointer;
}
button {
position: absolute;
top: 20px;
right: 6px;
min-width: 80px;
}
}
.settings-content {
max-height: 1px;
overflow-y: hidden;
padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out;
// Keep the section from expanding when we scroll over it
pointer-events: none;
.settings.expanded & {
max-height: none;
overflow-y: visible;
animation: expandMaxHeight 300ms ease-in;
// Reset and allow clicks again when expanded
pointer-events: auto;
}
.settings.no-animate & {
animation: none;
}
@media(max-width: map-get($grid-breakpoints, md)-1) {
padding-right: 20px;
}
&::before {
content: ' ';
display: block;
height: 1px;
overflow: hidden;
margin-bottom: 4px;
}
&::after {
content: ' ';
display: block;
height: 1px;
overflow: hidden;
margin-top: 20px;
}
.sub-section {
margin-bottom: 32px;
padding: 16px;
border: 1px solid var(--border-color, $border-color);
background-color: var(--gray-light, $gray-light);
}
.bs-callout,
.form-check:first-child,
.form-check .form-text.text-muted,
.form-check + .form-text.text-muted {
margin-top: 0;
}
.form-check .form-text.text-muted {
margin-bottom: $grid-size;
}
}
.settings-list-icon {
color: var(--gray-500, $gl-text-color-secondary);
font-size: $default-icon-size;
line-height: 42px;
}
.settings-message {
padding: 5px;
line-height: 1.3;
color: var(--gray-900, $gray-900);
background-color: var(--orange-50, $orange-50);
border: 1px solid var(--orange-200, $orange-200);
border-radius: $gl-border-radius-base;
}
.prometheus-metrics-monitoring {
.card {
.card-toggle {
width: 14px;
}
.badge.badge-pill {
font-size: 12px;
line-height: 12px;
}
.card-header .label-count {
color: var(--white, $white);
background: var(--gray-800, $gray-800);
}
.card-body {
padding: 0;
}
.flash-container {
margin-bottom: 0;
cursor: default;
.flash-notice {
border-radius: 0;
}
}
}
.custom-monitored-metrics {
.card-header {
display: flex;
align-items: center;
}
.custom-metric {
display: flex;
align-items: center;
}
.custom-metric-link-bold {
font-weight: $gl-font-weight-bold;
text-decoration: none;
}
}
.loading-metrics .metrics-load-spinner {
color: var(--gray-700, $gray-700);
}
.metrics-list {
margin-bottom: 0;
li {
padding: $gl-padding;
.badge.badge-pill {
margin-left: 5px;
background: $badge-bg;
}
/* Ensure we don't add border if there's only single li */
+ li {
border-top: 1px solid var(--border-color, $border-color);
}
}
}
}

View File

@ -22,3 +22,11 @@
display: none;
}
}
.warning-title {
color: var(--gray-900, $gray-900);
}
.danger-title {
color: var(--red-500, $red-500);
}

View File

@ -1,149 +1,3 @@
@keyframes expandMaxHeight {
0% {
max-height: 0;
}
99% {
max-height: 100vh;
}
100% {
max-height: none;
}
}
@keyframes collapseMaxHeight {
0% {
max-height: 100vh;
}
100% {
max-height: 0;
}
}
.settings {
// border-top for each item except the top one
border-top: 1px solid $border-color;
&:first-of-type {
margin-top: 10px;
padding-top: 0;
border: 0;
}
+ div .settings:first-of-type {
margin-top: 0;
border-top: 1px solid $border-color;
}
&.animating {
overflow: hidden;
}
}
.settings-header {
position: relative;
padding: 24px 110px 0 0;
h4 {
margin-top: 0;
}
.settings-title {
cursor: pointer;
}
button {
position: absolute;
top: 20px;
right: 6px;
min-width: 80px;
}
}
.settings-content {
max-height: 1px;
overflow-y: hidden;
padding-right: 110px;
animation: collapseMaxHeight 300ms ease-out;
// Keep the section from expanding when we scroll over it
pointer-events: none;
.settings.expanded & {
max-height: none;
overflow-y: visible;
animation: expandMaxHeight 300ms ease-in;
// Reset and allow clicks again when expanded
pointer-events: auto;
}
.settings.no-animate & {
animation: none;
}
@media(max-width: map-get($grid-breakpoints, md)-1) {
padding-right: 20px;
}
&::before {
content: ' ';
display: block;
height: 1px;
overflow: hidden;
margin-bottom: 4px;
}
&::after {
content: ' ';
display: block;
height: 1px;
overflow: hidden;
margin-top: 20px;
}
.sub-section {
margin-bottom: 32px;
padding: 16px;
border: 1px solid $border-color;
background-color: $gray-light;
}
.bs-callout,
.form-check:first-child,
.form-check .form-text.text-muted,
.form-check + .form-text.text-muted {
margin-top: 0;
}
.form-check .form-text.text-muted {
margin-bottom: $grid-size;
}
}
.settings-list-icon {
color: $gl-text-color-secondary;
font-size: $default-icon-size;
line-height: 42px;
}
.settings-message {
padding: 5px;
line-height: 1.3;
color: $gray-900;
background-color: $orange-50;
border: 1px solid $orange-200;
border-radius: $border-radius-base;
}
.warning-title {
color: $gray-900;
}
.danger-title {
color: $red-500;
}
.integration-settings-form {
.card.card-body,
.info-well {
@ -160,13 +14,13 @@
.option-title {
font-weight: $gl-font-weight-normal;
display: inline-block;
color: $gl-text-color;
color: var(--gl-text-color, $gl-text-color);
vertical-align: top;
}
.option-description,
.option-disabled-reason {
color: $project-option-descr-color;
color: var(--gray-700, $gray-700);
}
.option-disabled-reason {
@ -188,79 +42,9 @@
}
}
.prometheus-metrics-monitoring {
.card {
.card-toggle {
width: 14px;
}
.badge.badge-pill {
font-size: 12px;
line-height: 12px;
}
.card-header .label-count {
color: $white;
background: $common-gray-dark;
}
.card-body {
padding: 0;
}
.flash-container {
margin-bottom: 0;
cursor: default;
.flash-notice {
border-radius: 0;
}
}
}
.custom-monitored-metrics {
.card-header {
display: flex;
align-items: center;
}
.custom-metric {
display: flex;
align-items: center;
}
.custom-metric-link-bold {
font-weight: $gl-font-weight-bold;
text-decoration: none;
}
}
.loading-metrics .metrics-load-spinner {
color: $gray-700;
}
.metrics-list {
margin-bottom: 0;
li {
padding: $gl-padding;
.badge.badge-pill {
margin-left: 5px;
background: $badge-bg;
}
/* Ensure we don't add border if there's only single li */
+ li {
border-top: 1px solid $border-color;
}
}
}
}
.saml-settings.info-well {
.form-control[readonly] {
background: $white;
background: var(--white, $white);
}
}
@ -275,8 +59,8 @@
}
.btn-clipboard {
background-color: $white;
border: 1px solid $gray-100;
background-color: var(--white, $white);
border: 1px solid var(--gray-100, $gray-100);
}
.deploy-token-help-block {
@ -294,7 +78,7 @@
.ci-secure-files-table {
table {
thead {
border-bottom: 1px solid $white-normal;
border-bottom: 1px solid var(--gray-50, $gray-50);
}
tr {

View File

@ -525,7 +525,7 @@ class ApplicationController < ActionController::Base
end
def set_page_title_header
# Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
# Per https://www.rfc-editor.org/rfc/rfc5987, headers need to be ISO-8859-1, not UTF-8
response.headers['Page-Title'] = Addressable::URI.encode_component(page_title('GitLab'))
end

View File

@ -11,8 +11,6 @@ module Groups
def index
# To be used in ee/app/controllers/ee/groups/usage_quotas_controller.rb
@seat_count_data = seat_count_data
@current_namespace_usage = current_namespace_usage
@projects_usage = projects_usage
end
private
@ -24,10 +22,6 @@ module Groups
# To be overriden in ee/app/controllers/ee/groups/usage_quotas_controller.rb
def seat_count_data; end
def current_namespace_usage; end
def projects_usage; end
end
end

View File

@ -5,7 +5,6 @@ class Projects::ClustersController < Clusters::ClustersController
before_action :repository
before_action do
push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:show_gitlab_agent_feedback, type: :ops)
end

View File

@ -14,8 +14,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
authorize_metrics_dashboard!
push_frontend_feature_flag(:prometheus_computed_alerts)
end
before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect]

View File

@ -9,9 +9,6 @@ module Projects
include Gitlab::Utils::StrongMemoize
before_action :authorize_metrics_dashboard!
before_action do
push_frontend_feature_flag(:prometheus_computed_alerts)
end
feature_category :metrics
urgency :low

View File

@ -73,3 +73,5 @@ module Mutations
end
end
end
Mutations::WorkItems::Create.prepend_mod

View File

@ -20,6 +20,24 @@ module Types
null: true, complexity: 5,
description: 'Child work items.'
field :has_children, GraphQL::Types::Boolean,
null: false, description: 'Indicates if the work item has children.'
# rubocop: disable CodeReuse/ActiveRecord
def has_children?
BatchLoader::GraphQL.for(object.work_item.id).batch(default_value: false) do |ids, loader|
links_for_parents = ::WorkItems::ParentLink.for_parents(ids)
.select(:work_item_parent_id)
.group(:work_item_parent_id)
.reorder(nil)
links_for_parents.each { |link| loader.call(link.work_item_parent_id, true) }
end
end
# rubocop: enable CodeReuse/ActiveRecord
alias_method :has_children, :has_children?
def children
object.children.inc_relations_for_permission_check
end

View File

@ -172,8 +172,6 @@ module Ci
add_authentication_token_field :token, encrypted: :required
before_save :ensure_token, unless: :assign_token_on_scheduling?
after_save :stick_build_if_status_changed
after_create unless: :importing? do |build|
@ -247,11 +245,8 @@ module Ci
!build.waiting_for_deployment_approval? # If false is returned, it stops the transition
end
before_transition any => [:pending] do |build, transition|
if build.assign_token_on_scheduling?
build.ensure_token
end
before_transition any => [:pending] do |build|
build.ensure_token
true
end
@ -1140,10 +1135,6 @@ module Ci
end
end
def assign_token_on_scheduling?
::Feature.enabled?(:ci_assign_job_token_on_scheduling, project)
end
protected
def run_status_commit_hooks!

View File

@ -3,6 +3,7 @@
module Ci
module Sources
class Pipeline < Ci::ApplicationRecord
include Ci::Partitionable
include Ci::NamespacedModelName
self.table_name = "ci_sources_pipelines"
@ -15,6 +16,11 @@ module Ci
belongs_to :source_bridge, class_name: "Ci::Bridge", foreign_key: :source_job_id
belongs_to :source_pipeline, class_name: "Ci::Pipeline", foreign_key: :source_pipeline_id
partitionable scope: :pipeline
before_validation :set_source_partition_id, on: :create
validates :source_partition_id, presence: true
validates :project, presence: true
validates :pipeline, presence: true
@ -23,6 +29,15 @@ module Ci
validates :source_pipeline, presence: true
scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
private
def set_source_partition_id
return if source_partition_id_changed? && source_partition_id.present?
return unless source_job
self.source_partition_id = source_job.partition_id
end
end
end
end

View File

@ -37,6 +37,7 @@ module Ci
Ci::PendingBuild
Ci::RunningBuild
Ci::PipelineVariable
Ci::Sources::Pipeline
Ci::Stage
Ci::UnitTestFailure
].freeze
@ -67,14 +68,31 @@ module Ci
end
class_methods do
def partitionable(scope:, through: nil)
if through
define_singleton_method(:routing_table_name) { through[:table] }
define_singleton_method(:routing_table_name_flag) { through[:flag] }
def partitionable(scope:, through: nil, partitioned: false)
handle_partitionable_through(through)
handle_partitionable_dml(partitioned)
handle_partitionable_scope(scope)
end
include Partitionable::Switch
end
private
def handle_partitionable_through(options)
return unless options
define_singleton_method(:routing_table_name) { options[:table] }
define_singleton_method(:routing_table_name_flag) { options[:flag] }
include Partitionable::Switch
end
def handle_partitionable_dml(partitioned)
define_singleton_method(:partitioned?) { partitioned }
return unless partitioned
include Partitionable::PartitionedFilter
end
def handle_partitionable_scope(scope)
define_method(:partition_scope_value) do
strong_memoize(:partition_scope_value) do
next Ci::Pipeline.current_partition_value if respond_to?(:importing?) && importing?

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Ci
module Partitionable
# Used to patch the save, update, delete, destroy methods to use the
# partition_id attributes for their SQL queries.
module PartitionedFilter
extend ActiveSupport::Concern
if Rails::VERSION::MAJOR >= 7
# These methods are updated in Rails 7 to use `_primary_key_constraints_hash`
# by default, so this patch will no longer be required.
#
# rubocop:disable Gitlab/NoCodeCoverageComment
# :nocov:
raise "`#{__FILE__}` should be double checked" if Rails.env.test?
warn "Update `#{__FILE__}`. Patches Rails internals for partitioning"
# :nocov:
# rubocop:enable Gitlab/NoCodeCoverageComment
else
def _update_row(attribute_names, attempted_action = "update")
self.class._update_record(
attributes_with_values(attribute_names),
_primary_key_constraints_hash
)
end
def _delete_row
self.class._delete_record(_primary_key_constraints_hash)
end
end
# Introduced in Rails 7, but updated to include `partition_id` filter.
# https://github.com/rails/rails/blob/a4dbb153fd390ac31bb9808809e7ac4d3a2c5116/activerecord/lib/active_record/persistence.rb#L1031-L1033
def _primary_key_constraints_hash
{ @primary_key => id_in_database, partition_id: partition_id } # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
end
end

View File

@ -19,6 +19,8 @@ module WorkItems
validate :validate_max_children
validate :validate_confidentiality
scope :for_parents, ->(parent_ids) { where(work_item_parent_id: parent_ids) }
class << self
def has_public_children?(parent_id)
joins(:work_item).where(work_item_parent_id: parent_id, 'issues.confidential': false).exists?

View File

@ -16,19 +16,23 @@ module Ci
def execute(bridge)
@bridge = bridge
if bridge.has_downstream_pipeline?
if @bridge.has_downstream_pipeline?
Gitlab::ErrorTracking.track_exception(
DuplicateDownstreamPipelineError.new,
bridge_id: @bridge.id, project_id: @bridge.project_id
)
return error('Already has a downstream pipeline')
return ServiceResponse.error(message: 'Already has a downstream pipeline')
end
pipeline_params = @bridge.downstream_pipeline_params
target_ref = pipeline_params.dig(:target_revision, :ref)
return error('Pre-conditions not met') unless ensure_preconditions!(target_ref)
return ServiceResponse.error(message: 'Pre-conditions not met') unless ensure_preconditions!(target_ref)
if Feature.enabled?(:ci_run_bridge_for_pipeline_duration_calculation, project) && !@bridge.run
return ServiceResponse.error(message: 'Can not run the bridge')
end
service = ::Ci::CreatePipelineService.new(
pipeline_params.fetch(:project),
@ -40,10 +44,7 @@ module Ci
.payload
log_downstream_pipeline_creation(downstream_pipeline)
downstream_pipeline.tap do |pipeline|
update_bridge_status!(@bridge, pipeline)
end
update_bridge_status!(@bridge, downstream_pipeline)
end
private
@ -54,9 +55,12 @@ module Ci
# If bridge uses `strategy:depend` we leave it running
# and update the status when the downstream pipeline completes.
subject.success! unless subject.dependent?
ServiceResponse.success(payload: pipeline)
else
subject.options[:downstream_errors] = pipeline.errors.full_messages
message = pipeline.errors.full_messages
subject.options[:downstream_errors] = message
subject.drop!(:downstream_pipeline_creation_failed)
ServiceResponse.error(payload: pipeline, message: message)
end
end
rescue StateMachines::InvalidTransition => e
@ -64,6 +68,7 @@ module Ci
Ci::Bridge::InvalidTransitionError.new(e.message),
bridge_id: bridge.id,
downstream_pipeline_id: pipeline.id)
ServiceResponse.error(payload: pipeline, message: e.message)
end
def ensure_preconditions!(target_ref)

View File

@ -28,7 +28,7 @@ module PagesDomains
api_order = ::Gitlab::LetsEncrypt::Client.new.load_order(acme_order.url)
# https://tools.ietf.org/html/rfc8555#section-7.1.6 - statuses diagram
# https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6 - statuses diagram
case api_order.status
when 'ready'
api_order.request_certificate(private_key: acme_order.private_key, domain: pages_domain.domain)

View File

@ -1,4 +1,5 @@
- page_title _("Appearance")
- @content_class = "limit-container-width" unless fluid_layout
- add_page_specific_style 'page_bundles/settings'
= render 'form'

View File

@ -1,5 +1,6 @@
- breadcrumb_title _("CI/CD")
- page_title _("CI/CD")
- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.no-animate#js-ci-cd-variables{ class: ('expanded' if expanded_by_default?) }

View File

@ -1,5 +1,6 @@
- breadcrumb_title _("General")
- page_title _("General")
- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded_by_default?) }

View File

@ -1,5 +1,6 @@
- breadcrumb_title s_('Integrations|Instance-level integration management')
- page_title s_('Integrations|Instance-level integration management')
- add_page_specific_style 'page_bundles/settings'
- @content_class = 'limit-container-width' unless fluid_layout
%h3= s_('Integrations|Instance-level integration management')

View File

@ -2,6 +2,7 @@
- breadcrumb_title _("Metrics and profiling")
- page_title _("Metrics and profiling")
- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-prometheus.no-animate#js-prometheus-settings{ class: ('expanded' if expanded_by_default?) }

View File

@ -1,5 +1,6 @@
- breadcrumb_title _("Network")
- page_title _("Network")
- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded_by_default?) }

View File

@ -1,5 +1,6 @@
- breadcrumb_title _("Preferences")
- page_title _("Preferences")
- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'email_content' } }

View File

@ -1,5 +1,6 @@
- breadcrumb_title _("Reporting")
- page_title _("Reporting")
- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-spam.no-animate#js-spam-settings{ class: ('expanded' if expanded_by_default?) }

View File

@ -1,5 +1,6 @@
- breadcrumb_title _("Repository")
- page_title _("Repository")
- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-default-branch-name.no-animate#js-default-branch-name{ class: ('expanded' if expanded_by_default?) }

View File

@ -2,6 +2,7 @@
- breadcrumb_title name
- page_title name
- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
- payload_class = 'js-service-ping-payload'

View File

@ -32,7 +32,8 @@
= sprite_icon('project', size: 16, css_class: 'gl-text-gray-700')
%h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, Project)
.gl-mt-3.text-uppercase= s_('AdminArea|Projects')
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-default")
= render Pajamas::ButtonComponent.new(href: new_project_path) do
= s_('AdminArea|New project')
= c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest projects'), admin_projects_path(sort: 'created_desc'))
@ -55,7 +56,8 @@
.gl-mt-3.text-uppercase
= s_('AdminArea|Users')
= link_to(s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: "text-capitalize gl-ml-2")
= link_to(s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-default")
= render Pajamas::ButtonComponent.new(href: new_admin_user_path) do
= s_('AdminArea|New user')
= c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest users'), admin_users_path({ sort: 'created_desc' }))
@ -68,7 +70,8 @@
= sprite_icon('group', size: 16, css_class: 'gl-text-gray-700')
%h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, Group)
.gl-mt-3.text-uppercase= s_('AdminArea|Groups')
= link_to(s_('AdminArea|New group'), new_admin_group_path, class: "btn gl-button btn-default")
= render Pajamas::ButtonComponent.new(href: new_admin_group_path) do
= s_('AdminArea|New group')
= c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest groups'), admin_groups_path(sort: 'created_desc'))

View File

@ -33,5 +33,4 @@
%p.masking-validation-error.gl-field-error.hide
= s_("CiVariables|Cannot use Masked Variable with current value")
= link_to sprite_icon('question-o'), help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'), target: '_blank', rel: 'noopener noreferrer'
%button.gl-button.btn.btn-default.btn-icon.js-row-remove-button.ci-variable-row-remove-button.table-section{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
= sprite_icon('close')
= render Pajamas::ButtonComponent.new(icon: 'close', button_options: { class: 'js-row-remove-button ci-variable-row-remove-button table-section', 'aria-label': s_('CiVariables|Remove variable row') })

View File

@ -1,4 +1,5 @@
- page_title _("Group applications")
- add_page_specific_style 'page_bundles/settings'
= render 'shared/doorkeeper/applications/index',
oauth_applications_enabled: user_oauth_applications?,

View File

@ -1,5 +1,6 @@
- page_title _("Settings")
- nav "group"
- add_page_specific_style 'page_bundles/settings'
- enable_search_settings locals: { container_class: 'gl-my-5' }
= render template: "layouts/group"

View File

@ -1,5 +1,6 @@
- page_title _("Settings")
- nav "project"
- add_page_specific_style 'page_bundles/settings'
- enable_search_settings locals: { container_class: 'gl-my-5' }

View File

@ -11,9 +11,15 @@ module Ci
def perform(bridge_id)
::Ci::Bridge.find_by_id(bridge_id).try do |bridge|
::Ci::CreateDownstreamPipelineService
result = ::Ci::CreateDownstreamPipelineService
.new(bridge.project, bridge.user)
.execute(bridge)
if result.success?
log_extra_metadata_on_done(:new_pipeline_id, result.payload.id)
else
log_extra_metadata_on_done(:create_error_message, result.message)
end
end
end
end

View File

@ -320,6 +320,7 @@ module Gitlab
config.assets.precompile << "page_bundles/runner_details.css"
config.assets.precompile << "page_bundles/security_dashboard.css"
config.assets.precompile << "page_bundles/security_discover.css"
config.assets.precompile << "page_bundles/settings.css"
config.assets.precompile << "page_bundles/signup.css"
config.assets.precompile << "page_bundles/terminal.css"
config.assets.precompile << "page_bundles/terms.css"

View File

@ -1,8 +1,8 @@
---
name: ci_assign_job_token_on_scheduling
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/103377
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382042
milestone: '15.6'
name: ci_run_bridge_for_pipeline_duration_calculation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/99473
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356759
milestone: '15.7'
type: development
group: group::pipeline execution
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: prometheus_computed_alerts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/13443
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255304
milestone: '12.0'
type: development
group: group::respond
default_enabled: false

View File

@ -855,7 +855,7 @@ production: &base
# Filter LDAP users
#
# Format: RFC 4515 https://tools.ietf.org/search/rfc4515
# Format: RFC 4515 https://www.rfc-editor.org/rfc/rfc4515
# Ex. (employeeType=developer)
#
# Note: GitLab does not support omniauth-ldap's custom filter syntax.

View File

@ -9,7 +9,7 @@ product_category: gitaly
value_type: number
status: active
time_frame: all
data_source: database
data_source: system
distribution:
- ce
- ee

View File

@ -9,7 +9,7 @@ product_category: gitaly
value_type: number
status: active
time_frame: all
data_source: database
data_source: system
distribution:
- ce
- ee

View File

@ -10,7 +10,7 @@ product_category: container registry
value_type: number
status: active
time_frame: all
data_source: database
data_source: system
distribution:
- ee
- ce

View File

@ -9,7 +9,7 @@ product_category: collection
value_type: string
status: active
time_frame: none
data_source: database
data_source: system
distribution:
- ce
- ee

View File

@ -9,7 +9,7 @@ product_category: integrations
value_type: boolean
status: active
time_frame: none
data_source: database
data_source: system
distribution:
- ce
- ee

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddSourcePartitionIdToCiSourcesPipeline < Gitlab::Database::Migration[2.0]
enable_lock_retries!
def change
add_column :ci_sources_pipelines, :source_partition_id, :bigint, default: 100, null: false
end
end

View File

@ -0,0 +1 @@
2b763fd1fe9aee5631f9a8f3bdf699a19003e56f5c857efe4410ec21e5dad8f7

View File

@ -13509,7 +13509,8 @@ CREATE TABLE ci_sources_pipelines (
source_project_id integer,
source_pipeline_id integer,
source_job_id bigint,
partition_id bigint DEFAULT 100 NOT NULL
partition_id bigint DEFAULT 100 NOT NULL,
source_partition_id bigint DEFAULT 100 NOT NULL
);
CREATE SEQUENCE ci_sources_pipelines_id_seq

View File

@ -5929,6 +5929,7 @@ Input type: `WorkItemCreateInput`
| <a id="mutationworkitemcreateconfidential"></a>`confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. |
| <a id="mutationworkitemcreatedescription"></a>`description` | [`String`](#string) | Description of the work item. |
| <a id="mutationworkitemcreatehierarchywidget"></a>`hierarchyWidget` | [`WorkItemWidgetHierarchyCreateInput`](#workitemwidgethierarchycreateinput) | Input for hierarchy widget. |
| <a id="mutationworkitemcreateiterationwidget"></a>`iterationWidget` | [`WorkItemWidgetIterationInput`](#workitemwidgetiterationinput) | Iteration widget of the work item. |
| <a id="mutationworkitemcreatemilestonewidget"></a>`milestoneWidget` | [`WorkItemWidgetMilestoneInput`](#workitemwidgetmilestoneinput) | Input for milestone widget. |
| <a id="mutationworkitemcreateprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project the work item is associated with. |
| <a id="mutationworkitemcreatetitle"></a>`title` | [`String!`](#string) | Title of the work item. |
@ -20733,6 +20734,7 @@ Represents a hierarchy widget.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgethierarchychildren"></a>`children` | [`WorkItemConnection`](#workitemconnection) | Child work items. (see [Connections](#connections)) |
| <a id="workitemwidgethierarchyhaschildren"></a>`hasChildren` | [`Boolean!`](#boolean) | Indicates if the work item has children. |
| <a id="workitemwidgethierarchyparent"></a>`parent` | [`WorkItem`](#workitem) | Parent work item. |
| <a id="workitemwidgethierarchytype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
@ -24532,6 +24534,7 @@ Represents an escalation rule.
| <a id="negatedboardissueinputassigneeusername"></a>`assigneeUsername` | [`[String]`](#string) | Filter by assignee username. |
| <a id="negatedboardissueinputauthorusername"></a>`authorUsername` | [`String`](#string) | Filter by author username. |
| <a id="negatedboardissueinputepicid"></a>`epicId` | [`EpicID`](#epicid) | Filter by epic ID. Incompatible with epicWildcardId. |
| <a id="negatedboardissueinputhealthstatusfilter"></a>`healthStatusFilter` | [`HealthStatus`](#healthstatus) | Health status not applied to the issue. Includes issues where health status is not set. |
| <a id="negatedboardissueinputiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example `["1", "2"]`. |
| <a id="negatedboardissueinputiterationid"></a>`iterationId` | [`[IterationID!]`](#iterationid) | Filter by a list of iteration IDs. Incompatible with iterationWildcardId. |
| <a id="negatedboardissueinputiterationtitle"></a>`iterationTitle` | [`String`](#string) | Filter by iteration title. |
@ -24574,6 +24577,7 @@ Represents an escalation rule.
| <a id="negatedissuefilterinputassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users not assigned to the issue. |
| <a id="negatedissuefilterinputauthorusername"></a>`authorUsername` | [`String`](#string) | Username of a user who didn't author the issue. |
| <a id="negatedissuefilterinputepicid"></a>`epicId` | [`String`](#string) | ID of an epic not associated with the issues. |
| <a id="negatedissuefilterinputhealthstatusfilter"></a>`healthStatusFilter` | [`HealthStatus`](#healthstatus) | Health status not applied to the issue. Includes issues where health status is not set. |
| <a id="negatedissuefilterinputiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues to exclude. For example, `[1, 2]`. |
| <a id="negatedissuefilterinputiterationid"></a>`iterationId` | [`[ID!]`](#id) | List of iteration Global IDs not applied to the issue. |
| <a id="negatedissuefilterinputiterationwildcardid"></a>`iterationWildcardId` | [`IterationWildcardId`](#iterationwildcardid) | Filter by negated iteration ID wildcard. |

View File

@ -59,11 +59,8 @@ their GitHub authors and assignees in the database of the GitLab instance. Pull
GitLab.
For this association to succeed, each GitHub author and assignee in the repository
must meet one of the following conditions prior to the import:
- Have previously logged in to a GitLab account using the GitHub icon.
- Have a GitHub account with a [public-facing email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address)
that matches their GitLab account's email address.
must have a [public-facing email address](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address)
on GitHub that matches their GitLab email address (regardless of how the account was created).
GitLab content imports that use GitHub accounts require that the GitHub public-facing email address is populated. This means
all comments and contributions are properly mapped to the same user in GitLab. GitHub Enterprise does not require this
@ -73,10 +70,8 @@ field to be populated so you may have to add it on existing accounts.
### Use the GitHub integration
Before you begin, ensure that any GitHub users who you want to map to GitLab users have either:
- A GitLab account that has logged in using the GitHub icon.
- A GitLab account with an email address that matches the [publicly visible email address](https://docs.github.com/en/rest/users#get-a-user) in the profile of the GitHub user
Before you begin, ensure that any GitHub user you want to map to a GitLab user has a GitLab email address that matches their
[publicly visible email address](https://docs.github.com/en/rest/users#get-a-user) on GitHub.
If you are importing to GitLab.com, you can alternatively import GitHub repositories using a [personal access token](#use-a-github-token).
We do not recommend this method, as it does not associate all user activity (such as issues and pull requests) with matching GitLab users.

View File

@ -202,39 +202,52 @@ worker and it would not recognize `incoming_email` emails.
To configure a custom mailbox for Service Desk with IMAP, add the following snippets to your configuration file in full:
- Example for installations from source:
::Tabs
```yaml
service_desk_email:
enabled: true
address: "project_contact+%{key}@example.com"
user: "project_contact@example.com"
password: "[REDACTED]"
host: "imap.gmail.com"
port: 993
ssl: true
start_tls: false
log_path: "log/mailroom.log"
mailbox: "inbox"
idle_timeout: 60
expunge_deleted: true
```
:::TabTitle Linux package (Omnibus)
- Example for Omnibus GitLab installations:
NOTE:
In GitLab 15.3 and later, Service Desk uses `webhook` (internal API call) by default instead of enqueuing a Sidekiq job.
To use `webhook` on an Omnibus installation running GitLab 15.3, you must generate a secret file.
For more context, visit [Omnibus GitLab MR 5927](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5927).
In GitLab 15.4, reconfiguring an Omnibus installation generates this secret file automatically, so no secret file configuration setting is needed.
For details, visit [issue 1462](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1462).
```ruby
gitlab_rails['service_desk_email_enabled'] = true
gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@gmail.com"
gitlab_rails['service_desk_email_email'] = "project_contact@gmail.com"
gitlab_rails['service_desk_email_password'] = "[REDACTED]"
gitlab_rails['service_desk_email_mailbox_name'] = "inbox"
gitlab_rails['service_desk_email_idle_timeout'] = 60
gitlab_rails['service_desk_email_log_file'] = "/var/log/gitlab/mailroom/mail_room_json.log"
gitlab_rails['service_desk_email_host'] = "imap.gmail.com"
gitlab_rails['service_desk_email_port'] = 993
gitlab_rails['service_desk_email_ssl'] = true
gitlab_rails['service_desk_email_start_tls'] = false
```
```ruby
gitlab_rails['service_desk_email_enabled'] = true
gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@gmail.com"
gitlab_rails['service_desk_email_email'] = "project_contact@gmail.com"
gitlab_rails['service_desk_email_password'] = "[REDACTED]"
gitlab_rails['service_desk_email_mailbox_name'] = "inbox"
gitlab_rails['service_desk_email_idle_timeout'] = 60
gitlab_rails['service_desk_email_log_file'] = "/var/log/gitlab/mailroom/mail_room_json.log"
gitlab_rails['service_desk_email_host'] = "imap.gmail.com"
gitlab_rails['service_desk_email_port'] = 993
gitlab_rails['service_desk_email_ssl'] = true
gitlab_rails['service_desk_email_start_tls'] = false
```
:::TabTitle Self-compiled (source)
```yaml
service_desk_email:
enabled: true
address: "project_contact+%{key}@example.com"
user: "project_contact@example.com"
password: "[REDACTED]"
host: "imap.gmail.com"
delivery_method: webhook
secret_file: .gitlab-mailroom-secret
port: 993
ssl: true
start_tls: false
log_path: "log/mailroom.log"
mailbox: "inbox"
idle_timeout: 60
expunge_deleted: true
```
::EndTabs
The configuration options are the same as for configuring
[incoming email](../../administration/incoming_email.md#set-it-up).

View File

@ -172,14 +172,24 @@ module API
desc: 'The url encoded path to the file.', documentation: { example: 'lib%2Fclass%2Erb' }
optional :ref, type: String,
desc: 'The name of branch, tag or commit', allow_blank: false, documentation: { example: 'main' }
optional :lfs, type: Boolean,
desc: 'Retrieve binary data for a file that is an lfs pointer',
default: false
end
get ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS, urgency: :low do
assign_file_vars!
no_cache_headers
set_http_headers(blob_data)
if params[:lfs] && @blob.stored_externally?
lfs_object = LfsObject.find_by_oid(@blob.lfs_oid)
not_found! unless lfs_object&.project_allowed_access?(@project)
send_git_blob @repo, @blob
present_carrierwave_file!(lfs_object.file)
else
no_cache_headers
set_http_headers(blob_data)
send_git_blob @repo, @blob
end
end
desc 'Get file metadata from repository'

View File

@ -51,7 +51,7 @@ module Gitlab
##
# Parse a DN into key value pairs using ASN from
# http://tools.ietf.org/html/rfc2253 section 3.
# https://www.rfc-editor.org/rfc/rfc2253 section 3.
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
@ -231,7 +231,7 @@ module Gitlab
self.class.new(*to_a).to_s.downcase
end
# https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
# https://www.rfc-editor.org/rfc/rfc4514 section 2.4 lists these exceptions
# for DN values. All of the following must be escaped in any normal string
# using a single backslash ('\') as escape. The space character is left
# out here because in a "normalized" string, spaces should only be escaped

View File

@ -31,7 +31,8 @@ module Gitlab
source_pipeline: @command.bridge.pipeline,
source_project: @command.bridge.project,
source_bridge: @command.bridge,
project: @command.project
project: @command.project,
source_partition_id: @command.bridge.partition_id
)
end

View File

@ -192,7 +192,7 @@ module Gitlab
auto_submitted = mail.header['Auto-Submitted']&.value
# Mail::Field#value would strip leading and trailing whitespace
# See also https://tools.ietf.org/html/rfc3834
# See also https://www.rfc-editor.org/rfc/rfc3834
auto_submitted && auto_submitted != 'no'
end

View File

@ -3,7 +3,7 @@
module Gitlab
module JwtAuthenticatable
# Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
# bytes https://tools.ietf.org/html/rfc4868#section-2.6
# bytes https://www.rfc-editor.org/rfc/rfc4868#section-2.6
SECRET_LENGTH = 32
def self.included(base)

View File

@ -77,13 +77,14 @@ module Gitlab
status, headers, body = @app.call(env)
return [status, headers, body] if health_endpoint
urgency = urgency_for_env(env)
if ::Gitlab::Metrics.record_duration_for_status?(status)
elapsed = ::Gitlab::Metrics::System.monotonic_time - started
self.class.http_request_duration_seconds.observe({ method: method }, elapsed)
record_apdex(env, elapsed)
record_apdex(urgency, elapsed)
end
record_error(env, status)
record_error(urgency, status)
[status, headers, body]
rescue StandardError
@ -116,20 +117,18 @@ module Gitlab
::Gitlab::ApplicationContext.current_context_attribute(:caller_id)
end
def record_apdex(env, elapsed)
urgency = urgency_for_env(env)
def record_apdex(urgency, elapsed)
Gitlab::Metrics::RailsSlis.request_apdex.increment(
labels: labels_from_context.merge(request_urgency: urgency.name),
success: elapsed < urgency.duration
)
end
def record_error(env, status)
def record_error(urgency, status)
return unless Feature.enabled?(:gitlab_metrics_error_rate_sli, type: :development)
Gitlab::Metrics::RailsSlis.request_error_rate.increment(
labels: labels_from_context,
labels: labels_from_context.merge(request_urgency: urgency.name),
error: ::Gitlab::Metrics.server_error?(status)
)
end

View File

@ -49,7 +49,7 @@ module Gitlab
#
# - Retry-After: the remaining duration in seconds until the quota is
# reset. This is a standardized HTTP header:
# https://tools.ietf.org/html/rfc7231#page-69
# https://www.rfc-editor.org/rfc/rfc7231#page-69
#
# - RateLimit-Reset: the point of time that the request quota is reset, in Unix time
#

View File

@ -30,7 +30,7 @@ module Gitlab
# Remove all invalid scheme characters before checking against the
# list of unsafe protocols.
#
# See https://tools.ietf.org/html/rfc3986#section-3.1
# See https://www.rfc-editor.org/rfc/rfc3986#section-3.1
#
def safe_protocol?(scheme)
return false unless scheme

View File

@ -48490,7 +48490,7 @@ msgstr ""
msgid "ciReport|Found %{issuesWithCount}"
msgstr ""
msgid "ciReport|Full Report"
msgid "ciReport|Full report"
msgstr ""
msgid "ciReport|Generic Report"

View File

@ -50,7 +50,7 @@ RSpec.describe 'Database schema' do
ci_resources: %w[partition_id],
ci_runner_projects: %w[runner_id],
ci_running_builds: %w[partition_id],
ci_sources_pipelines: %w[partition_id],
ci_sources_pipelines: %w[partition_id source_partition_id],
ci_stages: %w[partition_id],
ci_trigger_requests: %w[commit_id],
ci_unit_test_failures: %w[partition_id],

View File

@ -83,6 +83,7 @@ FactoryBot.define do
end
trait :running do
started_at { Time.current }
status { :running }
end

View File

@ -4,8 +4,8 @@ FactoryBot.define do
factory :ci_sources_pipeline, class: 'Ci::Sources::Pipeline' do
after(:build) do |source|
source.project ||= source.pipeline.project
source.source_pipeline ||= source.source_job.pipeline
source.source_project ||= source.source_pipeline.project
source.source_pipeline ||= source.source_job&.pipeline
source.source_project ||= source.source_pipeline&.project
end
source_job factory: :ci_build

View File

@ -1,7 +1,7 @@
import { GlAlert, GlKeysetPagination, GlLoadingIcon, GlBanner } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import Vue, { nextTick } from 'vue';
import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue';
import AgentTable from '~/clusters_list/components/agent_table.vue';
import Agents from '~/clusters_list/components/agents.vue';
@ -12,10 +12,10 @@ import {
} from '~/clusters_list/constants';
import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
const localVue = createLocalVue();
localVue.use(VueApollo);
Vue.use(VueApollo);
describe('Agents', () => {
let wrapper;
@ -34,9 +34,10 @@ describe('Agents', () => {
pageInfo = null,
trees = [],
count = 0,
queryResponse = null,
}) => {
const provide = provideData;
const apolloQueryResponse = {
const queryResponseData = {
data: {
project: {
id: '1',
@ -51,13 +52,12 @@ describe('Agents', () => {
},
},
};
const agentQueryResponse =
queryResponse || jest.fn().mockResolvedValue(queryResponseData, provide);
const apolloProvider = createMockApollo([
[getAgentsQuery, jest.fn().mockResolvedValue(apolloQueryResponse, provide)],
]);
const apolloProvider = createMockApollo([[getAgentsQuery, agentQueryResponse]]);
wrapper = shallowMount(Agents, {
localVue,
apolloProvider,
propsData: {
...defaultProps,
@ -313,24 +313,11 @@ describe('Agents', () => {
});
describe('when agents query is loading', () => {
const mocks = {
$apollo: {
queries: {
agents: {
loading: true,
},
},
},
};
beforeEach(async () => {
wrapper = shallowMount(Agents, {
mocks,
propsData: defaultProps,
provide: provideData,
beforeEach(() => {
createWrapper({
queryResponse: jest.fn().mockReturnValue(new Promise(() => {})),
});
await nextTick();
return waitForPromises();
});
it('displays a loading icon', () => {

View File

@ -1,6 +1,5 @@
import { GlSorting, GlSortingItem, GlTab } from '@gitlab/ui';
import { nextTick } from 'vue';
import { createLocalVue } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import OverviewTabs from '~/groups/components/overview_tabs.vue';
@ -18,8 +17,7 @@ import {
} from '~/groups/constants';
import axios from '~/lib/utils/axios_utils';
const localVue = createLocalVue();
localVue.component('GroupFolder', GroupFolderComponent);
Vue.component('GroupFolder', GroupFolderComponent);
const router = createRouter();
const [SORTING_ITEM_NAME, , SORTING_ITEM_UPDATED] = OVERVIEW_TABS_SORTING_ITEMS;
@ -57,7 +55,6 @@ describe('OverviewTabs', () => {
...defaultProvide,
...provide,
},
localVue,
mocks: { $route: route, $router: routerMock },
});

View File

@ -373,7 +373,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
href: '#',
target: '_blank',
id: 'full-report-button',
text: 'Full Report',
text: 'Full report',
},
],
},
@ -391,7 +391,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('when full report is clicked it should call the respective telemetry event', async () => {
expect(wrapper.vm.telemetryHub.fullReportClicked).not.toHaveBeenCalled();
wrapper.findByText('Full Report').vm.$emit('click');
wrapper.findByText('Full report').vm.$emit('click');
await nextTick();
expect(wrapper.vm.telemetryHub.fullReportClicked).toHaveBeenCalledTimes(1);
});

View File

@ -2,9 +2,9 @@
require 'spec_helper'
RSpec.describe Types::WorkItems::Widgets::HierarchyType do
RSpec.describe Types::WorkItems::Widgets::HierarchyType, feature_category: :team_planning do
it 'exposes the expected fields' do
expected_fields = %i[parent children type]
expected_fields = %i[parent children has_children type]
expect(described_class).to have_graphql_fields(*expected_fields)
end

View File

@ -2,11 +2,12 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do
RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations, feature_category: :continuous_integration do
let_it_be_with_reload(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
let(:pipeline) { Ci::Pipeline.new }
# Assigning partition_id here to validate it is being propagated correctly
let(:pipeline) { Ci::Pipeline.new(partition_id: ci_testing_partition_id) }
let(:bridge) { nil }
let(:variables_attributes) do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures, feature_category: :error_budgets do
let(:app) { double('app') }
subject { described_class.new(app) }
@ -39,7 +39,16 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, success: true)
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, error: false)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, error: false)
subject.call(env)
end
it 'guarantees SLI metrics are incremented with all the required labels' do
described_class.initialize_metrics
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).and_call_original
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).and_call_original
subject.call(env)
end
@ -103,7 +112,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(described_class).not_to receive(:http_request_duration_seconds)
expect(Gitlab::Metrics::RailsSlis).not_to receive(:request_apdex)
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, error: true)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, error: true)
subject.call(env)
end
@ -153,7 +162,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(Gitlab::Metrics::RailsSlis.request_apdex)
.to receive(:increment).with(labels: { feature_category: 'team_planning', endpoint_id: 'IssuesController#show', request_urgency: :default }, success: true)
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment)
.with(labels: { feature_category: 'team_planning', endpoint_id: 'IssuesController#show' }, error: false)
.with(labels: { feature_category: 'team_planning', endpoint_id: 'IssuesController#show', request_urgency: :default }, error: false)
subject.call(env)
end
@ -192,7 +201,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, success: true)
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown' }, error: false)
.with(labels: { feature_category: 'unknown', endpoint_id: 'unknown', request_urgency: :default }, error: false)
subject.call(env)
end
@ -251,7 +260,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
labels: {
feature_category: 'hello_world',
endpoint_id: 'GET /projects/:id/archive'
endpoint_id: 'GET /projects/:id/archive',
request_urgency: request_urgency_name
},
error: false
)
@ -292,7 +302,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
labels: {
feature_category: 'hello_world',
endpoint_id: 'AnonymousController#index'
endpoint_id: 'AnonymousController#index',
request_urgency: request_urgency_name
},
error: false
)
@ -326,7 +337,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown'
endpoint_id: 'unknown',
request_urgency: :default
},
error: false
)
@ -344,7 +356,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown'
endpoint_id: 'unknown',
request_urgency: :default
},
error: false
)
@ -374,7 +387,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown'
endpoint_id: 'unknown',
request_urgency: :default
},
error: false
)
@ -392,7 +406,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown'
endpoint_id: 'unknown',
request_urgency: :default
},
error: false
)
@ -418,7 +433,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown'
endpoint_id: 'unknown',
request_urgency: :default
},
error: false
)
@ -436,7 +452,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
labels: {
feature_category: 'unknown',
endpoint_id: 'unknown'
endpoint_id: 'unknown',
request_urgency: :default
},
error: false
)

View File

@ -3817,22 +3817,6 @@ RSpec.describe Ci::Build do
it 'assigns the token' do
expect { build.enqueue }.to change(build, :token).from(nil).to(an_instance_of(String))
end
context 'with ci_assign_job_token_on_scheduling disabled' do
before do
stub_feature_flags(ci_assign_job_token_on_scheduling: false)
end
it 'assigns the token on creation' do
expect(build.token).to be_present
end
it 'does not change the token when enqueuing' do
expect { build.enqueue }.not_to change(build, :token)
expect(build).to be_pending
end
end
end
describe 'state transition: pending: :running' do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Ci::Sources::Pipeline do
RSpec.describe Ci::Sources::Pipeline, feature_category: :continuous_integration do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:pipeline) }
@ -31,4 +31,20 @@ RSpec.describe Ci::Sources::Pipeline do
let!(:model) { create(:ci_sources_pipeline, project: parent) }
end
end
describe 'partitioning', :ci_partitioning do
include Ci::PartitioningHelpers
let(:new_pipeline) { create(:ci_pipeline) }
let(:source_pipeline) { create(:ci_sources_pipeline, pipeline: new_pipeline) }
before do
stub_current_partition_id
end
it 'assigns partition_id and source_partition_id from pipeline and source_job', :aggregate_failures do
expect(source_pipeline.partition_id).to eq(ci_testing_partition_id)
expect(source_pipeline.source_partition_id).to eq(ci_testing_partition_id)
end
end
end

View File

@ -0,0 +1,80 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::Partitionable::PartitionedFilter, :aggregate_failures, feature_category: :continuous_integration do
before do
create_tables(<<~SQL)
CREATE TABLE _test_ci_jobs_metadata (
id serial NOT NULL,
partition_id int NOT NULL DEFAULT 10,
name text,
PRIMARY KEY (id, partition_id)
) PARTITION BY LIST(partition_id);
CREATE TABLE _test_ci_jobs_metadata_1
PARTITION OF _test_ci_jobs_metadata
FOR VALUES IN (10);
SQL
end
let(:model) do
Class.new(Ci::ApplicationRecord) do
include Ci::Partitionable::PartitionedFilter
self.primary_key = :id
self.table_name = :_test_ci_jobs_metadata
def self.name
'TestCiJobMetadata'
end
end
end
let!(:record) { model.create! }
let(:where_filter) do
/WHERE "_test_ci_jobs_metadata"."id" = #{record.id} AND "_test_ci_jobs_metadata"."partition_id" = 10/
end
describe '#save' do
it 'uses id and partition_id' do
record.name = 'test'
recorder = ActiveRecord::QueryRecorder.new { record.save! }
expect(recorder.log).to include(where_filter)
expect(record.name).to eq('test')
end
end
describe '#update' do
it 'uses id and partition_id' do
recorder = ActiveRecord::QueryRecorder.new { record.update!(name: 'test') }
expect(recorder.log).to include(where_filter)
expect(record.name).to eq('test')
end
end
describe '#delete' do
it 'uses id and partition_id' do
recorder = ActiveRecord::QueryRecorder.new { record.delete }
expect(recorder.log).to include(where_filter)
expect(model.count).to be_zero
end
end
describe '#destroy' do
it 'uses id and partition_id' do
recorder = ActiveRecord::QueryRecorder.new { record.destroy! }
expect(recorder.log).to include(where_filter)
expect(model.count).to be_zero
end
end
def create_tables(table_sql)
Ci::ApplicationRecord.connection.execute(table_sql)
end
end

View File

@ -40,4 +40,28 @@ RSpec.describe Ci::Partitionable do
it { expect(ci_model.ancestors).to include(described_class::Switch) }
end
context 'with partitioned options' do
before do
stub_const("#{described_class}::Testing::PARTITIONABLE_MODELS", [ci_model.name])
ci_model.include(described_class)
ci_model.partitionable scope: ->(r) { 1 }, partitioned: partitioned
end
context 'when partitioned is true' do
let(:partitioned) { true }
it { expect(ci_model.ancestors).to include(described_class::PartitionedFilter) }
it { expect(ci_model).to be_partitioned }
end
context 'when partitioned is false' do
let(:partitioned) { false }
it { expect(ci_model.ancestors).not_to include(described_class::PartitionedFilter) }
it { expect(ci_model).not_to be_partitioned }
end
end
end

View File

@ -4,8 +4,10 @@ require 'spec_helper'
RSpec.describe TriggerableHooks do
before do
class TestableHook < WebHook
include TriggerableHooks
stub_const('TestableHook', Class.new(WebHook))
TestableHook.class_eval do
include TriggerableHooks # rubocop:disable Rspec/DescribedClass
triggerable_hooks [:push_hooks]
end
end

View File

@ -5,7 +5,9 @@ require 'spec_helper'
RSpec.describe Repository do
include RepoHelpers
TestBlob = Struct.new(:path)
before do
stub_const('TestBlob', Struct.new(:path))
end
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }

View File

@ -141,6 +141,25 @@ RSpec.describe WorkItems::ParentLink, feature_category: :portfolio_management do
end
end
describe 'scopes' do
let_it_be(:project) { create(:project) }
let_it_be(:issue1) { build(:work_item, project: project) }
let_it_be(:issue2) { build(:work_item, project: project) }
let_it_be(:issue3) { build(:work_item, project: project) }
let_it_be(:task1) { build(:work_item, :task, project: project) }
let_it_be(:task2) { build(:work_item, :task, project: project) }
let_it_be(:link1) { create(:parent_link, work_item_parent: issue1, work_item: task1) }
let_it_be(:link2) { create(:parent_link, work_item_parent: issue2, work_item: task2) }
describe 'for_parents' do
it 'includes the correct records' do
result = described_class.for_parents([issue1.id, issue2.id, issue3.id])
expect(result).to include(link1, link2)
end
end
end
context 'with confidential work items' do
let_it_be(:confidential_child) { create(:work_item, :task, confidential: true, project: project) }
let_it_be(:putlic_child) { create(:work_item, :task, project: project) }

View File

@ -774,6 +774,63 @@ RSpec.describe API::Files do
let(:request) { get api(route(file_path), current_user), params: params }
end
end
context 'when lfs parameter is true and the project has lfs enabled' do
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
project.update_attribute(:lfs_enabled, true)
end
let(:request) { get api(route(file_path) + '/raw', current_user), params: params.merge(lfs: true) }
let(:file_path) { 'files%2Flfs%2Flfs_object.iso' }
it_behaves_like '404 response'
context 'and the file has an lfs object' do
let_it_be(:lfs_object) { create(:lfs_object, :with_file, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897') }
it_behaves_like '404 response'
context 'and the project has access to the lfs object' do
before do
project.lfs_objects << lfs_object
end
context 'and lfs uses local file storage' do
before do
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:sendfile).with(lfs_object.file.path)
end
end
after do
Grape::Endpoint.before_each nil
end
it 'responds with the lfs object file' do
request
expect(response.headers["Content-Disposition"]).to eq(
"attachment; filename=\"#{lfs_object.file.filename}\"; filename*=UTF-8''#{lfs_object.file.filename}"
)
end
end
context 'and lfs uses remote object storage' do
before do
stub_lfs_object_storage
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
it 'redirects to the lfs object file' do
request
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to include(lfs_object.reload.file.path)
end
end
end
end
end
end
context 'when unauthenticated' do

View File

@ -112,6 +112,57 @@ RSpec.describe 'getting a work item list for a project' do
end
end
context 'when querying WorkItemWidgetHierarchy' do
let_it_be(:children) { create_list(:work_item, 3, :task, project: project) }
let_it_be(:child_link1) { create(:parent_link, work_item_parent: item1, work_item: children[0]) }
let(:fields) do
<<~GRAPHQL
nodes {
widgets {
type
... on WorkItemWidgetHierarchy {
hasChildren
parent { id }
children { nodes { id } }
}
}
}
GRAPHQL
end
it 'executes limited number of N+1 queries' do
post_graphql(query, current_user: current_user) # warm-up
control = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: current_user)
end
parent_work_items = create_list(:work_item, 2, project: project)
create(:parent_link, work_item_parent: parent_work_items[0], work_item: children[1])
create(:parent_link, work_item_parent: parent_work_items[1], work_item: children[2])
# There are 2 extra queries for fetching the children field
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/363569
expect { post_graphql(query, current_user: current_user) }
.not_to exceed_query_limit(control).with_threshold(2)
end
it 'avoids N+1 queries when children are added to a work item' do
post_graphql(query, current_user: current_user) # warm-up
control = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: current_user)
end
create(:parent_link, work_item_parent: item1, work_item: children[1])
create(:parent_link, work_item_parent: item1, work_item: children[2])
expect { post_graphql(query, current_user: current_user) }
.not_to exceed_query_limit(control)
end
end
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)

View File

@ -116,6 +116,7 @@ RSpec.describe 'Query.work_item(id)' do
id
}
}
hasChildren
}
}
GRAPHQL
@ -132,7 +133,8 @@ RSpec.describe 'Query.work_item(id)' do
[
hash_including('id' => child_link1.work_item.to_gid.to_s),
hash_including('id' => child_link2.work_item.to_gid.to_s)
]) }
]) },
'hasChildren' => true
)
)
)
@ -165,7 +167,8 @@ RSpec.describe 'Query.work_item(id)' do
'children' => { 'nodes' => match_array(
[
hash_including('id' => child_link1.work_item.to_gid.to_s)
]) }
]) },
'hasChildren' => true
)
)
)
@ -183,7 +186,8 @@ RSpec.describe 'Query.work_item(id)' do
hash_including(
'type' => 'HIERARCHY',
'parent' => hash_including('id' => parent_link.work_item_parent.to_gid.to_s),
'children' => { 'nodes' => match_array([]) }
'children' => { 'nodes' => match_array([]) },
'hasChildren' => false
)
)
)

View File

@ -13,7 +13,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
let(:downstream_project) { create(:project, :repository) }
let!(:upstream_pipeline) do
create(:ci_pipeline, :running, project: upstream_project)
create(:ci_pipeline, :created, project: upstream_project)
end
let(:trigger) do
@ -33,6 +33,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
let(:service) { described_class.new(upstream_project, user) }
let(:pipeline) { subject.payload }
before do
upstream_project.add_developer(user)
@ -48,6 +49,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
expect(subject).to be_error
expect(subject.message).to eq("Pre-conditions not met")
end
it 'changes pipeline bridge job status to failed' do
@ -63,9 +66,11 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a new pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
expect(subject).to be_error
expect(subject.message).to eq("Pre-conditions not met")
end
it 'changes status of the bridge build' do
it 'changes status of the bridge build to failed' do
subject
expect(bridge.reload).to be_failed
@ -82,9 +87,11 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a new pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
expect(subject).to be_error
expect(subject.message).to eq("Pre-conditions not met")
end
it 'changes status of the bridge build' do
it 'changes status of the bridge build to failed' do
subject
expect(bridge.reload).to be_failed
@ -103,11 +110,10 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates only one new pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }.by(1)
expect(subject).to be_success
end
it 'creates a new pipeline in a downstream project' do
pipeline = subject
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq downstream_project
expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
@ -117,18 +123,33 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
it_behaves_like 'logs downstream pipeline creation' do
let(:downstream_pipeline) { pipeline }
let(:expected_root_pipeline) { upstream_pipeline }
let(:expected_hierarchy_size) { 2 }
let(:expected_downstream_relationship) { :multi_project }
end
it 'updates bridge status when downstream pipeline gets processed' do
pipeline = subject
expect(pipeline.reload).to be_created
expect(bridge.reload).to be_success
end
it 'triggers the upstream pipeline duration calculation', :sidekiq_inline do
expect { subject }
.to change { upstream_pipeline.reload.duration }.from(nil).to(an_instance_of(Integer))
end
context 'when feature flag ci_run_bridge_for_pipeline_duration_calculation is disabled' do
before do
stub_feature_flags(ci_run_bridge_for_pipeline_duration_calculation: false)
end
it 'does not trigger the upstream pipeline duration calculation', :sidekiq_inline do
expect { subject }
.not_to change { upstream_pipeline.reload.duration }.from(nil)
end
end
context 'when bridge job has already any downstream pipelines' do
before do
bridge.sourced_pipelines.create!(
@ -147,7 +168,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
bridge_id: bridge.id, project_id: bridge.project.id)
.and_call_original
expect(Ci::CreatePipelineService).not_to receive(:new)
expect(subject).to eq({ message: "Already has a downstream pipeline", status: :error })
expect(subject).to be_error
expect(subject.message).to eq("Already has a downstream pipeline")
end
end
@ -157,8 +179,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
it 'is using default branch name' do
pipeline = subject
expect(pipeline.ref).to eq 'master'
end
end
@ -171,11 +191,11 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates only one new pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }.by(1)
expect(subject).to be_error
expect(subject.message).to match_array(["jobs job config should implement a script: or a trigger: keyword"])
end
it 'creates a new pipeline in a downstream project' do
pipeline = subject
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq downstream_project
expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
@ -185,8 +205,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
it 'updates the bridge status when downstream pipeline gets processed' do
pipeline = subject
expect(pipeline.reload).to be_failed
expect(bridge.reload).to be_failed
end
@ -201,6 +219,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a new pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
expect(subject).to be_error
expect(subject.message).to eq("Pre-conditions not met")
end
it 'changes status of the bridge build' do
@ -222,12 +242,10 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates only one new pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }.by(1)
expect(subject).to be_success
end
it 'creates a child pipeline in the same project' do
pipeline = subject
pipeline.reload
expect(pipeline.builds.map(&:name)).to match_array(%w[rspec echo])
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq bridge.project
@ -238,16 +256,11 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
it 'updates bridge status when downstream pipeline gets processed' do
pipeline = subject
expect(pipeline.reload).to be_created
expect(bridge.reload).to be_success
end
it 'propagates parent pipeline settings to the child pipeline' do
pipeline = subject
pipeline.reload
expect(pipeline.ref).to eq(upstream_pipeline.ref)
expect(pipeline.sha).to eq(upstream_pipeline.sha)
expect(pipeline.source_sha).to eq(upstream_pipeline.source_sha)
@ -276,6 +289,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it_behaves_like 'creates a child pipeline'
it_behaves_like 'logs downstream pipeline creation' do
let(:downstream_pipeline) { pipeline }
let(:expected_root_pipeline) { upstream_pipeline }
let(:expected_hierarchy_size) { 2 }
let(:expected_downstream_relationship) { :parent_child }
@ -283,6 +297,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'updates the bridge job to success' do
expect { subject }.to change { bridge.status }.to 'success'
expect(subject).to be_success
end
context 'when bridge uses "depend" strategy' do
@ -292,8 +307,9 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
}
end
it 'does not update the bridge job status' do
expect { subject }.not_to change { bridge.status }
it 'update the bridge job to running status' do
expect { subject }.to change { bridge.status }.from('pending').to('running')
expect(subject).to be_success
end
end
@ -323,8 +339,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it_behaves_like 'creates a child pipeline'
it 'propagates the merge request to the child pipeline' do
pipeline = subject
expect(pipeline.merge_request).to eq(merge_request)
expect(pipeline).to be_merge_request
end
@ -341,11 +355,13 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates the pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }.by(1)
expect(subject).to be_success
expect(bridge.reload).to be_success
end
it_behaves_like 'logs downstream pipeline creation' do
let(:downstream_pipeline) { pipeline }
let(:expected_root_pipeline) { upstream_pipeline.parent_pipeline }
let(:expected_hierarchy_size) { 3 }
let(:expected_downstream_relationship) { :parent_child }
@ -394,6 +410,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'create the pipeline' do
expect { subject }.to change { Ci::Pipeline.count }.by(1)
expect(subject).to be_success
end
end
@ -406,11 +423,10 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates a new pipeline allowing variables to be passed downstream' do
expect { subject }.to change { Ci::Pipeline.count }.by(1)
expect(subject).to be_success
end
it 'passes variables downstream from the bridge' do
pipeline = subject
pipeline.variables.map(&:key).tap do |variables|
expect(variables).to include 'BRIDGE'
end
@ -466,6 +482,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a new pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
expect(subject).to be_error
expect(subject.message).to eq("Pre-conditions not met")
end
it 'changes status of the bridge build' do
@ -480,6 +498,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates a new pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }
expect(subject).to be_success
end
it 'expect bridge build not to be failed' do
@ -559,18 +578,16 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates only one new pipeline' do
expect { subject }
.to change { Ci::Pipeline.count }.by(1)
expect(subject).to be_error
expect(subject.message).to match_array(["jobs invalid config should implement a script: or a trigger: keyword"])
end
it 'creates a new pipeline in the downstream project' do
pipeline = subject
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq downstream_project
end
it 'drops the bridge' do
pipeline = subject
expect(pipeline.reload).to be_failed
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
@ -585,15 +602,30 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
bridge.drop!
end
it 'tracks the exception' do
expect(Gitlab::ErrorTracking)
.to receive(:track_exception)
.with(
instance_of(Ci::Bridge::InvalidTransitionError),
bridge_id: bridge.id,
downstream_pipeline_id: kind_of(Numeric))
it 'returns the error' do
expect { subject }.not_to change(downstream_project.ci_pipelines, :count)
expect(subject).to be_error
expect(subject.message).to eq('Can not run the bridge')
end
subject
context 'when feature flag ci_run_bridge_for_pipeline_duration_calculation is disabled' do
before do
stub_feature_flags(ci_run_bridge_for_pipeline_duration_calculation: false)
end
it 'tracks the exception' do
expect(Gitlab::ErrorTracking)
.to receive(:track_exception)
.with(
instance_of(Ci::Bridge::InvalidTransitionError),
bridge_id: bridge.id,
downstream_pipeline_id: kind_of(Numeric))
expect(subject).to be_error
expect(subject.message).to eq(
"Cannot transition status via :drop from :failed (Reason(s): Status cannot transition via \"drop\")"
)
end
end
end
@ -603,8 +635,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
it 'passes bridge variables to downstream pipeline' do
pipeline = subject
expect(pipeline.variables.first)
.to have_attributes(key: 'BRIDGE', value: 'var')
end
@ -616,8 +646,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
it 'does not pass pipeline variables directly downstream' do
pipeline = subject
pipeline.variables.map(&:key).tap do |variables|
expect(variables).not_to include 'PIPELINE_VARIABLE'
end
@ -629,8 +657,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
it 'makes it possible to pass pipeline variable downstream' do
pipeline = subject
pipeline.variables.find_by(key: 'BRIDGE').tap do |variable|
expect(variable.value).to eq 'my-value-var'
end
@ -644,11 +670,11 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a new pipeline' do
expect { subject }
.not_to change { Ci::Pipeline.count }
expect(subject).to be_error
expect(subject.message).to match_array(["Insufficient permissions to set pipeline variables"])
end
it 'ignores variables passed downstream from the bridge' do
pipeline = subject
pipeline.variables.map(&:key).tap do |variables|
expect(variables).not_to include 'BRIDGE'
end
@ -668,7 +694,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
# TODO: Move this context into a feature spec that uses
# multiple pipeline processing services. Location TBD in:
# https://gitlab.com/gitlab-org/gitlab/issues/36216
context 'when configured with bridge job rules' do
context 'when configured with bridge job rules', :sidekiq_inline do
before do
stub_ci_pipeline_yaml_file(config)
downstream_project.add_maintainer(upstream_project.first_owner)
@ -701,6 +727,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates the downstream pipeline' do
expect { subject }
.to change(downstream_project.ci_pipelines, :count).by(1)
expect(subject).to be_error
expect(subject.message).to eq("Already has a downstream pipeline")
end
end
end
@ -731,6 +759,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a pipeline and drops the bridge' do
expect { subject }.not_to change(downstream_project.ci_pipelines, :count)
expect(subject).to be_error
expect(subject.message).to match_array(["Reference not found"])
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
@ -754,6 +784,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a pipeline and drops the bridge' do
expect { subject }.not_to change(downstream_project.ci_pipelines, :count)
expect(subject).to be_error
expect(subject.message).to match_array(["No stages / jobs for this pipeline."])
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
@ -776,6 +808,10 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates the pipeline but drops the bridge' do
expect { subject }.to change(downstream_project.ci_pipelines, :count).by(1)
expect(subject).to be_error
expect(subject.message).to eq(
["test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post"]
)
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
@ -808,6 +844,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates the pipeline' do
expect { subject }.to change(downstream_project.ci_pipelines, :count).by(1)
expect(subject).to be_success
expect(bridge.reload).to be_success
end
@ -822,6 +859,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
context 'when a downstream pipeline has sibling pipelines' do
it_behaves_like 'logs downstream pipeline creation' do
let(:downstream_pipeline) { pipeline }
let(:expected_root_pipeline) { upstream_pipeline }
let(:expected_downstream_relationship) { :multi_project }
@ -849,6 +887,8 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'does not create a new pipeline' do
expect { subject }.not_to change { Ci::Pipeline.count }
expect(subject).to be_error
expect(subject.message).to eq("Pre-conditions not met")
end
it 'drops the trigger job with an explanatory reason' do
@ -865,6 +905,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
it 'creates a new pipeline' do
expect { subject }.to change { Ci::Pipeline.count }.by(1)
expect(subject).to be_success
end
it 'marks the bridge job as successful' do

View File

@ -57,7 +57,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute', :yaml_processor_feature_fl
pipeline = create_pipeline!
test = pipeline.statuses.find_by(name: 'instrumentation_test')
expect(test).to be_pending
expect(test).to be_running
expect(pipeline.triggered_pipelines.count).to eq(1)
end

View File

@ -108,7 +108,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute', :yaml_processor_feature_fl
pipeline = create_pipeline!
test = pipeline.statuses.find_by(name: 'instrumentation_test')
expect(test).to be_pending
expect(test).to be_running
expect(pipeline.triggered_pipelines.count).to eq(1)
end

View File

@ -197,8 +197,7 @@ RSpec.describe Ci::PipelineTriggerService do
end
it_behaves_like 'logs downstream pipeline creation' do
subject { result[:pipeline] }
let(:downstream_pipeline) { result[:pipeline] }
let(:expected_root_pipeline) { pipeline }
let(:expected_hierarchy_size) { 2 }
let(:expected_downstream_relationship) { :multi_project }

View File

@ -13,10 +13,8 @@ RSpec.shared_examples 'logs downstream pipeline creation' do
end
it 'logs details' do
pipeline = nil
log_entry = record_downstream_pipeline_logs do
pipeline = subject
downstream_pipeline
end
expect(log_entry).to be_present
@ -24,7 +22,7 @@ RSpec.shared_examples 'logs downstream pipeline creation' do
message: "downstream pipeline created",
class: described_class.name,
root_pipeline_id: expected_root_pipeline.id,
downstream_pipeline_id: pipeline.id,
downstream_pipeline_id: downstream_pipeline.id,
downstream_pipeline_relationship: expected_downstream_relationship,
hierarchy_size: expected_hierarchy_size,
root_pipeline_plan: expected_root_pipeline.project.actual_plan_name,

View File

@ -1,21 +1,23 @@
# frozen_string_literal: true
RSpec.shared_examples 'issuable quick actions' do
QuickAction = Struct.new(:action_text, :expectation, :before_action, keyword_init: true) do
# Pass a block as :before_action if
# issuable state needs to be changed before
# the quick action is executed.
def call_before_action
before_action.call if before_action
end
before do
stub_const('QuickAction', Struct.new(:action_text, :expectation, :before_action, keyword_init: true) do
# Pass a block as :before_action if
# issuable state needs to be changed before
# the quick action is executed.
def call_before_action
before_action.call if before_action
end
def skip_access_check
action_text["/todo"] ||
action_text["/done"] ||
action_text["/subscribe"] ||
action_text["/shrug"] ||
action_text["/tableflip"]
end
def skip_access_check
action_text["/todo"] ||
action_text["/done"] ||
action_text["/subscribe"] ||
action_text["/shrug"] ||
action_text["/tableflip"]
end
end)
end
let(:unlabel_expectation) do

View File

@ -9,19 +9,52 @@ RSpec.describe Ci::CreateDownstreamPipelineWorker do
let(:bridge) { create(:ci_bridge, user: user, pipeline: pipeline) }
let(:service) { double('pipeline creation service') }
describe '#perform' do
context 'when bridge exists' do
it 'calls cross project pipeline creation service' do
let(:service) { double('pipeline creation service') }
let(:service_result) { ServiceResponse.success(payload: instance_double(Ci::Pipeline, id: 100)) }
it 'calls cross project pipeline creation service and logs the new pipeline id' do
expect(Ci::CreateDownstreamPipelineService)
.to receive(:new)
.with(project, user)
.and_return(service)
expect(service).to receive(:execute).with(bridge)
expect(service)
.to receive(:execute)
.with(bridge)
.and_return(service_result)
described_class.new.perform(bridge.id)
worker = described_class.new
worker.perform(bridge.id)
expect(worker.logging_extras).to eq({ "extra.ci_create_downstream_pipeline_worker.new_pipeline_id" => 100 })
end
context 'when downstream pipeline creation errors' do
let(:service_result) { ServiceResponse.error(message: 'Already has a downstream pipeline') }
it 'calls cross project pipeline creation service and logs the error' do
expect(Ci::CreateDownstreamPipelineService)
.to receive(:new)
.with(project, user)
.and_return(service)
expect(service)
.to receive(:execute)
.with(bridge)
.and_return(service_result)
worker = described_class.new
worker.perform(bridge.id)
expect(worker.logging_extras).to eq(
{
"extra.ci_create_downstream_pipeline_worker.create_error_message" => "Already has a downstream pipeline"
}
)
end
end
end

View File

@ -451,7 +451,7 @@ When storing your encrypted data, please consider the length requirements of the
It is advisable to also store metadata regarding the circumstances of your encrypted data. Namely, you should store information about the key used to encrypt your data, as well as the algorithm. Having this metadata with every record will make key rotation and migrating to a new algorithm signficantly easier. It will allow you to continue to decrypt old data using the information provided in the metadata and new data can be encrypted using your new key and algorithm of choice.
#### Enforcing the IV as a nonce
On a related note, most algorithms require that your IV be unique for every record and key combination. You can enforce this using composite unique indexes on your IV and encryption key name/id column. [RFC 5084](https://tools.ietf.org/html/rfc5084#section-1.5)
On a related note, most algorithms require that your IV be unique for every record and key combination. You can enforce this using composite unique indexes on your IV and encryption key name/id column. [RFC 5084](https://www.rfc-editor.org/rfc/rfc5084#section-1.5)
#### Unique key per record
Lastly, while the `:per_attribute_iv_and_salt` mode is more secure than `:per_attribute_iv` mode because it uses a unique key per record, it uses a PBKDF function which introduces a huge performance hit (175x slower by my benchmarks). There are other ways of deriving a unique key per record that would be much faster.

View File

@ -226,7 +226,7 @@ func (api *API) newRequest(r *http.Request, suffix string) (*http.Request, error
authReq := &http.Request{
Method: r.Method,
URL: rebaseUrl(r.URL, api.URL, suffix),
Header: helper.HeaderClone(r.Header),
Header: r.Header.Clone(),
}
authReq = authReq.WithContext(r.Context())
@ -307,7 +307,7 @@ func (api *API) PreAuthorizeFixedPath(r *http.Request, method string, path strin
if err != nil {
return nil, fmt.Errorf("construct auth request: %w", err)
}
authReq.Header = helper.HeaderClone(r.Header)
authReq.Header = r.Header.Clone()
authReq.URL.RawQuery = r.URL.RawQuery
failureResponse, apiResponse, err := api.PreAuthorize(path, authReq)
@ -361,7 +361,7 @@ func (api *API) doRequestWithoutRedirects(authReq *http.Request) (*http.Response
}
// removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h.
// See https://tools.ietf.org/html/rfc7230#section-6.1
// See https://www.rfc-editor.org/rfc/rfc7230#section-6.1
func removeConnectionHeaders(h http.Header) {
for _, f := range h["Connection"] {
for _, sf := range strings.Split(f, ",") {

View File

@ -8,8 +8,6 @@ import (
"net/url"
"github.com/gorilla/websocket"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
)
type ChannelSettings struct {
@ -53,7 +51,10 @@ func (t *ChannelSettings) Dialer() *websocket.Dialer {
func (t *ChannelSettings) Clone() *ChannelSettings {
// Doesn't clone the strings, but that's OK as strings are immutable in go
cloned := *t
cloned.Header = helper.HeaderClone(t.Header)
cloned.Header = t.Header.Clone()
if cloned.Header == nil {
cloned.Header = make(http.Header)
}
return &cloned
}

View File

@ -177,9 +177,8 @@ func RegisterHandler(h http.Handler, watchHandler WatchKeyHandler, pollingDurati
}
func cloneRequestWithNewBody(r *http.Request, body []byte) *http.Request {
newReq := *r
newReq := r.Clone(r.Context())
newReq.Body = io.NopCloser(bytes.NewReader(body))
newReq.Header = helper.HeaderClone(r.Header)
newReq.ContentLength = int64(len(body))
return &newReq
return newReq
}

View File

@ -74,7 +74,7 @@ func (p *Injector) Inject(w http.ResponseWriter, r *http.Request, sendData strin
if err != nil {
helper.Fail500(w, r, fmt.Errorf("dependency proxy: failed to create request: %w", err))
}
saveFileRequest.Header = helper.HeaderClone(r.Header)
saveFileRequest.Header = r.Header.Clone()
// forward headers from dependencyResponse to rails and client
for key, values := range dependencyResponse.Header {

View File

@ -70,16 +70,6 @@ func URLMustParse(s string) *url.URL {
return u
}
func HeaderClone(h http.Header) http.Header {
h2 := make(http.Header, len(h))
for k, vv := range h {
vv2 := make([]string, len(vv))
copy(vv2, vv)
h2[k] = vv2
}
return h2
}
func IsContentType(expected, actual string) bool {
parsed, _, err := mime.ParseMediaType(actual)
return err == nil && parsed == expected

View File

@ -46,6 +46,14 @@ func NewProxy(myURL *url.URL, version string, roundTripper http.RoundTripper, op
u.Path = ""
p.reverseProxy = httputil.NewSingleHostReverseProxy(&u)
p.reverseProxy.Transport = roundTripper
chainDirector(p.reverseProxy, func(r *http.Request) {
r.Header.Set("Gitlab-Workhorse", p.Version)
r.Header.Set("Gitlab-Workhorse-Proxy-Start", fmt.Sprintf("%d", time.Now().UnixNano()))
for k, v := range p.customHeaders {
r.Header.Set(k, v)
}
})
for _, option := range options {
option(&p)
@ -55,10 +63,7 @@ func NewProxy(myURL *url.URL, version string, roundTripper http.RoundTripper, op
// because of https://github.com/golang/go/issues/28168, the
// upstream won't receive the expected Host header unless this
// is forced in the Director func here
previousDirector := p.reverseProxy.Director
p.reverseProxy.Director = func(request *http.Request) {
previousDirector(request)
chainDirector(p.reverseProxy, func(request *http.Request) {
// send original host along for the upstream
// to know it's being proxied under a different Host
// (for redirects and other stuff that depends on this)
@ -67,25 +72,21 @@ func NewProxy(myURL *url.URL, version string, roundTripper http.RoundTripper, op
// override the Host with the target
request.Host = request.URL.Host
}
})
}
return &p
}
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Clone request
req := *r
req.Header = helper.HeaderClone(r.Header)
// Set Workhorse version
req.Header.Set("Gitlab-Workhorse", p.Version)
req.Header.Set("Gitlab-Workhorse-Proxy-Start", fmt.Sprintf("%d", time.Now().UnixNano()))
for k, v := range p.customHeaders {
req.Header.Set(k, v)
func chainDirector(rp *httputil.ReverseProxy, nextDirector func(*http.Request)) {
previous := rp.Director
rp.Director = func(r *http.Request) {
previous(r)
nextDirector(r)
}
}
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if p.AllowResponseBuffering {
nginx.AllowResponseBuffering(w)
}
@ -101,5 +102,5 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}()
p.reverseProxy.ServeHTTP(w, &req)
p.reverseProxy.ServeHTTP(w, r)
}

View File

@ -492,9 +492,9 @@ func TestSendURLForArtifacts(t *testing.T) {
transferEncoding []string
contentLength int
}{
{"No content-length, chunked TE", chunkedHandler, []string{"chunked"}, -1}, // Case 3 in https://tools.ietf.org/html/rfc7230#section-3.3.2
{"Known content-length, identity TE", regularHandler, nil, len(expectedBody)}, // Case 5 in https://tools.ietf.org/html/rfc7230#section-3.3.2
{"No content-length, identity TE", rawHandler, []string{"chunked"}, -1}, // Case 7 in https://tools.ietf.org/html/rfc7230#section-3.3.2
{"No content-length, chunked TE", chunkedHandler, []string{"chunked"}, -1}, // Case 3 in https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
{"Known content-length, identity TE", regularHandler, nil, len(expectedBody)}, // Case 5 in https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
{"No content-length, identity TE", rawHandler, []string{"chunked"}, -1}, // Case 7 in https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
} {
t.Run(tc.name, func(t *testing.T) {
server := httptest.NewServer(tc.handler)