Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-11-03 18:11:17 +00:00
parent e6fa9529b4
commit 44434461b3
65 changed files with 1500 additions and 712 deletions

View File

@ -49,7 +49,6 @@ GITALY_SERVER_VERSION @project_278964_bot6 @gitlab-org/maintainers/rails-backend
/ee/lib/ee/gitlab/background_migration/ @gitlab-org/maintainers/database
/lib/gitlab/database/ @gitlab-org/maintainers/database
/lib/gitlab/sql/ @gitlab-org/maintainers/database
/lib/gitlab/github_import/ @gitlab-org/maintainers/database
/app/finders/ @gitlab-org/maintainers/database
/ee/app/finders/ @gitlab-org/maintainers/database
/rubocop/rubocop-migrations.yml @gitlab-org/maintainers/database

View File

@ -109,6 +109,7 @@ const addDismissFlashClickListener = (flashEl, fadeTransition) => {
*
* @param {object} options - Options to control the flash message
* @param {string} options.message - Alert message text
* @param {string} [options.title] - Alert title
* @param {VARIANT_SUCCESS|VARIANT_WARNING|VARIANT_DANGER|VARIANT_INFO|VARIANT_TIP} [options.variant] - Which GlAlert variant to use; it defaults to VARIANT_DANGER.
* @param {object} [options.parent] - Reference to parent element under which alert needs to appear. Defaults to `document`.
* @param {Function} [options.onDismiss] - Handler to call when this alert is dismissed.
@ -126,6 +127,7 @@ const addDismissFlashClickListener = (flashEl, fadeTransition) => {
*/
const createAlert = function createAlert({
message,
title,
variant = VARIANT_DANGER,
parent = document,
containerSelector = '.flash-container',
@ -183,6 +185,7 @@ const createAlert = function createAlert({
GlAlert,
{
props: {
title,
dismissible: true,
dismissLabel: __('Dismiss'),
variant,

View File

@ -1,23 +0,0 @@
import Vue from 'vue';
import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
const mountGitlabVersionCheck = (el) => {
const { size } = el.dataset;
const actionable = parseBoolean(el.dataset.actionable);
return new Vue({
el,
render(createElement) {
return createElement(GitlabVersionCheck, {
props: {
size,
actionable,
},
});
},
});
};
export default () =>
[...document.querySelectorAll('.js-gitlab-version-check')].map(mountGitlabVersionCheck);

View File

@ -2,20 +2,10 @@
import { GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { helpPagePath } from '~/helpers/help_page_helper';
const STATUS_TYPES = {
SUCCESS: 'success',
WARNING: 'warning',
DANGER: 'danger',
};
const UPGRADE_DOCS_URL = helpPagePath('update/index');
import { STATUS_TYPES, UPGRADE_DOCS_URL } from '../constants';
export default {
name: 'GitlabVersionCheck',
name: 'GitlabVersionCheckBadge',
components: {
GlBadge,
},
@ -31,11 +21,10 @@ export default {
required: false,
default: false,
},
},
data() {
return {
status: null,
};
status: {
type: String,
required: true,
},
},
computed: {
title() {
@ -53,28 +42,13 @@ export default {
return this.actionable ? UPGRADE_DOCS_URL : null;
},
},
created() {
this.checkGitlabVersion();
mounted() {
this.track('render', {
label: 'version_badge',
property: this.title,
});
},
methods: {
checkGitlabVersion() {
axios
.get(joinPaths('/', gon.relative_url_root, '/admin/version_check.json'))
.then((res) => {
if (res.data) {
this.status = res.data.severity;
this.track('render', {
label: 'version_badge',
property: this.title,
});
}
})
.catch(() => {
// Silently fail
this.status = null;
});
},
onClick() {
if (!this.actionable) return;
@ -91,7 +65,7 @@ export default {
<template>
<!-- TODO: remove the span element once bootstrap-vue is updated to version 2.21.1 -->
<!-- TODO: https://github.com/bootstrap-vue/bootstrap-vue/issues/6219 -->
<span v-if="status" data-testid="badge-click-wrapper" @click="onClick">
<span data-testid="badge-click-wrapper" @click="onClick">
<gl-badge :href="badgeUrl" class="version-check-badge" :variant="status" :size="size">{{
title
}}</gl-badge>

View File

@ -0,0 +1,9 @@
import { helpPagePath } from '~/helpers/help_page_helper';
export const STATUS_TYPES = {
SUCCESS: 'success',
WARNING: 'warning',
DANGER: 'danger',
};
export const UPGRADE_DOCS_URL = helpPagePath('update/index');

View File

@ -0,0 +1,50 @@
import Vue from 'vue';
import * as Sentry from '@sentry/browser';
import { parseBoolean } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import GitlabVersionCheckBadge from './components/gitlab_version_check_badge.vue';
const mountGitlabVersionCheckBadge = ({ el, status }) => {
const { size } = el.dataset;
const actionable = parseBoolean(el.dataset.actionable);
return new Vue({
el,
render(createElement) {
return createElement(GitlabVersionCheckBadge, {
props: {
size,
actionable,
status,
},
});
},
});
};
export default async () => {
const versionCheckBadges = [...document.querySelectorAll('.js-gitlab-version-check-badge')];
// If there are no version check elements, exit out
if (versionCheckBadges?.length <= 0) {
return null;
}
const status = await axios
.get(joinPaths('/', gon.relative_url_root, '/admin/version_check.json'))
.then((res) => {
return res.data?.severity;
})
.catch((e) => {
Sentry.captureException(e);
return null;
});
// If we don't have a status there is nothing to render
if (status) {
return versionCheckBadges.map((el) => mountGitlabVersionCheckBadge({ el, status }));
}
return null;
};

View File

@ -3,39 +3,60 @@
class Admin::BroadcastMessagesController < Admin::ApplicationController
include BroadcastMessagesHelper
before_action :finder, only: [:edit, :update, :destroy]
before_action :find_broadcast_message, only: [:edit, :update, :destroy]
before_action :find_broadcast_messages, only: [:index, :create]
before_action :push_features, only: [:index, :edit]
feature_category :onboarding
urgency :low
# rubocop: disable CodeReuse/ActiveRecord
def index
push_frontend_feature_flag(:vue_broadcast_messages, current_user)
push_frontend_feature_flag(:role_targeted_broadcast_messages, current_user)
@broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page])
@broadcast_message = BroadcastMessage.new
@broadcast_message = BroadcastMessage.new
end
# rubocop: enable CodeReuse/ActiveRecord
def edit
end
def create
@broadcast_message = BroadcastMessage.new(broadcast_message_params)
success = @broadcast_message.save
if @broadcast_message.save
redirect_to admin_broadcast_messages_path, notice: _('Broadcast Message was successfully created.')
else
render :index
respond_to do |format|
format.json do
if success
render json: @broadcast_message, status: :ok
else
render json: { errors: @broadcast_message.errors.full_messages }, status: :bad_request
end
end
format.html do
if success
redirect_to admin_broadcast_messages_path, notice: _('Broadcast Message was successfully created.')
else
render :index
end
end
end
end
def update
if @broadcast_message.update(broadcast_message_params)
redirect_to admin_broadcast_messages_path, notice: _('Broadcast Message was successfully updated.')
else
render :edit
success = @broadcast_message.update(broadcast_message_params)
respond_to do |format|
format.json do
if success
render json: @broadcast_message, status: :ok
else
render json: { errors: @broadcast_message.errors.full_messages }, status: :bad_request
end
end
format.html do
if success
redirect_to admin_broadcast_messages_path, notice: _('Broadcast Message was successfully updated.')
else
render :edit
end
end
end
end
@ -55,10 +76,14 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
protected
def finder
def find_broadcast_message
@broadcast_message = BroadcastMessage.find(params[:id])
end
def find_broadcast_messages
@broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page]) # rubocop: disable CodeReuse/ActiveRecord
end
def broadcast_message_params
params.require(:broadcast_message)
.permit(%i(
@ -71,4 +96,9 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
dismissable
), target_access_levels: []).reverse_merge!(target_access_levels: [])
end
def push_features
push_frontend_feature_flag(:vue_broadcast_messages, current_user)
push_frontend_feature_flag(:role_targeted_broadcast_messages, current_user)
end
end

View File

@ -243,6 +243,10 @@ module DiffHelper
{}
end
def params_with_whitespace
hide_whitespace? ? safe_params.except(:w) : safe_params.merge(w: 1)
end
private
def diff_btn(title, name, selected)
@ -276,10 +280,6 @@ module DiffHelper
params[:w] == '1'
end
def params_with_whitespace
hide_whitespace? ? request.query_parameters.except(:w) : request.query_parameters.merge(w: 1)
end
def toggle_whitespace_link(url, options)
options[:class] = [*options[:class], 'btn gl-button btn-default'].join(' ')
toggle_text = hide_whitespace? ? s_('Diffs|Show whitespace changes') : s_('Diffs|Hide whitespace changes')

View File

@ -87,6 +87,16 @@ module Ci
ensure_metadata.id_tokens = value
end
def enqueue_immediately?
!!options[:enqueue_immediately]
end
def set_enqueue_immediately!
# ensures that even if `config_options: nil` in the database we set the
# new value correctly.
self.options = options.merge(enqueue_immediately: true)
end
private
def read_metadata_attribute(legacy_key, metadata_key, default_value = nil)

View File

@ -15,6 +15,8 @@ module Ci
private
def process(build)
return enqueue(build) if Feature.enabled?(:ci_retry_job_fix, project) && build.enqueue_immediately?
if build.schedulable?
build.schedule
elsif build.action?

View File

@ -19,7 +19,7 @@ module Ci
end
# rubocop: disable CodeReuse/ActiveRecord
def clone!(job, variables: [])
def clone!(job, variables: [], enqueue_if_actionable: false)
# Cloning a job requires a strict type check to ensure
# the attributes being used for the clone are taken straight
# from the model and not overridden by other abstractions.
@ -28,6 +28,9 @@ module Ci
check_access!(job)
new_job = job.clone(current_user: current_user, new_job_variables_attributes: variables)
if Feature.enabled?(:ci_retry_job_fix, project) && enqueue_if_actionable && new_job.action?
new_job.set_enqueue_immediately!
end
new_job.run_after_commit do
::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job)
@ -56,13 +59,20 @@ module Ci
def check_assignable_runners!(job); end
def retry_job(job, variables: [])
clone!(job, variables: variables).tap do |new_job|
clone!(job, variables: variables, enqueue_if_actionable: true).tap do |new_job|
check_assignable_runners!(new_job) if new_job.is_a?(Ci::Build)
next if new_job.failed?
Gitlab::OptimisticLocking.retry_lock(new_job, name: 'retry_build', &:enqueue)
Gitlab::OptimisticLocking.retry_lock(new_job, name: 'retry_build', &:enqueue) if Feature.disabled?(
:ci_retry_job_fix, project)
AfterRequeueJobService.new(project, current_user).execute(job)
if Feature.enabled?(:ci_retry_job_fix, project)
Ci::PipelineCreation::StartPipelineService.new(job.pipeline).execute
new_job.reset
end
end
end

View File

@ -122,7 +122,7 @@
= s_('AdminArea|Components')
- if show_version_check?
.float-right
.js-gitlab-version-check{ data: { "size": "lg", "actionable": "true" } }
.js-gitlab-version-check-badge{ data: { "size": "lg", "actionable": "true" } }
= link_to(sprite_icon('question'), "https://gitlab.com/gitlab-org/gitlab/-/blob/master/CHANGELOG.md", class: 'gl-ml-2', target: '_blank', rel: 'noopener noreferrer')
%p
= link_to _('GitLab'), general_admin_application_settings_path

View File

@ -11,7 +11,7 @@
%span= link_to_version
- if show_version_check?
%span.gl-mt-5.gl-mb-3.gl-ml-3
.js-gitlab-version-check{ data: { "size": "lg", "actionable": "true" } }
.js-gitlab-version-check-badge{ data: { "size": "lg", "actionable": "true" } }
%hr
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?

View File

@ -17,4 +17,4 @@
%span.gl-font-sm.gl-text-gray-500
#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}
%span.gl-ml-2
.js-gitlab-version-check{ data: { "size": "sm" } }
.js-gitlab-version-check-badge{ data: { "size": "sm" } }

View File

@ -1,8 +1,8 @@
---
name: ipynb_semantic_diff
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85079
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/358917
milestone: '15.0'
name: ci_retry_job_fix
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100712
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/207988
milestone: '15.6'
type: development
group: group::code review
default_enabled: true
group: group::pipeline execution
default_enabled: false

View File

@ -39,6 +39,8 @@ metadata:
description: Operations related to merge requests
- name: metadata
description: Operations related to metadata of the GitLab instance
- name: project_hooks
description: Operations related to project hooks
- name: release_links
description: Operations related to release assets (links)
- name: releases

View File

@ -12,8 +12,9 @@ miss a step.
Here is a list of steps you should take to attempt to fix problem:
1. Perform [basic troubleshooting](#basic-troubleshooting).
1. Fix any [replication errors](#fixing-replication-errors).
1. Fix any [PostgreSQL database replication errors](#fixing-postgresql-database-replication-errors).
1. Fix any [common](#fixing-common-errors) errors.
1. Fix any [non-PostgreSQL replication failures](#fixing-non-postgresql-replication-failures).
## Basic troubleshooting
@ -131,6 +132,8 @@ http://secondary.example.com/
To find more details about failed items, check
[the `gitlab-rails/geo.log` file](../../logs/log_parsing.md#find-most-common-geo-sync-errors)
If you notice replication or verification failures, you can try to [resolve them](#fixing-non-postgresql-replication-failures).
### Check if PostgreSQL replication is working
To check if PostgreSQL replication is working, check if:
@ -346,52 +349,7 @@ sudo gitlab-rake gitlab:geo:check
When performing a PostgreSQL major version (9 > 10) update this is expected. Follow
the [initiate-the-replication-process](../setup/database.md#step-3-initiate-the-replication-process).
### Repository verification failures
[Start a Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session)
to gather the following, basic troubleshooting information.
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
#### Get the number of verification failed repositories
```ruby
Geo::ProjectRegistry.verification_failed('repository').count
```
#### Find the verification failed repositories
```ruby
Geo::ProjectRegistry.verification_failed('repository')
```
#### Find repositories that failed to sync
```ruby
Geo::ProjectRegistry.sync_failed('repository')
```
### Resync repositories
[Start a Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session)
to enact the following, basic troubleshooting steps.
#### Queue up all repositories for resync. Sidekiq handles each sync
```ruby
Geo::ProjectRegistry.update_all(resync_repository: true, resync_wiki: true)
```
#### Sync individual repository now
```ruby
project = Project.find_by_full_path('<group/project>')
Geo::RepositorySyncService.new(project).execute
```
## Fixing replication errors
## Fixing PostgreSQL database replication errors
The following sections outline troubleshooting steps for fixing replication
error messages (indicated by `Database replication working? ... no` in the
@ -872,120 +830,6 @@ This behavior affects only the following data types through GitLab 14.6:
to make Geo visibly surface data loss risks. The sync/verification loop is
therefore short-circuited. `last_sync_failure` is now set to `The file is missing on the Geo primary site`.
### Blob types
- `Ci::JobArtifact`
- `Ci::PipelineArtifact`
- `Ci::SecureFile`
- `LfsObject`
- `MergeRequestDiff`
- `Packages::PackageFile`
- `PagesDeployment`
- `Terraform::StateVersion`
- `Upload`
`Packages::PackageFile` is used in the following
[Rails console](../../../administration/operations/rails_console.md#starting-a-rails-console-session)
examples, but things generally work the same for the other types.
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
#### The Replicator
The main kinds of classes are Registry, Model, and Replicator. If you have an instance of one of these classes, you can get the others. The Registry and Model mostly manage PostgreSQL DB state. The Replicator knows how to replicate/verify (or it can call a service to do it):
```ruby
model_record = Packages::PackageFile.last
model_record.replicator.registry.replicator.model_record # just showing that these methods exist
```
#### Replicate a package file, synchronously, given an ID
```ruby
model_record = Packages::PackageFile.find(id)
model_record.replicator.send(:download)
```
#### Replicate a package file, synchronously, given a registry ID
```ruby
registry = Geo::PackageFileRegistry.find(registry_id)
registry.replicator.send(:download)
```
#### Verify package files on the secondary manually
This iterates over all package files on the secondary, looking at the
`verification_checksum` stored in the database (which came from the primary)
and then calculate this value on the secondary to check if they match. This
does not change anything in the UI:
```ruby
# Run on secondary
status = {}
Packages::PackageFile.find_each do |package_file|
primary_checksum = package_file.verification_checksum
secondary_checksum = Packages::PackageFile.hexdigest(package_file.file.path)
verification_status = (primary_checksum == secondary_checksum)
status[verification_status.to_s] ||= []
status[verification_status.to_s] << package_file.id
end
# Count how many of each value we get
status.keys.each {|key| puts "#{key} count: #{status[key].count}"}
# See the output in its entirety
status
```
### Repository types newer than project/wiki repositories
- `SnippetRepository`
- `GroupWikiRepository`
`SnippetRepository` is used in the examples below, but things generally work the same for the other Repository types.
#### The Replicator
The main kinds of classes are Registry, Model, and Replicator. If you have an instance of one of these classes, you can get the others. The Registry and Model mostly manage PostgreSQL DB state. The Replicator knows how to replicate/verify (or it can call a service to do it).
```ruby
model_record = SnippetRepository.last
model_record.replicator.registry.replicator.model_record # just showing that these methods exist
```
#### Replicate a snippet repository, synchronously, given an ID
```ruby
model_record = SnippetRepository.find(id)
model_record.replicator.send(:sync_repository)
```
#### Replicate a snippet repository, synchronously, given a registry ID
```ruby
registry = Geo::SnippetRepositoryRegistry.find(registry_id)
registry.replicator.send(:sync_repository)
```
### Find failed artifacts
[Start a Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session)
to run the following commands:
```ruby
Geo::JobArtifactRegistry.failed
```
#### Find `ID` of synced artifacts that are missing on primary
```ruby
Geo::JobArtifactRegistry.synced.missing_on_primary.pluck(:artifact_id)
```
#### Failed syncs with GitLab-managed object storage replication
There is [an issue in GitLab 14.2 through 14.7](https://gitlab.com/gitlab-org/gitlab/-/issues/299819#note_822629467)
@ -1356,6 +1200,217 @@ To fix this issue, set the primary site's internal URL to a URL that is:
GeoNode.where(primary: true).first.update!(internal_url: "https://unique.url.for.primary.site")
```
## Fixing non-PostgreSQL replication failures
If you notice replication failures in `Admin > Geo > Sites` or the [Sync status Rake task](#sync-status-rake-task), you can try to resolve the failures with the following general steps:
1. Geo will automatically retry failures. If the failures are new and few in number, or if you suspect the root cause is already resolved, then you can wait to see if the failures go away.
1. If failures were present for a long time, then many retries have already occurred, and the interval between automatic retries has increased to up to 4 hours depending on the type of failure. If you suspect the root cause is already resolved, you can [manually retry replication or verification](#manually-retry-replication-or-verification).
1. If the failures persist, use the following sections to try to resolve them.
### Manually retry replication or verification
Project Git repositories and Project Wiki Git repositories have the ability in `Admin > Geo > Replication` to `Resync all`, `Reverify all`, or for a single resource, `Resync` or `Reverify`.
Adding this ability to other data types is proposed in issue [364725](https://gitlab.com/gitlab-org/gitlab/-/issues/364725).
The following sections describe how to use internal application commands in the [Rails console](../../../administration/operations/rails_console.md#starting-a-rails-console-session) to cause replication or verification immediately.
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
### Blob types
- `Ci::JobArtifact`
- `Ci::PipelineArtifact`
- `Ci::SecureFile`
- `LfsObject`
- `MergeRequestDiff`
- `Packages::PackageFile`
- `PagesDeployment`
- `Terraform::StateVersion`
- `Upload`
`Packages::PackageFile` is used in the following
[Rails console](../../../administration/operations/rails_console.md#starting-a-rails-console-session)
examples, but things generally work the same for the other types.
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
#### The Replicator
The main kinds of classes are Registry, Model, and Replicator. If you have an instance of one of these classes, you can get the others. The Registry and Model mostly manage PostgreSQL DB state. The Replicator knows how to replicate/verify (or it can call a service to do it):
```ruby
model_record = Packages::PackageFile.last
model_record.replicator.registry.replicator.model_record # just showing that these methods exist
```
#### Replicate a package file, synchronously, given an ID
```ruby
model_record = Packages::PackageFile.find(id)
model_record.replicator.send(:download)
```
#### Replicate a package file, synchronously, given a registry ID
```ruby
registry = Geo::PackageFileRegistry.find(registry_id)
registry.replicator.send(:download)
```
#### Verify package files on the secondary manually
This iterates over all package files on the secondary, looking at the
`verification_checksum` stored in the database (which came from the primary)
and then calculate this value on the secondary to check if they match. This
does not change anything in the UI:
```ruby
# Run on secondary
status = {}
Packages::PackageFile.find_each do |package_file|
primary_checksum = package_file.verification_checksum
secondary_checksum = Packages::PackageFile.hexdigest(package_file.file.path)
verification_status = (primary_checksum == secondary_checksum)
status[verification_status.to_s] ||= []
status[verification_status.to_s] << package_file.id
end
# Count how many of each value we get
status.keys.each {|key| puts "#{key} count: #{status[key].count}"}
# See the output in its entirety
status
```
### Reverify all uploads (or any SSF data type which is verified)
1. SSH into a GitLab Rails node in the primary Geo site.
1. Open [Rails console](../../../administration/operations/rails_console.md#starting-a-rails-console-session).
1. Mark all uploads as "pending verification":
```ruby
Upload.verification_state_table_class.each_batch do |relation|
relation.update_all(verification_state: 0)
end
```
1. This will cause the primary to start checksumming all Uploads.
1. When a primary successfully checksums a record, then all secondaries rechecksum as well, and they compare the values.
For other SSF data types replace `Upload` in the command above with the desired model class.
NOTE:
There is an [issue to implement this functionality in the Admin Area UI](https://gitlab.com/gitlab-org/gitlab/-/issues/364729).
### Repository types, except for project or project wiki repositories
- `SnippetRepository`
- `GroupWikiRepository`
`SnippetRepository` is used in the examples below, but things generally work the same for the other Repository types.
[Start a Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session)
to enact the following, basic troubleshooting steps.
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
#### The Replicator
The main kinds of classes are Registry, Model, and Replicator. If you have an instance of one of these classes, you can get the others. The Registry and Model mostly manage PostgreSQL DB state. The Replicator knows how to replicate/verify (or it can call a service to do it).
```ruby
model_record = SnippetRepository.last
model_record.replicator.registry.replicator.model_record # just showing that these methods exist
```
#### Replicate a snippet repository, synchronously, given an ID
```ruby
model_record = SnippetRepository.find(id)
model_record.replicator.send(:sync_repository)
```
#### Replicate a snippet repository, synchronously, given a registry ID
```ruby
registry = Geo::SnippetRepositoryRegistry.find(registry_id)
registry.replicator.send(:sync_repository)
```
### Find failed artifacts
[Start a Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session)
to run the following commands:
```ruby
Geo::JobArtifactRegistry.failed
```
#### Find `ID` of synced artifacts that are missing on primary
```ruby
Geo::JobArtifactRegistry.synced.missing_on_primary.pluck(:artifact_id)
```
### Project or project wiki repositories
#### Find repository verification failures
[Start a Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session)
to gather the following, basic troubleshooting information.
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
##### Get the number of verification failed repositories
```ruby
Geo::ProjectRegistry.verification_failed('repository').count
```
##### Find the verification failed repositories
```ruby
Geo::ProjectRegistry.verification_failed('repository')
```
##### Find repositories that failed to sync
```ruby
Geo::ProjectRegistry.sync_failed('repository')
```
#### Resync project and project wiki repositories
[Start a Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session)
to enact the following, basic troubleshooting steps.
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
##### Queue up all repositories for resync
When you run this, Sidekiq handles each sync.
```ruby
Geo::ProjectRegistry.update_all(resync_repository: true, resync_wiki: true)
```
##### Sync individual repository now
```ruby
project = Project.find_by_full_path('<group/project>')
Geo::RepositorySyncService.new(project).execute
```
## Fixing client errors
### Authorization errors from LFS HTTP(S) client requests
@ -1426,10 +1481,6 @@ If the above steps are **not successful**, proceed through the next steps:
1. Verify you can connect to the newly-promoted **primary** site using the URL used previously for the **secondary** site.
1. If successful, the **secondary** site is now promoted to the **primary** site.
## Additional tools
There are useful snippets for manipulating Geo internals in the [GitLab Rails Cheat Sheet](../../troubleshooting/gitlab_rails_cheat_sheet.md#geo). For example, you can find how to manually sync or verify a replicable in Rails console.
## Check OS locale data compatibility
If different operating systems or different operating system versions are deployed across Geo sites, we recommend that you perform a locale data compatibility check setting up Geo.

View File

@ -130,6 +130,7 @@ record. For example:
| `interval` | The minimum time in seconds between checking the DNS record. | 60 |
| `disconnect_timeout` | The time in seconds after which an old connection is closed, after the list of hosts was updated. | 120 |
| `use_tcp` | Lookup DNS resources using TCP instead of UDP | false |
| `max_replica_pools` | The maximum number of replicas each Rails process connects to. This is useful if you run a lot of Postgres replicas and a lot of Rails processes because without this limit every Rails process connects to every replica by default. The default behavior is unlimited if not set. | nil |
If `record_type` is set to `SRV`, then GitLab continues to use round-robin algorithm
and ignores the `weight` and `priority` in the record. Since `SRV` records usually

View File

@ -64,17 +64,23 @@ This content has been moved to [Troubleshooting Sidekiq](../sidekiq/sidekiq_trou
## Geo
### Reverify all uploads (or any SSF data type which is verified)
Moved to [Geo replication troubleshooting](../geo/replication/troubleshooting.md#reverify-all-uploads-or-any-ssf-data-type-which-is-verified).
### Artifacts
Moved to [Geo replication troubleshooting](../geo/replication/troubleshooting.md#find-failed-artifacts).
### Repository verification failures
Moved to [Geo replication troubleshooting](../geo/replication/troubleshooting.md#repository-verification-failures).
Moved to [Geo replication troubleshooting](../geo/replication/troubleshooting.md#find-repository-verification-failures).
### Resync repositories
Moved to [Geo replication troubleshooting](../geo/replication/troubleshooting.md#resync-repositories).
Moved to [Geo replication troubleshooting - Resync repository types except for project or project wiki repositories](../geo/replication/troubleshooting.md#repository-types-except-for-project-or-project-wiki-repositories).
Moved to [Geo replication troubleshooting - Resync project and project wiki repositories](../geo/replication/troubleshooting.md#resync-project-and-project-wiki-repositories).
### Blob types

View File

@ -80,7 +80,7 @@ This section is for links to information elsewhere in the GitLab documentation.
HINT: Free one or increase max_replication_slots.
```
- Geo [replication errors](../geo/replication/troubleshooting.md#fixing-replication-errors) including:
- Geo [replication errors](../geo/replication/troubleshooting.md#fixing-postgresql-database-replication-errors) including:
```plaintext
ERROR: replication slots can only be used if max_replication_slots > 0

View File

@ -11390,6 +11390,7 @@ Represents a DAST Site Profile.
| <a id="dastsiteprofiletargettype"></a>`targetType` | [`DastTargetTypeEnum`](#dasttargettypeenum) | Type of target to be scanned. |
| <a id="dastsiteprofiletargeturl"></a>`targetUrl` | [`String`](#string) | URL of the target to be scanned. |
| <a id="dastsiteprofileuserpermissions"></a>`userPermissions` | [`DastSiteProfilePermissions!`](#dastsiteprofilepermissions) | Permissions for the current user on the resource. |
| <a id="dastsiteprofilevalidationstartedat"></a>`validationStartedAt` | [`Time`](#time) | Site profile validation start time. |
| <a id="dastsiteprofilevalidationstatus"></a>`validationStatus` | [`DastSiteProfileValidationStatusEnum`](#dastsiteprofilevalidationstatusenum) | Current validation status of the site profile. |
### `DastSiteProfileAuth`

View File

@ -243,6 +243,7 @@ When the user is authenticated and `simple` is not set this returns something li
"suggestion_commit_message": null,
"merge_commit_template": null,
"squash_commit_template": null,
"issue_branch_template": "gitlab/%{id}-%{title}",
"auto_devops_enabled": false,
"auto_devops_deploy_strategy": "continuous",
"autoclose_referenced_issues": true,
@ -421,6 +422,7 @@ GET /users/:user_id/projects
"suggestion_commit_message": null,
"merge_commit_template": null,
"squash_commit_template": null,
"issue_branch_template": "gitlab/%{id}-%{title}",
"marked_for_deletion_at": "2020-04-03", // Deprecated and will be removed in API v5 in favor of marked_for_deletion_on
"marked_for_deletion_on": "2020-04-03",
"statistics": {
@ -544,6 +546,7 @@ GET /users/:user_id/projects
"suggestion_commit_message": null,
"merge_commit_template": null,
"squash_commit_template": null,
"issue_branch_template": "gitlab/%{id}-%{title}",
"statistics": {
"commit_count": 12,
"storage_size": 2066080,
@ -677,6 +680,7 @@ Example response:
"suggestion_commit_message": null,
"merge_commit_template": null,
"squash_commit_template": null,
"issue_branch_template": "gitlab/%{id}-%{title}",
"statistics": {
"commit_count": 37,
"storage_size": 1038090,
@ -793,6 +797,7 @@ Example response:
"suggestion_commit_message": null,
"merge_commit_template": null,
"squash_commit_template": null,
"issue_branch_template": "gitlab/%{id}-%{title}",
"statistics": {
"commit_count": 12,
"storage_size": 2066080,
@ -969,6 +974,7 @@ GET /projects/:id
"enforce_auth_checks_on_uploads": true,
"merge_commit_template": null,
"squash_commit_template": null,
"issue_branch_template": "gitlab/%{id}-%{title}",
"marked_for_deletion_at": "2020-04-03", // Deprecated and will be removed in API v5 in favor of marked_for_deletion_on
"marked_for_deletion_on": "2020-04-03",
"compliance_frameworks": [ "sox" ],
@ -1334,6 +1340,7 @@ POST /projects/user/:user_id
| `shared_runners_enabled` | boolean | **{dotted-circle}** No | Enable shared runners for this project. |
| `snippets_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. |
| `snippets_enabled` | boolean | **{dotted-circle}** No | _(Deprecated)_ Enable snippets for this project. Use `snippets_access_level` instead. |
| `issue_branch_template` | string | **{dotted-circle}** No | Template used to suggest names for [branches created from issues](../user/project/repository/web_editor.md#create-a-new-branch-from-an-issue). _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21243) in GitLab 15.6.)_ |
| `squash_commit_template` | string | **{dotted-circle}** No | [Template](../user/project/merge_requests/commit_templates.md) used to create squash commit message in merge requests. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345275) in GitLab 14.6.)_ |
| `squash_option` | string | **{dotted-circle}** No | One of `never`, `always`, `default_on`, or `default_off`. |
| `suggestion_commit_message` | string | **{dotted-circle}** No | The commit message used to apply merge request [suggestions](../user/project/merge_requests/reviews/suggestions.md). |
@ -1439,6 +1446,7 @@ Supported attributes:
| `shared_runners_enabled` | boolean | **{dotted-circle}** No | Enable shared runners for this project. |
| `snippets_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. |
| `snippets_enabled` | boolean | **{dotted-circle}** No | _(Deprecated)_ Enable snippets for this project. Use `snippets_access_level` instead. |
| `issue_branch_template` | string | **{dotted-circle}** No | Template used to suggest names for [branches created from issues](../user/project/repository/web_editor.md#create-a-new-branch-from-an-issue). _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21243) in GitLab 15.6.)_ |
| `squash_commit_template` | string | **{dotted-circle}** No | [Template](../user/project/merge_requests/commit_templates.md) used to create squash commit message in merge requests. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345275) in GitLab 14.6.)_ |
| `squash_option` | string | **{dotted-circle}** No | One of `never`, `always`, `default_on`, or `default_off`. |
| `suggestion_commit_message` | string | **{dotted-circle}** No | The commit message used to apply merge request suggestions. |

View File

@ -109,12 +109,13 @@ Example response:
```
Users on [GitLab Premium or Ultimate](https://about.gitlab.com/pricing/) may also see
the `file_template_project_id`, `delayed_project_deletion`, `delayed_group_deletion`, `deletion_adjourned_period`, or the `geo_node_allowed_ips` parameters:
the `group_owners_can_manage_default_branch_protection`, `file_template_project_id`, `delayed_project_deletion`, `delayed_group_deletion`, `deletion_adjourned_period`, or the `geo_node_allowed_ips` parameters:
```json
{
"id" : 1,
"signup_enabled" : true,
"group_owners_can_manage_default_branch_protection" : true,
"file_template_project_id": 1,
"geo_node_allowed_ips": "0.0.0.0/0, ::/0",
"delayed_project_deletion": false,
@ -224,6 +225,7 @@ Example response:
Users on [GitLab Premium or Ultimate](https://about.gitlab.com/pricing/) may also see
these parameters:
- `group_owners_can_manage_default_branch_protection`
- `file_template_project_id`
- `geo_node_allowed_ips`
- `geo_status_timeout`
@ -353,6 +355,7 @@ listed in the descriptions of the relevant settings.
| `grafana_enabled` | boolean | no | Enable Grafana. |
| `grafana_url` | string | no | Grafana URL. |
| `gravatar_enabled` | boolean | no | Enable Gravatar. |
| `group_owners_can_manage_default_branch_protection` **(PREMIUM SELF)** | boolean | no | Prevent overrides of default branch protection. |
| `hashed_storage_enabled` | boolean | no | Create new projects using hashed storage paths: Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. (Always enabled in GitLab versions 13.0 and later, configuration is scheduled for removal in 14.0) |
| `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help. |
| `help_page_support_url` | string | no | Alternate support URL for help page and help dropdown list. |

View File

@ -41,6 +41,10 @@ vendor field to be `gitlab` to avoid cve matching old versions.
Changelog: changed
```
If your merge request has multiple commits,
[make sure to add the `Changelog` entry to the first commit](changelog.md#how-to-generate-a-changelog-entry).
This ensures that the correct entry is generated when commits are squashed.
### Overriding the associated merge request
GitLab automatically links the merge request to the commit when generating the

View File

@ -32,7 +32,7 @@ Assuming `project-name` is the trigger command, the slash commands are:
| `/project-name deploy <from> to <to>` | [Deploys](#deploy-command) from the `<from>` environment to the `<to>` environment. |
| `/project-name run <job name> <arguments>` | Executes the [ChatOps](../ci/chatops/index.md) job `<job name>` on the default branch. |
If you are using the [GitLab Slack application](../user/project/integrations/gitlab_slack_application.md) for
If you are using the [GitLab for Slack app](../user/project/integrations/gitlab_slack_application.md) for
your GitLab.com projects, [add the `gitlab` keyword at the beginning of the command](../user/project/integrations/gitlab_slack_application.md#usage).
## Issue commands

View File

@ -6,16 +6,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Git abuse rate limit **(ULTIMATE SELF)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/8066) in GitLab 15.2 [with flags](../../../administration/feature_flags.md) named `git_abuse_rate_limit_feature_flag` and `auto_ban_user_on_excessive_projects_download`. Both flags are disabled by default.
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/8066) in GitLab 15.2 [with a flag](../../../administration/feature_flags.md) named `git_abuse_rate_limit_feature_flag`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flags](../../../administration/feature_flags.md) named `git_abuse_rate_limit_feature_flag` and `auto_ban_user_on_excessive_projects_download`.
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `git_abuse_rate_limit_feature_flag`. On GitLab.com, this feature is not available.
Git abuse rate limiting is a feature to automatically [ban users](../moderate_users.md#ban-and-unban-users) who download more than a specified number of repositories in a given time. When the `git_abuse_rate_limit_feature_flag` feature flag is enabled, the administrator receives an email when a user is about to be banned.
Git abuse rate limiting is a feature to automatically [ban users](../moderate_users.md#ban-and-unban-users) who download or clone more than a specified number of repositories in any project in the instance within a given time frame. Banned users cannot sign in to the instance and cannot access any non-public group via HTTP or SSH.
When the `auto_ban_user_on_excessive_projects_download` is not enabled, the user is not banned automatically. You can use this setup to determine the correct values of the rate limit settings.
If the `git_abuse_rate_limit_feature_flag` feature flag is enabled, all application administrators receive an email when a user is about to be banned.
When both flags are enabled, the administrator receives an email when a user is about to be banned, and the user is automatically banned from the GitLab instance.
If automatic banning is disabled, a user is not banned automatically when they exceed the limit. However, administrators are still notified. You can use this setup to determine the correct values of the rate limit settings before enabling automatic banning.
If automatic banning is enabled, administrators receive an email when a user is about to be banned, and the user is automatically banned from the GitLab instance.
## Configure Git abuse rate limiting
@ -24,6 +26,15 @@ When both flags are enabled, the administrator receives an email when a user is
1. Expand **Git abuse rate limit**.
1. Update the Git abuse rate limit settings:
1. Enter a number in the **Number of repositories** field, greater than or equal to `0` and less than or equal to `10,000`. This number specifies the maximum amount of unique repositories a user can download in the specified time period before they're banned. When set to `0`, Git abuse rate limiting is disabled.
1. Enter a number in the **Reporting time period (seconds)** field, greater than or equal to `0` and less than or equal to `86,400`. This number specifies the time in seconds a user can download the maximum amount of repositories before they're banned. When set to `0`, Git abuse rate limiting is disabled.
1. Optional. Exclude users by adding them to the **Excluded users** field. Excluded users are not automatically banned.
1. Enter a number in the **Reporting time period (seconds)** field, greater than or equal to `0` and less than or equal to `86,400` (10 days). This number specifies the time in seconds a user can download the maximum amount of repositories before they're banned. When set to `0`, Git abuse rate limiting is disabled.
1. Optional. Exclude up to `100` users by adding them to the **Excluded users** field. Excluded users are not automatically banned.
1. Optional. Turn on the **Automatically ban users from this namespace when they exceed the specified limits** toggle to enable automatic banning.
1. Select **Save changes**.
## Unban a user
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, select **Overview > Users**.
1. Select the **Banned** tab and search for the account you want to unban.
1. From the **User administration** dropdown list select **Unban user**.
1. On the confirmation dialog, select **Unban user**.

View File

@ -0,0 +1,36 @@
---
stage: Anti-Abuse
group: Anti-Abuse
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Git abuse rate limit (administration) **(ULTIMATE SELF)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/8066) in GitLab 15.2 [with a flag](../../../administration/feature_flags.md) named `limit_unique_project_downloads_per_namespace_user`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `limit_unique_project_downloads_per_namespace_user`. On GitLab.com, this feature is not available.
Git abuse rate limiting is a feature to automatically ban users who download or clone more than a specified number of repositories in a group or any of its subgroups within a given time frame. Banned users cannot access the main group or any of its non-public subgroups via HTTP or SSH. Access to unrelated groups is unaffected.
If the `limit_unique_project_downloads_per_namespace_user` feature flag is enabled, all users with the Owner role for the main group receive an email when a user is about to be banned.
If automatic banning is disabled, a user is not banned automatically when they exceed the limit. However, users with the Owner role for the main group are still notified. You can use this setup to determine the correct values of the rate limit settings before enabling automatic banning.
If automatic banning is enabled, users with the Owner role for the main group receive an email when a user is about to be banned, and the user is automatically banned from the group and its subgroups.
## Configure Git abuse rate limiting
1. On the left sidebar, select **Settings > Reporting**.
1. Update the Git abuse rate limit settings:
1. Enter a number in the **Number of repositories** field, greater than or equal to `0` and less than or equal to `10,000`. This number specifies the maximum amount of unique repositories a user can download in the specified time period before they're banned. When set to `0`, Git abuse rate limiting is disabled.
1. Enter a number in the **Reporting time period (seconds)** field, greater than or equal to `0` and less than or equal to `86,400` (10 days). This number specifies the time in seconds a user can download the maximum amount of repositories before they're banned. When set to `0`, Git abuse rate limiting is disabled.
1. Optional. Exclude up to `100` users by adding them to the **Excluded users** field. Excluded users are not automatically banned.
1. Optional. Turn on the **Automatically ban users from this namespace when they exceed the specified limits** toggle to enable automatic banning.
1. Select **Save changes**.
## Unban a user
1. On the left sidebar, select **Group information > Members**.
1. Select the **Banned** tab.
1. For the account you want to unban, select **Unban**.

View File

@ -4,35 +4,35 @@ group: Integrations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# GitLab Slack application **(FREE SAAS)**
# GitLab for Slack app **(FREE SAAS)**
NOTE:
The GitLab Slack application is only configurable for GitLab.com. It will **not**
work for on-premises installations where you can configure the
[Slack slash commands](slack_slash_commands.md) integration instead. We're planning
to make this configurable for all GitLab installations, but there's
no ETA - see [#28164](https://gitlab.com/gitlab-org/gitlab/-/issues/28164).
The GitLab for Slack app is only configurable for GitLab.com. It does **not**
work for on-premises installations where you can configure
[Slack slash commands](slack_slash_commands.md) instead. See
[Slack application integration for self-managed instances](https://gitlab.com/gitlab-org/gitlab/-/issues/28164)
for our plans to make the app configurable for all GitLab installations.
Slack provides a native application which you can enable via your project's
Slack provides a native application that you can enable with your project's
integrations on GitLab.com.
## Slack App Directory
The simplest way to enable the GitLab Slack application for your workspace is to
install the [GitLab application](https://slack-platform.slack.com/apps/A676ADMV5-gitlab) from
the [Slack App Directory](https://slack.com/apps).
To enable the GitLab for Slack app for your workspace,
install the [GitLab application](https://slack-platform.slack.com/apps/A676ADMV5-gitlab)
from the [Slack App Directory](https://slack.com/apps).
Selecting install takes you to the [GitLab Slack application landing page](https://gitlab.com/-/profile/slack/edit)
where you can select a project to enable the GitLab Slack application for.
On the [GitLab for Slack app landing page](https://gitlab.com/-/profile/slack/edit),
you can select a GitLab project to link with your Slack workspace.
## Configuration
Alternatively, you can configure the Slack application with a project's
Alternatively, you can configure the GitLab for Slack app with your project's
integration settings.
Keep in mind that you must have the appropriate permissions for your Slack
workspace to be able to install a new application. Read more in Slack's
documentation on [Adding an app to your workspace](https://slack.com/help/articles/202035138-Add-apps-to-your-Slack-workspace).
You must have the appropriate permissions for your Slack
workspace to be able to install a new application. See
[Add apps to your Slack workspace](https://slack.com/help/articles/202035138-Add-apps-to-your-Slack-workspace).
To enable the GitLab integration for your Slack workspace:
@ -41,23 +41,21 @@ To enable the GitLab integration for your Slack workspace:
1. Select **Install Slack app**.
1. Select **Allow** on Slack's confirmation screen.
That's all! You can now start using the Slack slash commands.
You can also select **Reinstall Slack app** to update the app in your Slack workspace
to the latest version. See the [Version history](#version-history) for details.
to the latest version. See [Version history](#version-history) for details.
## Create a project alias for Slack
To create a project alias on GitLab.com for Slack integration:
1. Go to your project's home page.
1. Go to **Settings > Integrations** (only visible on GitLab.com)
1. Go to **Settings > Integrations** (only visible on GitLab.com).
1. On the **Integrations** page, select **Slack application**.
1. The current **Project Alias**, if any, is displayed. To edit this value,
select **Edit**.
1. Enter your desired alias, and select **Save changes**.
Some Slack commands require a project alias, and fail with the following error
Some Slack commands require a project alias and fail with the following error
if the project alias is incorrect or missing from the command:
```plaintext
@ -66,17 +64,15 @@ GitLab error: project or alias not found
## Usage
After confirming the installation, you, and everyone else in your Slack workspace,
can use all the [slash commands](../../../integration/slash_commands.md).
When you perform your first slash command, you are asked to authorize your
Slack user on GitLab.com.
After installing the app, everyone in your Slack workspace can
use the [slash commands](../../../integration/slash_commands.md).
When you perform your first slash command, you are asked to
authorize your Slack user on GitLab.com.
The only difference with the [manually configurable Slack slash commands](slack_slash_commands.md)
is that all the commands should be prefixed with the `/gitlab` keyword.
For example, to show the issue number `1001` under the `gitlab-org/gitlab`
project, you would do:
is that you must prefix all commands with the `/gitlab` keyword. For example,
to show the issue number `1001` under the `gitlab-org/gitlab`
project, you must run the following command:
```plaintext
/gitlab gitlab-org/gitlab issue show 1001
@ -84,15 +80,11 @@ project, you would do:
## Version history
### 15.0+
In GitLab 15.0 the Slack app is updated to [Slack's new granular permissions app model](https://medium.com/slack-developer-blog/more-precision-less-restrictions-a3550006f9c3).
There is no change in functionality. A reinstall is not required but recommended.
In GitLab 15.0 and later, the GitLab for Slack app is updated to [Slack's new granular permissions model](https://medium.com/slack-developer-blog/more-precision-less-restrictions-a3550006f9c3). While there is no change in functionality, you should reinstall the app.
## Troubleshooting
When you work with the Slack app, the
When you work with the GitLab for Slack app, the
[App Home](https://api.slack.com/start/overview#app_home) might not display properly.
As a workaround, ensure your app is up to date.

View File

@ -14,7 +14,7 @@ GitLab can also send events (for example, `issue created`) to Slack as notificat
The [Slack notifications service](slack.md) is configured separately.
NOTE:
For GitLab.com, use the [GitLab Slack app](gitlab_slack_application.md) instead.
For GitLab.com, use the [GitLab for Slack app](gitlab_slack_application.md) instead.
## Configure GitLab and Slack

View File

@ -28,24 +28,20 @@ GitLab.
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/6589) in GitLab 14.5 as an [Alpha](../../../../policy/alpha-beta-support.md#alpha-features) release [with a flag](../../../../administration/feature_flags.md) named `jupyter_clean_diffs`. Enabled by default.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75500) in GitLab 14.9. Feature flag `jupyter_clean_diffs` removed.
> - [Reintroduced toggle](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85079) in GitLab 15.0 [with a flag](../../../../administration/feature_flags.md) named `ipynb_semantic_diff`. Enabled by default.
FLAG:
On self-managed GitLab, by default semantic diffs are available. To hide the feature, ask an administrator to [disable the feature flag](../../../../administration/feature_flags.md) named `ipynb_semantic_diff`.
On GitLab.com, this feature is available.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95373) in GitLab 15.6. Feature flag `ipynb_semantic_diff` removed.
When commits include changes to Jupyter Notebook files, GitLab:
- Transforms the machine-readable `.ipynb` file into a human-readable Markdown file.
- Displays a cleaner version of the diff that includes syntax highlighting.
- Enables switching between raw and rendered diffs on the Commit and Compare pages. (Not available on merge request pages.)
- Renders images on the diffs.
Code suggestions are not available on diffs and merge requests for `.ipynb` files.
![Jupyter Notebook Clean Diff](img/jupyter_notebook_diff_v14_5.png)
Cleaner notebook diffs are not generated when the notebook is too large.
This feature is an [Alpha](../../../../policy/alpha-beta-support.md#alpha-features) release,
and might lead to performance degradation. On self-managed GitLab, if unexpected issues
arise, disable the feature.
![Jupyter Notebook Clean Diff](img/jupyter_notebook_diff_v14_5.png)
## Jupyter Git integration

View File

@ -188,13 +188,15 @@ module API
mount ::API::Keys
mount ::API::Metadata
mount ::API::MergeRequestDiffs
mount ::API::ProjectHooks
mount ::API::ProjectRepositoryStorageMoves
mount ::API::Releases
mount ::API::Release::Links
mount ::API::ResourceAccessTokens
mount ::API::ProjectSnapshots
mount ::API::ProtectedTags
mount ::API::SnippetRepositoryStorageMoves
mount ::API::ProtectedBranches
mount ::API::SnippetRepositoryStorageMoves
mount ::API::Statistics
mount ::API::Submodules
mount ::API::Suggestions
@ -291,11 +293,9 @@ module API
mount ::API::ProjectDebianDistributions
mount ::API::ProjectEvents
mount ::API::ProjectExport
mount ::API::ProjectHooks
mount ::API::ProjectImport
mount ::API::ProjectMilestones
mount ::API::ProjectPackages
mount ::API::ProjectSnapshots
mount ::API::ProjectSnippets
mount ::API::ProjectStatistics
mount ::API::ProjectTemplates

View File

@ -3,10 +3,17 @@
module API
module Entities
class ProjectHook < Hook
expose :project_id, :issues_events, :confidential_issues_events
expose :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events, :deployment_events
expose :job_events, :releases_events
expose :push_events_branch_filter
expose :project_id, documentation: { type: 'string', example: 1 }
expose :issues_events, documentation: { type: 'boolean' }
expose :confidential_issues_events, documentation: { type: 'boolean' }
expose :note_events, documentation: { type: 'boolean' }
expose :confidential_note_events, documentation: { type: 'boolean' }
expose :pipeline_events, documentation: { type: 'boolean' }
expose :wiki_page_events, documentation: { type: 'boolean' }
expose :deployment_events, documentation: { type: 'boolean' }
expose :job_events, documentation: { type: 'boolean' }
expose :releases_events, documentation: { type: 'boolean' }
expose :push_events_branch_filter, documentation: { type: 'string', example: 'my-branch-*' }
end
end
end

View File

@ -6,7 +6,7 @@ module API
extend Grape::API::Helpers
params :requires_url do
requires :url, type: String, desc: "The URL to send the request to"
requires :url, type: String, desc: "The URL to send the request to", documentation: { example: 'http://example.com/hook' }
end
params :optional_url do
@ -15,8 +15,8 @@ module API
params :url_variables do
optional :url_variables, type: Array, desc: 'URL variables for interpolation' do
requires :key, type: String, desc: 'Name of the variable'
requires :value, type: String, desc: 'Value of the variable'
requires :key, type: String, desc: 'Name of the variable', documentation: { example: 'token' }
requires :value, type: String, desc: 'Value of the variable', documentation: { example: '123' }
end
end

View File

@ -4,6 +4,8 @@ module API
class ProjectHooks < ::API::Base
include PaginationParams
project_hooks_tags = %w[project_hooks]
before { authenticate! }
before { authorize_admin_project }
@ -44,8 +46,11 @@ module API
mount ::API::Hooks::UrlVariables
end
desc 'Get project hooks' do
desc 'List project hooks' do
detail 'Get a list of project hooks'
success Entities::ProjectHook
is_array true
tags project_hooks_tags
end
params do
use :pagination
@ -54,8 +59,13 @@ module API
present paginate(user_project.hooks), with: Entities::ProjectHook
end
desc 'Get a project hook' do
desc 'Get project hook' do
detail 'Get a specific hook for a project'
success Entities::ProjectHook
failure [
{ code: 404, message: 'Not found' }
]
tags project_hooks_tags
end
params do
requires :hook_id, type: Integer, desc: 'The ID of a project hook'
@ -65,8 +75,15 @@ module API
present hook, with: Entities::ProjectHook
end
desc 'Add hook to project' do
desc 'Add project hook' do
detail 'Adds a hook to a specified project'
success Entities::ProjectHook
failure [
{ code: 400, message: 'Validation error' },
{ code: 404, message: 'Not found' },
{ code: 422, message: 'Unprocessable entity' }
]
tags project_hooks_tags
end
params do
use :requires_url
@ -79,11 +96,18 @@ module API
save_hook(hook, Entities::ProjectHook)
end
desc 'Update an existing hook' do
desc 'Edit project hook' do
detail 'Edits a hook for a specified project.'
success Entities::ProjectHook
failure [
{ code: 400, message: 'Validation error' },
{ code: 404, message: 'Not found' },
{ code: 422, message: 'Unprocessable entity' }
]
tags project_hooks_tags
end
params do
requires :hook_id, type: Integer, desc: "The ID of the hook to update"
requires :hook_id, type: Integer, desc: 'The ID of the project hook'
use :optional_url
use :common_hook_parameters
end
@ -91,11 +115,16 @@ module API
update_hook(entity: Entities::ProjectHook)
end
desc 'Deletes project hook' do
desc 'Delete a project hook' do
detail 'Removes a hook from a project. This is an idempotent method and can be called multiple times. Either the hook is available or not.'
success Entities::ProjectHook
failure [
{ code: 404, message: 'Not found' }
]
tags project_hooks_tags
end
params do
requires :hook_id, type: Integer, desc: 'The ID of the hook to delete'
requires :hook_id, type: Integer, desc: 'The ID of the project hook'
end
delete ":id/hooks/:hook_id" do
hook = find_hook

View File

@ -11,6 +11,11 @@ module API
resource :projects do
desc 'Download a (possibly inconsistent) snapshot of a repository' do
detail 'This feature was introduced in GitLab 10.7'
success File
produces 'application/x-tar'
failure [
{ code: 401, message: 'Unauthorized' }
]
end
params do
optional :wiki, type: Boolean, desc: 'Set to true to receive the wiki repository'

View File

@ -57,7 +57,8 @@ module Gitlab
record_type: 'A',
interval: 60,
disconnect_timeout: 120,
use_tcp: false
use_tcp: false,
max_replica_pools: nil
}
end

View File

@ -48,6 +48,7 @@ module Gitlab
# forcefully disconnected.
# use_tcp - Use TCP instaed of UDP to look up resources
# load_balancer - The load balancer instance to use
# rubocop:disable Metrics/ParameterLists
def initialize(
load_balancer,
nameserver:,
@ -56,7 +57,8 @@ module Gitlab
record_type: 'A',
interval: 60,
disconnect_timeout: 120,
use_tcp: false
use_tcp: false,
max_replica_pools: nil
)
@nameserver = nameserver
@port = port
@ -66,7 +68,9 @@ module Gitlab
@disconnect_timeout = disconnect_timeout
@use_tcp = use_tcp
@load_balancer = load_balancer
@max_replica_pools = max_replica_pools
end
# rubocop:enable Metrics/ParameterLists
def start
Thread.new do
@ -170,6 +174,8 @@ module Gitlab
addresses_from_srv_record(response)
end
addresses = sampler.sample(addresses)
raise EmptyDnsResponse if addresses.empty?
# Addresses are sorted so we can directly compare the old and new
@ -221,6 +227,11 @@ module Gitlab
def addresses_from_a_record(resources)
resources.map { |r| Address.new(r.address.to_s) }
end
def sampler
@sampler ||= ::Gitlab::Database::LoadBalancing::ServiceDiscovery::Sampler
.new(max_replica_pools: @max_replica_pools)
end
end
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
module Gitlab
module Database
module LoadBalancing
class ServiceDiscovery
class Sampler
def initialize(max_replica_pools:, seed: rand)
# seed must be set once and consistent
# for every invocation of #sample on
# the same instance of Sampler
@seed = seed
@max_replica_pools = max_replica_pools
end
def sample(addresses)
return addresses if @max_replica_pools.nil? || addresses.count <= @max_replica_pools
::Gitlab::Database::LoadBalancing::Logger.info(
event: :host_list_limit_exceeded,
message: "Host list length exceeds max_replica_pools so random hosts will be chosen.",
max_replica_pools: @max_replica_pools,
total_host_list_length: addresses.count,
randomization_seed: @seed
)
# First sort them in case the ordering from DNS server changes
# then randomly order all addresses using consistent seed so
# this process always gives the same set for this instance of
# Sampler
addresses = addresses.sort
addresses = addresses.shuffle(random: Random.new(@seed))
# Group by hostname so that we can sample evenly across hosts
addresses_by_host = addresses.group_by(&:hostname)
selected_addresses = []
while selected_addresses.count < @max_replica_pools
# Loop over all hostnames grabbing one address at a time to
# evenly distribute across all hostnames
addresses_by_host.each do |host, addresses|
next if addresses.empty?
selected_addresses << addresses.pop
break unless selected_addresses.count < @max_replica_pools
end
end
selected_addresses
end
end
end
end
end
end

View File

@ -7,6 +7,7 @@ module Gitlab
BASE_RESULT_DIR = Rails.root.join('tmp', 'migration-testing').freeze
METADATA_FILENAME = 'metadata.json'
SCHEMA_VERSION = 4 # Version of the output format produced by the runner
POST_MIGRATION_MATCHER = %r{db/post_migrate/}.freeze
class << self
def up(database:, legacy_mode: false)
@ -116,7 +117,10 @@ module Gitlab
verbose_was = ActiveRecord::Migration.verbose
ActiveRecord::Migration.verbose = true
sorted_migrations = migrations.sort_by(&:version)
sorted_migrations = migrations.sort_by do |m|
[m.filename.match?(POST_MIGRATION_MATCHER) ? 1 : 0, m.version]
end
sorted_migrations.reverse! if direction == :down
instrumentation = Instrumentation.new(result_dir: result_dir)

View File

@ -44,10 +44,6 @@ module Gitlab
add_blobs_to_batch_loader
end
def use_semantic_ipynb_diff?
strong_memoize(:_use_semantic_ipynb_diff) { Feature.enabled?(:ipynb_semantic_diff, repository.project) }
end
def has_renderable?
rendered&.has_renderable?
end
@ -372,7 +368,7 @@ module Gitlab
end
def rendered
return unless use_semantic_ipynb_diff? && ipynb? && modified_file? && !collapsed? && !too_large?
return unless ipynb? && modified_file? && !collapsed? && !too_large?
strong_memoize(:rendered) { Rendered::Notebook::DiffFile.new(self) }
end

View File

@ -25,7 +25,7 @@ module RuboCop
return unless method_name
add_offense(node) do |corrector|
replacement = "Gitlab::Json.#{method_name}(#{arg_source})"
replacement = "#{cbased(node)}Gitlab::Json.#{method_name}(#{arg_source})"
corrector.replace(node.source_range, replacement)
end
@ -38,7 +38,7 @@ module RuboCop
# Only match if the method is implemented by Gitlab::Json
if method_name && AVAILABLE_METHODS.include?(method_name)
return [method_name, arg_nodes.map(&:source).join(', ')]
return [method_name, arg_nodes.map(&:source).join(", ")]
end
receiver = to_json_call?(node)
@ -46,6 +46,12 @@ module RuboCop
nil
end
def cbased(node)
return unless %r{/ee/}.match?(node.location.expression.source_buffer.name)
"::"
end
end
end
end

View File

@ -4,7 +4,7 @@ FactoryBot.define do
factory :ci_processable, class: 'Ci::Processable' do
name { 'processable' }
stage { 'test' }
stage_idx { 0 }
stage_idx { ci_stage.try(:position) || 0 }
ref { 'master' }
tag { false }
pipeline factory: :ci_pipeline

View File

@ -2,7 +2,7 @@
FactoryBot.define do
factory :ci_stage, class: 'Ci::Stage' do
project factory: :project
project { pipeline.project }
pipeline factory: :ci_empty_pipeline
name { 'test' }

View File

@ -41,7 +41,7 @@ RSpec.describe 'Help Pages' do
end
it 'renders the version check badge' do
expect(page).to have_selector('.js-gitlab-version-check')
expect(page).to have_selector('.js-gitlab-version-check-badge')
end
end

View File

@ -193,6 +193,20 @@ describe('Flash', () => {
);
});
describe('with title', () => {
const mockTitle = 'my title';
it('shows title and message', () => {
createAlert({
title: mockTitle,
message: mockMessage,
});
const text = document.querySelector('.flash-container').textContent.trim();
expect(text).toBe(`${mockTitle} ${mockMessage}`);
});
});
describe('with buttons', () => {
const findAlertAction = () => document.querySelector('.flash-container .gl-alert-action');

View File

@ -0,0 +1,102 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import GitlabVersionCheckBadge from '~/gitlab_version_check/components/gitlab_version_check_badge.vue';
import { STATUS_TYPES, UPGRADE_DOCS_URL } from '~/gitlab_version_check/constants';
describe('GitlabVersionCheckBadge', () => {
let wrapper;
let trackingSpy;
const defaultProps = {
status: STATUS_TYPES.SUCCESS,
};
const createComponent = (props = {}) => {
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
wrapper = shallowMountExtended(GitlabVersionCheckBadge, {
propsData: {
...defaultProps,
...props,
},
});
};
afterEach(() => {
unmockTracking();
wrapper.destroy();
});
const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper');
const findGlBadge = () => wrapper.findComponent(GlBadge);
describe('template', () => {
describe.each`
status | expectedUI
${STATUS_TYPES.SUCCESS} | ${{ title: 'Up to date', variant: 'success' }}
${STATUS_TYPES.WARNING} | ${{ title: 'Update available', variant: 'warning' }}
${STATUS_TYPES.DANGER} | ${{ title: 'Update ASAP', variant: 'danger' }}
`('badge ui', ({ status, expectedUI }) => {
beforeEach(() => {
createComponent({ status, actionable: true });
});
describe(`when status is ${status}`, () => {
it(`title is ${expectedUI.title}`, () => {
expect(findGlBadge().text()).toBe(expectedUI.title);
});
it(`variant is ${expectedUI.variant}`, () => {
expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
});
it(`tracks rendered_version_badge with label ${expectedUI.title}`, () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
label: 'version_badge',
property: expectedUI.title,
});
});
it(`link is ${UPGRADE_DOCS_URL}`, () => {
expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL);
});
it(`tracks click_version_badge with label ${expectedUI.title} when badge is clicked`, async () => {
await findGlBadgeClickWrapper().trigger('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
label: 'version_badge',
property: expectedUI.title,
});
});
});
});
describe('when actionable is false', () => {
beforeEach(() => {
createComponent({ actionable: false });
});
it('tracks rendered_version_badge correctly', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
label: 'version_badge',
property: 'Up to date',
});
});
it('does not provide a link to GlBadge', () => {
expect(findGlBadge().attributes('href')).toBe(undefined);
});
it('does not track click_version_badge', async () => {
await findGlBadgeClickWrapper().trigger('click');
expect(trackingSpy).not.toHaveBeenCalledWith(undefined, 'click_link', {
label: 'version_badge',
property: 'Up to date',
});
});
});
});
});

View File

@ -0,0 +1,116 @@
import Vue from 'vue';
import * as Sentry from '@sentry/browser';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initGitlabVersionCheck from '~/gitlab_version_check';
describe('initGitlabVersionCheck', () => {
let originalGon;
let mock;
let vueApps;
const defaultResponse = {
code: 200,
res: { severity: 'success' },
};
const dummyGon = {
relative_url_root: '/',
};
const createApp = async (mockResponse, htmlClass) => {
originalGon = window.gon;
const response = {
...defaultResponse,
...mockResponse,
};
mock = new MockAdapter(axios);
mock.onGet().replyOnce(response.code, response.res);
setHTMLFixture(`<div class="${htmlClass}"></div>`);
vueApps = await initGitlabVersionCheck();
};
afterEach(() => {
mock.restore();
window.gon = originalGon;
resetHTMLFixture();
});
describe('with no .js-gitlab-version-check-badge elements', () => {
beforeEach(async () => {
await createApp();
});
it('does not make axios GET request', () => {
expect(mock.history.get.length).toBe(0);
});
it('does not render the Version Check Badge', () => {
expect(vueApps).toBeNull();
});
});
describe('with .js-gitlab-version-check-badge element but API errors', () => {
beforeEach(async () => {
jest.spyOn(Sentry, 'captureException');
await createApp({ code: 500, res: null }, 'js-gitlab-version-check-badge');
});
it('does make axios GET request', () => {
expect(mock.history.get.length).toBe(1);
expect(mock.history.get[0].url).toContain('/admin/version_check.json');
});
it('logs error to Sentry', () => {
expect(Sentry.captureException).toHaveBeenCalled();
});
it('does not render the Version Check Badge', () => {
expect(vueApps).toBeNull();
});
});
describe('with .js-gitlab-version-check-badge element and successful API call', () => {
beforeEach(async () => {
await createApp({}, 'js-gitlab-version-check-badge');
});
it('does make axios GET request', () => {
expect(mock.history.get.length).toBe(1);
expect(mock.history.get[0].url).toContain('/admin/version_check.json');
});
it('does render the Version Check Badge', () => {
expect(vueApps).toHaveLength(1);
expect(vueApps[0]).toBeInstanceOf(Vue);
});
});
describe.each`
root | description
${'/'} | ${'not used (uses its own (sub)domain)'}
${'/gitlab'} | ${'custom path'}
${'/service/gitlab'} | ${'custom path with 2 depth'}
`('path for version_check.json', ({ root, description }) => {
describe(`when relative url is ${description}: ${root}`, () => {
beforeEach(async () => {
originalGon = window.gon;
window.gon = { ...dummyGon };
window.gon.relative_url_root = root;
await createApp({}, 'js-gitlab-version-check-badge');
});
it('reflects the relative url setting', () => {
expect(mock.history.get.length).toBe(1);
const pathRegex = new RegExp(`^${root}`);
expect(mock.history.get[0].url).toMatch(pathRegex);
});
});
});
});

View File

@ -1,169 +0,0 @@
import { GlBadge } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking } from 'helpers/tracking_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
import axios from '~/lib/utils/axios_utils';
import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
describe('GitlabVersionCheck', () => {
let wrapper;
let mock;
const UPGRADE_DOCS_URL = helpPagePath('update/index');
const defaultResponse = {
code: 200,
res: { severity: 'success' },
};
const createComponent = (mockResponse, propsData = {}) => {
const response = {
...defaultResponse,
...mockResponse,
};
mock = new MockAdapter(axios);
mock.onGet().replyOnce(response.code, response.res);
wrapper = shallowMountExtended(GitlabVersionCheck, {
propsData,
});
};
const dummyGon = {
relative_url_root: '/',
};
let originalGon;
afterEach(() => {
wrapper.destroy();
mock.restore();
window.gon = originalGon;
});
const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper');
const findGlBadge = () => wrapper.findComponent(GlBadge);
describe.each`
root | description
${'/'} | ${'not used (uses its own (sub)domain)'}
${'/gitlab'} | ${'custom path'}
${'/service/gitlab'} | ${'custom path with 2 depth'}
`('path for version_check.json', ({ root, description }) => {
describe(`when relative url is ${description}: ${root}`, () => {
beforeEach(async () => {
originalGon = window.gon;
window.gon = { ...dummyGon };
window.gon.relative_url_root = root;
createComponent(defaultResponse);
await waitForPromises(); // Ensure we wrap up the axios call
});
it('reflects the relative url setting', () => {
expect(mock.history.get.length).toBe(1);
const pathRegex = new RegExp(`^${root}`);
expect(mock.history.get[0].url).toMatch(pathRegex);
});
});
});
describe('template', () => {
describe.each`
description | mockResponse | renders
${'successful but null'} | ${{ code: 200, res: null }} | ${false}
${'successful and valid'} | ${{ code: 200, res: { severity: 'success' } }} | ${true}
${'an error'} | ${{ code: 500, res: null }} | ${false}
`('version_check.json response', ({ description, mockResponse, renders }) => {
describe(`is ${description}`, () => {
beforeEach(async () => {
createComponent(mockResponse);
await waitForPromises(); // Ensure we wrap up the axios call
});
it(`does${renders ? '' : ' not'} render Badge Click Wrapper and GlBadge`, () => {
expect(findGlBadgeClickWrapper().exists()).toBe(renders);
expect(findGlBadge().exists()).toBe(renders);
});
});
});
describe.each`
mockResponse | expectedUI
${{ code: 200, res: { severity: 'success' } }} | ${{ title: 'Up to date', variant: 'success' }}
${{ code: 200, res: { severity: 'warning' } }} | ${{ title: 'Update available', variant: 'warning' }}
${{ code: 200, res: { severity: 'danger' } }} | ${{ title: 'Update ASAP', variant: 'danger' }}
`('badge ui', ({ mockResponse, expectedUI }) => {
describe(`when response is ${mockResponse.res.severity}`, () => {
let trackingSpy;
beforeEach(async () => {
createComponent(mockResponse, { actionable: true });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
await waitForPromises(); // Ensure we wrap up the axios call
});
it(`title is ${expectedUI.title}`, () => {
expect(findGlBadge().text()).toBe(expectedUI.title);
});
it(`variant is ${expectedUI.variant}`, () => {
expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
});
it(`tracks rendered_version_badge with label ${expectedUI.title}`, () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
label: 'version_badge',
property: expectedUI.title,
});
});
it(`link is ${UPGRADE_DOCS_URL}`, () => {
expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL);
});
it(`tracks click_version_badge with label ${expectedUI.title} when badge is clicked`, async () => {
await findGlBadgeClickWrapper().trigger('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
label: 'version_badge',
property: expectedUI.title,
});
});
});
});
describe('when actionable is false', () => {
let trackingSpy;
beforeEach(async () => {
createComponent(defaultResponse, { actionable: false });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
await waitForPromises(); // Ensure we wrap up the axios call
});
it('tracks rendered_version_badge correctly', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
label: 'version_badge',
property: 'Up to date',
});
});
it('does not provide a link to GlBadge', () => {
expect(findGlBadge().attributes('href')).toBe(undefined);
});
it('does not track click_version_badge', async () => {
await findGlBadgeClickWrapper().trigger('click');
expect(trackingSpy).not.toHaveBeenCalledWith(undefined, 'click_link', {
label: 'version_badge',
property: 'Up to date',
});
});
});
});
});

View File

@ -445,6 +445,19 @@ RSpec.describe DiffHelper do
end
end
describe '#params_with_whitespace' do
before do
controller.params[:protocol] = 'HACKED!'
controller.params[:host] = 'HACKED!'
end
subject { helper.params_with_whitespace }
it "filters with safe_params" do
expect(subject).to eq({ 'w' => 1 })
end
end
describe "#render_fork_suggestion" do
subject { helper.render_fork_suggestion }

View File

@ -23,7 +23,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration, :request_store do
record_type: 'A',
interval: 60,
disconnect_timeout: 120,
use_tcp: false
use_tcp: false,
max_replica_pools: nil
)
expect(config.pool_size).to eq(Gitlab::Database.default_pool_size)
end
@ -39,7 +40,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration, :request_store do
replica_check_interval: 3,
hosts: %w[foo bar],
discover: {
'record' => 'foo.example.com'
record: 'foo.example.com',
max_replica_pools: 5
}
}
}
@ -59,7 +61,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration, :request_store do
record_type: 'A',
interval: 60,
disconnect_timeout: 120,
use_tcp: false
use_tcp: false,
max_replica_pools: 5
)
expect(config.pool_size).to eq(4)
end
@ -95,7 +98,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration, :request_store do
record_type: 'A',
interval: 60,
disconnect_timeout: 120,
use_tcp: false
use_tcp: false,
max_replica_pools: nil
)
expect(config.pool_size).to eq(4)
end

View File

@ -0,0 +1,80 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Gitlab::Database::LoadBalancing::ServiceDiscovery::Sampler do
let(:sampler) { described_class.new(max_replica_pools: max_replica_pools, seed: 100) }
let(:max_replica_pools) { 3 }
let(:address_class) { ::Gitlab::Database::LoadBalancing::ServiceDiscovery::Address }
let(:addresses) do
[
address_class.new("127.0.0.1", 6432),
address_class.new("127.0.0.1", 6433),
address_class.new("127.0.0.1", 6434),
address_class.new("127.0.0.1", 6435),
address_class.new("127.0.0.2", 6432),
address_class.new("127.0.0.2", 6433),
address_class.new("127.0.0.2", 6434),
address_class.new("127.0.0.2", 6435)
]
end
describe '#sample' do
it 'samples max_replica_pools addresses' do
expect(sampler.sample(addresses).count).to eq(max_replica_pools)
end
it 'samples random ports across all hosts' do
expect(sampler.sample(addresses)).to eq([
address_class.new("127.0.0.1", 6432),
address_class.new("127.0.0.2", 6435),
address_class.new("127.0.0.1", 6435)
])
end
it 'returns the same answer for the same input when called multiple times' do
result = sampler.sample(addresses)
expect(sampler.sample(addresses)).to eq(result)
expect(sampler.sample(addresses)).to eq(result)
end
it 'gives a consistent answer regardless of input ordering' do
expect(sampler.sample(addresses.reverse)).to eq(sampler.sample(addresses))
end
it 'samples fairly across all hosts' do
# Choose a bunch of different seeds to prove that it always chooses 2
# different ports from each host when selecting 4
(1..10).each do |seed|
sampler = described_class.new(max_replica_pools: 4, seed: seed)
result = sampler.sample(addresses)
expect(result.count { |r| r.hostname == "127.0.0.1" }).to eq(2)
expect(result.count { |r| r.hostname == "127.0.0.2" }).to eq(2)
end
end
context 'when input is an empty array' do
it 'returns an empty array' do
expect(sampler.sample([])).to eq([])
end
end
context 'when there are less replicas than max_replica_pools' do
let(:max_replica_pools) { 100 }
it 'returns the same addresses' do
expect(sampler.sample(addresses)).to eq(addresses)
end
end
context 'when max_replica_pools is nil' do
let(:max_replica_pools) { nil }
it 'returns the same addresses' do
expect(sampler.sample(addresses)).to eq(addresses)
end
end
end
end

View File

@ -231,10 +231,13 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do
nameserver: 'localhost',
port: 8600,
record: 'foo',
record_type: record_type
record_type: record_type,
max_replica_pools: max_replica_pools
)
end
let(:max_replica_pools) { nil }
let(:packet) { double(:packet, answer: [res1, res2]) }
before do
@ -266,24 +269,51 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do
let(:res1) { double(:resource, host: 'foo1.service.consul.', port: 5432, weight: 1, priority: 1, ttl: 90) }
let(:res2) { double(:resource, host: 'foo2.service.consul.', port: 5433, weight: 1, priority: 1, ttl: 90) }
let(:res3) { double(:resource, host: 'foo3.service.consul.', port: 5434, weight: 1, priority: 1, ttl: 90) }
let(:packet) { double(:packet, answer: [res1, res2, res3], additional: []) }
let(:res4) { double(:resource, host: 'foo4.service.consul.', port: 5432, weight: 1, priority: 1, ttl: 90) }
let(:packet) { double(:packet, answer: [res1, res2, res3, res4], additional: []) }
before do
expect_next_instance_of(Gitlab::Database::LoadBalancing::SrvResolver) do |resolver|
allow(resolver).to receive(:address_for).with('foo1.service.consul.').and_return(IPAddr.new('255.255.255.0'))
allow(resolver).to receive(:address_for).with('foo2.service.consul.').and_return(IPAddr.new('127.0.0.1'))
allow(resolver).to receive(:address_for).with('foo3.service.consul.').and_return(nil)
allow(resolver).to receive(:address_for).with('foo4.service.consul.').and_return("127.0.0.2")
end
end
it 'returns a TTL and ordered list of hosts' do
addresses = [
described_class::Address.new('127.0.0.1', 5433),
described_class::Address.new('127.0.0.2', 5432),
described_class::Address.new('255.255.255.0', 5432)
]
expect(service.addresses_from_dns).to eq([90, addresses])
end
context 'when max_replica_pools is set' do
context 'when the number of addresses exceeds max_replica_pools' do
let(:max_replica_pools) { 2 }
it 'limits to max_replica_pools' do
expect(service.addresses_from_dns[1].count).to eq(2)
end
end
context 'when the number of addresses is less than max_replica_pools' do
let(:max_replica_pools) { 5 }
it 'returns all addresses' do
addresses = [
described_class::Address.new('127.0.0.1', 5433),
described_class::Address.new('127.0.0.2', 5432),
described_class::Address.new('255.255.255.0', 5432)
]
expect(service.addresses_from_dns).to eq([90, addresses])
end
end
end
end
context 'when the resolver returns an empty response' do

View File

@ -7,19 +7,60 @@ RSpec.describe Gitlab::Database::Migrations::Runner, :reestablished_active_recor
let(:migration_runs) { [] } # This list gets populated as the runner tries to run migrations
# Tests depend on all of these lists being sorted in the order migrations would be applied
let(:applied_migrations_other_branches) { [double(ActiveRecord::Migration, version: 1, name: 'migration_complete_other_branch')] }
let(:applied_migrations_other_branches) do
[
double(
ActiveRecord::Migration,
version: 1,
name: 'migration_complete_other_branch',
filename: 'db/migrate/1_migration_complete_other_branch.rb'
)
]
end
let(:applied_migrations_this_branch) do
[
double(ActiveRecord::Migration, version: 2, name: 'older_migration_complete_this_branch'),
double(ActiveRecord::Migration, version: 3, name: 'newer_migration_complete_this_branch')
double(
ActiveRecord::Migration,
version: 2,
name: 'older_migration_complete_this_branch',
filename: 'db/migrate/2_older_migration_complete_this_branch.rb'
),
double(
ActiveRecord::Migration,
version: 3,
name: 'post_migration_complete_this_branch',
filename: 'db/post_migrate/3_post_migration_complete_this_branch.rb'
),
double(
ActiveRecord::Migration,
version: 4,
name: 'newer_migration_complete_this_branch',
filename: 'db/migrate/4_newer_migration_complete_this_branch.rb'
)
].sort_by(&:version)
end
let(:pending_migrations) do
[
double(ActiveRecord::Migration, version: 4, name: 'older_migration_pending'),
double(ActiveRecord::Migration, version: 5, name: 'newer_migration_pending')
double(
ActiveRecord::Migration,
version: 5,
name: 'older_migration_pending',
filename: 'db/migrate/5_older_migration_pending.rb'
),
double(
ActiveRecord::Migration,
version: 6,
name: 'post_migration_pending',
filename: 'db/post_migrate/6_post_migration_pending.rb'
),
double(
ActiveRecord::Migration,
version: 7,
name: 'newer_migration_pending',
filename: 'db/migrate/7_newer_migration_pending.rb'
)
].sort_by(&:version)
end
@ -85,11 +126,11 @@ RSpec.describe Gitlab::Database::Migrations::Runner, :reestablished_active_recor
context 'running migrations' do
subject(:up) { described_class.up(database: database, legacy_mode: legacy_mode) }
it 'runs the unapplied migrations in version order', :aggregate_failures do
it 'runs the unapplied migrations in regular/post order, then version order', :aggregate_failures do
up.run
expect(migration_runs.map(&:dir)).to match_array([:up, :up])
expect(migration_runs.map(&:version_to_migrate)).to eq(pending_migrations.map(&:version))
expect(migration_runs.map(&:dir)).to match_array([:up, :up, :up])
expect(migration_runs.map(&:version_to_migrate)).to eq([5, 7, 6])
end
it 'writes a metadata file with the current schema version and database name' do
@ -128,8 +169,8 @@ RSpec.describe Gitlab::Database::Migrations::Runner, :reestablished_active_recor
it 'runs the applied migrations for the current branch in reverse order', :aggregate_failures do
down.run
expect(migration_runs.map(&:dir)).to match_array([:down, :down])
expect(migration_runs.map(&:version_to_migrate)).to eq(applied_migrations_this_branch.reverse.map(&:version))
expect(migration_runs.map(&:dir)).to match_array([:down, :down, :down])
expect(migration_runs.map(&:version_to_migrate)).to eq([3, 4, 2])
end
end

View File

@ -55,22 +55,8 @@ RSpec.describe Gitlab::Diff::File do
let(:commit) { project.commit("532c837") }
context 'when file is ipynb' do
let(:ipynb_semantic_diff) { false }
before do
stub_feature_flags(ipynb_semantic_diff: ipynb_semantic_diff)
end
subject { diff_file.rendered }
context 'when ipynb_semantic_diff is off' do
it { is_expected.to be_nil }
end
context 'and rendered_viewer is on' do
let(:ipynb_semantic_diff) { true }
it { is_expected.not_to be_nil }
it 'creates a rendered diff file' do
expect(diff_file.rendered).not_to be_nil
end
end
end
@ -152,20 +138,6 @@ RSpec.describe Gitlab::Diff::File do
expect(diff_file.rendered).to be_nil
end
end
context 'when semantic ipynb is off' do
before do
stub_feature_flags(ipynb_semantic_diff: false)
end
it 'returns nil' do
expect(diff_file).not_to receive(:modified_file?)
expect(diff_file).not_to receive(:ipynb?)
expect(diff).not_to receive(:too_large?)
expect(diff_file.rendered).to be_nil
end
end
end
end

View File

@ -27,6 +27,8 @@ RSpec.describe Ci::Bridge do
it_behaves_like 'has ID tokens', :ci_bridge
it_behaves_like 'a retryable job'
it 'has one downstream pipeline' do
expect(bridge).to have_one(:sourced_pipeline)
expect(bridge).to have_one(:downstream_pipeline)

View File

@ -86,6 +86,8 @@ RSpec.describe Ci::Build do
it_behaves_like 'has ID tokens', :ci_build
it_behaves_like 'a retryable job'
describe '.manual_actions' do
let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) }
let!(:manual_but_succeeded) { create(:ci_build, :manual, status: :success, pipeline: pipeline) }

View File

@ -389,7 +389,7 @@ RSpec.describe Ci::Stage, :models do
end
context 'without pipeline' do
subject(:stage) { build(:ci_stage, pipeline: nil) }
subject(:stage) { build(:ci_stage, pipeline: nil, project: build_stubbed(:project)) }
it { is_expected.to validate_presence_of(:partition_id) }

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe PgFullTextSearchable do
let(:project) { create(:project) }
let(:project) { build(:project) }
let(:model_class) do
Class.new(ActiveRecord::Base) do

View File

@ -3,10 +3,25 @@
require 'spec_helper'
RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode do
let(:broadcast_message) { build(:broadcast_message) }
let(:broadcast_message_params) { broadcast_message.as_json(root: true, only: [:message, :starts_at, :ends_at]) }
let_it_be(:invalid_broadcast_message) { { broadcast_message: { message: '' } } }
let_it_be(:test_message) { 'you owe me a new acorn' }
before do
sign_in(create(:admin))
end
describe 'GET #index' do
it 'renders index template' do
get admin_broadcast_messages_path
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to render_template(:index)
end
end
describe 'POST /preview' do
it 'renders preview partial' do
post preview_admin_broadcast_messages_path, params: { broadcast_message: { message: "Hello, world!" } }
@ -15,4 +30,78 @@ RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode do
expect(response.body).to render_template(:_preview)
end
end
describe 'POST #create' do
context 'when format json' do
it 'persists the message and returns ok on success' do
post admin_broadcast_messages_path(format: :json), params: broadcast_message_params
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)['message']).to eq(broadcast_message.message)
end
it 'does not persist the message on failure' do
post admin_broadcast_messages_path(format: :json), params: invalid_broadcast_message
expect(response).to have_gitlab_http_status(:bad_request)
expect(Gitlab::Json.parse(response.body)['errors']).to be_present
end
end
context 'when format html' do
it 'persists the message and redirects to broadcast_messages on success' do
post admin_broadcast_messages_path(format: :html), params: broadcast_message_params
expect(response).to redirect_to(admin_broadcast_messages_path)
end
it 'does not persist and renders the index page on failure' do
post admin_broadcast_messages_path(format: :html), params: invalid_broadcast_message
expect(response.body).to render_template(:index)
end
end
end
describe 'PATCH #update' do
context 'when format json' do
it 'persists the message and returns ok on success' do
broadcast_message.save!
patch admin_broadcast_message_path(format: :json, id: broadcast_message.id), params: {
broadcast_message: { message: test_message }
}
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)['message']).to eq(test_message)
end
it 'does not persist the message on failure' do
broadcast_message.message = test_message
broadcast_message.save!
patch admin_broadcast_message_path(format: :json, id: broadcast_message.id), params: {
broadcast_message: { message: '' }
}
expect(response).to have_gitlab_http_status(:bad_request)
expect(Gitlab::Json.parse(response.body)['errors']).to be_present
end
end
context 'when format html' do
it 'persists the message and redirects to broadcast_messages on success' do
broadcast_message.save!
patch admin_broadcast_message_path(id: broadcast_message.id), params: {
broadcast_message: { message: test_message }
}
expect(response).to redirect_to(admin_broadcast_messages_path)
end
it 'does not persist and renders the edit page on failure' do
broadcast_message.message = test_message
broadcast_message.save!
patch admin_broadcast_message_path(id: broadcast_message.id), params: {
**invalid_broadcast_message
}
expect(response.body).to render_template(:edit)
end
end
end
end

View File

@ -25,6 +25,27 @@ RSpec.describe RuboCop::Cop::Gitlab::Json do
end
end
context 'when ::JSON is called in EE' do
it 'registers an offense and autocorrects' do
expect_offense(<<~RUBY, '/path/to/ee/foo.rb')
class Foo
def bar
JSON.parse('{ "foo": "bar" }')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `Gitlab::Json` over calling `JSON` or `to_json` directly. [...]
end
end
RUBY
expect_correction(<<~RUBY)
class Foo
def bar
::Gitlab::Json.parse('{ "foo": "bar" }')
end
end
RUBY
end
end
context 'when ActiveSupport::JSON is called' do
it 'registers an offense and autocorrects' do
expect_offense(<<~RUBY)

View File

@ -2,148 +2,119 @@
require 'spec_helper'
RSpec.describe Ci::ProcessBuildService, '#execute' do
let(:user) { create(:user) }
let(:project) { create(:project) }
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, ref: 'master', project: project) }
subject { described_class.new(project, user).execute(build, current_status) }
before do
before_all do
project.add_maintainer(user)
end
context 'when build has on_success option' do
let(:build) { create(:ci_build, :created, when: :on_success, user: user, project: project) }
context 'when current status is success' do
let(:current_status) { 'success' }
it 'changes the build status' do
expect { subject }.to change { build.status }.to('pending')
end
end
context 'when current status is skipped' do
let(:current_status) { 'skipped' }
it 'changes the build status' do
expect { subject }.to change { build.status }.to('pending')
end
end
context 'when current status is failed' do
let(:current_status) { 'failed' }
it 'does not change the build status' do
expect { subject }.to change { build.status }.to('skipped')
end
end
end
context 'when build has on_failure option' do
let(:build) { create(:ci_build, :created, when: :on_failure, user: user, project: project) }
context 'when current status is success' do
let(:current_status) { 'success' }
it 'changes the build status' do
expect { subject }.to change { build.status }.to('skipped')
end
end
context 'when current status is failed' do
let(:current_status) { 'failed' }
it 'does not change the build status' do
expect { subject }.to change { build.status }.to('pending')
end
end
end
context 'when build has always option' do
let(:build) { create(:ci_build, :created, when: :always, user: user, project: project) }
context 'when current status is success' do
let(:current_status) { 'success' }
it 'changes the build status' do
expect { subject }.to change { build.status }.to('pending')
end
end
context 'when current status is failed' do
let(:current_status) { 'failed' }
it 'does not change the build status' do
expect { subject }.to change { build.status }.to('pending')
end
end
end
context 'when build has manual option' do
let(:build) { create(:ci_build, :created, :actionable, user: user, project: project) }
context 'when current status is success' do
let(:current_status) { 'success' }
it 'changes the build status' do
expect { subject }.to change { build.status }.to('manual')
end
end
context 'when current status is failed' do
let(:current_status) { 'failed' }
it 'does not change the build status' do
expect { subject }.to change { build.status }.to('skipped')
end
end
end
context 'when build has delayed option' do
shared_context 'with enqueue_immediately set' do
before do
allow(Ci::BuildScheduleWorker).to receive(:perform_at) {}
build.set_enqueue_immediately!
end
end
shared_context 'with ci_retry_job_fix disabled' do
before do
stub_feature_flags(ci_retry_job_fix: false)
end
end
context 'for single build' do
let!(:build) { create(:ci_build, *[trait].compact, :created, **conditions, pipeline: pipeline) }
where(:trait, :conditions, :current_status, :after_status, :retry_after_status, :retry_disabled_after_status) do
nil | { when: :on_success } | 'success' | 'pending' | 'pending' | 'pending'
nil | { when: :on_success } | 'skipped' | 'pending' | 'pending' | 'pending'
nil | { when: :on_success } | 'failed' | 'skipped' | 'skipped' | 'skipped'
nil | { when: :on_failure } | 'success' | 'skipped' | 'skipped' | 'skipped'
nil | { when: :on_failure } | 'skipped' | 'skipped' | 'skipped' | 'skipped'
nil | { when: :on_failure } | 'failed' | 'pending' | 'pending' | 'pending'
nil | { when: :always } | 'success' | 'pending' | 'pending' | 'pending'
nil | { when: :always } | 'skipped' | 'pending' | 'pending' | 'pending'
nil | { when: :always } | 'failed' | 'pending' | 'pending' | 'pending'
:actionable | { when: :manual } | 'success' | 'manual' | 'pending' | 'manual'
:actionable | { when: :manual } | 'skipped' | 'manual' | 'pending' | 'manual'
:actionable | { when: :manual } | 'failed' | 'skipped' | 'skipped' | 'skipped'
:schedulable | { when: :delayed } | 'success' | 'scheduled' | 'pending' | 'scheduled'
:schedulable | { when: :delayed } | 'skipped' | 'scheduled' | 'pending' | 'scheduled'
:schedulable | { when: :delayed } | 'failed' | 'skipped' | 'skipped' | 'skipped'
end
let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) }
context 'when current status is success' do
let(:current_status) { 'success' }
it 'changes the build status' do
expect { subject }.to change { build.status }.to('scheduled')
with_them do
it 'updates the job status to after_status' do
expect { subject }.to change { build.status }.to(after_status)
end
end
context 'when current status is failed' do
let(:current_status) { 'failed' }
context 'when build is set to enqueue immediately' do
include_context 'with enqueue_immediately set'
it 'does not change the build status' do
expect { subject }.to change { build.status }.to('skipped')
it 'updates the job status to retry_after_status' do
expect { subject }.to change { build.status }.to(retry_after_status)
end
context 'when feature flag ci_retry_job_fix is disabled' do
include_context 'with ci_retry_job_fix disabled'
it "updates the job status to retry_disabled_after_status" do
expect { subject }.to change { build.status }.to(retry_disabled_after_status)
end
end
end
end
end
context 'when build is scheduled with DAG' do
using RSpec::Parameterized::TableSyntax
let!(:build) do
create(
:ci_build,
*[trait].compact,
:dependent,
:created,
when: build_when,
pipeline: pipeline,
needed: other_build
)
end
let(:pipeline) { create(:ci_pipeline, ref: 'master', project: project) }
let!(:build) { create(:ci_build, :created, when: build_when, pipeline: pipeline, scheduling_type: :dag) }
let!(:other_build) { create(:ci_build, :created, when: :on_success, pipeline: pipeline) }
let!(:build_on_other_build) { create(:ci_build_need, build: build, name: other_build.name) }
where(:build_when, :current_status, :after_status) do
:on_success | 'success' | 'pending'
:on_success | 'skipped' | 'skipped'
:manual | 'success' | 'manual'
:manual | 'skipped' | 'skipped'
:delayed | 'success' | 'manual'
:delayed | 'skipped' | 'skipped'
where(:trait, :build_when, :current_status, :after_status, :retry_after_status, :retry_disabled_after_status) do
nil | :on_success | 'success' | 'pending' | 'pending' | 'pending'
nil | :on_success | 'skipped' | 'skipped' | 'skipped' | 'skipped'
nil | :manual | 'success' | 'manual' | 'pending' | 'manual'
nil | :manual | 'skipped' | 'skipped' | 'skipped' | 'skipped'
nil | :delayed | 'success' | 'manual' | 'pending' | 'manual'
nil | :delayed | 'skipped' | 'skipped' | 'skipped' | 'skipped'
:schedulable | :delayed | 'success' | 'scheduled' | 'pending' | 'scheduled'
:schedulable | :delayed | 'skipped' | 'skipped' | 'skipped' | 'skipped'
end
with_them do
it 'proceeds the build' do
it 'updates the job status to after_status' do
expect { subject }.to change { build.status }.to(after_status)
end
context 'when build is set to enqueue immediately' do
include_context 'with enqueue_immediately set'
it 'updates the job status to retry_after_status' do
expect { subject }.to change { build.status }.to(retry_after_status)
end
context 'when feature flag ci_retry_job_fix is disabled' do
include_context 'with ci_retry_job_fix disabled'
it "updates the job status to retry_disabled_after_status" do
expect { subject }.to change { build.status }.to(retry_disabled_after_status)
end
end
end
end
end
end

View File

@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::RetryJobService do
using RSpec::Parameterized::TableSyntax
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
@ -11,11 +12,11 @@ RSpec.describe Ci::RetryJobService do
end
let_it_be(:stage) do
create(:ci_stage, project: project,
pipeline: pipeline,
name: 'test')
create(:ci_stage, pipeline: pipeline, name: 'test')
end
let_it_be(:deploy_stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy', position: stage.position + 1) }
let(:job_variables_attributes) { [{ key: 'MANUAL_VAR', value: 'manual test var' }] }
let(:user) { developer }
@ -43,7 +44,10 @@ RSpec.describe Ci::RetryJobService do
end
shared_context 'retryable build' do
let_it_be_with_refind(:job) { create(:ci_build, :success, pipeline: pipeline, ci_stage: stage) }
let_it_be_with_reload(:job) do
create(:ci_build, :success, pipeline: pipeline, ci_stage: stage)
end
let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
let_it_be(:job_to_clone) do
@ -60,6 +64,12 @@ RSpec.describe Ci::RetryJobService do
end
end
shared_context 'with ci_retry_job_fix disabled' do
before do
stub_feature_flags(ci_retry_job_fix: false)
end
end
shared_examples_for 'clones the job' do
let(:job) { job_to_clone }
@ -87,8 +97,7 @@ RSpec.describe Ci::RetryJobService do
context 'when the job has needs' do
before do
create(:ci_build_need, build: job, name: 'build1')
create(:ci_build_need, build: job, name: 'build2')
create_list(:ci_build_need, 2, build: job)
end
it 'bulk inserts all the needs' do
@ -123,16 +132,12 @@ RSpec.describe Ci::RetryJobService do
end
context 'when there are subsequent processables that are skipped' do
let_it_be(:stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy') }
let!(:subsequent_build) do
create(:ci_build, :skipped, stage_idx: 2,
pipeline: pipeline,
ci_stage: stage)
create(:ci_build, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
let!(:subsequent_bridge) do
create(:ci_bridge, :skipped, stage_idx: 2, pipeline: pipeline, ci_stage: stage)
create(:ci_bridge, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
it 'resumes pipeline processing in the subsequent stage' do
@ -152,10 +157,9 @@ RSpec.describe Ci::RetryJobService do
end
context 'when the pipeline has other jobs' do
let!(:stage2) { create(:ci_stage, project: project, pipeline: pipeline, name: 'deploy') }
let!(:build2) { create(:ci_build, pipeline: pipeline, ci_stage: stage) }
let!(:deploy) { create(:ci_build, pipeline: pipeline, ci_stage: stage2) }
let!(:deploy_needs_build2) { create(:ci_build_need, build: deploy, name: build2.name) }
let!(:other_test_build) { create(:ci_build, pipeline: pipeline, ci_stage: stage) }
let!(:deploy) { create(:ci_build, pipeline: pipeline, ci_stage: deploy_stage) }
let!(:deploy_needs_build2) { create(:ci_build_need, build: deploy, name: other_test_build.name) }
context 'when job has a nil scheduling_type' do
before do
@ -166,7 +170,7 @@ RSpec.describe Ci::RetryJobService do
it 'populates scheduling_type of processables' do
expect(new_job.scheduling_type).to eq('stage')
expect(job.reload.scheduling_type).to eq('stage')
expect(build2.reload.scheduling_type).to eq('stage')
expect(other_test_build.reload.scheduling_type).to eq('stage')
expect(deploy.reload.scheduling_type).to eq('dag')
end
end
@ -193,6 +197,13 @@ RSpec.describe Ci::RetryJobService do
end
end
shared_examples_for 'checks enqueue_immediately?' do
it "returns enqueue_immediately" do
subject
expect(new_job.enqueue_immediately?).to eq enqueue_immediately
end
end
describe '#clone!' do
let(:new_job) { service.clone!(job) }
@ -224,7 +235,7 @@ RSpec.describe Ci::RetryJobService do
context 'when a build with a deployment is retried' do
let!(:job) do
create(:ci_build, :with_deployment, :deploy_to_production,
pipeline: pipeline, ci_stage: stage, project: project)
pipeline: pipeline, ci_stage: stage)
end
it 'creates a new deployment' do
@ -247,7 +258,6 @@ RSpec.describe Ci::RetryJobService do
options: { environment: { name: environment_name } },
pipeline: pipeline,
ci_stage: stage,
project: project,
user: other_developer)
end
@ -282,10 +292,44 @@ RSpec.describe Ci::RetryJobService do
end
end
end
context 'when enqueue_if_actionable is provided' do
let!(:job) do
create(:ci_build, *[trait].compact, :failed, pipeline: pipeline, ci_stage: stage)
end
let(:new_job) { subject }
subject { service.clone!(job, enqueue_if_actionable: enqueue_if_actionable) }
where(:enqueue_if_actionable, :trait, :enqueue_immediately) do
true | nil | false
true | :manual | true
true | :expired_scheduled | true
false | nil | false
false | :manual | false
false | :expired_scheduled | false
end
with_them do
it_behaves_like 'checks enqueue_immediately?'
context 'when feature flag is disabled' do
include_context 'with ci_retry_job_fix disabled'
it_behaves_like 'checks enqueue_immediately?' do
let(:enqueue_immediately) { false }
end
end
end
end
end
describe '#execute' do
let(:new_job) { service.execute(job)[:job] }
let(:new_job) { subject[:job] }
subject { service.execute(job) }
context 'when the job to be retried is a bridge' do
include_context 'retryable bridge'
@ -307,24 +351,18 @@ RSpec.describe Ci::RetryJobService do
it_behaves_like 'retries the job'
context 'when there are subsequent jobs that are skipped' do
let_it_be(:stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy') }
let!(:subsequent_build) do
create(:ci_build, :skipped, stage_idx: 2,
pipeline: pipeline,
stage_id: stage.id)
create(:ci_build, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
let!(:subsequent_bridge) do
create(:ci_bridge, :skipped, stage_idx: 2,
pipeline: pipeline,
stage_id: stage.id)
create(:ci_bridge, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
it 'does not cause an N+1 when updating the job ownership' do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { service.execute(job) }.count
create_list(:ci_build, 2, :skipped, stage_idx: job.stage_idx + 1, pipeline: pipeline, stage_id: stage.id)
create_list(:ci_build, 2, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
expect { service.execute(job) }.not_to exceed_all_query_limit(control_count)
end
@ -352,5 +390,161 @@ RSpec.describe Ci::RetryJobService do
end
end
end
context 'when job being retried has jobs in previous stages' do
let!(:job) do
create(
:ci_build,
:failed,
name: 'deploy_a',
pipeline: pipeline,
ci_stage: deploy_stage
)
end
before do
create(
:ci_build,
previous_stage_job_status,
name: 'test_a',
pipeline: pipeline,
ci_stage: stage
)
end
where(:previous_stage_job_status, :after_status) do
:created | 'created'
:pending | 'created'
:running | 'created'
:manual | 'created'
:scheduled | 'created'
:success | 'pending'
:failed | 'skipped'
:skipped | 'pending'
end
with_them do
it 'updates the new job status to after_status' do
expect(subject).to be_success
expect(new_job.status).to eq after_status
end
context 'when feature flag is disabled' do
include_context 'with ci_retry_job_fix disabled'
it 'enqueues the new job' do
expect(subject).to be_success
expect(new_job).to be_pending
end
end
end
end
context 'when job being retried has DAG dependencies' do
let!(:job) do
create(
:ci_build,
:failed,
:dependent,
name: 'deploy_a',
pipeline: pipeline,
ci_stage: deploy_stage,
needed: dependency
)
end
let(:dependency) do
create(
:ci_build,
dag_dependency_status,
name: 'test_a',
pipeline: pipeline,
ci_stage: stage
)
end
where(:dag_dependency_status, :after_status) do
:created | 'created'
:pending | 'created'
:running | 'created'
:manual | 'created'
:scheduled | 'created'
:success | 'pending'
:failed | 'skipped'
:skipped | 'skipped'
end
with_them do
it 'updates the new job status to after_status' do
expect(subject).to be_success
expect(new_job.status).to eq after_status
end
context 'when feature flag is disabled' do
include_context 'with ci_retry_job_fix disabled'
it 'enqueues the new job' do
expect(subject).to be_success
expect(new_job).to be_pending
end
end
end
end
context 'when there are other manual/scheduled jobs' do
let_it_be(:test_manual_build) do
create(:ci_build, :manual, pipeline: pipeline, ci_stage: stage)
end
let_it_be(:subsequent_manual_build) do
create(:ci_build, :manual, pipeline: pipeline, ci_stage: deploy_stage)
end
let_it_be(:test_scheduled_build) do
create(:ci_build, :scheduled, pipeline: pipeline, ci_stage: stage)
end
let_it_be(:subsequent_scheduled_build) do
create(:ci_build, :scheduled, pipeline: pipeline, ci_stage: deploy_stage)
end
let!(:job) do
create(:ci_build, *[trait].compact, :failed, pipeline: pipeline, ci_stage: stage)
end
where(:trait, :enqueue_immediately) do
nil | false
:manual | true
:expired_scheduled | true
end
with_them do
it 'retries the given job but not the other manual/scheduled jobs' do
expect { subject }
.to change { Ci::Build.count }.by(1)
.and not_change { test_manual_build.reload.status }
.and not_change { subsequent_manual_build.reload.status }
.and not_change { test_scheduled_build.reload.status }
.and not_change { subsequent_scheduled_build.reload.status }
expect(new_job).to be_pending
end
it_behaves_like 'checks enqueue_immediately?'
context 'when feature flag is disabled' do
include_context 'with ci_retry_job_fix disabled'
it 'enqueues the new job' do
expect(subject).to be_success
expect(new_job).to be_pending
end
it_behaves_like 'checks enqueue_immediately?' do
let(:enqueue_immediately) { false }
end
end
end
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
RSpec.shared_examples 'a retryable job' do
describe '#enqueue_immediately?' do
it 'defaults to false' do
expect(subject.enqueue_immediately?).to eq(false)
end
end
describe '#set_enqueue_immediately!' do
it 'changes #enqueue_immediately? to true' do
expect { subject.set_enqueue_immediately! }
.to change(subject, :enqueue_immediately?).from(false).to(true)
end
end
end

View File

@ -60,7 +60,7 @@ RSpec.describe 'admin/dashboard/index.html.haml' do
end
it 'renders the version check badge' do
expect(rendered).to have_selector('.js-gitlab-version-check')
expect(rendered).to have_selector('.js-gitlab-version-check-badge')
end
end

View File

@ -10,7 +10,7 @@ RSpec.describe 'layouts/header/_gitlab_version' do
end
it 'renders the version check badge' do
expect(rendered).to have_selector('.js-gitlab-version-check')
expect(rendered).to have_selector('.js-gitlab-version-check-badge')
end
it 'renders the container as a link' do