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 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"

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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]

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::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

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,
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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

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))
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)

View File

@ -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)

View File

@ -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|

View File

@ -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

View File

@ -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
{

View File

@ -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')} }

View File

@ -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) } }

View File

@ -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 } }

View File

@ -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,

View File

@ -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

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,
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);

View File

@ -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

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="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.

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.
[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.

View File

@ -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

View File

@ -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",

View File

@ -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"]
}
}
}

View File

@ -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');

View File

@ -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',
}),
);
});
});
});

View File

@ -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(() => {

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
project
tags pipelines metadata versions
status
]
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 @>' }
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}" }

View File

@ -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

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_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

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'
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

View File

@ -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

View File

@ -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"