diff options
56 files changed, 993 insertions, 180 deletions
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index cef5ae41106..0ab032b25d3 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -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" diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql index d0c3f8d8d54..07c076af54b 100644 --- a/app/assets/javascripts/repository/queries/blob_info.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql @@ -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 } } } diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 65b7a78cb58..cbee33853bb 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -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 diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 94fd6d86abc..58a05d498c7 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -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] diff --git a/app/graphql/mutations/ci/job/base.rb b/app/graphql/mutations/ci/job/base.rb new file mode 100644 index 00000000000..3359def159a --- /dev/null +++ b/app/graphql/mutations/ci/job/base.rb @@ -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 diff --git a/app/graphql/mutations/ci/job/play.rb b/app/graphql/mutations/ci/job/play.rb new file mode 100644 index 00000000000..f87904f8b25 --- /dev/null +++ b/app/graphql/mutations/ci/job/play.rb @@ -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 diff --git a/app/graphql/mutations/ci/job/retry.rb b/app/graphql/mutations/ci/job/retry.rb new file mode 100644 index 00000000000..a61d5dddb40 --- /dev/null +++ b/app/graphql/mutations/ci/job/retry.rb @@ -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 diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index a820ef94719..053dfef58f9 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -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 diff --git a/app/graphql/types/packages/package_status_enum.rb b/app/graphql/types/packages/package_status_enum.rb new file mode 100644 index 00000000000..2e6ea5d0a50 --- /dev/null +++ b/app/graphql/types/packages/package_status_enum.rb @@ -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 diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb index 9ea4c4b74ea..659af4a7057 100644 --- a/app/graphql/types/packages/package_type.rb +++ b/app/graphql/types/packages/package_type.rb @@ -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 diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb index db5df6c2c9f..8fe34632430 100644 --- a/app/models/concerns/triggerable_hooks.rb +++ b/app/models/concerns/triggerable_hooks.rb @@ -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 diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index b625a70b444..83858ee0c77 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -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') diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index 4caa45a13d4..b35542d5b93 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -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 diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index dbd5a1b032a..44538134318 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -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 diff --git a/app/models/member.rb b/app/models/member.rb index ee9b2c8cef3..a44b7f0ff7e 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -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) diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb index 3ebf0894b98..be43b2e04f7 100644 --- a/app/models/namespaces/traversal/linear.rb +++ b/app/models/namespaces/traversal/linear.rb @@ -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) diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index 825faf59c13..4b802803a07 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -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| diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index d0099283afa..2a2053cb912 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -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 diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index 5a51b42f9f9..0535bc625ac 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -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 { diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index 01ba7c06154..001ca80dbd6 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -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')} } diff --git a/app/views/projects/feature_flags/edit.html.haml b/app/views/projects/feature_flags/edit.html.haml index 028595aba0b..1549f5cf6d6 100644 --- a/app/views/projects/feature_flags/edit.html.haml +++ b/app/views/projects/feature_flags/edit.html.haml @@ -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) } } diff --git a/app/views/projects/feature_flags/new.html.haml b/app/views/projects/feature_flags/new.html.haml index 3bad1d9773c..bc52f52ecf7 100644 --- a/app/views/projects/feature_flags/new.html.haml +++ b/app/views/projects/feature_flags/new.html.haml @@ -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 } } diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index bbef5150a62..f56fd7f557d 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -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, diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml index 2a2a1a911af..9c59d5ae1fa 100644 --- a/app/views/shared/access_tokens/_table.html.haml +++ b/app/views/shared/access_tokens/_table.html.haml @@ -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 diff --git a/changelogs/unreleased/285467-package-registry-graphql-api-4.yml b/changelogs/unreleased/285467-package-registry-graphql-api-4.yml new file mode 100644 index 00000000000..6b239152b10 --- /dev/null +++ b/changelogs/unreleased/285467-package-registry-graphql-api-4.yml @@ -0,0 +1,5 @@ +--- +title: Add missing status type and enum to package graphql type +merge_request: 61002 +author: +type: added diff --git a/changelogs/unreleased/324747-linear_descendants.yml b/changelogs/unreleased/324747-linear_descendants.yml new file mode 100644 index 00000000000..6d410c6d560 --- /dev/null +++ b/changelogs/unreleased/324747-linear_descendants.yml @@ -0,0 +1,5 @@ +--- +title: Linear traversal query for Namespace#descendants +merge_request: 59632 +author: +type: performance diff --git a/changelogs/unreleased/ajk-disable-broken-webhooks.yml b/changelogs/unreleased/ajk-disable-broken-webhooks.yml new file mode 100644 index 00000000000..5f09552068c --- /dev/null +++ b/changelogs/unreleased/ajk-disable-broken-webhooks.yml @@ -0,0 +1,5 @@ +--- +title: Disable web-hooks that fail repeatedly +merge_request: 60837 +author: +type: changed diff --git a/changelogs/unreleased/jivanvl-add-ci-job-mutations-graphql.yml b/changelogs/unreleased/jivanvl-add-ci-job-mutations-graphql.yml new file mode 100644 index 00000000000..fa69900b75d --- /dev/null +++ b/changelogs/unreleased/jivanvl-add-ci-job-mutations-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Add Ci::Build graphql mutations +merge_request: 60443 +author: +type: added diff --git a/changelogs/unreleased/mmj-correct-invitation-spec.yml b/changelogs/unreleased/mmj-correct-invitation-spec.yml new file mode 100644 index 00000000000..4f1a7d08c53 --- /dev/null +++ b/changelogs/unreleased/mmj-correct-invitation-spec.yml @@ -0,0 +1,5 @@ +--- +title: Correct the 'blocked' scope in 'Member' class +merge_request: 61108 +author: +type: fixed diff --git a/changelogs/unreleased/update-docs-links-clusters.yml b/changelogs/unreleased/update-docs-links-clusters.yml new file mode 100644 index 00000000000..6afe55ccb18 --- /dev/null +++ b/changelogs/unreleased/update-docs-links-clusters.yml @@ -0,0 +1,5 @@ +--- +title: Update UI links to docs +merge_request: 60247 +author: +type: other diff --git a/config/feature_flags/development/show_relevant_approval_rule_approvers.yml b/config/feature_flags/development/show_relevant_approval_rule_approvers.yml new file mode 100644 index 00000000000..40a486e380b --- /dev/null +++ b/config/feature_flags/development/show_relevant_approval_rule_approvers.yml @@ -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 diff --git a/config/feature_flags/development/web_hooks_disable_failed.yml b/config/feature_flags/development/web_hooks_disable_failed.yml new file mode 100644 index 00000000000..a54034d73e8 --- /dev/null +++ b/config/feature_flags/development/web_hooks_disable_failed.yml @@ -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 diff --git a/db/migrate/20210429181325_add_failure_tracking_to_web_hooks.rb b/db/migrate/20210429181325_add_failure_tracking_to_web_hooks.rb new file mode 100644 index 00000000000..4a34c2dd307 --- /dev/null +++ b/db/migrate/20210429181325_add_failure_tracking_to_web_hooks.rb @@ -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 diff --git a/db/migrate/20210504085144_add_index_on_web_hook_project_id_recent_failures.rb b/db/migrate/20210504085144_add_index_on_web_hook_project_id_recent_failures.rb new file mode 100644 index 00000000000..898a0ccd1c5 --- /dev/null +++ b/db/migrate/20210504085144_add_index_on_web_hook_project_id_recent_failures.rb @@ -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 diff --git a/db/schema_migrations/20210429181325 b/db/schema_migrations/20210429181325 new file mode 100644 index 00000000000..d778566a580 --- /dev/null +++ b/db/schema_migrations/20210429181325 @@ -0,0 +1 @@ +9674f04640f897928925ff1e23ff6d3ff918627b7c2374713a31071678956614
\ No newline at end of file diff --git a/db/schema_migrations/20210504085144 b/db/schema_migrations/20210504085144 new file mode 100644 index 00000000000..67abcd8eece --- /dev/null +++ b/db/schema_migrations/20210504085144 @@ -0,0 +1 @@ +3cdf8e93c4b80867a5d8e086f3f44eaeb479e875abf16187b94b3f6238faf062
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c962794e928..5bf73b4955a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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); diff --git a/doc/administration/external_pipeline_validation.md b/doc/administration/external_pipeline_validation.md index ba5adecf7ed..2c26e356552 100644 --- a/doc/administration/external_pipeline_validation.md +++ b/doc/administration/external_pipeline_validation.md @@ -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 diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f88ed6dfcd6..be5de5c86ac 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -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. diff --git a/doc/development/import_project.md b/doc/development/import_project.md index a4917cc0c3d..0c8406e2ebc 100644 --- a/doc/development/import_project.md +++ b/doc/development/import_project.md @@ -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. diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index d32971a7618..a62188b5a6e 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -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. - -- 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). +Personal access tokens are required when [Two-Factor Authentication (2FA)](account/two_factor_authentication.md) is enabled. -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). +For examples of how you can use a personal access token to authenticate with the API, see the [API documentation](../../api/README.md#personalproject-access-tokens). -GitLab 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. +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. -## Creating a personal access token +## Create a personal access token -You can create as many personal access tokens as you like from your GitLab -profile. +You can create as many personal access tokens as you like. -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. + +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**. -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. +## View the last time a token was used -## Limiting scopes of a personal access token +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). -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. +To view the last time a token was used: -| Scope | Introduced in | Description | +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. | - -## Programmatically creating a personal access token - -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. - -To create a token belonging to a user with username `automation-bot`, run the -following in the Rails console (`sudo gitlab-rails console`): - -```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! -``` +| `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). | + +## When personal access tokens expire -This can be shortened into a single-line shell command using the +Personal access tokens expire on the date you define, at midnight UTC. + +- 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). + +## Create a personal access token programmatically **(FREE SELF)** + +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: + + ```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 can be shortened into a single-line shell command using the +This code can be shortened into a single-line shell command using the [Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner): ```shell diff --git a/package.json b/package.json index d2a99debada..4975ff8937a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json index f59b67ba64a..89e73d48c60 100644 --- a/spec/fixtures/api/schemas/graphql/packages/package_details.json +++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json @@ -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"] } } } diff --git a/spec/frontend/integrations/edit/components/active_checkbox_spec.js b/spec/frontend/integrations/edit/components/active_checkbox_spec.js index 76fd6dd3a48..0e56fb6454e 100644 --- a/spec/frontend/integrations/edit/components/active_checkbox_spec.js +++ b/spec/frontend/integrations/edit/components/active_checkbox_spec.js @@ -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'); diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js index b4204b434e2..fbc47a106d1 100644 --- a/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -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); - afterEach(() => { - wrapper.destroy(); + beforeEach(() => { + factory({ mockData: { blobInfo: simpleMockData } }); }); - beforeEach(() => { - factory('some_file.js'); + afterEach(() => { + wrapper.destroy(); }); 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' }, + }); + }); + + 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(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', + expect(findBlobHeader().props('activeViewerType')).toEqual('simple'); + expect(findBlobContent().props('activeViewer')).toEqual( + expect.objectContaining({ + type: 'simple', + }), + ); }); }); }); diff --git a/spec/frontend/repository/pages/blob_spec.js b/spec/frontend/repository/pages/blob_spec.js index 74338777376..41ab4d616b8 100644 --- a/spec/frontend/repository/pages/blob_spec.js +++ b/spec/frontend/repository/pages/blob_spec.js @@ -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(() => { diff --git a/spec/graphql/types/packages/package_status_enum_spec.rb b/spec/graphql/types/packages/package_status_enum_spec.rb new file mode 100644 index 00000000000..71d05da35ea --- /dev/null +++ b/spec/graphql/types/packages/package_status_enum_spec.rb @@ -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 diff --git a/spec/graphql/types/packages/package_type_spec.rb b/spec/graphql/types/packages/package_type_spec.rb index 544d6ddc3af..07573044abb 100644 --- a/spec/graphql/types/packages/package_type_spec.rb +++ b/spec/graphql/types/packages/package_type_spec.rb @@ -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) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index b701556e735..7d3b178a4a8 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -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}" } diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 413e69fb071..b528dbedd2c 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -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 diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index f8faa4070a3..b57f8576a53 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb b/spec/requests/api/graphql/mutations/ci/job_play_spec.rb new file mode 100644 index 00000000000..0874e225259 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/job_play_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb new file mode 100644 index 00000000000..a14935379dc --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb @@ -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 diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb index 52df21897b9..19694a0a354 100644 --- a/spec/services/git/branch_hooks_service_spec.rb +++ b/spec/services/git/branch_hooks_service_spec.rb @@ -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 - 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 + time = Time.zone.now - it 'does not track the event' do - execute_service + 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 diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index 2fe72ab31c2..46dab4fa171 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -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 diff --git a/yarn.lock b/yarn.lock index 1dfb291c1a7..722fc8f998b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" |