Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
52dbfea964
commit
454973238c
|
@ -3,6 +3,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
|
|||
import { uniqueId } from 'lodash';
|
||||
import BlobContent from '~/blob/components/blob_content.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 { __ } from '~/locale';
|
||||
import blobInfoQuery from '../queries/blob_info.query.graphql';
|
||||
|
@ -22,6 +23,11 @@ export default {
|
|||
filePath: this.path,
|
||||
};
|
||||
},
|
||||
result() {
|
||||
this.switchViewer(
|
||||
this.hasRichViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
|
||||
);
|
||||
},
|
||||
error() {
|
||||
createFlash({ message: __('An error occurred while loading the file. Please try again.') });
|
||||
},
|
||||
|
@ -44,6 +50,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
activeViewerType: SIMPLE_BLOB_VIEWER,
|
||||
project: {
|
||||
repository: {
|
||||
blobs: {
|
||||
|
@ -69,7 +76,7 @@ export default {
|
|||
canModifyBlob: true,
|
||||
forkPath: '',
|
||||
simpleViewer: {},
|
||||
richViewer: {},
|
||||
richViewer: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -87,10 +94,19 @@ export default {
|
|||
return nodes[0] || {};
|
||||
},
|
||||
viewer() {
|
||||
const viewer = this.blobInfo.richViewer || this.blobInfo.simpleViewer;
|
||||
const { fileType, tooLarge, type } = viewer;
|
||||
|
||||
return { fileType, tooLarge, type };
|
||||
const { richViewer, simpleViewer } = this.blobInfo;
|
||||
return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
|
||||
},
|
||||
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>
|
||||
<div>
|
||||
<gl-loading-icon v-if="isLoading" />
|
||||
<div v-if="blobInfo && !isLoading">
|
||||
<blob-header :blob="blobInfo" />
|
||||
<div v-if="blobInfo && !isLoading" class="file-holder">
|
||||
<blob-header
|
||||
:blob="blobInfo"
|
||||
:hide-viewer-switcher="!hasRichViewer"
|
||||
:active-viewer-type="viewer.type"
|
||||
:has-render-error="hasRenderError"
|
||||
@viewer-changed="switchViewer"
|
||||
/>
|
||||
<blob-content
|
||||
:blob="blobInfo"
|
||||
:content="blobInfo.rawTextBlob"
|
||||
|
|
|
@ -6,6 +6,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
|
|||
nodes {
|
||||
webPath
|
||||
name
|
||||
size
|
||||
rawSize
|
||||
rawTextBlob
|
||||
fileType
|
||||
|
@ -18,11 +19,13 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
|
|||
fileType
|
||||
tooLarge
|
||||
type
|
||||
renderError
|
||||
}
|
||||
richViewer {
|
||||
fileType
|
||||
tooLarge
|
||||
type
|
||||
renderError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,10 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
|
||||
feature_category :source_code_management
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
def new
|
||||
commit unless @repository.empty?
|
||||
end
|
||||
|
|
|
@ -59,6 +59,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:mr_collapsed_approval_rules, @project)
|
||||
push_frontend_feature_flag(:show_relevant_approval_rule_approvers, @project, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -94,6 +94,8 @@ module Types
|
|||
mount_mutation Mutations::Ci::Pipeline::Destroy
|
||||
mount_mutation Mutations::Ci::Pipeline::Retry
|
||||
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::UserCallouts::Create
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -25,6 +25,7 @@ module Types
|
|||
field :versions, ::Types::Packages::PackageType.connection_type, null: true,
|
||||
description: 'The other versions of the package.',
|
||||
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
|
||||
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
|
||||
|
|
|
@ -29,11 +29,11 @@ module TriggerableHooks
|
|||
callable_scopes = triggers.keys + [:all]
|
||||
return none unless callable_scopes.include?(trigger)
|
||||
|
||||
public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend
|
||||
executable.public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
|
||||
def select_active(hooks_scope, data)
|
||||
select do |hook|
|
||||
executable.select do |hook|
|
||||
ActiveHookFilter.new(hook).matches?(hooks_scope, data)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,6 +29,10 @@ class ProjectHook < WebHook
|
|||
def pluralized_name
|
||||
_('Webhooks')
|
||||
end
|
||||
|
||||
def web_hooks_disable_failed?
|
||||
Feature.enabled?(:web_hooks_disable_failed, project)
|
||||
end
|
||||
end
|
||||
|
||||
ProjectHook.prepend_if_ee('EE::ProjectHook')
|
||||
|
|
|
@ -6,9 +6,7 @@ class ServiceHook < WebHook
|
|||
belongs_to :service
|
||||
validates :service, presence: true
|
||||
|
||||
# rubocop: disable CodeReuse/ServiceClass
|
||||
def execute(data, hook_name = 'service_hook')
|
||||
WebHookService.new(self, data, hook_name).execute
|
||||
super(data, hook_name)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ServiceClass
|
||||
end
|
||||
|
|
|
@ -3,6 +3,11 @@
|
|||
class WebHook < ApplicationRecord
|
||||
include Sortable
|
||||
|
||||
FAILURE_THRESHOLD = 3 # three strikes
|
||||
INITIAL_BACKOFF = 10.minutes
|
||||
MAX_BACKOFF = 1.day
|
||||
BACKOFF_GROWTH_FACTOR = 2.0
|
||||
|
||||
attr_encrypted :token,
|
||||
mode: :per_attribute_iv,
|
||||
algorithm: 'aes-256-gcm',
|
||||
|
@ -21,15 +26,27 @@ class WebHook < ApplicationRecord
|
|||
validates :token, format: { without: /\n/ }
|
||||
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
|
||||
def execute(data, hook_name)
|
||||
WebHookService.new(self, data, hook_name).execute
|
||||
WebHookService.new(self, data, hook_name).execute if executable?
|
||||
end
|
||||
# rubocop: enable CodeReuse/ServiceClass
|
||||
|
||||
# rubocop: disable CodeReuse/ServiceClass
|
||||
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
|
||||
# rubocop: enable CodeReuse/ServiceClass
|
||||
|
||||
|
@ -41,4 +58,26 @@ class WebHook < ApplicationRecord
|
|||
def help_path
|
||||
'user/project/integrations/webhooks'
|
||||
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
|
||||
|
|
|
@ -84,10 +84,9 @@ class Member < ApplicationRecord
|
|||
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_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_blocked)
|
||||
|
||||
left_join_users
|
||||
.where(user_ok)
|
||||
.where(user_is_blocked)
|
||||
.where.not(is_external_invite)
|
||||
.non_request
|
||||
.non_minimal_access
|
||||
.reorder(nil)
|
||||
|
|
|
@ -63,6 +63,12 @@ module Namespaces
|
|||
lineage(top: self)
|
||||
end
|
||||
|
||||
def descendants
|
||||
return super unless use_traversal_ids?
|
||||
|
||||
self_and_descendants.where.not(id: id)
|
||||
end
|
||||
|
||||
def ancestors(hierarchy_order: nil)
|
||||
return super() unless use_traversal_ids?
|
||||
return super() unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml)
|
||||
|
|
|
@ -96,7 +96,6 @@ module Git
|
|||
|
||||
def track_ci_config_change_event
|
||||
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?
|
||||
|
||||
commits_changing_ci_config.each do |commit|
|
||||
|
|
|
@ -10,7 +10,7 @@ class SystemHooksService
|
|||
end
|
||||
|
||||
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')
|
||||
end
|
||||
|
||||
|
|
|
@ -6,6 +6,18 @@ class WebHookService
|
|||
|
||||
attr_reader :body, :headers, :code
|
||||
|
||||
def success?
|
||||
false
|
||||
end
|
||||
|
||||
def redirection?
|
||||
false
|
||||
end
|
||||
|
||||
def internal_server_error?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize
|
||||
@headers = Gitlab::HTTP::Response::Headers.new({})
|
||||
@body = ''
|
||||
|
@ -33,6 +45,8 @@ class WebHookService
|
|||
end
|
||||
|
||||
def execute
|
||||
return { status: :error, message: 'Hook disabled' } unless hook.executable?
|
||||
|
||||
start_time = Gitlab::Metrics::System.monotonic_time
|
||||
|
||||
response = if parsed_url.userinfo.blank?
|
||||
|
@ -104,6 +118,8 @@ class WebHookService
|
|||
end
|
||||
|
||||
def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
|
||||
handle_failure(response, hook)
|
||||
|
||||
WebHookLog.create(
|
||||
web_hook: hook,
|
||||
trigger: trigger,
|
||||
|
@ -118,6 +134,17 @@ class WebHookService
|
|||
)
|
||||
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)
|
||||
@headers ||= begin
|
||||
{
|
||||
|
|
|
@ -28,13 +28,13 @@
|
|||
pre_installed_knative: @cluster.knative_pre_installed? ? 'true': 'false',
|
||||
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'),
|
||||
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_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'),
|
||||
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,
|
||||
cluster_id: @cluster.id,
|
||||
cilium_help_path: help_page_path('user/clusters/applications.md', anchor: 'install-cilium-using-gitlab-cicd')} }
|
||||
|
|
|
@ -12,5 +12,5 @@
|
|||
user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
|
||||
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'),
|
||||
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) } }
|
||||
|
|
|
@ -10,5 +10,5 @@
|
|||
user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
|
||||
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'),
|
||||
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 } }
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
|
||||
"repository_url" => escape_once(@project.container_registry_url),
|
||||
"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'),
|
||||
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
|
||||
"project_path": @project.full_path,
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
%th= s_('AccessTokens|Created')
|
||||
%th
|
||||
= _('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= _('Scopes')
|
||||
%th
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add missing status type and enum to package graphql type
|
||||
merge_request: 61002
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Linear traversal query for Namespace#descendants
|
||||
merge_request: 59632
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Disable web-hooks that fail repeatedly
|
||||
merge_request: 60837
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Ci::Build graphql mutations
|
||||
merge_request: 60443
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Correct the 'blocked' scope in 'Member' class
|
||||
merge_request: 61108
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update UI links to docs
|
||||
merge_request: 60247
|
||||
author:
|
||||
type: other
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
9674f04640f897928925ff1e23ff6d3ff918627b7c2374713a31071678956614
|
|
@ -0,0 +1 @@
|
|||
3cdf8e93c4b80867a5d8e086f3f44eaeb479e875abf16187b94b3f6238faf062
|
|
@ -19111,7 +19111,10 @@ CREATE TABLE web_hooks (
|
|||
releases_events boolean DEFAULT false NOT NULL,
|
||||
feature_flag_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
|
||||
|
@ -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_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_type ON web_hooks USING btree (type);
|
||||
|
|
|
@ -22,12 +22,12 @@ invalidated.
|
|||
Response codes:
|
||||
|
||||
- `200`: Accepted
|
||||
- `4XX`: Not accepted
|
||||
- `4XX`: Rejected
|
||||
- All other codes: accepted and logged
|
||||
|
||||
### 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
|
||||
|
||||
|
|
|
@ -2604,6 +2604,44 @@ Input type: `JiraImportUsersInput`
|
|||
| <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. |
|
||||
|
||||
### `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`
|
||||
|
||||
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="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="packagestatus"></a>`status` | [`PackageStatus!`](#packagestatus) | Package status. |
|
||||
| <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="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="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="packagedetailstypestatus"></a>`status` | [`PackageStatus!`](#packagestatus) | Package status. |
|
||||
| <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="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_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`
|
||||
|
||||
| Value | Description |
|
||||
|
@ -14557,6 +14606,12 @@ An example `BoardsEpicListID` is: `"gid://gitlab/Boards::EpicList/1"`.
|
|||
|
||||
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`
|
||||
|
||||
A `CiPipelineID` is a global ID. It is encoded as a string.
|
||||
|
|
|
@ -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.
|
||||
|
||||
[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.
|
||||
|
|
|
@ -8,112 +8,146 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
# Personal access tokens
|
||||
|
||||
> - [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 about expired tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/214721) added in GitLab 13.3.
|
||||
> - [Notifications for expiring tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) added in GitLab 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.
|
||||
- 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 [API documentation](../../api/README.md#personalproject-access-tokens).
|
||||
|
||||
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. Select **Edit profile**.
|
||||
1. In the left sidebar, select **Access Tokens**.
|
||||
1. Choose a name and optional expiry date for the token.
|
||||
1. Choose the [desired scopes](#limiting-scopes-of-a-personal-access-token).
|
||||
1. Enter a name and optional expiry date for the token.
|
||||
1. Select the [desired scopes](#personal-access-token-scopes).
|
||||
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
|
||||
respective **Revoke** button under the **Active Personal Access Token** area.
|
||||
## Revoke a personal access token
|
||||
|
||||
### 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
|
||||
actions that a given token can perform. The available scopes are depicted in
|
||||
the following table.
|
||||
Token usage is updated once every 24 hours. It is updated each time the token is used to request
|
||||
[API resources](../../api/api_resources.md) and the [GraphQL API](../../api/graphql/index.md).
|
||||
|
||||
| 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` | [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_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_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. |
|
||||
| `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. |
|
||||
| `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_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_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. |
|
||||
| `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. |
|
||||
| `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` | [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_repository` | [10.7](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17894) | Read-only (pull) for the repository through `git clone`. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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
|
||||
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.
|
||||
Personal access tokens expire on the date you define, at midnight UTC.
|
||||
|
||||
To create a token belonging to a user with username `automation-bot`, run the
|
||||
following in the Rails console (`sudo gitlab-rails console`):
|
||||
- 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.
|
||||
- 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
|
||||
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!
|
||||
```
|
||||
## Create a personal access token programmatically **(FREE SELF)**
|
||||
|
||||
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):
|
||||
|
||||
```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!"
|
||||
```
|
||||
|
||||
NOTE:
|
||||
The token string must be 20 characters in length to be
|
||||
recognized as a valid personal access token.
|
||||
## Revoke a personal access token programmatically **(FREE SELF)**
|
||||
|
||||
The list of valid scopes and what they do can be found
|
||||
[in the source code](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/auth.rb).
|
||||
You can programmatically revoke a personal access token
|
||||
as part of your tests or automation.
|
||||
|
||||
## Programmatically revoking a personal access token
|
||||
Prerequisite:
|
||||
|
||||
You can programmatically revoke a personal access token. You need
|
||||
sufficient access to run a [Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session)
|
||||
for your GitLab instance.
|
||||
- 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 revoke a known token `token-string-here123`, run the following in the Rails
|
||||
console (`sudo gitlab-rails console`):
|
||||
To revoke a token programmatically:
|
||||
|
||||
```ruby
|
||||
token = PersonalAccessToken.find_by_token('token-string-here123')
|
||||
token.revoke!
|
||||
```
|
||||
1. Open a Rails console:
|
||||
|
||||
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):
|
||||
|
||||
```shell
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "1.192.0",
|
||||
"@gitlab/tributejs": "1.0.0",
|
||||
"@gitlab/ui": "29.16.0",
|
||||
"@gitlab/ui": "29.17.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "^6.0.3-4",
|
||||
"@rails/ujs": "^6.0.3-4",
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
"tags",
|
||||
"pipelines",
|
||||
"versions",
|
||||
"metadata"
|
||||
"metadata",
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
|
@ -92,6 +93,10 @@
|
|||
"edges": { "type": "array" },
|
||||
"nodes": { "type": "array" }
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": ["string"],
|
||||
"enum": ["DEFAULT", "HIDDEN", "PROCESSING", "ERROR"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,31 +6,27 @@ import { createStore } from '~/integrations/edit/store';
|
|||
describe('ActiveCheckbox', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (customStateProps = {}, isInheriting = false) => {
|
||||
const createComponent = (customStateProps = {}, { isInheriting = false } = {}) => {
|
||||
wrapper = mount(ActiveCheckbox, {
|
||||
store: createStore({
|
||||
customState: { ...customStateProps },
|
||||
override: !isInheriting,
|
||||
defaultState: isInheriting ? {} : undefined,
|
||||
}),
|
||||
computed: {
|
||||
isInheriting: () => isInheriting,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
}
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findGlFormCheckbox = () => wrapper.find(GlFormCheckbox);
|
||||
const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);
|
||||
const findInputInCheckbox = () => findGlFormCheckbox().find('input');
|
||||
|
||||
describe('template', () => {
|
||||
describe('is inheriting adminSettings', () => {
|
||||
it('renders GlFormCheckbox as disabled', () => {
|
||||
createComponent({}, true);
|
||||
createComponent({}, { isInheriting: true });
|
||||
|
||||
expect(findGlFormCheckbox().exists()).toBe(true);
|
||||
expect(findInputInCheckbox().attributes('disabled')).toBe('disabled');
|
||||
|
|
|
@ -5,9 +5,10 @@ import BlobHeader from '~/blob/components/blob_header.vue';
|
|||
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
|
||||
|
||||
let wrapper;
|
||||
const mockData = {
|
||||
const simpleMockData = {
|
||||
name: 'some_file.js',
|
||||
size: 123,
|
||||
rawSize: 123,
|
||||
rawTextBlob: 'raw content',
|
||||
type: 'text',
|
||||
fileType: 'text',
|
||||
|
@ -29,15 +30,26 @@ const mockData = {
|
|||
fileType: 'text',
|
||||
tooLarge: false,
|
||||
type: 'simple',
|
||||
renderError: 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, {
|
||||
propsData: {
|
||||
path,
|
||||
path: 'some_file.js',
|
||||
projectPath: 'some/path',
|
||||
...props,
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
|
@ -50,7 +62,7 @@ function factory(path, loading = false) {
|
|||
},
|
||||
});
|
||||
|
||||
wrapper.setData({ blobInfo: mockData });
|
||||
wrapper.setData(mockData);
|
||||
}
|
||||
|
||||
describe('Blob content viewer component', () => {
|
||||
|
@ -58,34 +70,84 @@ describe('Blob content viewer component', () => {
|
|||
const findBlobHeader = () => wrapper.find(BlobHeader);
|
||||
const findBlobContent = () => wrapper.find(BlobContent);
|
||||
|
||||
beforeEach(() => {
|
||||
factory({ mockData: { blobInfo: simpleMockData } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
factory('some_file.js');
|
||||
});
|
||||
|
||||
it('renders a GlLoadingIcon component', () => {
|
||||
factory('some_file.js', true);
|
||||
factory({ mockData: { blobInfo: simpleMockData } }, true);
|
||||
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders a BlobHeader component', () => {
|
||||
expect(findBlobHeader().exists()).toBe(true);
|
||||
describe('simple viewer', () => {
|
||||
it('renders a BlobHeader component', () => {
|
||||
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', () => {
|
||||
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: 'text',
|
||||
tooLarge: false,
|
||||
type: 'simple',
|
||||
renderError: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a BlobContent component', () => {
|
||||
expect(findBlobContent().exists()).toBe(true);
|
||||
describe('rich viewer', () => {
|
||||
beforeEach(() => {
|
||||
factory({
|
||||
mockData: { blobInfo: richMockData, activeViewerType: 'rich' },
|
||||
});
|
||||
});
|
||||
|
||||
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: 'text',
|
||||
tooLarge: false,
|
||||
type: 'simple',
|
||||
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',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,9 @@ describe('Repository blob page component', () => {
|
|||
const path = 'file.js';
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(BlobPage, { propsData: { path, projectPath: 'some/path' } });
|
||||
wrapper = shallowMount(BlobPage, {
|
||||
propsData: { path, projectPath: 'some/path' },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -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
|
|
@ -9,6 +9,7 @@ RSpec.describe GitlabSchema.types['Package'] do
|
|||
created_at updated_at
|
||||
project
|
||||
tags pipelines metadata versions
|
||||
status
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
|
|
|
@ -441,6 +441,10 @@ RSpec.describe Group do
|
|||
it { expect(group.self_and_descendants.to_sql).not_to include 'traversal_ids @>' }
|
||||
end
|
||||
|
||||
describe '#descendants' do
|
||||
it { expect(group.descendants.to_sql).not_to include 'traversal_ids @>' }
|
||||
end
|
||||
|
||||
describe '#ancestors' do
|
||||
it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
|
||||
end
|
||||
|
@ -453,6 +457,10 @@ RSpec.describe Group do
|
|||
it { expect(group.self_and_descendants.to_sql).to include 'traversal_ids @>' }
|
||||
end
|
||||
|
||||
describe '#descendants' do
|
||||
it { expect(group.descendants.to_sql).to include 'traversal_ids @>' }
|
||||
end
|
||||
|
||||
describe '#ancestors' do
|
||||
it { expect(group.ancestors.to_sql).to include "\"namespaces\".\"id\" = #{group.parent_id}" }
|
||||
|
||||
|
|
|
@ -3,7 +3,15 @@
|
|||
require 'spec_helper'
|
||||
|
||||
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
|
||||
it { is_expected.to have_many(:web_hook_logs) }
|
||||
|
@ -69,18 +77,30 @@ RSpec.describe WebHook do
|
|||
let(:data) { { key: 'value' } }
|
||||
let(:hook_name) { 'project hook' }
|
||||
|
||||
before do
|
||||
expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original
|
||||
it '#execute' do
|
||||
expect_next(WebHookService).to receive(:execute)
|
||||
|
||||
hook.execute(data, hook_name)
|
||||
end
|
||||
|
||||
it '#execute' do
|
||||
expect_any_instance_of(WebHookService).to receive(:execute)
|
||||
it 'does not execute non-executable hooks' do
|
||||
hook.update!(disabled_until: 1.day.from_now)
|
||||
|
||||
expect(WebHookService).not_to receive(:new)
|
||||
|
||||
hook.execute(data, hook_name)
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
@ -94,4 +114,170 @@ RSpec.describe WebHook do
|
|||
expect { web_hook.destroy! }.to change(web_hook.web_hook_logs, :count).by(-3)
|
||||
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
|
||||
|
|
|
@ -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_developer = project.members.find_by(user_id: @blocked_developer_user.id, access_level: Gitlab::Access::DEVELOPER)
|
||||
|
||||
@invited_member = create(:project_member, :developer,
|
||||
project: project,
|
||||
invite_token: '1234',
|
||||
invite_email: 'toto1@example.com')
|
||||
@invited_member = create(:project_member, :invited, :developer, project: project)
|
||||
|
||||
accepted_invite_user = build(:user, state: :active)
|
||||
@accepted_invite_member = create(:project_member, :developer,
|
||||
project: project,
|
||||
invite_token: '1234',
|
||||
invite_email: 'toto2@example.com')
|
||||
@accepted_invite_member = create(:project_member, :invited, :developer, project: project)
|
||||
.tap { |u| u.accept_invite!(accepted_invite_user) }
|
||||
|
||||
requested_user = create(:user).tap { |u| project.request_access(u) }
|
||||
|
@ -325,12 +319,12 @@ RSpec.describe Member do
|
|||
|
||||
describe '.search_invite_email' 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.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)
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Git::BranchHooksService do
|
||||
RSpec.describe Git::BranchHooksService, :clean_gitlab_redis_shared_state do
|
||||
include RepoHelpers
|
||||
include ProjectForksHelper
|
||||
|
||||
|
@ -116,8 +116,6 @@ RSpec.describe Git::BranchHooksService do
|
|||
allow_next_instance_of(Gitlab::Git::Diff) do |diff|
|
||||
allow(diff).to receive(:new_path).and_return('.gitlab-ci.yml')
|
||||
end
|
||||
|
||||
allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
|
||||
end
|
||||
|
||||
let!(:commit_author) { create(:user, email: sample_commit.author_email) }
|
||||
|
@ -127,23 +125,11 @@ RSpec.describe Git::BranchHooksService do
|
|||
end
|
||||
|
||||
it 'tracks the event' do
|
||||
time = Time.zone.now
|
||||
|
||||
execute_service
|
||||
|
||||
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
|
||||
.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
|
||||
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)
|
||||
end
|
||||
|
||||
context 'when usage ping is disabled' do
|
||||
|
@ -155,7 +141,7 @@ RSpec.describe Git::BranchHooksService do
|
|||
execute_service
|
||||
|
||||
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
|
||||
.not_to have_received(:track_event).with(*tracking_params)
|
||||
.not_to receive(:track_event).with(*tracking_params)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -166,7 +152,7 @@ RSpec.describe Git::BranchHooksService do
|
|||
execute_service
|
||||
|
||||
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
|
||||
.not_to have_received(:track_event).with(*tracking_params)
|
||||
.not_to receive(:track_event).with(*tracking_params)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -179,7 +165,7 @@ RSpec.describe Git::BranchHooksService do
|
|||
execute_service
|
||||
|
||||
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
|
||||
.not_to have_received(:track_event).with(*tracking_params)
|
||||
.not_to receive(:track_event).with(*tracking_params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,6 +21,10 @@ RSpec.describe WebHookService do
|
|||
|
||||
let(:service_instance) { described_class.new(project_hook, data, :push_hooks) }
|
||||
|
||||
around do |example|
|
||||
travel_to(Time.current) { example.run }
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
before do
|
||||
stub_application_setting(setting_name => setting)
|
||||
|
@ -120,10 +124,21 @@ RSpec.describe WebHookService do
|
|||
expect { service_instance.execute }.to raise_error(StandardError)
|
||||
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
|
||||
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|
|
||||
exception = exception_class.new('Exception message')
|
||||
project_hook.enable!
|
||||
|
||||
stub_full_request(project_hook.url, method: :post).to_raise(exception)
|
||||
expect(service_instance.execute).to eq({ status: :error, message: exception.to_s })
|
||||
|
@ -166,10 +181,11 @@ RSpec.describe WebHookService do
|
|||
context 'with success' do
|
||||
before do
|
||||
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
|
||||
service_instance.execute
|
||||
end
|
||||
|
||||
it 'log successful execution' do
|
||||
service_instance.execute
|
||||
|
||||
expect(hook_log.trigger).to eq('push_hooks')
|
||||
expect(hook_log.url).to eq(project_hook.url)
|
||||
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.internal_error_message).to be_nil
|
||||
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
|
||||
|
||||
context 'with exception' do
|
||||
before do
|
||||
stub_full_request(project_hook.url, method: :post).to_raise(SocketError.new('Some HTTP Post error'))
|
||||
service_instance.execute
|
||||
end
|
||||
|
||||
it 'log failed execution' do
|
||||
service_instance.execute
|
||||
|
||||
expect(hook_log.trigger).to eq('push_hooks')
|
||||
expect(hook_log.url).to eq(project_hook.url)
|
||||
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.internal_error_message).to eq('Some HTTP Post error')
|
||||
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
|
||||
|
||||
context 'with unsafe response body' do
|
||||
|
|
|
@ -907,10 +907,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
|
||||
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
|
||||
|
||||
"@gitlab/ui@29.16.0":
|
||||
version "29.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.16.0.tgz#80e16c6e046bae1c98774dddfdd6b829f299df5e"
|
||||
integrity sha512-Pq6Ycguq2AruJGfDQvJ8xy7J5Ldz8fgx5Z/Bq0Dq0fiCvuLiHDu0nQMjuFTGXMM/fDSFcBzPxE0+7LsNS37v5g==
|
||||
"@gitlab/ui@29.17.0":
|
||||
version "29.17.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.17.0.tgz#405eb5448741f1b7ea17afd913d6e918d783cbce"
|
||||
integrity sha512-BsHPEBD9wIq0LnewxgTmPYKDn0rE3b3aluN3hn9A7zVgnofifmA9c0Cn25u0ha3sOV13K1NNtDKlEgcwgOWgQQ==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
"@gitlab/vue-toasted" "^1.3.0"
|
||||
|
|
Loading…
Reference in New Issue