Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e6fa9529b4
commit
44434461b3
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
|
@ -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>
|
|
@ -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');
|
|
@ -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;
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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" } }
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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**.
|
||||
|
|
|
@ -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**.
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue