Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e799c1393a
commit
2a501f63df
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">·</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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@
|
|||
z-index: 1;
|
||||
|
||||
&:hover .clear-search-icon {
|
||||
color: $common-gray-dark;
|
||||
color: $gray-800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,3 +22,11 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
color: var(--gray-900, $gray-900);
|
||||
}
|
||||
|
||||
.danger-title {
|
||||
color: var(--red-500, $red-500);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -73,3 +73,5 @@ module Mutations
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Mutations::WorkItems::Create.prepend_mod
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
- page_title _("Appearance")
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
- add_page_specific_style 'page_bundles/settings'
|
||||
|
||||
= render 'form'
|
||||
|
|
|
|||
|
|
@ -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?) }
|
||||
|
|
|
|||
|
|
@ -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?) }
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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?) }
|
||||
|
|
|
|||
|
|
@ -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?) }
|
||||
|
|
|
|||
|
|
@ -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' } }
|
||||
|
|
|
|||
|
|
@ -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?) }
|
||||
|
|
|
|||
|
|
@ -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?) }
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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') })
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ product_category: gitaly
|
|||
value_type: number
|
||||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
data_source: system
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ product_category: gitaly
|
|||
value_type: number
|
||||
status: active
|
||||
time_frame: all
|
||||
data_source: database
|
||||
data_source: system
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ product_category: collection
|
|||
value_type: string
|
||||
status: active
|
||||
time_frame: none
|
||||
data_source: database
|
||||
data_source: system
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ product_category: integrations
|
|||
value_type: boolean
|
||||
status: active
|
||||
time_frame: none
|
||||
data_source: database
|
||||
data_source: system
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
2b763fd1fe9aee5631f9a8f3bdf699a19003e56f5c857efe4410ec21e5dad8f7
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -48490,7 +48490,7 @@ msgstr ""
|
|||
msgid "ciReport|Found %{issuesWithCount}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Full Report"
|
||||
msgid "ciReport|Full report"
|
||||
msgstr ""
|
||||
|
||||
msgid "ciReport|Generic Report"
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :running do
|
||||
started_at { Time.current }
|
||||
status { :running }
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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, ",") {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue