Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-05-06 21:10:07 +00:00
parent 52dbfea964
commit 454973238c
56 changed files with 989 additions and 176 deletions

View File

@ -3,6 +3,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import BlobContent from '~/blob/components/blob_content.vue'; import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue'; import BlobHeader from '~/blob/components/blob_header.vue';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import blobInfoQuery from '../queries/blob_info.query.graphql'; import blobInfoQuery from '../queries/blob_info.query.graphql';
@ -22,6 +23,11 @@ export default {
filePath: this.path, filePath: this.path,
}; };
}, },
result() {
this.switchViewer(
this.hasRichViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
);
},
error() { error() {
createFlash({ message: __('An error occurred while loading the file. Please try again.') }); createFlash({ message: __('An error occurred while loading the file. Please try again.') });
}, },
@ -44,6 +50,7 @@ export default {
}, },
data() { data() {
return { return {
activeViewerType: SIMPLE_BLOB_VIEWER,
project: { project: {
repository: { repository: {
blobs: { blobs: {
@ -69,7 +76,7 @@ export default {
canModifyBlob: true, canModifyBlob: true,
forkPath: '', forkPath: '',
simpleViewer: {}, simpleViewer: {},
richViewer: {}, richViewer: null,
}, },
], ],
}, },
@ -87,10 +94,19 @@ export default {
return nodes[0] || {}; return nodes[0] || {};
}, },
viewer() { viewer() {
const viewer = this.blobInfo.richViewer || this.blobInfo.simpleViewer; const { richViewer, simpleViewer } = this.blobInfo;
const { fileType, tooLarge, type } = viewer; return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
},
return { fileType, tooLarge, type }; hasRichViewer() {
return Boolean(this.blobInfo.richViewer);
},
hasRenderError() {
return Boolean(this.viewer.renderError);
},
},
methods: {
switchViewer(newViewer) {
this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER;
}, },
}, },
}; };
@ -99,8 +115,14 @@ export default {
<template> <template>
<div> <div>
<gl-loading-icon v-if="isLoading" /> <gl-loading-icon v-if="isLoading" />
<div v-if="blobInfo && !isLoading"> <div v-if="blobInfo && !isLoading" class="file-holder">
<blob-header :blob="blobInfo" /> <blob-header
:blob="blobInfo"
:hide-viewer-switcher="!hasRichViewer"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
@viewer-changed="switchViewer"
/>
<blob-content <blob-content
:blob="blobInfo" :blob="blobInfo"
:content="blobInfo.rawTextBlob" :content="blobInfo.rawTextBlob"

View File

@ -6,6 +6,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
nodes { nodes {
webPath webPath
name name
size
rawSize rawSize
rawTextBlob rawTextBlob
fileType fileType
@ -18,11 +19,13 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
fileType fileType
tooLarge tooLarge
type type
renderError
} }
richViewer { richViewer {
fileType fileType
tooLarge tooLarge
type type
renderError
} }
} }
} }

View File

@ -36,6 +36,10 @@ class Projects::BlobController < Projects::ApplicationController
feature_category :source_code_management feature_category :source_code_management
before_action do
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
end
def new def new
commit unless @repository.empty? commit unless @repository.empty?
end end

View File

@ -59,6 +59,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action do before_action do
push_frontend_feature_flag(:mr_collapsed_approval_rules, @project) push_frontend_feature_flag(:mr_collapsed_approval_rules, @project)
push_frontend_feature_flag(:show_relevant_approval_rule_approvers, @project, default_enabled: :yaml)
end end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions] around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Mutations
module Ci
module Job
class Base < BaseMutation
JobID = ::Types::GlobalIDType[::Ci::Build]
argument :id, JobID,
required: true,
description: 'The ID of the job to mutate.'
def find_object(id: )
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = JobID.coerce_isolated_input(id)
GlobalID::Locator.locate(id)
end
end
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Mutations
module Ci
module Job
class Play < Base
graphql_name 'JobPlay'
field :job,
Types::Ci::JobType,
null: true,
description: 'The job after the mutation.'
authorize :update_build
def resolve(id:)
job = authorized_find!(id: id)
project = job.project
::Ci::PlayBuildService.new(project, current_user).execute(job)
{
job: job,
errors: errors_on_object(job)
}
end
end
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Mutations
module Ci
module Job
class Retry < Base
graphql_name 'JobRetry'
field :job,
Types::Ci::JobType,
null: true,
description: 'The job after the mutation.'
authorize :update_build
def resolve(id:)
job = authorized_find!(id: id)
project = job.project
::Ci::RetryBuildService.new(project, current_user).execute(job)
{
job: job,
errors: errors_on_object(job)
}
end
end
end
end
end

View File

@ -94,6 +94,8 @@ module Types
mount_mutation Mutations::Ci::Pipeline::Destroy mount_mutation Mutations::Ci::Pipeline::Destroy
mount_mutation Mutations::Ci::Pipeline::Retry mount_mutation Mutations::Ci::Pipeline::Retry
mount_mutation Mutations::Ci::CiCdSettingsUpdate mount_mutation Mutations::Ci::CiCdSettingsUpdate
mount_mutation Mutations::Ci::Job::Play
mount_mutation Mutations::Ci::Job::Retry
mount_mutation Mutations::Namespace::PackageSettings::Update mount_mutation Mutations::Namespace::PackageSettings::Update
mount_mutation Mutations::UserCallouts::Create mount_mutation Mutations::UserCallouts::Create
end end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Types
module Packages
class PackageStatusEnum < BaseEnum
graphql_name 'PackageStatus'
::Packages::Package.statuses.keys.each do |status|
value status.to_s.upcase, description: "Packages with a #{status} status", value: status.to_s
end
end
end
end

View File

@ -25,6 +25,7 @@ module Types
field :versions, ::Types::Packages::PackageType.connection_type, null: true, field :versions, ::Types::Packages::PackageType.connection_type, null: true,
description: 'The other versions of the package.', description: 'The other versions of the package.',
deprecated: { reason: 'This field is now only returned in the PackageDetailsType', milestone: '13.11' } deprecated: { reason: 'This field is now only returned in the PackageDetailsType', milestone: '13.11' }
field :status, Types::Packages::PackageStatusEnum, null: false, description: 'Package status.'
def project def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find

View File

@ -29,11 +29,11 @@ module TriggerableHooks
callable_scopes = triggers.keys + [:all] callable_scopes = triggers.keys + [:all]
return none unless callable_scopes.include?(trigger) return none unless callable_scopes.include?(trigger)
public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend executable.public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend
end end
def select_active(hooks_scope, data) def select_active(hooks_scope, data)
select do |hook| executable.select do |hook|
ActiveHookFilter.new(hook).matches?(hooks_scope, data) ActiveHookFilter.new(hook).matches?(hooks_scope, data)
end end
end end

View File

@ -29,6 +29,10 @@ class ProjectHook < WebHook
def pluralized_name def pluralized_name
_('Webhooks') _('Webhooks')
end end
def web_hooks_disable_failed?
Feature.enabled?(:web_hooks_disable_failed, project)
end
end end
ProjectHook.prepend_if_ee('EE::ProjectHook') ProjectHook.prepend_if_ee('EE::ProjectHook')

View File

@ -6,9 +6,7 @@ class ServiceHook < WebHook
belongs_to :service belongs_to :service
validates :service, presence: true validates :service, presence: true
# rubocop: disable CodeReuse/ServiceClass
def execute(data, hook_name = 'service_hook') def execute(data, hook_name = 'service_hook')
WebHookService.new(self, data, hook_name).execute super(data, hook_name)
end end
# rubocop: enable CodeReuse/ServiceClass
end end

View File

@ -3,6 +3,11 @@
class WebHook < ApplicationRecord class WebHook < ApplicationRecord
include Sortable include Sortable
FAILURE_THRESHOLD = 3 # three strikes
INITIAL_BACKOFF = 10.minutes
MAX_BACKOFF = 1.day
BACKOFF_GROWTH_FACTOR = 2.0
attr_encrypted :token, attr_encrypted :token,
mode: :per_attribute_iv, mode: :per_attribute_iv,
algorithm: 'aes-256-gcm', algorithm: 'aes-256-gcm',
@ -21,15 +26,27 @@ class WebHook < ApplicationRecord
validates :token, format: { without: /\n/ } validates :token, format: { without: /\n/ }
validates :push_events_branch_filter, branch_filter: true validates :push_events_branch_filter, branch_filter: true
scope :executable, -> do
next all unless Feature.enabled?(:web_hooks_disable_failed)
where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current)
end
def executable?
return true unless web_hooks_disable_failed?
recent_failures <= FAILURE_THRESHOLD && (disabled_until.nil? || disabled_until < Time.current)
end
# rubocop: disable CodeReuse/ServiceClass # rubocop: disable CodeReuse/ServiceClass
def execute(data, hook_name) def execute(data, hook_name)
WebHookService.new(self, data, hook_name).execute WebHookService.new(self, data, hook_name).execute if executable?
end end
# rubocop: enable CodeReuse/ServiceClass # rubocop: enable CodeReuse/ServiceClass
# rubocop: disable CodeReuse/ServiceClass # rubocop: disable CodeReuse/ServiceClass
def async_execute(data, hook_name) def async_execute(data, hook_name)
WebHookService.new(self, data, hook_name).async_execute WebHookService.new(self, data, hook_name).async_execute if executable?
end end
# rubocop: enable CodeReuse/ServiceClass # rubocop: enable CodeReuse/ServiceClass
@ -41,4 +58,26 @@ class WebHook < ApplicationRecord
def help_path def help_path
'user/project/integrations/webhooks' 'user/project/integrations/webhooks'
end end
def next_backoff
return MAX_BACKOFF if backoff_count >= 8 # optimization to prevent expensive exponentiation and possible overflows
(INITIAL_BACKOFF * (BACKOFF_GROWTH_FACTOR**backoff_count))
.clamp(INITIAL_BACKOFF, MAX_BACKOFF)
.seconds
end
def disable!
update!(recent_failures: FAILURE_THRESHOLD + 1)
end
def enable!
update!(recent_failures: 0, disabled_until: nil, backoff_count: 0)
end
private
def web_hooks_disable_failed?
Feature.enabled?(:web_hooks_disable_failed)
end
end end

View File

@ -84,10 +84,9 @@ class Member < ApplicationRecord
is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil)) is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
user_is_blocked = User.arel_table[:state].eq(:blocked) user_is_blocked = User.arel_table[:state].eq(:blocked)
user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_blocked)
left_join_users left_join_users
.where(user_ok) .where(user_is_blocked)
.where.not(is_external_invite)
.non_request .non_request
.non_minimal_access .non_minimal_access
.reorder(nil) .reorder(nil)

View File

@ -63,6 +63,12 @@ module Namespaces
lineage(top: self) lineage(top: self)
end end
def descendants
return super unless use_traversal_ids?
self_and_descendants.where.not(id: id)
end
def ancestors(hierarchy_order: nil) def ancestors(hierarchy_order: nil)
return super() unless use_traversal_ids? return super() unless use_traversal_ids?
return super() unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml) return super() unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml)

View File

@ -96,7 +96,6 @@ module Git
def track_ci_config_change_event def track_ci_config_change_event
return unless Gitlab::CurrentSettings.usage_ping_enabled? return unless Gitlab::CurrentSettings.usage_ping_enabled?
return unless ::Feature.enabled?(:usage_data_unique_users_committing_ciconfigfile, project, default_enabled: :yaml)
return unless default_branch? return unless default_branch?
commits_changing_ci_config.each do |commit| commits_changing_ci_config.each do |commit|

View File

@ -10,7 +10,7 @@ class SystemHooksService
end end
def execute_hooks(data, hooks_scope = :all) def execute_hooks(data, hooks_scope = :all)
SystemHook.hooks_for(hooks_scope).find_each do |hook| SystemHook.executable.hooks_for(hooks_scope).find_each do |hook|
hook.async_execute(data, 'system_hooks') hook.async_execute(data, 'system_hooks')
end end

View File

@ -6,6 +6,18 @@ class WebHookService
attr_reader :body, :headers, :code attr_reader :body, :headers, :code
def success?
false
end
def redirection?
false
end
def internal_server_error?
true
end
def initialize def initialize
@headers = Gitlab::HTTP::Response::Headers.new({}) @headers = Gitlab::HTTP::Response::Headers.new({})
@body = '' @body = ''
@ -33,6 +45,8 @@ class WebHookService
end end
def execute def execute
return { status: :error, message: 'Hook disabled' } unless hook.executable?
start_time = Gitlab::Metrics::System.monotonic_time start_time = Gitlab::Metrics::System.monotonic_time
response = if parsed_url.userinfo.blank? response = if parsed_url.userinfo.blank?
@ -104,6 +118,8 @@ class WebHookService
end end
def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil) def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
handle_failure(response, hook)
WebHookLog.create( WebHookLog.create(
web_hook: hook, web_hook: hook,
trigger: trigger, trigger: trigger,
@ -118,6 +134,17 @@ class WebHookService
) )
end end
def handle_failure(response, hook)
if response.success? || response.redirection?
hook.enable!
elsif response.internal_server_error?
next_backoff = hook.next_backoff
hook.update!(disabled_until: next_backoff.from_now, backoff_count: hook.backoff_count + 1)
else
hook.update!(recent_failures: hook.recent_failures + 1)
end
end
def build_headers(hook_name) def build_headers(hook_name)
@headers ||= begin @headers ||= begin
{ {

View File

@ -28,13 +28,13 @@
pre_installed_knative: @cluster.knative_pre_installed? ? 'true': 'false', pre_installed_knative: @cluster.knative_pre_installed? ? 'true': 'false',
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'), help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
helm_help_path: help_page_path('user/clusters/applications.md', anchor: 'helm'), helm_help_path: help_page_path('user/clusters/applications.md', anchor: 'helm'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-endpoint'), ingress_help_path: help_page_path('user/clusters/applications.md', anchor: 'determining-the-external-endpoint-automatically'),
ingress_dns_help_path: help_page_path('user/clusters/applications.md', anchor: 'pointing-your-dns-at-the-external-endpoint'), ingress_dns_help_path: help_page_path('user/clusters/applications.md', anchor: 'pointing-your-dns-at-the-external-endpoint'),
ingress_mod_security_help_path: help_page_path('user/clusters/applications.md', anchor: 'web-application-firewall-modsecurity'), ingress_mod_security_help_path: help_page_path('user/clusters/applications.md', anchor: 'web-application-firewall-modsecurity'),
environments_help_path: help_page_path('ci/environments/index.md', anchor: 'defining-environments'), environments_help_path: help_page_path('ci/environments/index.md', anchor: 'create-a-static-environment'),
clusters_help_path: help_page_path('user/project/clusters/index.md', anchor: 'deploying-to-a-kubernetes-cluster'), clusters_help_path: help_page_path('user/project/clusters/index.md', anchor: 'deploying-to-a-kubernetes-cluster'),
deploy_boards_help_path: help_page_path('user/project/deploy_boards.md', anchor: 'enabling-deploy-boards'), deploy_boards_help_path: help_page_path('user/project/deploy_boards.md', anchor: 'enabling-deploy-boards'),
cloud_run_help_path: help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'cloud-run-for-anthos'), cloud_run_help_path: help_page_path('user/project/clusters/add_gke_clusters.md', anchor: 'cloud-run-for-anthos'),
manage_prometheus_path: manage_prometheus_path, manage_prometheus_path: manage_prometheus_path,
cluster_id: @cluster.id, cluster_id: @cluster.id,
cilium_help_path: help_page_path('user/clusters/applications.md', anchor: 'install-cilium-using-gitlab-cicd')} } cilium_help_path: help_page_path('user/clusters/applications.md', anchor: 'install-cilium-using-gitlab-cicd')} }

View File

@ -12,5 +12,5 @@
user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERSION, user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
show_user_callout: show_feature_flags_new_version?.to_s, show_user_callout: show_feature_flags_new_version?.to_s,
strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'), strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
environments_scope_docs_path: help_page_path('ci/environments', anchor: 'scoping-environments-with-specs'), environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scoping-environments-with-specs'),
feature_flag_issues_endpoint: feature_flag_issues_links_endpoint(@project, @feature_flag, current_user) } } feature_flag_issues_endpoint: feature_flag_issues_links_endpoint(@project, @feature_flag, current_user) } }

View File

@ -10,5 +10,5 @@
user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERSION, user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
show_user_callout: show_feature_flags_new_version?.to_s, show_user_callout: show_feature_flags_new_version?.to_s,
strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'), strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
environments_scope_docs_path: help_page_path('ci/environments', anchor: 'scoping-environments-with-specs'), environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scoping-environments-with-specs'),
project_id: @project.id } } project_id: @project.id } }

View File

@ -12,7 +12,7 @@
"containers_error_image" => image_path('illustrations/docker-error-state.svg'), "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"repository_url" => escape_once(@project.container_registry_url), "repository_url" => escape_once(@project.container_registry_url),
"registry_host_url_with_port" => escape_once(registry_config.host_port), "registry_host_url_with_port" => escape_once(registry_config.host_port),
"expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'), "expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index.md', anchor: 'cleanup-policy'),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'), "garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'), "run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"project_path": @project.full_path, "project_path": @project.full_path,

View File

@ -18,7 +18,7 @@
%th= s_('AccessTokens|Created') %th= s_('AccessTokens|Created')
%th %th
= _('Last Used') = _('Last Used')
= link_to sprite_icon('question-o'), help_page_path('user/profile/personal_access_tokens.md', anchor: 'token-activity'), target: '_blank' = link_to sprite_icon('question-o'), help_page_path('user/profile/personal_access_tokens.md', anchor: 'view-the-last-time-a-token-was-used'), target: '_blank'
%th= _('Expires') %th= _('Expires')
%th= _('Scopes') %th= _('Scopes')
%th %th

View File

@ -0,0 +1,5 @@
---
title: Add missing status type and enum to package graphql type
merge_request: 61002
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Linear traversal query for Namespace#descendants
merge_request: 59632
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Disable web-hooks that fail repeatedly
merge_request: 60837
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add Ci::Build graphql mutations
merge_request: 60443
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Correct the 'blocked' scope in 'Member' class
merge_request: 61108
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Update UI links to docs
merge_request: 60247
author:
type: other

View File

@ -0,0 +1,8 @@
---
name: show_relevant_approval_rule_approvers
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329153
milestone: '13.12'
type: development
group: group::source code
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: web_hooks_disable_failed
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60837
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329849
milestone: '13.12'
type: development
group: group::ecosystem
default_enabled: false

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddFailureTrackingToWebHooks < ActiveRecord::Migration[6.0]
def change
change_table(:web_hooks, bulk: true) do |t|
t.integer :recent_failures, null: false, limit: 2, default: 0
t.integer :backoff_count, null: false, limit: 2, default: 0
t.column :disabled_until, :timestamptz
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddIndexOnWebHookProjectIdRecentFailures < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_web_hooks_on_project_id_recent_failures'
disable_ddl_transaction!
def up
add_concurrent_index(:web_hooks, [:project_id, :recent_failures], name: INDEX_NAME)
end
def down
remove_concurrent_index_by_name(:web_hooks, INDEX_NAME)
end
end

View File

@ -0,0 +1 @@
9674f04640f897928925ff1e23ff6d3ff918627b7c2374713a31071678956614

View File

@ -0,0 +1 @@
3cdf8e93c4b80867a5d8e086f3f44eaeb479e875abf16187b94b3f6238faf062

View File

@ -19111,7 +19111,10 @@ CREATE TABLE web_hooks (
releases_events boolean DEFAULT false NOT NULL, releases_events boolean DEFAULT false NOT NULL,
feature_flag_events boolean DEFAULT false NOT NULL, feature_flag_events boolean DEFAULT false NOT NULL,
member_events boolean DEFAULT false NOT NULL, member_events boolean DEFAULT false NOT NULL,
subgroup_events boolean DEFAULT false NOT NULL subgroup_events boolean DEFAULT false NOT NULL,
recent_failures smallint DEFAULT 0 NOT NULL,
backoff_count smallint DEFAULT 0 NOT NULL,
disabled_until timestamp with time zone
); );
CREATE SEQUENCE web_hooks_id_seq CREATE SEQUENCE web_hooks_id_seq
@ -24578,6 +24581,8 @@ CREATE INDEX index_web_hooks_on_group_id ON web_hooks USING btree (group_id) WHE
CREATE INDEX index_web_hooks_on_project_id ON web_hooks USING btree (project_id); CREATE INDEX index_web_hooks_on_project_id ON web_hooks USING btree (project_id);
CREATE INDEX index_web_hooks_on_project_id_recent_failures ON web_hooks USING btree (project_id, recent_failures);
CREATE INDEX index_web_hooks_on_service_id ON web_hooks USING btree (service_id); CREATE INDEX index_web_hooks_on_service_id ON web_hooks USING btree (service_id);
CREATE INDEX index_web_hooks_on_type ON web_hooks USING btree (type); CREATE INDEX index_web_hooks_on_type ON web_hooks USING btree (type);

View File

@ -22,12 +22,12 @@ invalidated.
Response codes: Response codes:
- `200`: Accepted - `200`: Accepted
- `4XX`: Not accepted - `4XX`: Rejected
- All other codes: accepted and logged - All other codes: accepted and logged
### Service Result ### Service Result
Pipelines not accepted by the external validation service aren't created or visible in pipeline lists, in either the GitLab user interface or API. Creating an unaccepted pipeline when using the GitLab user interface displays an error message that states: `Pipeline cannot be run. External validation failed` Pipelines rejected by the external validation service aren't created or visible in pipeline lists, in either the GitLab user interface or API. Creating an unaccepted pipeline when using the GitLab user interface displays an error message that states: `Pipeline cannot be run. External validation failed`
## Configuration ## Configuration

View File

@ -2604,6 +2604,44 @@ Input type: `JiraImportUsersInput`
| <a id="mutationjiraimportuserserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationjiraimportuserserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationjiraimportusersjirausers"></a>`jiraUsers` | [`[JiraUser!]`](#jirauser) | Users returned from Jira, matched by email and name if possible. | | <a id="mutationjiraimportusersjirausers"></a>`jiraUsers` | [`[JiraUser!]`](#jirauser) | Users returned from Jira, matched by email and name if possible. |
### `Mutation.jobPlay`
Input type: `JobPlayInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationjobplayclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationjobplayid"></a>`id` | [`CiBuildID!`](#cibuildid) | The ID of the job to mutate. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationjobplayclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationjobplayerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationjobplayjob"></a>`job` | [`CiJob`](#cijob) | The job after the mutation. |
### `Mutation.jobRetry`
Input type: `JobRetryInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationjobretryclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationjobretryid"></a>`id` | [`CiBuildID!`](#cibuildid) | The ID of the job to mutate. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationjobretryclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationjobretryerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationjobretryjob"></a>`job` | [`CiJob`](#cijob) | The job after the mutation. |
### `Mutation.labelCreate` ### `Mutation.labelCreate`
Input type: `LabelCreateInput` Input type: `LabelCreateInput`
@ -10365,6 +10403,7 @@ Represents a package in the Package Registry. Note that this type is in beta and
| <a id="packagepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. | | <a id="packagepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. |
| <a id="packagepipelines"></a>`pipelines` | [`PipelineConnection`](#pipelineconnection) | Pipelines that built the package. (see [Connections](#connections)) | | <a id="packagepipelines"></a>`pipelines` | [`PipelineConnection`](#pipelineconnection) | Pipelines that built the package. (see [Connections](#connections)) |
| <a id="packageproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. | | <a id="packageproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. |
| <a id="packagestatus"></a>`status` | [`PackageStatus!`](#packagestatus) | Package status. |
| <a id="packagetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) | | <a id="packagetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) |
| <a id="packageupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. | | <a id="packageupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
| <a id="packageversion"></a>`version` | [`String`](#string) | Version string. | | <a id="packageversion"></a>`version` | [`String`](#string) | Version string. |
@ -10399,6 +10438,7 @@ Represents a package details in the Package Registry. Note that this type is in
| <a id="packagedetailstypepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. | | <a id="packagedetailstypepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. |
| <a id="packagedetailstypepipelines"></a>`pipelines` | [`PipelineConnection`](#pipelineconnection) | Pipelines that built the package. (see [Connections](#connections)) | | <a id="packagedetailstypepipelines"></a>`pipelines` | [`PipelineConnection`](#pipelineconnection) | Pipelines that built the package. (see [Connections](#connections)) |
| <a id="packagedetailstypeproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. | | <a id="packagedetailstypeproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. |
| <a id="packagedetailstypestatus"></a>`status` | [`PackageStatus!`](#packagestatus) | Package status. |
| <a id="packagedetailstypetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) | | <a id="packagedetailstypetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) |
| <a id="packagedetailstypeupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. | | <a id="packagedetailstypeupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
| <a id="packagedetailstypeversion"></a>`version` | [`String`](#string) | Version string. | | <a id="packagedetailstypeversion"></a>`version` | [`String`](#string) | Version string. |
@ -14049,6 +14089,15 @@ Values for sorting package.
| <a id="packagesortversion_asc"></a>`VERSION_ASC` | Ordered by version in ascending order. | | <a id="packagesortversion_asc"></a>`VERSION_ASC` | Ordered by version in ascending order. |
| <a id="packagesortversion_desc"></a>`VERSION_DESC` | Ordered by version in descending order. | | <a id="packagesortversion_desc"></a>`VERSION_DESC` | Ordered by version in descending order. |
### `PackageStatus`
| Value | Description |
| ----- | ----------- |
| <a id="packagestatusdefault"></a>`DEFAULT` | Packages with a default status. |
| <a id="packagestatuserror"></a>`ERROR` | Packages with a error status. |
| <a id="packagestatushidden"></a>`HIDDEN` | Packages with a hidden status. |
| <a id="packagestatusprocessing"></a>`PROCESSING` | Packages with a processing status. |
### `PackageTypeEnum` ### `PackageTypeEnum`
| Value | Description | | Value | Description |
@ -14557,6 +14606,12 @@ An example `BoardsEpicListID` is: `"gid://gitlab/Boards::EpicList/1"`.
Represents `true` or `false` values. Represents `true` or `false` values.
### `CiBuildID`
A `CiBuildID` is a global ID. It is encoded as a string.
An example `CiBuildID` is: `"gid://gitlab/Ci::Build/1"`.
### `CiPipelineID` ### `CiPipelineID`
A `CiPipelineID` is a global ID. It is encoded as a string. A `CiPipelineID` is a global ID. It is encoded as a string.

View File

@ -216,6 +216,6 @@ This is due to a [n+1 calls limit being set for development setups](gitaly.md#to
Many of the tests also require a GitLab Personal Access Token. This is due to numerous endpoints themselves requiring authentication. Many of the tests also require a GitLab Personal Access Token. This is due to numerous endpoints themselves requiring authentication.
[The official GitLab docs detail how to create this token](../user/profile/personal_access_tokens.md#creating-a-personal-access-token). The tests require that the token is generated by an admin user and that it has the `API` and `read_repository` permissions. [The official GitLab docs detail how to create this token](../user/profile/personal_access_tokens.md#create-a-personal-access-token). The tests require that the token is generated by an admin user and that it has the `API` and `read_repository` permissions.
Details on how to use the Access Token with each type of test are found in their respective documentation. Details on how to use the Access Token with each type of test are found in their respective documentation.

View File

@ -8,112 +8,146 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Personal access tokens # Personal access tokens
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/3749) in GitLab 8.8. > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/3749) in GitLab 8.8.
> - [Notifications about expiring tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) added in GitLab 12.6. > - [Notifications for expiring tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) added in GitLab 12.6.
> - [Notifications about expired tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/214721) added in GitLab 13.3.
> - [Token lifetime limits](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) added in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.6. > - [Token lifetime limits](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) added in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.6.
> - [Additional notifications for expiring tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/214721) added in GitLab 13.3.
If you're unable to use [OAuth2](../../api/oauth2.md), you can use a personal access token to authenticate with the [GitLab API](../../api/README.md#personalproject-access-tokens). If you're unable to use [OAuth2](../../api/oauth2.md), you can use a personal access token to authenticate with the [GitLab API](../../api/README.md#personalproject-access-tokens). You can also use a personal access token with Git to authenticate over HTTP.
You can also use personal access tokens with Git to authenticate over HTTP. Personal access tokens are required when [Two-Factor Authentication (2FA)](account/two_factor_authentication.md) is enabled. In both cases, you can authenticate with a token in place of your password. In both cases, you authenticate with a personal access token in place of your password.
Personal access tokens expire on the date you define, at midnight UTC. Personal access tokens are required when [Two-Factor Authentication (2FA)](account/two_factor_authentication.md) is enabled.
- GitLab runs a check at 01:00 AM UTC every day to identify personal access tokens that expire in under seven days. The owners of these tokens are notified by email. For examples of how you can use a personal access token to authenticate with the API, see the [API documentation](../../api/README.md#personalproject-access-tokens).
- GitLab runs a check at 02:00 AM UTC every day to identify personal access tokens that expired on the current date. The owners of these tokens are notified by email.
- In GitLab Ultimate, administrators may [limit the lifetime of personal access tokens](../admin_area/settings/account_and_limit_settings.md#limiting-lifetime-of-personal-access-tokens).
- In GitLab Ultimate, administrators may [toggle enforcement of personal access token expiration](../admin_area/settings/account_and_limit_settings.md#optional-non-enforcement-of-personal-access-token-expiration).
For examples of how you can use a personal access token to authenticate with the API, see the following section from our [API Docs](../../api/README.md#personalproject-access-tokens). Alternately, GitLab administrators can use the API to create [impersonation tokens](../../api/README.md#impersonation-tokens).
Use impersonation tokens to automate authentication as a specific user.
GitLab also offers [impersonation tokens](../../api/README.md#impersonation-tokens) which are created by administrators via the API. They're a great fit for automated authentication as a specific user. ## Create a personal access token
## Creating a personal access token You can create as many personal access tokens as you like.
You can create as many personal access tokens as you like from your GitLab
profile.
1. Sign in to GitLab.
1. In the top-right corner, select your avatar. 1. In the top-right corner, select your avatar.
1. Select **Edit profile**. 1. Select **Edit profile**.
1. In the left sidebar, select **Access Tokens**. 1. In the left sidebar, select **Access Tokens**.
1. Choose a name and optional expiry date for the token. 1. Enter a name and optional expiry date for the token.
1. Choose the [desired scopes](#limiting-scopes-of-a-personal-access-token). 1. Select the [desired scopes](#personal-access-token-scopes).
1. Select **Create personal access token**. 1. Select **Create personal access token**.
1. Save the personal access token somewhere safe. If you navigate away or refresh
your page, and you did not save the token, you must create a new one.
### Revoking a personal access token Save the personal access token somewhere safe. After you leave the page,
you no longer have access to the token.
At any time, you can revoke any personal access token by clicking the ## Revoke a personal access token
respective **Revoke** button under the **Active Personal Access Token** area.
### Token activity At any time, you can revoke a personal access token.
You can see when a token was last used from the **Personal Access Tokens** page. Updates to the token usage is fixed at once per 24 hours. Requests to [API resources](../../api/api_resources.md) and the [GraphQL API](../../api/graphql/index.md) update a token's usage. 1. In the top-right corner, select your avatar.
1. Select **Edit profile**.
1. In the left sidebar, select **Access Tokens**.
1. In the **Active personal access tokens** area, next to the key, select **Revoke**.
## Limiting scopes of a personal access token ## View the last time a token was used
Personal access tokens can be created with one or more scopes that allow various Token usage is updated once every 24 hours. It is updated each time the token is used to request
actions that a given token can perform. The available scopes are depicted in [API resources](../../api/api_resources.md) and the [GraphQL API](../../api/graphql/index.md).
the following table.
| Scope | Introduced in | Description | To view the last time a token was used:
1. In the top-right corner, select your avatar.
1. Select **Edit profile**.
1. In the left sidebar, select **Access Tokens**.
1. In the **Active personal access tokens** area, next to the key, view the **Last Used** date.
## Personal access token scopes
A personal access token can perform actions based on the assigned scopes.
| Scope | Introduced in | Access |
| ------------------ | ------------- | ----------- | | ------------------ | ------------- | ----------- |
| `read_user` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5951) | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API](../../api/users.md) are allowed. | | `api` | [8.15](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5951) | Read-write for the complete API, including all groups and projects, the Container Registry, and the Package Registry. |
| `api` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5951) | Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. | | `read_user` | [8.15](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5951) | Read-only for endpoints under `/users`. Essentially, access to any of the `GET` requests in the [Users API](../../api/users.md). |
| `read_api` | [GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28944) | Grants read access to the API, including all groups and projects, the container registry, and the package registry. | | `read_api` | [12.10](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28944) | Read-only for the complete API, including all groups and projects, the Container Registry, and the Package Registry. |
| `read_registry` | [GitLab 9.3](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11845) | Allows to read (pull) [container registry](../packages/container_registry/index.md) images if a project is private and authorization is required. | | `read_repository` | [10.7](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17894) | Read-only (pull) for the repository through `git clone`. |
| `write_registry` | [GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28958) | Allows to write (push) [container registry](../packages/container_registry/index.md) images if a project is private and authorization is required. | | `write_repository` | [11.11](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/26021) | Read-write (pull, push) for the repository through `git clone`. Required for accessing Git repositories over HTTP when 2FA is enabled. |
| `sudo` | [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14838) | Allows performing API actions as any user in the system (if the authenticated user is an administrator). | | `read_registry` | [9.3](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11845) | Read-only (pull) for [Container Registry](../packages/container_registry/index.md) images if a project is private and authorization is required. |
| `read_repository` | [GitLab 10.7](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17894) | Allows read-only access (pull) to the repository through `git clone`. | | `write_registry` | [12.10](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28958) | Read-write (push) for [Container Registry](../packages/container_registry/index.md) images if a project is private and authorization is required. |
| `write_repository` | [GitLab 11.11](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/26021) | Allows read-write access (pull, push) to the repository through `git clone`. Required for accessing Git repositories over HTTP when 2FA is enabled. | | `sudo` | [10.2](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14838) | API actions as any user in the system (if the authenticated user is an administrator). |
## Programmatically creating a personal access token ## When personal access tokens expire
You can programmatically create a predetermined personal access token for use in Personal access tokens expire on the date you define, at midnight UTC.
automation or tests. You need sufficient access to run a
[Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session)
for your GitLab instance.
To create a token belonging to a user with username `automation-bot`, run the - GitLab runs a check at 01:00 AM UTC every day to identify personal access tokens that expire in the next seven days. The owners of these tokens are notified by email.
following in the Rails console (`sudo gitlab-rails console`): - GitLab runs a check at 02:00 AM UTC every day to identify personal access tokens that expire on the current date. The owners of these tokens are notified by email.
- In GitLab Ultimate, administrators can [limit the lifetime of personal access tokens](../admin_area/settings/account_and_limit_settings.md#limiting-lifetime-of-personal-access-tokens).
- In GitLab Ultimate, administrators can choose whether or not to [enforce personal access token expiration](../admin_area/settings/account_and_limit_settings.md#optional-non-enforcement-of-personal-access-token-expiration).
```ruby ## Create a personal access token programmatically **(FREE SELF)**
user = User.find_by_username('automation-bot')
token = user.personal_access_tokens.create(scopes: [:read_user, :read_repository], name: 'Automation token')
token.set_token('token-string-here123')
token.save!
```
This can be shortened into a single-line shell command using the You can create a predetermined personal access token
as part of your tests or automation.
Prerequisite:
- You need sufficient access to run a
[Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session)
for your GitLab instance.
To create a personal access token programmatically:
1. Open a Rails console:
```shell
sudo gitlab-rails console
```
1. Run the following commands to reference the username, the token, and the scopes.
The token must be 20 characters long. The scopes must be valid and are visible
[in the source code](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/auth.rb).
For example, to create a token that belongs to a user with username `automation-bot`:
```ruby
user = User.find_by_username('automation-bot')
token = user.personal_access_tokens.create(scopes: [:read_user, :read_repository], name: 'Automation token')
token.set_token('token-string-here123')
token.save!
```
This code can be shortened into a single-line shell command by using the
[Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner): [Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner):
```shell ```shell
sudo gitlab-rails runner "token = User.find_by_username('automation-bot').personal_access_tokens.create(scopes: [:read_user, :read_repository], name: 'Automation token'); token.set_token('token-string-here123'); token.save!" sudo gitlab-rails runner "token = User.find_by_username('automation-bot').personal_access_tokens.create(scopes: [:read_user, :read_repository], name: 'Automation token'); token.set_token('token-string-here123'); token.save!"
``` ```
NOTE: ## Revoke a personal access token programmatically **(FREE SELF)**
The token string must be 20 characters in length to be
recognized as a valid personal access token.
The list of valid scopes and what they do can be found You can programmatically revoke a personal access token
[in the source code](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/auth.rb). as part of your tests or automation.
## Programmatically revoking a personal access token Prerequisite:
You can programmatically revoke a personal access token. You need - You need sufficient access to run a [Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session)
sufficient access to run a [Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session) for your GitLab instance.
for your GitLab instance.
To revoke a known token `token-string-here123`, run the following in the Rails To revoke a token programmatically:
console (`sudo gitlab-rails console`):
```ruby 1. Open a Rails console:
token = PersonalAccessToken.find_by_token('token-string-here123')
token.revoke!
```
This can be shortened into a single-line shell command using the ```shell
sudo gitlab-rails console
```
1. To revoke a token of `token-string-here123`, run the following commands:
```ruby
token = PersonalAccessToken.find_by_token('token-string-here123')
token.revoke!
```
This code can be shortened into a single-line shell command using the
[Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner): [Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner):
```shell ```shell

View File

@ -53,7 +53,7 @@
"@gitlab/favicon-overlay": "2.0.0", "@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.192.0", "@gitlab/svgs": "1.192.0",
"@gitlab/tributejs": "1.0.0", "@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "29.16.0", "@gitlab/ui": "29.17.0",
"@gitlab/visual-review-tools": "1.6.1", "@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-4", "@rails/actioncable": "^6.0.3-4",
"@rails/ujs": "^6.0.3-4", "@rails/ujs": "^6.0.3-4",

View File

@ -12,7 +12,8 @@
"tags", "tags",
"pipelines", "pipelines",
"versions", "versions",
"metadata" "metadata",
"status"
], ],
"properties": { "properties": {
"id": { "id": {
@ -92,6 +93,10 @@
"edges": { "type": "array" }, "edges": { "type": "array" },
"nodes": { "type": "array" } "nodes": { "type": "array" }
} }
},
"status": {
"type": ["string"],
"enum": ["DEFAULT", "HIDDEN", "PROCESSING", "ERROR"]
} }
} }
} }

View File

@ -6,31 +6,27 @@ import { createStore } from '~/integrations/edit/store';
describe('ActiveCheckbox', () => { describe('ActiveCheckbox', () => {
let wrapper; let wrapper;
const createComponent = (customStateProps = {}, isInheriting = false) => { const createComponent = (customStateProps = {}, { isInheriting = false } = {}) => {
wrapper = mount(ActiveCheckbox, { wrapper = mount(ActiveCheckbox, {
store: createStore({ store: createStore({
customState: { ...customStateProps }, customState: { ...customStateProps },
override: !isInheriting,
defaultState: isInheriting ? {} : undefined,
}), }),
computed: {
isInheriting: () => isInheriting,
},
}); });
}; };
afterEach(() => { afterEach(() => {
if (wrapper) {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}
}); });
const findGlFormCheckbox = () => wrapper.find(GlFormCheckbox); const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findInputInCheckbox = () => findGlFormCheckbox().find('input'); const findInputInCheckbox = () => findGlFormCheckbox().find('input');
describe('template', () => { describe('template', () => {
describe('is inheriting adminSettings', () => { describe('is inheriting adminSettings', () => {
it('renders GlFormCheckbox as disabled', () => { it('renders GlFormCheckbox as disabled', () => {
createComponent({}, true); createComponent({}, { isInheriting: true });
expect(findGlFormCheckbox().exists()).toBe(true); expect(findGlFormCheckbox().exists()).toBe(true);
expect(findInputInCheckbox().attributes('disabled')).toBe('disabled'); expect(findInputInCheckbox().attributes('disabled')).toBe('disabled');

View File

@ -5,9 +5,10 @@ import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue'; import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
let wrapper; let wrapper;
const mockData = { const simpleMockData = {
name: 'some_file.js', name: 'some_file.js',
size: 123, size: 123,
rawSize: 123,
rawTextBlob: 'raw content', rawTextBlob: 'raw content',
type: 'text', type: 'text',
fileType: 'text', fileType: 'text',
@ -29,15 +30,26 @@ const mockData = {
fileType: 'text', fileType: 'text',
tooLarge: false, tooLarge: false,
type: 'simple', type: 'simple',
renderError: null,
}, },
richViewer: null, richViewer: null,
}; };
const richMockData = {
...simpleMockData,
richViewer: {
fileType: 'markup',
tooLarge: false,
type: 'rich',
renderError: null,
},
};
function factory(path, loading = false) { function factory({ props = {}, mockData = {} } = {}, loading = false) {
wrapper = shallowMount(BlobContentViewer, { wrapper = shallowMount(BlobContentViewer, {
propsData: { propsData: {
path, path: 'some_file.js',
projectPath: 'some/path', projectPath: 'some/path',
...props,
}, },
mocks: { mocks: {
$apollo: { $apollo: {
@ -50,7 +62,7 @@ function factory(path, loading = false) {
}, },
}); });
wrapper.setData({ blobInfo: mockData }); wrapper.setData(mockData);
} }
describe('Blob content viewer component', () => { describe('Blob content viewer component', () => {
@ -58,27 +70,29 @@ describe('Blob content viewer component', () => {
const findBlobHeader = () => wrapper.find(BlobHeader); const findBlobHeader = () => wrapper.find(BlobHeader);
const findBlobContent = () => wrapper.find(BlobContent); const findBlobContent = () => wrapper.find(BlobContent);
beforeEach(() => {
factory({ mockData: { blobInfo: simpleMockData } });
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
beforeEach(() => {
factory('some_file.js');
});
it('renders a GlLoadingIcon component', () => { it('renders a GlLoadingIcon component', () => {
factory('some_file.js', true); factory({ mockData: { blobInfo: simpleMockData } }, true);
expect(findLoadingIcon().exists()).toBe(true); expect(findLoadingIcon().exists()).toBe(true);
}); });
describe('simple viewer', () => {
it('renders a BlobHeader component', () => { it('renders a BlobHeader component', () => {
expect(findBlobHeader().exists()).toBe(true); expect(findBlobHeader().props('activeViewerType')).toEqual('simple');
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(true);
expect(findBlobHeader().props('blob')).toEqual(simpleMockData);
}); });
it('renders a BlobContent component', () => { it('renders a BlobContent component', () => {
expect(findBlobContent().exists()).toBe(true);
expect(findBlobContent().props('loading')).toEqual(false); expect(findBlobContent().props('loading')).toEqual(false);
expect(findBlobContent().props('content')).toEqual('raw content'); expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true); expect(findBlobContent().props('isRawContent')).toBe(true);
@ -86,6 +100,54 @@ describe('Blob content viewer component', () => {
fileType: 'text', fileType: 'text',
tooLarge: false, tooLarge: false,
type: 'simple', type: 'simple',
renderError: null,
});
});
});
describe('rich viewer', () => {
beforeEach(() => {
factory({
mockData: { blobInfo: richMockData, activeViewerType: 'rich' },
});
});
it('renders a BlobHeader component', () => {
expect(findBlobHeader().props('activeViewerType')).toEqual('rich');
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(false);
expect(findBlobHeader().props('blob')).toEqual(richMockData);
});
it('renders a BlobContent component', () => {
expect(findBlobContent().props('loading')).toEqual(false);
expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'markup',
tooLarge: false,
type: 'rich',
renderError: null,
});
});
it('updates viewer type when viewer changed is clicked', async () => {
expect(findBlobContent().props('activeViewer')).toEqual(
expect.objectContaining({
type: 'rich',
}),
);
expect(findBlobHeader().props('activeViewerType')).toEqual('rich');
findBlobHeader().vm.$emit('viewer-changed', 'simple');
await wrapper.vm.$nextTick();
expect(findBlobHeader().props('activeViewerType')).toEqual('simple');
expect(findBlobContent().props('activeViewer')).toEqual(
expect.objectContaining({
type: 'simple',
}),
);
}); });
}); });
}); });

View File

@ -11,7 +11,9 @@ describe('Repository blob page component', () => {
const path = 'file.js'; const path = 'file.js';
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(BlobPage, { propsData: { path, projectPath: 'some/path' } }); wrapper = shallowMount(BlobPage, {
propsData: { path, projectPath: 'some/path' },
});
}); });
afterEach(() => { afterEach(() => {

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageStatus'] do
it 'exposes all package statuses' do
expect(described_class.values.keys).to contain_exactly(*%w[DEFAULT HIDDEN PROCESSING ERROR])
end
end

View File

@ -9,6 +9,7 @@ RSpec.describe GitlabSchema.types['Package'] do
created_at updated_at created_at updated_at
project project
tags pipelines metadata versions tags pipelines metadata versions
status
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)

View File

@ -441,6 +441,10 @@ RSpec.describe Group do
it { expect(group.self_and_descendants.to_sql).not_to include 'traversal_ids @>' } it { expect(group.self_and_descendants.to_sql).not_to include 'traversal_ids @>' }
end end
describe '#descendants' do
it { expect(group.descendants.to_sql).not_to include 'traversal_ids @>' }
end
describe '#ancestors' do describe '#ancestors' do
it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' } it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
end end
@ -453,6 +457,10 @@ RSpec.describe Group do
it { expect(group.self_and_descendants.to_sql).to include 'traversal_ids @>' } it { expect(group.self_and_descendants.to_sql).to include 'traversal_ids @>' }
end end
describe '#descendants' do
it { expect(group.descendants.to_sql).to include 'traversal_ids @>' }
end
describe '#ancestors' do describe '#ancestors' do
it { expect(group.ancestors.to_sql).to include "\"namespaces\".\"id\" = #{group.parent_id}" } it { expect(group.ancestors.to_sql).to include "\"namespaces\".\"id\" = #{group.parent_id}" }

View File

@ -3,7 +3,15 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe WebHook do RSpec.describe WebHook do
let(:hook) { build(:project_hook) } include AfterNextHelpers
let_it_be(:project) { create(:project) }
let(:hook) { build(:project_hook, project: project) }
around do |example|
freeze_time { example.run }
end
describe 'associations' do describe 'associations' do
it { is_expected.to have_many(:web_hook_logs) } it { is_expected.to have_many(:web_hook_logs) }
@ -69,18 +77,30 @@ RSpec.describe WebHook do
let(:data) { { key: 'value' } } let(:data) { { key: 'value' } }
let(:hook_name) { 'project hook' } let(:hook_name) { 'project hook' }
before do it '#execute' do
expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original expect_next(WebHookService).to receive(:execute)
hook.execute(data, hook_name)
end end
it '#execute' do it 'does not execute non-executable hooks' do
expect_any_instance_of(WebHookService).to receive(:execute) hook.update!(disabled_until: 1.day.from_now)
expect(WebHookService).not_to receive(:new)
hook.execute(data, hook_name) hook.execute(data, hook_name)
end end
it '#async_execute' do it '#async_execute' do
expect_any_instance_of(WebHookService).to receive(:async_execute) expect_next(WebHookService).to receive(:async_execute)
hook.async_execute(data, hook_name)
end
it 'does not async execute non-executable hooks' do
hook.update!(disabled_until: 1.day.from_now)
expect(WebHookService).not_to receive(:new)
hook.async_execute(data, hook_name) hook.async_execute(data, hook_name)
end end
@ -94,4 +114,170 @@ RSpec.describe WebHook do
expect { web_hook.destroy! }.to change(web_hook.web_hook_logs, :count).by(-3) expect { web_hook.destroy! }.to change(web_hook.web_hook_logs, :count).by(-3)
end end
end end
describe '.executable' do
let(:not_executable) do
[
[0, Time.current],
[0, 1.minute.from_now],
[1, 1.minute.from_now],
[3, 1.minute.from_now],
[4, nil],
[4, 1.day.ago],
[4, 1.minute.from_now]
].map do |(recent_failures, disabled_until)|
create(:project_hook, project: project, recent_failures: recent_failures, disabled_until: disabled_until)
end
end
let(:executables) do
[
[0, nil],
[0, 1.day.ago],
[1, nil],
[1, 1.day.ago],
[3, nil],
[3, 1.day.ago]
].map do |(recent_failures, disabled_until)|
create(:project_hook, project: project, recent_failures: recent_failures, disabled_until: disabled_until)
end
end
it 'finds the correct set of project hooks' do
expect(described_class.where(project_id: project.id).executable).to match_array executables
end
context 'when the feature flag is not enabled' do
before do
stub_feature_flags(web_hooks_disable_failed: false)
end
it 'is the same as all' do
expect(described_class.where(project_id: project.id).executable).to match_array(executables + not_executable)
end
end
end
describe '#executable?' do
let(:web_hook) { create(:project_hook, project: project) }
where(:recent_failures, :not_until, :executable) do
[
[0, :not_set, true],
[0, :past, true],
[0, :future, false],
[0, :now, false],
[1, :not_set, true],
[1, :past, true],
[1, :future, false],
[3, :not_set, true],
[3, :past, true],
[3, :future, false],
[4, :not_set, false],
[4, :past, false],
[4, :future, false]
]
end
with_them do
# Phasing means we cannot put these values in the where block,
# which is not subject to the frozen time context.
let(:disabled_until) do
case not_until
when :not_set
nil
when :past
1.minute.ago
when :future
1.minute.from_now
when :now
Time.current
end
end
before do
web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until)
end
it 'has the correct state' do
expect(web_hook.executable?).to eq(executable)
end
context 'when the feature flag is enabled for a project' do
before do
stub_feature_flags(web_hooks_disable_failed: project)
end
it 'has the expected value' do
expect(web_hook.executable?).to eq(executable)
end
end
context 'when the feature flag is not enabled' do
before do
stub_feature_flags(web_hooks_disable_failed: false)
end
it 'is executable' do
expect(web_hook).to be_executable
end
end
end
end
describe '#next_backoff' do
context 'when there was no last backoff' do
before do
hook.backoff_count = 0
end
it 'is 10 minutes' do
expect(hook.next_backoff).to eq(described_class::INITIAL_BACKOFF)
end
end
context 'when we have backed off once' do
before do
hook.backoff_count = 1
end
it 'is twice the initial value' do
expect(hook.next_backoff).to eq(20.minutes)
end
end
context 'when we have backed off 3 times' do
before do
hook.backoff_count = 3
end
it 'grows exponentially' do
expect(hook.next_backoff).to eq(80.minutes)
end
end
context 'when the previous backoff was large' do
before do
hook.backoff_count = 8 # last value before MAX_BACKOFF
end
it 'does not exceed the max backoff value' do
expect(hook.next_backoff).to eq(described_class::MAX_BACKOFF)
end
end
end
describe '#enable!' do
it 'makes a hook executable' do
hook.recent_failures = 1000
expect { hook.enable! }.to change(hook, :executable?).from(false).to(true)
end
end
describe '#disable!' do
it 'disables a hook' do
expect { hook.disable! }.to change(hook, :executable?).from(true).to(false)
end
end
end end

View File

@ -143,16 +143,10 @@ RSpec.describe Member do
@blocked_maintainer = project.members.find_by(user_id: @blocked_maintainer_user.id, access_level: Gitlab::Access::MAINTAINER) @blocked_maintainer = project.members.find_by(user_id: @blocked_maintainer_user.id, access_level: Gitlab::Access::MAINTAINER)
@blocked_developer = project.members.find_by(user_id: @blocked_developer_user.id, access_level: Gitlab::Access::DEVELOPER) @blocked_developer = project.members.find_by(user_id: @blocked_developer_user.id, access_level: Gitlab::Access::DEVELOPER)
@invited_member = create(:project_member, :developer, @invited_member = create(:project_member, :invited, :developer, project: project)
project: project,
invite_token: '1234',
invite_email: 'toto1@example.com')
accepted_invite_user = build(:user, state: :active) accepted_invite_user = build(:user, state: :active)
@accepted_invite_member = create(:project_member, :developer, @accepted_invite_member = create(:project_member, :invited, :developer, project: project)
project: project,
invite_token: '1234',
invite_email: 'toto2@example.com')
.tap { |u| u.accept_invite!(accepted_invite_user) } .tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) } requested_user = create(:user).tap { |u| project.request_access(u) }
@ -325,12 +319,12 @@ RSpec.describe Member do
describe '.search_invite_email' do describe '.search_invite_email' do
it 'returns only members the matching e-mail' do it 'returns only members the matching e-mail' do
create(:group_member, :invited) invited_member = create(:group_member, :invited, invite_email: 'invited@example.com')
invited = described_class.search_invite_email(@invited_member.invite_email) invited = described_class.search_invite_email(invited_member.invite_email)
expect(invited.count).to eq(1) expect(invited.count).to eq(1)
expect(invited.first).to eq(@invited_member) expect(invited.first).to eq(invited_member)
expect(described_class.search_invite_email('bad-email@example.com').count).to eq(0) expect(described_class.search_invite_email('bad-email@example.com').count).to eq(0)
end end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'JobPlay' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
let_it_be(:job) { create(:ci_build, pipeline: pipeline, name: 'build') }
let(:mutation) do
variables = {
id: job.to_global_id.to_s
}
graphql_mutation(:job_play, variables,
<<-QL
errors
job {
id
}
QL
)
end
let(:mutation_response) { graphql_mutation_response(:job_play) }
before_all do
project.add_maintainer(user)
end
it 'returns an error if the user is not allowed to play the job' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty
end
it 'plays a job' do
job_id = ::Gitlab::GlobalId.build(job, id: job.id).to_s
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['job']['id']).to eq(job_id)
end
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'JobRetry' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
let_it_be(:job) { create(:ci_build, :success, pipeline: pipeline, name: 'build') }
let(:mutation) do
variables = {
id: job.to_global_id.to_s
}
graphql_mutation(:job_retry, variables,
<<-QL
errors
job {
id
}
QL
)
end
let(:mutation_response) { graphql_mutation_response(:job_retry) }
before_all do
project.add_maintainer(user)
end
it 'returns an error if the user is not allowed to retry the job' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty
end
it 'retries a job' do
job_id = ::Gitlab::GlobalId.build(job, id: job.id).to_s
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['job']['id']).to eq(job_id)
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Git::BranchHooksService do RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state do
include RepoHelpers include RepoHelpers
include ProjectForksHelper include ProjectForksHelper
@ -116,8 +116,6 @@ RSpec.describe Git::BranchHooksService do
allow_next_instance_of(Gitlab::Git::Diff) do |diff| allow_next_instance_of(Gitlab::Git::Diff) do |diff|
allow(diff).to receive(:new_path).and_return('.gitlab-ci.yml') allow(diff).to receive(:new_path).and_return('.gitlab-ci.yml')
end end
allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
end end
let!(:commit_author) { create(:user, email: sample_commit.author_email) } let!(:commit_author) { create(:user, email: sample_commit.author_email) }
@ -127,23 +125,11 @@ RSpec.describe Git::BranchHooksService do
end end
it 'tracks the event' do it 'tracks the event' do
time = Time.zone.now
execute_service execute_service
expect(Gitlab::UsageDataCounters::HLLRedisCounter) expect(Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'o_pipeline_authoring_unique_users_committing_ciconfigfile', start_date: time, end_date: time + 7.days)).to eq(1)
.to have_received(:track_event).with(*tracking_params)
end
context 'when the FF usage_data_unique_users_committing_ciconfigfile is disabled' do
before do
stub_feature_flags(usage_data_unique_users_committing_ciconfigfile: false)
end
it 'does not track the event' do
execute_service
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.not_to have_received(:track_event).with(*tracking_params)
end
end end
context 'when usage ping is disabled' do context 'when usage ping is disabled' do
@ -155,7 +141,7 @@ RSpec.describe Git::BranchHooksService do
execute_service execute_service
expect(Gitlab::UsageDataCounters::HLLRedisCounter) expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.not_to have_received(:track_event).with(*tracking_params) .not_to receive(:track_event).with(*tracking_params)
end end
end end
@ -166,7 +152,7 @@ RSpec.describe Git::BranchHooksService do
execute_service execute_service
expect(Gitlab::UsageDataCounters::HLLRedisCounter) expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.not_to have_received(:track_event).with(*tracking_params) .not_to receive(:track_event).with(*tracking_params)
end end
end end
@ -179,7 +165,7 @@ RSpec.describe Git::BranchHooksService do
execute_service execute_service
expect(Gitlab::UsageDataCounters::HLLRedisCounter) expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.not_to have_received(:track_event).with(*tracking_params) .not_to receive(:track_event).with(*tracking_params)
end end
end end
end end

View File

@ -21,6 +21,10 @@ RSpec.describe WebHookService do
let(:service_instance) { described_class.new(project_hook, data, :push_hooks) } let(:service_instance) { described_class.new(project_hook, data, :push_hooks) }
around do |example|
travel_to(Time.current) { example.run }
end
describe '#initialize' do describe '#initialize' do
before do before do
stub_application_setting(setting_name => setting) stub_application_setting(setting_name => setting)
@ -120,10 +124,21 @@ RSpec.describe WebHookService do
expect { service_instance.execute }.to raise_error(StandardError) expect { service_instance.execute }.to raise_error(StandardError)
end end
it 'does not execute disabled hooks' do
project_hook.update!(recent_failures: 4)
expect(service_instance.execute).to eq({ status: :error, message: 'Hook disabled' })
end
it 'handles exceptions' do it 'handles exceptions' do
exceptions = [SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep] exceptions = [
SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED,
Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout,
Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep
]
exceptions.each do |exception_class| exceptions.each do |exception_class|
exception = exception_class.new('Exception message') exception = exception_class.new('Exception message')
project_hook.enable!
stub_full_request(project_hook.url, method: :post).to_raise(exception) stub_full_request(project_hook.url, method: :post).to_raise(exception)
expect(service_instance.execute).to eq({ status: :error, message: exception.to_s }) expect(service_instance.execute).to eq({ status: :error, message: exception.to_s })
@ -166,10 +181,11 @@ RSpec.describe WebHookService do
context 'with success' do context 'with success' do
before do before do
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success') stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
service_instance.execute
end end
it 'log successful execution' do it 'log successful execution' do
service_instance.execute
expect(hook_log.trigger).to eq('push_hooks') expect(hook_log.trigger).to eq('push_hooks')
expect(hook_log.url).to eq(project_hook.url) expect(hook_log.url).to eq(project_hook.url)
expect(hook_log.request_headers).to eq(headers) expect(hook_log.request_headers).to eq(headers)
@ -178,15 +194,62 @@ RSpec.describe WebHookService do
expect(hook_log.execution_duration).to be > 0 expect(hook_log.execution_duration).to be > 0
expect(hook_log.internal_error_message).to be_nil expect(hook_log.internal_error_message).to be_nil
end end
it 'does not increment the failure count' do
expect { service_instance.execute }.not_to change(project_hook, :recent_failures)
end
it 'does not change the disabled_until attribute' do
expect { service_instance.execute }.not_to change(project_hook, :disabled_until)
end
context 'when the hook had previously failed' do
before do
project_hook.update!(recent_failures: 2)
end
it 'resets the failure count' do
expect { service_instance.execute }.to change(project_hook, :recent_failures).to(0)
end
end
end
context 'with bad request' do
before do
stub_full_request(project_hook.url, method: :post).to_return(status: 400, body: 'Bad request')
end
it 'logs failed execution' do
service_instance.execute
expect(hook_log).to have_attributes(
trigger: eq('push_hooks'),
url: eq(project_hook.url),
request_headers: eq(headers),
response_body: eq('Bad request'),
response_status: eq('400'),
execution_duration: be > 0,
internal_error_message: be_nil
)
end
it 'increments the failure count' do
expect { service_instance.execute }.to change(project_hook, :recent_failures).by(1)
end
it 'does not change the disabled_until attribute' do
expect { service_instance.execute }.not_to change(project_hook, :disabled_until)
end
end end
context 'with exception' do context 'with exception' do
before do before do
stub_full_request(project_hook.url, method: :post).to_raise(SocketError.new('Some HTTP Post error')) stub_full_request(project_hook.url, method: :post).to_raise(SocketError.new('Some HTTP Post error'))
service_instance.execute
end end
it 'log failed execution' do it 'log failed execution' do
service_instance.execute
expect(hook_log.trigger).to eq('push_hooks') expect(hook_log.trigger).to eq('push_hooks')
expect(hook_log.url).to eq(project_hook.url) expect(hook_log.url).to eq(project_hook.url)
expect(hook_log.request_headers).to eq(headers) expect(hook_log.request_headers).to eq(headers)
@ -195,6 +258,47 @@ RSpec.describe WebHookService do
expect(hook_log.execution_duration).to be > 0 expect(hook_log.execution_duration).to be > 0
expect(hook_log.internal_error_message).to eq('Some HTTP Post error') expect(hook_log.internal_error_message).to eq('Some HTTP Post error')
end end
it 'does not increment the failure count' do
expect { service_instance.execute }.not_to change(project_hook, :recent_failures)
end
it 'sets the disabled_until attribute' do
expect { service_instance.execute }
.to change(project_hook, :disabled_until).to(project_hook.next_backoff.from_now)
end
it 'increases the backoff count' do
expect { service_instance.execute }.to change(project_hook, :backoff_count).by(1)
end
context 'when the previous cool-off was near the maximum' do
before do
project_hook.update!(disabled_until: 5.minutes.ago, backoff_count: 8)
end
it 'sets the disabled_until attribute' do
expect { service_instance.execute }.to change(project_hook, :disabled_until).to(1.day.from_now)
end
it 'sets the last_backoff attribute' do
expect { service_instance.execute }.to change(project_hook, :backoff_count).by(1)
end
end
context 'when we have backed-off many many times' do
before do
project_hook.update!(disabled_until: 5.minutes.ago, backoff_count: 365)
end
it 'sets the disabled_until attribute' do
expect { service_instance.execute }.to change(project_hook, :disabled_until).to(1.day.from_now)
end
it 'sets the last_backoff attribute' do
expect { service_instance.execute }.to change(project_hook, :backoff_count).by(1)
end
end
end end
context 'with unsafe response body' do context 'with unsafe response body' do

View File

@ -907,10 +907,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8" resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw== integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
"@gitlab/ui@29.16.0": "@gitlab/ui@29.17.0":
version "29.16.0" version "29.17.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.16.0.tgz#80e16c6e046bae1c98774dddfdd6b829f299df5e" resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.17.0.tgz#405eb5448741f1b7ea17afd913d6e918d783cbce"
integrity sha512-Pq6Ycguq2AruJGfDQvJ8xy7J5Ldz8fgx5Z/Bq0Dq0fiCvuLiHDu0nQMjuFTGXMM/fDSFcBzPxE0+7LsNS37v5g== integrity sha512-BsHPEBD9wIq0LnewxgTmPYKDn0rE3b3aluN3hn9A7zVgnofifmA9c0Cn25u0ha3sOV13K1NNtDKlEgcwgOWgQQ==
dependencies: dependencies:
"@babel/standalone" "^7.0.0" "@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0" "@gitlab/vue-toasted" "^1.3.0"