Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-07-15 12:09:01 +00:00
parent 32e53ae7d7
commit 01d2d6c869
92 changed files with 1529 additions and 322 deletions

View File

@ -247,10 +247,14 @@ danger-review:
script:
- >
if [ -z "$DANGER_GITLAB_API_TOKEN" ]; then
# Force danger to skip CI source GitLab and fallback to "local only git repo".
unset GITLAB_CI
# We need to base SHA to help danger determine the base commit for this shallow clone.
run_timed_command "bundle exec danger dry_run --fail-on-errors=true --verbose --base='$CI_MERGE_REQUEST_DIFF_BASE_SHA'"
run_timed_command danger_as_local
else
run_timed_command "bundle exec danger --fail-on-errors=true --verbose"
fi
danger-review-local:
extends:
- danger-review
- .review:rules:danger-local
script:
- run_timed_command danger_as_local

View File

@ -330,6 +330,12 @@
- ".dockerignore"
- "qa/**/*"
.code-backstage-danger-patterns: &code-backstage-danger-patterns
# Backstage changes
- "Dangerfile"
- "danger/**/*"
- "tooling/danger/**/*"
################
# Shared rules #
################
@ -1284,6 +1290,11 @@
rules:
- if: '$CI_MERGE_REQUEST_IID'
.review:rules:danger-local:
rules:
- if: '$CI_MERGE_REQUEST_IID'
changes: *code-backstage-danger-patterns
###############
# Setup rules #
###############

View File

@ -1 +1 @@
14.0.1
14.1.0

View File

@ -221,7 +221,7 @@ export default {
}
if (this.visibilityLevel !== visibilityOptions.PUBLIC) {
options.push([visibilityOptions.PUBLIC, PAGE_FEATURE_ACCESS_LEVEL]);
options.push([30, PAGE_FEATURE_ACCESS_LEVEL]);
}
}
return options;

View File

@ -3,6 +3,35 @@ import $ from 'jquery';
/**
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API.
*
* This component has two intervals:
*
* - current interval - when the page is visible - defined by `startingInterval`, `maxInterval`, and `incrementByFactorOf`
* - Example:
* - `startingInterval: 10000`, `maxInterval: 240000`, `incrementByFactorOf: 2`
* - results in `10s, 20s, 40s, 80s, ..., 240s`, it stops increasing at `240s` and keeps this interval indefinitely.
* - hidden interval - when the page is not visible
*
* Visibility transitions:
*
* - `visible -> not visible`
* - `document.addEventListener('visibilitychange', () => ...)`
*
* > This event fires with a visibilityState of hidden when a user navigates to a new page, switches tabs, closes the tab, minimizes or closes the browser, or, on mobile, switches from the browser to a different app.
*
* Source [Document: visibilitychange event - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event)
*
* - `window.addEventListener('blur', () => ...)` - every time user clicks somewhere else then in the browser page
* - `not visible -> visible`
* - `document.addEventListener('visibilitychange', () => ...)` same as the transition `visible -> not visible`
* - `window.addEventListener('focus', () => ...)`
*
* The combination of these two listeners can result in an unexpected resumption of polling:
*
* - switch to a different window (causes `blur`)
* - switch to a different desktop (causes `visibilitychange` (not visible))
* - switch back to the original desktop (causes `visibilitychange` (visible))
* - *now the polling happens even in window that user doesn't work in*
*/
export default class SmartInterval {

View File

@ -1,6 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { GlLoadingIcon, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { s__, __ } from '~/locale';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
@ -8,7 +8,6 @@ import modalEventHub from '~/projects/commit/event_hub';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import eventHub from '../../event_hub';
import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetMerged',
@ -17,7 +16,7 @@ export default {
},
components: {
MrWidgetAuthorTime,
statusIcon,
GlIcon,
ClipboardButton,
GlLoadingIcon,
GlButton,
@ -116,7 +115,7 @@ export default {
</script>
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
<gl-icon name="merge" :size="24" class="gl-text-blue-500 gl-mr-3 gl-mt-1" />
<div class="media-body">
<div class="space-children">
<mr-widget-author-time
@ -131,7 +130,6 @@ export default {
:title="revertTitle"
size="small"
category="secondary"
variant="warning"
data-qa-selector="revert_button"
@click="openRevertModal"
>
@ -144,7 +142,6 @@ export default {
:title="revertTitle"
size="small"
category="secondary"
variant="warning"
data-method="post"
>
{{ revertLabel }}
@ -169,6 +166,15 @@ export default {
>
{{ cherryPickLabel }}
</gl-button>
<gl-button
v-if="shouldShowRemoveSourceBranch"
:disabled="isMakingRequest"
size="small"
class="js-remove-branch-button"
@click="removeSourceBranch"
>
{{ s__('mrWidget|Delete source branch') }}
</gl-button>
</div>
<section class="mr-info-list" data-qa-selector="merged_status_content">
<p>
@ -196,17 +202,6 @@ export default {
<p v-if="mr.sourceBranchRemoved">
{{ s__('mrWidget|The source branch has been deleted') }}
</p>
<p v-if="shouldShowRemoveSourceBranch" class="space-children">
<span>{{ s__('mrWidget|You can delete the source branch now') }}</span>
<gl-button
:disabled="isMakingRequest"
size="small"
class="js-remove-branch-button"
@click="removeSourceBranch"
>
{{ s__('mrWidget|Delete source branch') }}
</gl-button>
</p>
<p v-if="shouldShowSourceBranchRemoving">
<gl-loading-icon size="sm" :inline="true" />
<span> {{ s__('mrWidget|The source branch is being deleted') }} </span>

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
module Members
module Mailgun
class PermanentFailuresController < ApplicationController
respond_to :json
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
before_action :ensure_feature_enabled!
before_action :authenticate_signature!
before_action :validate_invite_email!
feature_category :authentication_and_authorization
def create
webhook_processor.execute
head :ok
end
private
def ensure_feature_enabled!
render_406 unless Gitlab::CurrentSettings.mailgun_events_enabled?
end
def authenticate_signature!
access_denied! unless valid_signature?
end
def valid_signature?
return false if Gitlab::CurrentSettings.mailgun_signing_key.blank?
# per this guide: https://documentation.mailgun.com/en/latest/user_manual.html#webhooks
digest = OpenSSL::Digest.new('SHA256')
data = [params.dig(:signature, :timestamp), params.dig(:signature, :token)].join
hmac_digest = OpenSSL::HMAC.hexdigest(digest, Gitlab::CurrentSettings.mailgun_signing_key, data)
ActiveSupport::SecurityUtils.secure_compare(params.dig(:signature, :signature), hmac_digest)
end
def validate_invite_email!
# permanent_failures webhook does not provide a way to filter failures, so we'll get them all on this endpoint
# and we only care about our invite_emails
render_406 unless payload[:tags]&.include?(::Members::Mailgun::INVITE_EMAIL_TAG)
end
def webhook_processor
::Members::Mailgun::ProcessWebhookService.new(payload)
end
def payload
@payload ||= params.permit!['event-data']
end
def render_406
# failure to stop retries per https://documentation.mailgun.com/en/latest/user_manual.html#webhooks
head :not_acceptable
end
end
end
end

View File

@ -25,8 +25,6 @@ class ContainerRepositoriesFinder
end
def project_repositories
return unless @subject.container_registry_enabled
@subject.container_repositories
end

View File

@ -57,7 +57,7 @@ module PackagesHelper
def show_cleanup_policy_on_alert(project)
Gitlab.com? &&
Gitlab.config.registry.enabled &&
project.container_registry_enabled &&
project.feature_available?(:container_registry, current_user) &&
!Gitlab::CurrentSettings.container_expiration_policies_enable_historic_entries &&
Feature.enabled?(:container_expiration_policies_historic_entry, project) &&
project.container_expiration_policy.nil? &&

View File

@ -26,6 +26,12 @@ module SidebarsHelper
Sidebars::Projects::Context.new(**context_data)
end
def group_sidebar_context(group, user)
context_data = group_sidebar_context_data(group, user)
Sidebars::Groups::Context.new(**context_data)
end
private
def sidebar_attributes_for_object(object)
@ -89,6 +95,13 @@ module SidebarsHelper
show_cluster_hint: show_gke_cluster_integration_callout?(project)
}
end
def group_sidebar_context_data(group, user)
{
current_user: user,
container: group
}
end
end
SidebarsHelper.prepend_mod_with('SidebarsHelper')

View File

@ -150,10 +150,10 @@ module Emails
end
def invite_email_headers
if Gitlab.dev_env_or_com?
if Gitlab::CurrentSettings.mailgun_events_enabled?
{
'X-Mailgun-Tag' => 'invite_email',
'X-Mailgun-Variables' => { 'invite_token' => @token }.to_json
'X-Mailgun-Tag' => ::Members::Mailgun::INVITE_EMAIL_TAG,
'X-Mailgun-Variables' => { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => @token }.to_json
}
else
{}

View File

@ -37,12 +37,20 @@ module Ci
next [] unless processable.pipeline_id # we don't have any dependency when creating the pipeline
deps = model_class.where(pipeline_id: processable.pipeline_id).latest
deps = from_previous_stages(deps)
deps = from_needs(deps)
deps = find_dependencies(processable, deps)
from_dependencies(deps).to_a
end
end
def find_dependencies(processable, deps)
if processable.scheduling_type_dag?
from_needs(deps)
else
from_previous_stages(deps)
end
end
# Dependencies from the same parent-pipeline hierarchy excluding
# the current job's pipeline
def cross_pipeline
@ -125,8 +133,6 @@ module Ci
end
def from_needs(scope)
return scope unless processable.scheduling_type_dag?
needs_names = processable.needs.artifacts.select(:name)
scope.where(name: needs_names)
end

View File

@ -9,4 +9,15 @@ class ErrorTracking::Error < ApplicationRecord
validates :name, presence: true
validates :description, presence: true
validates :actor, presence: true
def self.report_error(name:, description:, actor:, platform:, timestamp:)
safe_find_or_create_by(
name: name,
description: description,
actor: actor,
platform: platform
) do |error|
error.update!(last_seen_at: timestamp)
end
end
end

View File

@ -377,6 +377,8 @@ class Project < ApplicationRecord
has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
has_many :error_tracking_errors, inverse_of: :project, class_name: 'ErrorTracking::Error'
has_many :timelogs
accepts_nested_attributes_for :variables, allow_destroy: true

View File

@ -10,8 +10,16 @@ module Ci
private
def process_subsequent_jobs(processable)
processable.pipeline.processables.skipped.after_stage(processable.stage_idx).find_each do |processable|
process(processable)
if Feature.enabled?(:ci_same_stage_job_needs, processable.project, default_enabled: :yaml)
(stage_dependent_jobs(processable) | needs_dependent_jobs(processable))
.each do |processable|
process(processable)
end
else
skipped_jobs(processable).after_stage(processable.stage_idx)
.find_each do |job|
process(job)
end
end
end
@ -24,5 +32,17 @@ module Ci
processable.process(current_user)
end
end
def skipped_jobs(processable)
processable.pipeline.processables.skipped
end
def stage_dependent_jobs(processable)
skipped_jobs(processable).scheduling_type_stage.after_stage(processable.stage_idx)
end
def needs_dependent_jobs(processable)
skipped_jobs(processable).scheduling_type_dag.with_needs([processable.name])
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module ErrorTracking
class CollectErrorService < ::BaseService
def execute
# Error is a way to group events based on common data like name or cause
# of exception. We need to keep a sane balance here between taking too little
# and too much data into group logic.
error = project.error_tracking_errors.report_error(
name: exception['type'], # Example: ActionView::MissingTemplate
description: exception['value'], # Example: Missing template posts/show in...
actor: event['transaction'], # Example: PostsController#show
platform: event['platform'], # Example: ruby
timestamp: event['timestamp']
)
# The payload field contains all the data on error including stacktrace in jsonb.
# Together with occured_at these are 2 main attributes that we need to save here.
error.events.create!(
environment: event['environment'],
description: exception['type'],
level: event['level'],
occurred_at: event['timestamp'],
payload: event
)
end
private
def event
params[:event]
end
def exception
event['exception']['values'].first
end
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module Members
module Mailgun
INVITE_EMAIL_TAG = 'invite_email'
INVITE_EMAIL_TOKEN_KEY = :invite_token
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module Members
module Mailgun
class ProcessWebhookService
ProcessWebhookServiceError = Class.new(StandardError)
def initialize(payload)
@payload = payload
end
def execute
@member = Member.find_by_invite_token(invite_token)
update_member_and_log if member
rescue ProcessWebhookServiceError => e
Gitlab::ErrorTracking.track_exception(e)
end
private
attr_reader :payload, :member
def update_member_and_log
log_update_event if member.update(invite_email_success: false)
end
def log_update_event
Gitlab::AppLogger.info "UPDATED MEMBER INVITE_EMAIL_SUCCESS: member_id: #{member.id}"
end
def invite_token
# may want to validate schema in some way using ::JSONSchemer.schema(SCHEMA_PATH).valid?(message) if this
# gets more complex
payload.dig('user-variables', ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY) ||
raise(ProcessWebhookServiceError, "Failed to receive #{::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY} in user-variables: #{payload}")
end
end
end
end

View File

@ -1,5 +1,3 @@
- return unless Feature.enabled?(:mailgun_events_receiver)
- expanded = integration_expanded?('mailgun_')
%section.settings.as-mailgun.no-animate#js-mailgun-settings{ class: ('expanded' if expanded) }
.settings-header

View File

@ -1,4 +1,4 @@
- autodevops_help_url = help_page_path('topics/autodevops/index.md', anchor: 'using-multiple-kubernetes-clusters')
- autodevops_help_url = help_page_path('topics/autodevops/index.md', anchor: 'use-multiple-kubernetes-clusters')
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- help_link_end = '</a>'.html_safe

View File

@ -0,0 +1 @@
= link_to _('Delete'), project, data: { confirm: remove_project_message(project) }, method: :delete, class: "btn gl-button btn-danger"

View File

@ -0,0 +1,2 @@
- if project.archived
%span.badge.badge-warning.badge-pill.gl-badge.md= _('archived')

View File

@ -15,13 +15,12 @@
.controls
= link_to _('Members'), project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button"
= link_to _('Edit'), edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button"
= link_to _('Delete'), project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn gl-button btn-danger"
= render 'delete_project_button', project: project
.stats
%span.badge.badge-pill
= storage_counter(project.statistics&.storage_size)
- if project.archived
%span.badge.badge-warning archived
= render 'project_badges', project: project
.title
= link_to(project_path(project)) do

View File

@ -1,176 +1,3 @@
- issues_count = cached_issuables_count(@group, type: :issues)
- merge_requests_count = cached_issuables_count(@group, type: :merge_requests)
- aside_title = @group.subgroup? ? _('Subgroup navigation') : _('Group navigation')
%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(@group), 'aria-label': aside_title }
.nav-sidebar-inner-scroll
%ul.sidebar-top-level-items.qa-group-sidebar
= nav_link(path: ['groups#show', 'groups#details'], html_options: { class: 'context-header' }) do
= link_to group_path(@group), title: @group.name, data: { qa_selector: 'group_scope_link' } do
%span{ class: ['avatar-container', 'rect-avatar', 'group-avatar' , 's32'] }
= group_icon(@group, class: ['avatar', 'avatar-tile', 's32'])
%span.sidebar-context-title
= @group.name
= render_if_exists 'layouts/nav/sidebar/group_trial_status_widget', group: @group
- if group_sidebar_link?(:overview)
- paths = group_overview_nav_link_paths
= nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do
= link_to activity_group_path(@group), class: 'has-sub-items', data: { qa_selector: 'group_information_link' } do
.nav-icon-container
= sprite_icon('group')
%span.nav-item-name
= group_information_title(@group)
%ul.sidebar-sub-level-items{ data: { qa_selector: 'group_information_submenu'} }
= nav_link(path: paths, html_options: { class: "fly-out-top-item" } ) do
= link_to activity_group_path(@group) do
%strong.fly-out-top-item-name
= group_information_title(@group)
%li.divider.fly-out-top-item
- if group_sidebar_link?(:activity)
= nav_link(path: 'groups#activity') do
= link_to activity_group_path(@group), title: _('Activity') do
%span
= _('Activity')
- if group_sidebar_link?(:labels)
= nav_link(path: 'labels#index') do
= link_to group_labels_path(@group), title: _('Labels') do
%span
= _('Labels')
- if group_sidebar_link?(:group_members)
= nav_link(path: 'group_members#index') do
= link_to group_group_members_path(@group), title: _('Members'), data: { qa_selector: 'group_members_item' } do
%span
= _('Members')
= render_if_exists "layouts/nav/ee/epic_link", group: @group
- if group_sidebar_link?(:issues)
= nav_link(path: group_issues_sub_menu_items, unless: -> { current_path?('issues_analytics#show') }) do
= link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' }, class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('issues')
%span.nav-item-name
= _('Issues')
%span.badge.badge-pill.count= issues_count
%ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} }
= nav_link(path: group_issues_sub_menu_items, html_options: { class: "fly-out-top-item" } ) do
= link_to issues_group_path(@group) do
%strong.fly-out-top-item-name
= _('Issues')
%span.badge.badge-pill.count.issue_counter.fly-out-badge= issues_count
%li.divider.fly-out-top-item
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
= link_to issues_group_path(@group), title: _('List') do
%span
= _('List')
- if group_sidebar_link?(:boards)
= nav_link(path: ['boards#index', 'boards#show']) do
= link_to group_boards_path(@group), title: boards_link_text, data: { qa_selector: 'group_issue_boards_link' } do
%span
= boards_link_text
- if group_sidebar_link?(:milestones)
= nav_link(path: 'milestones#index') do
= link_to group_milestones_path(@group), title: _('Milestones'), data: { qa_selector: 'group_milestones_link' } do
%span
= _('Milestones')
= render_if_exists 'layouts/nav/sidebar/group_iterations_link'
- if group_sidebar_link?(:merge_requests)
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group) do
.nav-icon-container
= sprite_icon('git-merge')
%span.nav-item-name
= _('Merge requests')
%span.badge.badge-pill.count= merge_requests_count
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
= link_to merge_requests_group_path(@group) do
%strong.fly-out-top-item-name
= _('Merge requests')
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= merge_requests_count
= render_if_exists "layouts/nav/ee/security_link" # EE-specific
= render_if_exists "layouts/nav/ee/push_rules_link" # EE-specific
- if group_sidebar_link?(:kubernetes)
= nav_link(controller: [:clusters]) do
= link_to group_clusters_path(@group) do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
= _('Kubernetes')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do
= link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%strong.fly-out-top-item-name
= _('Kubernetes')
= render 'groups/sidebar/packages'
= render 'layouts/nav/sidebar/analytics_links', links: group_analytics_navbar_links(@group, current_user)
- if group_sidebar_link?(:wiki)
= render 'layouts/nav/sidebar/wiki_link', wiki_url: @group.wiki.web_url
- if group_sidebar_link?(:settings)
= nav_link(path: group_settings_nav_link_paths) do
= link_to edit_group_path(@group), class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('settings')
%span.nav-item-name{ data: { qa_selector: 'group_settings' } }
= _('Settings')
%ul.sidebar-sub-level-items.qa-group-sidebar-submenu{ data: { testid: 'group-settings-menu' } }
= nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show groups/applications#index], html_options: { class: "fly-out-top-item" } ) do
= link_to edit_group_path(@group) do
%strong.fly-out-top-item-name
= _('Settings')
%li.divider.fly-out-top-item
= nav_link(path: 'groups#edit') do
= link_to edit_group_path(@group), title: _('General'), data: { qa_selector: 'general_settings_link' } do
%span
= _('General')
= nav_link(controller: :integrations) do
= link_to group_settings_integrations_path(@group), title: _('Integrations') do
%span
= _('Integrations')
= nav_link(path: 'groups#projects') do
= link_to projects_group_path(@group), title: _('Projects') do
%span
= _('Projects')
= nav_link(controller: :repository) do
= link_to group_settings_repository_path(@group), title: _('Repository') do
%span
= _('Repository')
= nav_link(controller: [:ci_cd, 'groups/runners']) do
= link_to group_settings_ci_cd_path(@group), title: _('CI/CD') do
%span
= _('CI/CD')
= nav_link(controller: :applications) do
= link_to group_settings_applications_path(@group), title: _('Applications') do
%span
= _('Applications')
= render 'groups/sidebar/packages_settings'
= render_if_exists "groups/ee/settings_nav"
= render_if_exists "groups/ee/administration_nav"
= render 'shared/sidebar_toggle_button'
-# We're migration the group sidebar to a logical model based structure. If you need to update
-# any of the existing menus, you can find them in app/views/layouts/nav/sidebar/_group_menus.html.haml.
= render partial: 'shared/nav/sidebar', object: Sidebars::Groups::Panel.new(group_sidebar_context(@group, current_user))

View File

@ -0,0 +1,166 @@
- issues_count = cached_issuables_count(@group, type: :issues)
- merge_requests_count = cached_issuables_count(@group, type: :merge_requests)
= render_if_exists 'layouts/nav/sidebar/group_trial_status_widget', group: @group
- if group_sidebar_link?(:overview)
- paths = group_overview_nav_link_paths
= nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do
= link_to activity_group_path(@group), class: 'has-sub-items', data: { qa_selector: 'group_information_link' } do
.nav-icon-container
= sprite_icon('group')
%span.nav-item-name
= group_information_title(@group)
%ul.sidebar-sub-level-items{ data: { qa_selector: 'group_information_submenu'} }
= nav_link(path: paths, html_options: { class: "fly-out-top-item" } ) do
= link_to activity_group_path(@group) do
%strong.fly-out-top-item-name
= group_information_title(@group)
%li.divider.fly-out-top-item
- if group_sidebar_link?(:activity)
= nav_link(path: 'groups#activity') do
= link_to activity_group_path(@group), title: _('Activity') do
%span
= _('Activity')
- if group_sidebar_link?(:labels)
= nav_link(path: 'labels#index') do
= link_to group_labels_path(@group), title: _('Labels') do
%span
= _('Labels')
- if group_sidebar_link?(:group_members)
= nav_link(path: 'group_members#index') do
= link_to group_group_members_path(@group), title: _('Members'), data: { qa_selector: 'group_members_item' } do
%span
= _('Members')
= render_if_exists "layouts/nav/ee/epic_link", group: @group
- if group_sidebar_link?(:issues)
= nav_link(path: group_issues_sub_menu_items, unless: -> { current_path?('issues_analytics#show') }) do
= link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' }, class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('issues')
%span.nav-item-name
= _('Issues')
%span.badge.badge-pill.count= issues_count
%ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} }
= nav_link(path: group_issues_sub_menu_items, html_options: { class: "fly-out-top-item" } ) do
= link_to issues_group_path(@group) do
%strong.fly-out-top-item-name
= _('Issues')
%span.badge.badge-pill.count.issue_counter.fly-out-badge= issues_count
%li.divider.fly-out-top-item
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
= link_to issues_group_path(@group), title: _('List') do
%span
= _('List')
- if group_sidebar_link?(:boards)
= nav_link(path: ['boards#index', 'boards#show']) do
= link_to group_boards_path(@group), title: boards_link_text, data: { qa_selector: 'group_issue_boards_link' } do
%span
= boards_link_text
- if group_sidebar_link?(:milestones)
= nav_link(path: 'milestones#index') do
= link_to group_milestones_path(@group), title: _('Milestones'), data: { qa_selector: 'group_milestones_link' } do
%span
= _('Milestones')
= render_if_exists 'layouts/nav/sidebar/group_iterations_link'
- if group_sidebar_link?(:merge_requests)
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group) do
.nav-icon-container
= sprite_icon('git-merge')
%span.nav-item-name
= _('Merge requests')
%span.badge.badge-pill.count= merge_requests_count
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
= link_to merge_requests_group_path(@group) do
%strong.fly-out-top-item-name
= _('Merge requests')
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= merge_requests_count
= render_if_exists "layouts/nav/ee/security_link" # EE-specific
= render_if_exists "layouts/nav/ee/push_rules_link" # EE-specific
- if group_sidebar_link?(:kubernetes)
= nav_link(controller: [:clusters]) do
= link_to group_clusters_path(@group) do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
= _('Kubernetes')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do
= link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%strong.fly-out-top-item-name
= _('Kubernetes')
= render 'groups/sidebar/packages'
= render 'layouts/nav/sidebar/analytics_links', links: group_analytics_navbar_links(@group, current_user)
- if group_sidebar_link?(:wiki)
= render 'layouts/nav/sidebar/wiki_link', wiki_url: @group.wiki.web_url
- if group_sidebar_link?(:settings)
= nav_link(path: group_settings_nav_link_paths) do
= link_to edit_group_path(@group), class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('settings')
%span.nav-item-name{ data: { qa_selector: 'group_settings' } }
= _('Settings')
%ul.sidebar-sub-level-items{ data: { testid: 'group-settings-menu', qa_selector: 'group_sidebar_submenu' } }
= nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show groups/applications#index], html_options: { class: "fly-out-top-item" } ) do
= link_to edit_group_path(@group) do
%strong.fly-out-top-item-name
= _('Settings')
%li.divider.fly-out-top-item
= nav_link(path: 'groups#edit') do
= link_to edit_group_path(@group), title: _('General'), data: { qa_selector: 'general_settings_link' } do
%span
= _('General')
= nav_link(controller: :integrations) do
= link_to group_settings_integrations_path(@group), title: _('Integrations') do
%span
= _('Integrations')
= nav_link(path: 'groups#projects') do
= link_to projects_group_path(@group), title: _('Projects') do
%span
= _('Projects')
= nav_link(controller: :repository) do
= link_to group_settings_repository_path(@group), title: _('Repository') do
%span
= _('Repository')
= nav_link(controller: [:ci_cd, 'groups/runners']) do
= link_to group_settings_ci_cd_path(@group), title: _('CI/CD') do
%span
= _('CI/CD')
= nav_link(controller: :applications) do
= link_to group_settings_applications_path(@group), title: _('Applications') do
%span
= _('Applications')
= render 'groups/sidebar/packages_settings'
= render_if_exists "groups/ee/settings_nav"
= render_if_exists "groups/ee/administration_nav"
= render 'shared/sidebar_toggle_button'

View File

@ -0,0 +1,6 @@
= nav_link(path: ['groups#show', 'groups#details'], html_options: { class: 'context-header' }) do
= link_to group_path(@group), title: @group.name, data: { qa_selector: 'group_scope_link' } do
%span{ class: ['avatar-container', 'rect-avatar', 'group-avatar' , 's32'] }
= group_icon(@group, class: ['avatar', 'avatar-tile', 's32'])
%span.sidebar-context-title
= @group.name

View File

@ -1,13 +1,14 @@
%aside.nav-sidebar{ class: ('sidebar-collapsed-desktop' if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(sidebar.container), 'aria-label': sidebar.aria_label }
.nav-sidebar-inner-scroll
- if sidebar.render_raw_scope_menu_partial
= render sidebar.render_raw_scope_menu_partial
%ul.sidebar-top-level-items{ data: { qa_selector: sidebar_qa_selector(sidebar.container) } }
- if sidebar.scope_menu
- if sidebar.render_raw_scope_menu_partial
= render sidebar.render_raw_scope_menu_partial
- elsif sidebar.scope_menu
= render partial: 'shared/nav/scope_menu', object: sidebar.scope_menu
- if sidebar.renderable_menus.any?
= render partial: 'shared/nav/sidebar_menu', collection: sidebar.renderable_menus
- if sidebar.render_raw_menus_partial
= render sidebar.render_raw_menus_partial

View File

@ -0,0 +1,8 @@
---
name: ci_same_stage_job_needs
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59668
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328253
milestone: '14.1'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: integrated_error_tracking
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65767
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335846
milestone: '14.1'
type: development
group: group::monitor
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: load_balancing_for_update_all_mirrors_worker
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64526
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334162
milestone: '14.1'
type: development
group: group::source code
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: mailgun_events_receiver
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64249
rollout_issue_url:
milestone: '14.1'
type: development
group: group::expansion
default_enabled: false

View File

@ -222,6 +222,7 @@ Rails.application.routes.draw do
draw :snippets
draw :profile
draw :members
# Product analytics collector
match '/collector/i', to: ProductAnalytics::CollectorApp.new, via: :all

7
config/routes/members.rb Normal file
View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
namespace :members do
namespace :mailgun do
resources :permanent_failures, only: [:create]
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddInviteEmailSuccessToMembers < ActiveRecord::Migration[6.1]
def change
add_column :members, :invite_email_success, :boolean, null: false, default: true
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddDevopsAdoptionVulnerabilityManagementUsedCount < ActiveRecord::Migration[6.1]
def change
add_column :analytics_devops_adoption_snapshots, :vulnerability_management_used_count, :integer
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddVulnerabilitiesCreatedAtIndex < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
INDEX_NAME = 'idx_vulnerabilities_partial_devops_adoption'
def up
add_concurrent_index :vulnerabilities, [:project_id, :created_at], where: 'state != 1', name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :vulnerabilities, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
8d1777941e1a4b5f9f8f5f5e3ae416d6d02aaee1174eff1f9b4b38a6cdf0103a

View File

@ -0,0 +1 @@
d7f8f7f5d8a6cf03d500825ef43234c69f7ad36908c0bade337591b05985c2fe

View File

@ -0,0 +1 @@
699ac7f8b9253920271686c497b57521bf4b0d26c802ca2a57447e4929cd147f

View File

@ -9139,6 +9139,7 @@ CREATE TABLE analytics_devops_adoption_snapshots (
dast_enabled_count integer,
dependency_scanning_enabled_count integer,
coverage_fuzzing_enabled_count integer,
vulnerability_management_used_count integer,
CONSTRAINT check_3f472de131 CHECK ((namespace_id IS NOT NULL))
);
@ -14661,7 +14662,8 @@ CREATE TABLE members (
requested_at timestamp without time zone,
expires_at date,
ldap boolean DEFAULT false NOT NULL,
override boolean DEFAULT false NOT NULL
override boolean DEFAULT false NOT NULL,
invite_email_success boolean DEFAULT true NOT NULL
);
CREATE SEQUENCE members_id_seq
@ -22717,6 +22719,8 @@ CREATE UNIQUE INDEX idx_vuln_signatures_on_occurrences_id_and_signature_sha ON v
CREATE UNIQUE INDEX idx_vuln_signatures_uniqueness_signature_sha ON vulnerability_finding_signatures USING btree (finding_id, algorithm_type, signature_sha);
CREATE INDEX idx_vulnerabilities_partial_devops_adoption ON vulnerabilities USING btree (project_id, created_at) WHERE (state <> 1);
CREATE UNIQUE INDEX idx_vulnerability_ext_issue_links_on_vulne_id_and_ext_issue ON vulnerability_external_issue_links USING btree (vulnerability_id, external_type, external_project_key, external_issue_key);
CREATE UNIQUE INDEX idx_vulnerability_ext_issue_links_on_vulne_id_and_link_type ON vulnerability_external_issue_links USING btree (vulnerability_id, link_type) WHERE (link_type = 1);

View File

@ -0,0 +1,41 @@
---
stage: Growth
group: Expansion
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference, howto
---
# Mailgun and GitLab **(FREE SELF)**
When you use Mailgun to send emails for your GitLab instance and [Mailgun](https://www.mailgun.com/)
integration is enabled and configured in GitLab, you can receive their webhook for
permanent invite email failures. To set up the integration, you must:
1. [Configure your Mailgun domain](#configure-your-mailgun-domain).
1. [Enable Mailgun integration](#enable-mailgun-integration).
After completing the integration, Mailgun `permanent_failure` webhooks are sent to your GitLab instance.
## Configure your Mailgun domain
Before you can enable Mailgun in GitLab, set up your own Mailgun permanent failure endpoint to receive the webhooks.
Using the [Mailgun webhook guide](https://www.mailgun.com/blog/a-guide-to-using-mailguns-webhooks/):
1. Add a webhook with the **Event type** set to **Permanent Failure**.
1. Fill in the URL of your instance and include the `/-/members/mailgun/permanent_failures` path.
- Example: `https://myinstance.gitlab.com/-/members/mailgun/permanent_failures`
## Enable Mailgun integration
After configuring your Mailgun domain for the permanent failures endpoint,
you're ready to enable the Mailgun integration:
1. Sign in to GitLab as an [Administrator](../../user/permissions.md) user.
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. In the left sidebar, go to **Settings > General** and expand the **Mailgun** section.
1. Select the **Enable Mailgun** check box.
1. Enter the Mailgun HTTP webhook signing key as described in
[the Mailgun documentation](https://documentation.mailgun.com/en/latest/user_manual.html#webhooks) and
shown in the [API security](https://app.mailgun.com/app/account/security/api_keys) section for your Mailgun account.
1. Select **Save changes**.

View File

@ -8555,6 +8555,7 @@ Snapshot.
| <a id="devopsadoptionsnapshotsecurityscansucceeded"></a>`securityScanSucceeded` | [`Boolean!`](#boolean) | At least one security scan succeeded. |
| <a id="devopsadoptionsnapshotstarttime"></a>`startTime` | [`Time!`](#time) | The start time for the snapshot where the data points were collected. |
| <a id="devopsadoptionsnapshottotalprojectscount"></a>`totalProjectsCount` | [`Int`](#int) | Total number of projects. |
| <a id="devopsadoptionsnapshotvulnerabilitymanagementusedcount"></a>`vulnerabilityManagementUsedCount` | [`Int`](#int) | Total number of projects with vulnerability management used at least once. |
### `DiffPosition`

View File

@ -328,7 +328,7 @@ listed in the descriptions of the relevant settings.
| `issues_create_limit` | integer | no | Max number of issue creation requests per minute per user. Disabled by default.|
| `keep_latest_artifact` | boolean | no | Prevent the deletion of the artifacts from the most recent successful jobs, regardless of the expiry time. Enabled by default. |
| `local_markdown_version` | integer | no | Increase this value when any cached Markdown should be invalidated. |
| `mailgun_signing_key` | string | no | The Mailgun HTTP webhook signing key for receiving events from webhook |
| `mailgun_signing_key` | string | no | The Mailgun HTTP webhook signing key for receiving events from webhook. |
| `mailgun_events_enabled` | boolean | no | Enable Mailgun event receiver. |
| `maintenance_mode_message` | string | no | **(PREMIUM)** Message displayed when instance is in maintenance mode. |
| `maintenance_mode` | boolean | no | **(PREMIUM)** When instance is in maintenance mode, non-administrative users can sign in with read-only access and make read-only API requests. |

View File

@ -1563,6 +1563,14 @@ production:
#### Requirements and limitations
- In [GitLab 14.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/30632)
you can refer to jobs in the same stage as the job you are configuring. This feature
is [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
- Disabled on GitLab.com.
- Not recommended for production use.
- For GitLab self-managed instances, GitLab adminsitrators
can choose to [disable it](#enable-or-disable-needs-for-jobs-in-the-same-stage)
- In GitLab 14.0 and older, you can only refer to jobs in earlier stages.
- In GitLab 13.9 and older, if `needs:` refers to a job that might not be added to
a pipeline because of `only`, `except`, or `rules`, the pipeline might fail to create.
- The maximum number of jobs that a single job can need in the `needs:` array is limited:
@ -1579,6 +1587,22 @@ production:
- Stages must be explicitly defined for all jobs
that have the keyword `needs:` or are referred to by one.
##### Enable or disable `needs` for jobs in the same stage **(FREE SELF)**
`needs` for jobs in the same stage is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails
console](../../administration/feature_flags.md)
can opt to disable it.
To enable it:
`Feature.enable(:ci_same_stage_job_needs)`
To disable it:
`Feature.disable(:ci_same_stage_job_needs)`
##### Changing the `needs:` job limit **(FREE SELF)**
The maximum number of jobs that can be defined in `needs:` defaults to 50.

View File

@ -26,6 +26,11 @@ and the advantage of the [special searches](../user/search/advanced_search.md).
| GitLab Enterprise Edition 9.0 through 11.4 | Elasticsearch 5.1 through 5.5 |
| GitLab Enterprise Edition 8.4 through 8.17 | Elasticsearch 2.4 with [Delete By Query Plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/2.4/plugins-delete-by-query.html) installed |
The Elasticsearch Integration is designed to work with supported versions of
Elasticsearch and follows Elasticsearch's [End of Life Policy](https://www.elastic.co/support/eol).
When we change Elasticsearch supported versions in GitLab, we announce them in [deprecation notes](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations) in monthly release posts
before the actual removal.
## System requirements
Elasticsearch requires additional resources in excess of those documented in the

View File

@ -9,9 +9,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Introduced in GitLab 11.0 for general availability.
GitLab Auto DevOps helps to reduce the complexity of software delivery by
setting up pipelines and integrations for you. Instead of requiring you to
manually configure your entire GitLab environment, Auto DevOps configures
many of these areas for you, including security auditing and vulnerability
setting up pipelines and integrations for you. Auto DevOps configures
GitLab CI/CD pipelines including security auditing and vulnerability
testing.
Using Auto DevOps, you can:
@ -54,17 +53,17 @@ following levels:
| GitLab SaaS | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| GitLab self-managed | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
When you enable AutoDevOps for your instance, it attempts to run on all
pipelines in each project, but will automatically disable itself for individual
When you enable Auto DevOps for your instance, it attempts to run on all
pipelines in each project. The Auto DevOps setting automatically disables itself for individual
projects on their first pipeline failure. An instance administrator can enable
or disable this default in the [Auto DevOps settings](../../user/admin_area/settings/continuous_integration.md#auto-devops).
Since [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/issues/26655),
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/26655) in GitLab 12.7,
Auto DevOps runs on pipelines automatically only if a [`Dockerfile` or matching buildpack](stages.md#auto-build)
exists.
If a [CI/CD configuration file](../../ci/yaml/index.md) is present in the
project, it isn't changed and won't be affected by Auto DevOps.
project, it remains unchanged and Auto DevOps doesn't affect it.
### At the project level
@ -88,9 +87,8 @@ After enabling the feature, an Auto DevOps pipeline is triggered on the default
Only administrators and group owners can enable or disable Auto DevOps at the group level.
When enabling or disabling Auto DevOps at group level, group configuration is
implicitly used for the subgroups and projects inside that group, unless Auto DevOps
is specifically enabled or disabled on the subgroup or project.
When you enable Auto DevOps at group level, the subgroups and projects in that group inherit the configuration. Auto DevOps
can be specifically enabled or disabled individually for projects and subgroups.
To enable or disable Auto DevOps at the group level:
@ -138,12 +136,12 @@ to minimize downtime and risk.
## Quick start
If you're using GitLab.com, see the [quick start guide](quick_start_guide.md)
for setting up Auto DevOps with GitLab.com and a Kubernetes cluster on Google Kubernetes
For GitLab.com users, see the [quick start guide](quick_start_guide.md)
for setting up Auto DevOps deploying to a Kubernetes cluster on Google Kubernetes
Engine (GKE).
If you use a self-managed instance of GitLab, you must configure the
[Google OAuth2 OmniAuth Provider](../../integration/google.md) before
[Google OAuth 2.0 OmniAuth Provider](../../integration/google.md) before
configuring a cluster on GKE. After configuring the provider, you can follow
the steps in the [quick start guide](quick_start_guide.md) to get started.
@ -174,7 +172,7 @@ NOTE:
Depending on your target platform, some features might not be available to you.
Comprised of a set of [stages](stages.md), Auto DevOps brings these best practices to your
project in a simple and automatic way:
project automatically:
- [Auto Browser Performance Testing](stages.md#auto-browser-performance-testing)
- [Auto Build](stages.md#auto-build)
@ -233,8 +231,7 @@ any of the following places:
The base domain variable `KUBE_INGRESS_BASE_DOMAIN` follows the same order of precedence
as other environment [variables](../../ci/variables/index.md#cicd-variable-precedence).
If the CI/CD variable is not set and the cluster setting is left blank, the instance-wide **Auto DevOps domain**
setting is used if set.
If this variable isn't set and the cluster setting is left blank, the instance-wide domain is used if set for your instance.
Auto DevOps requires a wildcard DNS A record matching the base domain(s). For
a base domain of `example.com`, you'd need a DNS entry like:
@ -259,14 +256,14 @@ to the Kubernetes pods running your application.
See [Auto DevOps requirements for Amazon ECS](requirements.md#auto-devops-requirements-for-amazon-ecs).
## Using multiple Kubernetes clusters
## Use multiple Kubernetes clusters
When using Auto DevOps, you can deploy different environments to
different Kubernetes clusters, due to the 1:1 connection
[existing between them](../../user/project/clusters/multiple_kubernetes_clusters.md).
The [Deploy Job template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml)
used by Auto DevOps currently defines 3 environment names:
used by Auto DevOps defines 3 environment names:
- `review/` (every environment starting with `review/`)
- `staging`
@ -297,8 +294,8 @@ To add a different cluster for each environment:
1. Navigate to each cluster's page, through **Infrastructure > Kubernetes clusters**,
and add the domain based on its Ingress IP address.
After completing configuration, you can test your setup by creating a merge request
and verifying your application is deployed as a Review App in the Kubernetes
After completing configuration, test your setup by creating a merge request.
Verify whether your application deployed as a Review App in the Kubernetes
cluster with the `review/*` environment scope. Similarly, you can check the
other environments.
@ -338,5 +335,23 @@ spec:
value: "PUT_YOUR_HTTPS_PROXY_HERE"
```
## Upgrade Auto DevOps dependencies when updating GitLab
When updating GitLab, you may need to upgrade Auto DevOps dependencies to
match your new GitLab version:
- [Upgrading Auto DevOps resources](upgrading_auto_deploy_dependencies.md):
- Auto DevOps template.
- Auto Deploy template.
- Auto Deploy image.
- Helm.
- Kubernetes.
- Environment variables.
- [Upgrading PostgreSQL](upgrading_postgresql.md).
## Troubleshooting
See [troubleshooting Auto DevOps](troubleshooting.md).
<!-- DO NOT ADD TROUBLESHOOTING INFO HERE -->
<!-- Troubleshooting information has moved to troubleshooting.md -->

View File

@ -314,7 +314,7 @@ all in GitLab. Despite its automatic nature, Auto DevOps can also be configured
and customized to fit your workflow. Here are some helpful resources for further reading:
1. [Auto DevOps](index.md)
1. [Multiple Kubernetes clusters](index.md#using-multiple-kubernetes-clusters)
1. [Multiple Kubernetes clusters](index.md#use-multiple-kubernetes-clusters)
1. [Incremental rollout to production](customize.md#incremental-rollout-to-production) **(PREMIUM)**
1. [Disable jobs you don't need with CI/CD variables](customize.md#cicd-variables)
1. [Use your own buildpacks to build your application](customize.md#custom-buildpacks)

View File

@ -39,6 +39,7 @@ To access the default page for Admin Area settings:
| ------ | ----------- |
| [Elasticsearch](../../../integration/elasticsearch.md#enabling-advanced-search) | Elasticsearch integration. Elasticsearch AWS IAM. |
| [Kroki](../../../administration/integration/kroki.md#enable-kroki-in-gitlab) | Allow rendering of diagrams in AsciiDoc and Markdown documents using [kroki.io](https://kroki.io). |
| [Mailgun](../../../administration/integration/mailgun.md) | Enable your GitLab instance to receive invite email bounce events from Mailgun, if it is your email provider. |
| [PlantUML](../../../administration/integration/plantuml.md) | Allow rendering of PlantUML diagrams in documents. |
| [Slack application](../../../user/project/integrations/gitlab_slack_application.md#configuration) **(FREE SAAS)** | Slack integration allows you to interact with GitLab via slash commands in a chat window. This option is only available on GitLab.com, though it may be [available for self-managed instances in the future](https://gitlab.com/gitlab-org/gitlab/-/issues/28164). |
| [Third party offers](third_party_offers.md) | Control the display of third party offers. |

View File

@ -167,6 +167,7 @@ module API
mount ::API::Deployments
mount ::API::Environments
mount ::API::ErrorTracking
mount ::API::ErrorTrackingCollector
mount ::API::Events
mount ::API::FeatureFlags
mount ::API::FeatureFlagsUserLists

View File

@ -0,0 +1,73 @@
# frozen_string_literal: true
module API
# This API is responsible for collecting error tracking information
# from sentry client. It allows us to use GitLab as an alternative to
# sentry backend. For more details see https://gitlab.com/gitlab-org/gitlab/-/issues/329596.
class ErrorTrackingCollector < ::API::Base
feature_category :error_tracking
content_type :envelope, 'application/x-sentry-envelope'
default_format :envelope
before do
not_found!('Project') unless project
not_found! unless feature_enabled?
end
helpers do
def project
@project ||= find_project(params[:id])
end
def feature_enabled?
::Feature.enabled?(:integrated_error_tracking, project) &&
project.error_tracking_setting&.enabled?
end
end
desc 'Submit error tracking event to the project' do
detail 'This feature was introduced in GitLab 14.1.'
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
post 'error_tracking/collector/api/:id/envelope' do
# There is a reason why we have such uncommon path.
# We depend on a client side error tracking software which
# modifies URL for its own reasons.
#
# When we give user a URL like this
# HOST/api/v4/error_tracking/collector/123
#
# Then error tracking software will convert it like this:
# HOST/api/v4/error_tracking/collector/api/123/envelope/
begin
parsed_request = ::ErrorTracking::Collector::SentryRequestParser.parse(request)
rescue StandardError
render_api_error!('Failed to parse sentry request', 400)
end
type = parsed_request[:request_type]
# Sentry sends 2 requests on each exception: transaction and event.
# Everything else is not a desired behavior.
unless type == 'transaction' || type == 'event'
render_api_error!('400 Bad Request', 400)
break
end
# We don't have use for transaction request yet,
# so we record only event one.
if type == 'event'
::ErrorTracking::CollectErrorService
.new(project, nil, event: parsed_request[:event])
.execute
end
no_content!
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module ErrorTracking
module Collector
class SentryRequestParser
def self.parse(request)
# Request body can be "" or "gzip".
# If later then body was compressed with Zlib.gzip
encoding = request.headers['Content-Encoding']
body = if encoding == 'gzip'
Zlib.gunzip(request.body.read)
else
request.body.read
end
# Request body contains 3 json objects merged together in one StringIO.
# We need to separate and parse them into array of hash objects.
json_objects = []
parser = Yajl::Parser.new
parser.parse(body) do |json_object|
json_objects << json_object
end
# The request contains 3 objects: sentry metadata, type data and event data.
# We need only last two. Type to decide what to do with the request.
# And event data as it contains all information about the exception.
_, type, event = json_objects
{
request_type: type['type'],
event: event
}
end
end
end
end

View File

@ -11,11 +11,16 @@ module Gitlab
delegate :dig, to: :@seed_attributes
def initialize(context, attributes, previous_stages)
def initialize(context, attributes, previous_stages, current_stage)
@context = context
@pipeline = context.pipeline
@seed_attributes = attributes
@previous_stages = previous_stages
@stages_for_needs_lookup = if Feature.enabled?(:ci_same_stage_job_needs, @pipeline.project, default_enabled: :yaml)
(previous_stages + [current_stage]).compact
else
previous_stages
end
@needs_attributes = dig(:needs_attributes)
@resource_group_key = attributes.delete(:resource_group_key)
@job_variables = @seed_attributes.delete(:job_variables)
@ -148,14 +153,18 @@ module Gitlab
@needs_attributes.flat_map do |need|
next if need[:optional]
result = @previous_stages.any? do |stage|
stage.seeds_names.include?(need[:name])
end
result = need_present?(need)
"'#{name}' job needs '#{need[:name]}' job, but it was not added to the pipeline" unless result
"'#{name}' job needs '#{need[:name]}' job, but '#{need[:name]}' is not in any previous stage" unless result
end.compact
end
def need_present?(need)
@stages_for_needs_lookup.any? do |stage|
stage.seeds_names.include?(need[:name])
end
end
def max_needs_allowed
@pipeline.project.actual_limits.ci_needs_size_limit
end

View File

@ -17,7 +17,7 @@ module Gitlab
@previous_stages = previous_stages
@builds = attributes.fetch(:builds).map do |attributes|
Seed::Build.new(context, attributes, previous_stages)
Seed::Build.new(context, attributes, previous_stages, self)
end
end

View File

@ -46,6 +46,10 @@ module Gitlab
@jobs.each do |name, job|
validate_job!(name, job)
end
if ::Feature.enabled?(:ci_same_stage_job_needs, @opts[:project], default_enabled: :yaml)
YamlProcessor::Dag.check_circular_dependencies!(@jobs)
end
end
def validate_job!(name, job)
@ -99,10 +103,16 @@ module Gitlab
job_stage_index = stage_index(name)
dependency_stage_index = stage_index(dependency)
# A dependency might be defined later in the configuration
# with a stage that does not exist
unless dependency_stage_index.present? && dependency_stage_index < job_stage_index
error!("#{name} job: #{dependency_type} #{dependency} is not defined in prior stages")
if ::Feature.enabled?(:ci_same_stage_job_needs, @opts[:project], default_enabled: :yaml)
unless dependency_stage_index.present? && dependency_stage_index <= job_stage_index
error!("#{name} job: #{dependency_type} #{dependency} is not defined in current or prior stages")
end
else
# A dependency might be defined later in the configuration
# with a stage that does not exist
unless dependency_stage_index.present? && dependency_stage_index < job_stage_index
error!("#{name} job: #{dependency_type} #{dependency} is not defined in prior stages")
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
# Represents Dag pipeline
module Gitlab
module Ci
class YamlProcessor
class Dag
include TSort
MissingNodeError = Class.new(StandardError)
def initialize(nodes)
@nodes = nodes
end
def self.check_circular_dependencies!(jobs)
nodes = jobs.values.to_h do |job|
name = job[:name].to_s
needs = job.dig(:needs, :job).to_a
[name, needs.map { |need| need[:name].to_s }]
end
new(nodes).tsort
rescue TSort::Cyclic
raise ValidationError, 'The pipeline has circular dependencies.'
rescue MissingNodeError
end
def tsort_each_child(node, &block)
raise MissingNodeError, "node #{node} is missing" unless @nodes[node]
@nodes[node].each(&block)
end
def tsort_each_node(&block)
@nodes.each_key(&block)
end
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Sidebars
module Groups
class Context < ::Sidebars::Context
def initialize(current_user:, container:, **args)
super(current_user: current_user, container: container, group: container, **args)
end
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Sidebars
module Groups
class Panel < ::Sidebars::Panel
override :render_raw_scope_menu_partial
def render_raw_scope_menu_partial
'layouts/nav/sidebar/group_scope_menu'
end
override :render_raw_menus_partial
def render_raw_menus_partial
'layouts/nav/sidebar/group_menus'
end
override :aria_label
def aria_label
context.group.subgroup? ? _('Subgroup navigation') : _('Group navigation')
end
end
end
end

View File

@ -39411,9 +39411,6 @@ msgstr ""
msgid "mrWidget|You are not allowed to edit this project directly. Please fork to make changes."
msgstr ""
msgid "mrWidget|You can delete the source branch now"
msgstr ""
msgid "mrWidget|You can merge after removing denied licenses"
msgstr ""

View File

@ -6,7 +6,7 @@ module QA
class Menu < Page::Base
include SubMenus::Common
view 'app/views/layouts/nav/sidebar/_group.html.haml' do
view 'app/views/layouts/nav/sidebar/_group_menus.html.haml' do
element :general_settings_link
element :group_issues_item
element :group_members_item

View File

@ -12,8 +12,8 @@ module QA
super
base.class_eval do
view 'app/views/layouts/nav/sidebar/_group.html.haml' do
element :group_sidebar
view 'app/views/shared/nav/_sidebar.html.haml' do
element :group_sidebar, 'qa_selector: sidebar_qa_selector(sidebar.container)' # rubocop:disable QA/ElementWithPattern
end
end
end

View File

@ -40,7 +40,7 @@ function bundle_install_script() {
bundle config set path 'vendor'
bundle config set clean 'true'
echo $BUNDLE_WITHOUT
echo "${BUNDLE_WITHOUT}"
bundle config
run_timed_command "bundle install ${BUNDLE_INSTALL_FLAGS} ${extra_install_args} && bundle check"
@ -134,3 +134,10 @@ function fail_pipeline_early() {
scripts/api/cancel_pipeline.rb
fi
}
function danger_as_local() {
# Force danger to skip CI source GitLab and fallback to "local only git repo".
unset GITLAB_CI
# We need to base SHA to help danger determine the base commit for this shallow clone.
bundle exec danger dry_run --fail-on-errors=true --verbose --base="${CI_MERGE_REQUEST_DIFF_BASE_SHA}"
}

View File

@ -269,10 +269,7 @@ RSpec.describe 'Admin updates settings' do
end
context 'Integrations page' do
let(:mailgun_events_receiver_enabled) { true }
before do
stub_feature_flags(mailgun_events_receiver: mailgun_events_receiver_enabled)
visit general_admin_application_settings_path
end
@ -286,26 +283,16 @@ RSpec.describe 'Admin updates settings' do
expect(current_settings.hide_third_party_offers).to be true
end
context 'when mailgun_events_receiver feature flag is enabled' do
it 'enabling Mailgun events', :aggregate_failures do
page.within('.as-mailgun') do
check 'Enable Mailgun event receiver'
fill_in 'Mailgun HTTP webhook signing key', with: 'MAILGUN_SIGNING_KEY'
click_button 'Save changes'
end
expect(page).to have_content 'Application settings saved successfully'
expect(current_settings.mailgun_events_enabled).to be true
expect(current_settings.mailgun_signing_key).to eq 'MAILGUN_SIGNING_KEY'
it 'enabling Mailgun events', :aggregate_failures do
page.within('.as-mailgun') do
check 'Enable Mailgun event receiver'
fill_in 'Mailgun HTTP webhook signing key', with: 'MAILGUN_SIGNING_KEY'
click_button 'Save changes'
end
end
context 'when mailgun_events_receiver feature flag is disabled' do
let(:mailgun_events_receiver_enabled) { false }
it 'does not have mailgun' do
expect(page).not_to have_selector('.as-mailgun')
end
expect(page).to have_content 'Application settings saved successfully'
expect(current_settings.mailgun_events_enabled).to be true
expect(current_settings.mailgun_signing_key).to eq 'MAILGUN_SIGNING_KEY'
end
end

View File

@ -23,7 +23,7 @@ RSpec.describe 'User browse group projects page' do
visit projects_group_path(group)
expect(page).to have_link project.name
expect(page).to have_xpath("//span[@class='badge badge-warning']", text: 'archived')
expect(page).to have_css('span.badge.badge-warning', text: 'archived')
end
end
end

View File

@ -7,12 +7,14 @@ RSpec.describe ContainerRepositoriesFinder do
let_it_be(:guest) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:project_repository) { create(:container_repository, name: 'my_image', project: project) }
let(:params) { {} }
before do
project.project_feature.update!(container_registry_access_level: ProjectFeature::PRIVATE)
group.add_reporter(reporter)
project.add_reporter(reporter)
end
@ -77,6 +79,14 @@ RSpec.describe ContainerRepositoriesFinder do
it_behaves_like 'with name search'
it_behaves_like 'with sorting'
context 'when project has container registry disabled' do
before do
project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
end
it { is_expected.to match_array([other_repository]) }
end
end
context 'when subject_type is project' do
@ -86,6 +96,14 @@ RSpec.describe ContainerRepositoriesFinder do
it_behaves_like 'with name search'
it_behaves_like 'with sorting'
context 'when project has container registry disabled' do
before do
project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
end
it { is_expected.to be nil }
end
end
context 'with invalid subject_type' do
@ -96,9 +114,19 @@ RSpec.describe ContainerRepositoriesFinder do
end
context 'with unauthorized user' do
subject { described_class.new(user: guest, subject: group).execute }
subject { described_class.new(user: guest, subject: subject_type).execute }
it { is_expected.to be nil }
context 'when subject_type is group' do
let(:subject_type) { group }
it { is_expected.to be nil }
end
context 'when subject_type is project' do
let(:subject_type) { project }
it { is_expected.to be nil }
end
end
end
end

View File

@ -0,0 +1,3 @@
{"event_id":"7c9ae6e58f03442b9203bbdcf6ae904c","dsn":"http://1fedb514e17f4b958435093deb03048c@localhost:3000/api/v4/projects/7/error_tracking/collector/7","sdk":{"name":"sentry.ruby","version":"4.5.1"},"sent_at":"2021-07-08T12:59:16Z"}
{"type":"event","content_type":"application/json"}
{"event_id":"7c9ae6e58f03442b9203bbdcf6ae904c","level":"error","timestamp":"2021-07-08T12:59:16Z","release":"db853d7","environment":"development","server_name":"MacBook.local","modules":{"rake":"13.0.3","concurrent-ruby":"1.1.9","i18n":"1.8.10","minitest":"5.14.4","thread_safe":"0.3.6","tzinfo":"1.2.9","uglifier":"4.2.0","web-console":"3.7.0"},"message":"","user":{},"tags":{"request_id":"4253dcd9-5e48-474a-89b4-0e945ab825af"},"contexts":{"os":{"name":"Darwin","version":"Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64","build":"20.5.0","kernel_version":"Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64"},"runtime":{"name":"ruby","version":"ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin19]"},"trace":{"trace_id":"d82b93fbc39e4d13b85762afa2e3ff36","span_id":"4a3ed8701e7f4ea4","parent_span_id":null,"description":null,"op":"rails.request","status":null}},"extra":{},"fingerprint":[],"breadcrumbs":{"values":[{"category":"start_processing.action_controller","data":{"controller":"PostsController","action":"error2","params":{"controller":"posts","action":"error2"},"format":"html","method":"GET","path":"/posts/error2","start_timestamp":1625749156.5553},"level":null,"message":"","timestamp":1625749156,"type":null},{"category":"process_action.action_controller","data":{"controller":"PostsController","action":"error2","params":{"controller":"posts","action":"error2"},"format":"html","method":"GET","path":"/posts/error2","start_timestamp":1625749156.55539,"view_runtime":null,"db_runtime":0},"level":null,"message":"","timestamp":1625749156,"type":null}]},"transaction":"PostsController#error2","platform":"ruby","sdk":{"name":"sentry.ruby.rails","version":"4.5.1"},"request":{"url":"http://localhost/posts/error2","method":"GET","headers":{},"env":{"SERVER_NAME":"localhost","SERVER_PORT":"4444"}},"exception":{"values":[{"type":"ActionView::MissingTemplate","value":"Missing template posts/error2, application/error2 with {:locale=>[:en], :formats=>[:html], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]}. Searched in:\n * \"/Users/developer/rails-project/app/views\"\n","module":"ActionView","thread_id":70254489510160,"stacktrace":{"frames":[{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/puma-3.12.6/lib/puma/thread_pool.rb","function":"block in spawn_thread","lineno":135,"in_app":false,"filename":"puma/thread_pool.rb","pre_context":[" end\n","\n"," begin\n"],"context_line":" block.call(work, *extra)\n","post_context":[" rescue Exception => e\n"," STDERR.puts \"Error reached top of thread-pool: #{e.message} (#{e.class})\"\n"," end\n"]},{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/puma-3.12.6/lib/puma/server.rb","function":"block in run","lineno":334,"in_app":false,"filename":"puma/server.rb","pre_context":[" client.close\n"," else\n"," if process_now\n"],"context_line":" process_client client, buffer\n","post_context":[" else\n"," client.set_timeout @first_data_timeout\n"," @reactor.add client\n"]},{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.6/lib/action_view/path_set.rb","function":"find","lineno":48,"in_app":false,"filename":"action_view/path_set.rb","pre_context":[" end\n","\n"," def find(*args)\n"],"context_line":" find_all(*args).first || raise(MissingTemplate.new(self, *args))\n","post_context":[" end\n","\n"," def find_file(path, prefixes = [], *args)\n"]}]}}]}}

View File

@ -0,0 +1 @@
{"event_id":"7c9ae6e58f03442b9203bbdcf6ae904c","level":"error","timestamp":"2021-07-08T12:59:16Z","release":"db853d7","environment":"development","server_name":"MacBook.local","modules":{"rake":"13.0.3","concurrent-ruby":"1.1.9","i18n":"1.8.10","minitest":"5.14.4","thread_safe":"0.3.6","tzinfo":"1.2.9","uglifier":"4.2.0","web-console":"3.7.0"},"message":"","user":{},"tags":{"request_id":"4253dcd9-5e48-474a-89b4-0e945ab825af"},"contexts":{"os":{"name":"Darwin","version":"Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64","build":"20.5.0","kernel_version":"Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64"},"runtime":{"name":"ruby","version":"ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin19]"},"trace":{"trace_id":"d82b93fbc39e4d13b85762afa2e3ff36","span_id":"4a3ed8701e7f4ea4","parent_span_id":null,"description":null,"op":"rails.request","status":null}},"extra":{},"fingerprint":[],"breadcrumbs":{"values":[{"category":"start_processing.action_controller","data":{"controller":"PostsController","action":"error2","params":{"controller":"posts","action":"error2"},"format":"html","method":"GET","path":"/posts/error2","start_timestamp":1625749156.5553},"level":null,"message":"","timestamp":1625749156,"type":null},{"category":"process_action.action_controller","data":{"controller":"PostsController","action":"error2","params":{"controller":"posts","action":"error2"},"format":"html","method":"GET","path":"/posts/error2","start_timestamp":1625749156.55539,"view_runtime":null,"db_runtime":0},"level":null,"message":"","timestamp":1625749156,"type":null}]},"transaction":"PostsController#error2","platform":"ruby","sdk":{"name":"sentry.ruby.rails","version":"4.5.1"},"request":{"url":"http://localhost/posts/error2","method":"GET","headers":{},"env":{"SERVER_NAME":"localhost","SERVER_PORT":"4444"}},"exception":{"values":[{"type":"ActionView::MissingTemplate","value":"Missing template posts/error2, application/error2 with {:locale=>[:en], :formats=>[:html], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]}. Searched in:\n * \"/Users/developer/rails-project/app/views\"\n","module":"ActionView","thread_id":70254489510160,"stacktrace":{"frames":[{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/puma-3.12.6/lib/puma/thread_pool.rb","function":"block in spawn_thread","lineno":135,"in_app":false,"filename":"puma/thread_pool.rb","pre_context":[" end\n","\n"," begin\n"],"context_line":" block.call(work, *extra)\n","post_context":[" rescue Exception => e\n"," STDERR.puts \"Error reached top of thread-pool: #{e.message} (#{e.class})\"\n"," end\n"]},{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/puma-3.12.6/lib/puma/server.rb","function":"block in run","lineno":334,"in_app":false,"filename":"puma/server.rb","pre_context":[" client.close\n"," else\n"," if process_now\n"],"context_line":" process_client client, buffer\n","post_context":[" else\n"," client.set_timeout @first_data_timeout\n"," @reactor.add client\n"]},{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.6/lib/action_view/path_set.rb","function":"find","lineno":48,"in_app":false,"filename":"action_view/path_set.rb","pre_context":[" end\n","\n"," def find(*args)\n"],"context_line":" find_all(*args).first || raise(MissingTemplate.new(self, *args))\n","post_context":[" end\n","\n"," def find_file(path, prefixes = [], *args)\n"]}]}}]}}

View File

@ -0,0 +1,3 @@
{"event_id":"4a304dbdf3404e87962e99bced2f6c8b","dsn":"","sdk":{"name":"sentry.ruby","version":"4.5.1"},"sent_at":"2021-07-08T12:58:29Z"}
{"type":"transaction","content_type":"application/json"}
{}

View File

@ -0,0 +1,3 @@
{"event_id":"7c9ae6e58f03442b9203bbdcf6ae904c","dsn":"","sdk":{"name":"sentry.ruby","version":"4.5.1"},"sent_at":"2021-07-08T12:59:16Z"}
{"type":"unknown","content_type":"application/json"}
{}

View File

@ -483,11 +483,11 @@ describe('Settings Panel', () => {
it.each`
visibilityLevel | pagesAccessControlForced | output
${visibilityOptions.PRIVATE} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]}
${visibilityOptions.PRIVATE} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [visibilityOptions.PUBLIC, 'Everyone']]}
${visibilityOptions.PRIVATE} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]}
${visibilityOptions.INTERNAL} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]}
${visibilityOptions.INTERNAL} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [visibilityOptions.PUBLIC, 'Everyone']]}
${visibilityOptions.INTERNAL} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]}
${visibilityOptions.PUBLIC} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]}
${visibilityOptions.PUBLIC} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [visibilityOptions.PUBLIC, 'Everyone']]}
${visibilityOptions.PUBLIC} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]}
`(
'renders correct options when pagesAccessControlForced is $pagesAccessControlForced and visibilityLevel is $visibilityLevel',
async ({ visibilityLevel, pagesAccessControlForced, output }) => {

View File

@ -217,7 +217,6 @@ describe('MRWidgetMerged', () => {
vm.mr.sourceBranchRemoved = false;
Vue.nextTick(() => {
expect(vm.$el.innerText).toContain('You can delete the source branch now');
expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
done();
});
@ -229,7 +228,6 @@ describe('MRWidgetMerged', () => {
Vue.nextTick(() => {
expect(vm.$el.innerText).toContain('The source branch is being deleted');
expect(vm.$el.innerText).not.toContain('You can delete the source branch now');
expect(vm.$el.innerText).not.toContain('The source branch has been deleted');
done();
});

View File

@ -66,6 +66,7 @@ RSpec.describe PackagesHelper do
end
describe '#show_cleanup_policy_on_alert' do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:container_repository) { create(:container_repository) }
subject { helper.show_cleanup_policy_on_alert(project.reload) }
@ -203,9 +204,10 @@ RSpec.describe PackagesHelper do
with_them do
before do
allow(helper).to receive(:current_user).and_return(user)
allow(Gitlab).to receive(:com?).and_return(com)
stub_config(registry: { enabled: config_registry })
allow(project).to receive(:container_registry_enabled).and_return(project_registry)
allow(project).to receive(:feature_available?).with(:container_registry, user).and_return(project_registry)
stub_application_setting(container_expiration_policies_enable_historic_entries: historic_entries)
stub_feature_flags(container_expiration_policies_historic_entry: false)
stub_feature_flags(container_expiration_policies_historic_entry: project) if historic_entry

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ErrorTracking::Collector::SentryRequestParser do
describe '.parse' do
let_it_be(:raw_event) { fixture_file('error_tracking/event.txt') }
let_it_be(:parsed_event) { Gitlab::Json.parse(fixture_file('error_tracking/parsed_event.json')) }
let(:body) { raw_event }
let(:headers) { { 'Content-Encoding' => '' } }
let(:request) { double('request', headers: headers, body: StringIO.new(body)) }
subject { described_class.parse(request) }
RSpec.shared_examples 'valid parser' do
it 'returns a valid hash' do
parsed_request = subject
expect(parsed_request[:request_type]).to eq('event')
expect(parsed_request[:event]).to eq(parsed_event)
end
end
context 'empty body content' do
let(:body) { '' }
it 'fails with exception' do
expect { subject }.to raise_error(StandardError)
end
end
context 'plain text sentry request' do
it_behaves_like 'valid parser'
end
context 'gzip encoded sentry request' do
let(:headers) { { 'Content-Encoding' => 'gzip' } }
let(:body) { Zlib.gzip(raw_event) }
it_behaves_like 'valid parser'
end
end
end

View File

@ -247,7 +247,7 @@ RSpec.describe Gitlab::Ci::Lint do
include_context 'advanced validations' do
it 'runs advanced logical validations' do
expect(subject).not_to be_valid
expect(subject.errors).to eq(["'test' job needs 'build' job, but it was not added to the pipeline"])
expect(subject.errors).to eq(["'test' job needs 'build' job, but 'build' is not in any previous stage"])
end
end

View File

@ -11,8 +11,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
let(:seed_context) { double(pipeline: pipeline, root_variables: root_variables) }
let(:attributes) { { name: 'rspec', ref: 'master', scheduling_type: :stage } }
let(:previous_stages) { [] }
let(:current_stage) { double(seeds_names: [attributes[:name]]) }
let(:seed_build) { described_class.new(seed_context, attributes, previous_stages) }
let(:seed_build) { described_class.new(seed_context, attributes, previous_stages, current_stage) }
describe '#attributes' do
subject { seed_build.attributes }
@ -1079,7 +1080,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it "returns an error" do
expect(subject.errors).to contain_exactly(
"'rspec' job needs 'build' job, but it was not added to the pipeline")
"'rspec' job needs 'build' job, but 'build' is not in any previous stage")
end
context 'when the needed job is optional' do
@ -1115,6 +1116,28 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
context 'when build job is part of the same stage' do
let(:current_stage) { double(seeds_names: [attributes[:name], 'build']) }
it 'is included' do
is_expected.to be_included
end
it 'does not have errors' do
expect(subject.errors).to be_empty
end
context 'when ci_same_stage_job_needs FF is disabled' do
before do
stub_feature_flags(ci_same_stage_job_needs: false)
end
it 'has errors' do
expect(subject.errors).to contain_exactly("'rspec' job needs 'build' job, but 'build' is not in any previous stage")
end
end
end
context 'when using 101 needs' do
let(:needs_count) { 101 }

View File

@ -34,6 +34,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do
described_class.new(seed_context, stages_attributes)
end
before do
stub_feature_flags(ci_same_stage_job_needs: false)
end
describe '#stages' do
it 'returns the stage resources' do
stages = seed.stages
@ -65,7 +69,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do
}
expect(seed.errors).to contain_exactly(
"'invalid_job' job needs 'non-existent' job, but it was not added to the pipeline")
"'invalid_job' job needs 'non-existent' job, but 'non-existent' is not in any previous stage")
end
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::YamlProcessor::Dag do
let(:nodes) { {} }
subject(:result) { described_class.new(nodes).tsort }
context 'when it is a regular pipeline' do
let(:nodes) do
{ 'job_c' => %w(job_b job_d), 'job_d' => %w(job_a), 'job_b' => %w(job_a), 'job_a' => %w() }
end
it 'returns ordered jobs' do
expect(result).to eq(%w(job_a job_b job_d job_c))
end
end
context 'when there is a circular dependency' do
let(:nodes) do
{ 'job_a' => %w(job_c), 'job_b' => %w(job_a), 'job_c' => %w(job_b) }
end
it 'raises TSort::Cyclic' do
expect { result }.to raise_error(TSort::Cyclic, /topological sort failed/)
end
end
context 'when there is a missing job' do
let(:nodes) do
{ 'job_a' => %w(job_d), 'job_b' => %w(job_a) }
end
it 'raises MissingNodeError' do
expect { result }.to raise_error(
Gitlab::Ci::YamlProcessor::Dag::MissingNodeError, 'node job_d is missing'
)
end
end
end

View File

@ -595,7 +595,15 @@ module Gitlab
EOYML
end
it_behaves_like 'has warnings and expected error', /build job: need test is not defined in prior stages/
it_behaves_like 'has warnings and expected error', /build job: need test is not defined in current or prior stages/
context 'with ci_same_stage_job_needs FF disabled' do
before do
stub_feature_flags(ci_same_stage_job_needs: false)
end
it_behaves_like 'has warnings and expected error', /build job: need test is not defined in prior stages/
end
end
end
end
@ -1858,7 +1866,7 @@ module Gitlab
build2: { stage: 'build', script: 'test' },
test1: { stage: 'test', script: 'test', dependencies: dependencies },
test2: { stage: 'test', script: 'test' },
deploy: { stage: 'test', script: 'test' }
deploy: { stage: 'deploy', script: 'test' }
}
end
@ -1891,7 +1899,15 @@ module Gitlab
context 'dependencies to deploy' do
let(:dependencies) { ['deploy'] }
it_behaves_like 'returns errors', 'test1 job: dependency deploy is not defined in prior stages'
it_behaves_like 'returns errors', 'test1 job: dependency deploy is not defined in current or prior stages'
context 'with ci_same_stage_job_needs FF disabled' do
before do
stub_feature_flags(ci_same_stage_job_needs: false)
end
it_behaves_like 'returns errors', 'test1 job: dependency deploy is not defined in prior stages'
end
end
context 'when a job depends on another job that references a not-yet defined stage' do
@ -1916,7 +1932,7 @@ module Gitlab
}
end
it_behaves_like 'returns errors', /is not defined in prior stages/
it_behaves_like 'returns errors', /is not defined in current or prior stages/
end
end
@ -1931,7 +1947,7 @@ module Gitlab
parallel: { stage: 'build', script: 'test', parallel: 2 },
test1: { stage: 'test', script: 'test', needs: needs, dependencies: dependencies },
test2: { stage: 'test', script: 'test' },
deploy: { stage: 'test', script: 'test' }
deploy: { stage: 'deploy', script: 'test' }
}
end
@ -1941,6 +1957,45 @@ module Gitlab
it { is_expected.to be_valid }
end
context 'needs a job from the same stage' do
let(:needs) { %w(test2) }
it 'creates jobs with valid specifications' do
expect(subject.builds.size).to eq(7)
expect(subject.builds[0]).to eq(
stage: 'build',
stage_idx: 1,
name: 'build1',
only: { refs: %w[branches tags] },
options: {
script: ['test']
},
when: 'on_success',
allow_failure: false,
yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
)
expect(subject.builds[4]).to eq(
stage: 'test',
stage_idx: 2,
name: 'test1',
only: { refs: %w[branches tags] },
options: { script: ['test'] },
needs_attributes: [
{ name: 'test2', artifacts: true, optional: false }
],
when: 'on_success',
allow_failure: false,
yaml_variables: [],
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :dag
)
end
end
context 'needs two builds' do
let(:needs) { %w(build1 build2) }
@ -2096,7 +2151,15 @@ module Gitlab
context 'needs to deploy' do
let(:needs) { ['deploy'] }
it_behaves_like 'returns errors', 'test1 job: need deploy is not defined in prior stages'
it_behaves_like 'returns errors', 'test1 job: need deploy is not defined in current or prior stages'
context 'with ci_same_stage_job_needs FF disabled' do
before do
stub_feature_flags(ci_same_stage_job_needs: false)
end
it_behaves_like 'returns errors', 'test1 job: need deploy is not defined in prior stages'
end
end
context 'needs and dependencies that are mismatching' do
@ -2767,6 +2830,29 @@ module Gitlab
it_behaves_like 'returns errors', 'jobs:rspec:parallel should be an integer or a hash'
end
context 'when the pipeline has a circular dependency' do
let(:config) do
<<~YAML
job_a:
stage: test
script: build
needs: [job_c]
job_b:
stage: test
script: test
needs: [job_a]
job_c:
stage: test
script: deploy
needs: [job_b]
YAML
end
it_behaves_like 'returns errors', 'The pipeline has circular dependencies.'
end
end
describe '#execute' do

View File

@ -578,6 +578,7 @@ project:
- merge_request_metrics
- security_orchestration_policy_configuration
- timelogs
- error_tracking_errors
award_emoji:
- awardable
- user

View File

@ -167,6 +167,7 @@ ProjectMember:
- expires_at
- ldap
- override
- invite_email_success
User:
- id
- username

View File

@ -827,15 +827,15 @@ RSpec.describe Notify do
end
end
context 'when on gitlab.com' do
context 'when mailgun events are enabled' do
before do
allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
stub_application_setting(mailgun_events_enabled: true)
end
it 'has custom headers' do
aggregate_failures do
expect(subject).to have_header('X-Mailgun-Tag', 'invite_email')
expect(subject).to have_header('X-Mailgun-Variables', { 'invite_token' => project_member.invite_token }.to_json)
expect(subject).to have_header('X-Mailgun-Tag', ::Members::Mailgun::INVITE_EMAIL_TAG)
expect(subject).to have_header('X-Mailgun-Variables', { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => project_member.invite_token }.to_json)
end
end
end

View File

@ -55,6 +55,24 @@ RSpec.describe Ci::BuildDependencies do
end
end
end
context 'when needs refer to jobs from the same stage' do
let(:job) do
create(:ci_build,
pipeline: pipeline,
name: 'dag_job',
scheduling_type: :dag,
stage_idx: 2,
stage: 'deploy'
)
end
before do
create(:ci_build_need, build: job, name: 'staging', artifacts: true)
end
it { is_expected.to contain_exactly(staging) }
end
end
describe 'jobs from specified dependencies' do

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::ErrorTrackingCollector do
let_it_be(:project) { create(:project, :private) }
let_it_be(:setting) { create(:project_error_tracking_setting, project: project) }
describe "POST /error_tracking/collector/api/:id/envelope" do
let_it_be(:raw_event) { fixture_file('error_tracking/event.txt') }
let_it_be(:url) { "/error_tracking/collector/api/#{project.id}/envelope" }
let(:params) { raw_event }
subject { post api(url), params: params }
RSpec.shared_examples 'not found' do
it 'reponds with 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
RSpec.shared_examples 'bad request' do
it 'responds with 400' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'error tracking feature is disabled' do
before do
setting.update!(enabled: false)
end
it_behaves_like 'not found'
end
context 'feature flag is disabled' do
before do
stub_feature_flags(integrated_error_tracking: false)
end
it_behaves_like 'not found'
end
context 'empty body' do
let(:params) { '' }
it_behaves_like 'bad request'
end
context 'unknown request type' do
let(:params) { fixture_file('error_tracking/unknown.txt') }
it_behaves_like 'bad request'
end
context 'transaction request type' do
let(:params) { fixture_file('error_tracking/transaction.txt') }
it 'does nothing and returns no content' do
expect { subject }.not_to change { ErrorTracking::ErrorEvent.count }
expect(response).to have_gitlab_http_status(:no_content)
end
end
it 'writes to the database and returns no content' do
expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1)
expect(response).to have_gitlab_http_status(:no_content)
end
end
end

View File

@ -0,0 +1,128 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'receive a permanent failure' do
describe 'POST /members/mailgun/permanent_failures', :aggregate_failures do
let_it_be(:member) { create(:project_member, :invited) }
let(:raw_invite_token) { member.raw_invite_token }
let(:mailgun_events) { true }
let(:mailgun_signing_key) { 'abc123' }
subject(:post_request) { post members_mailgun_permanent_failures_path(standard_params) }
before do
stub_application_setting(mailgun_events_enabled: mailgun_events, mailgun_signing_key: mailgun_signing_key)
end
it 'marks the member invite email success as false' do
expect { post_request }.to change { member.reload.invite_email_success }.from(true).to(false)
expect(response).to have_gitlab_http_status(:ok)
end
context 'when the change to a member is not made' do
context 'with incorrect signing key' do
context 'with incorrect signing key' do
let(:mailgun_signing_key) { '_foobar_' }
it 'does not change member status and responds as not_found' do
expect { post_request }.not_to change { member.reload.invite_email_success }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'with nil signing key' do
let(:mailgun_signing_key) { nil }
it 'does not change member status and responds as not_found' do
expect { post_request }.not_to change { member.reload.invite_email_success }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when the feature is not enabled' do
let(:mailgun_events) { false }
it 'does not change member status and responds as expected' do
expect { post_request }.not_to change { member.reload.invite_email_success }
expect(response).to have_gitlab_http_status(:not_acceptable)
end
end
context 'when it is not an invite email' do
before do
stub_const('::Members::Mailgun::INVITE_EMAIL_TAG', '_foobar_')
end
it 'does not change member status and responds as expected' do
expect { post_request }.not_to change { member.reload.invite_email_success }
expect(response).to have_gitlab_http_status(:not_acceptable)
end
end
end
def standard_params
{
"signature": {
"timestamp": "1625056677",
"token": "eb944d0ace7227667a1b97d2d07276ae51d2b849ed2cfa68f3",
"signature": "9790cc6686eb70f0b1f869180d906870cdfd496d27fee81da0aa86b9e539e790"
},
"event-data": {
"severity": "permanent",
"tags": ["invite_email"],
"timestamp": 1521233195.375624,
"storage": {
"url": "_anything_",
"key": "_anything_"
},
"log-level": "error",
"id": "_anything_",
"campaigns": [],
"reason": "suppress-bounce",
"user-variables": {
"invite_token": raw_invite_token
},
"flags": {
"is-routed": false,
"is-authenticated": true,
"is-system-test": false,
"is-test-mode": false
},
"recipient-domain": "example.com",
"envelope": {
"sender": "bob@mg.gitlab.com",
"transport": "smtp",
"targets": "alice@example.com"
},
"message": {
"headers": {
"to": "Alice <alice@example.com>",
"message-id": "20130503192659.13651.20287@mg.gitlab.com",
"from": "Bob <bob@mg.gitlab.com>",
"subject": "Test permanent_fail webhook"
},
"attachments": [],
"size": 111
},
"recipient": "alice@example.com",
"event": "failed",
"delivery-status": {
"attempt-no": 1,
"message": "",
"code": 605,
"description": "Not delivering to previously bounced address",
"session-seconds": 0
}
}
}
end
end
end

View File

@ -8,9 +8,9 @@ RSpec.describe Ci::AfterRequeueJobService do
let(:pipeline) { create(:ci_pipeline, project: project) }
let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
let!(:test1) { create(:ci_build, :success, pipeline: pipeline, stage_idx: 1) }
let!(:test2) { create(:ci_build, :skipped, pipeline: pipeline, stage_idx: 1) }
let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 0, name: 'build') }
subject(:execute_service) { described_class.new(project, user).execute(build) }
@ -24,6 +24,34 @@ RSpec.describe Ci::AfterRequeueJobService do
expect(test2.reload).to be_created
end
context 'when there is a job need from the same stage' do
let!(:test3) do
create(:ci_build,
:skipped,
pipeline: pipeline,
stage_idx: 0,
scheduling_type: :dag)
end
before do
create(:ci_build_need, build: test3, name: 'build')
end
it 'marks subsequent skipped jobs as processable' do
expect { execute_service }.to change { test3.reload.status }.from('skipped').to('created')
end
context 'with ci_same_stage_job_needs FF disabled' do
before do
stub_feature_flags(ci_same_stage_job_needs: false)
end
it 'does nothing with the build' do
expect { execute_service }.not_to change { test3.reload.status }
end
end
end
context 'when the pipeline is a downstream pipeline and the bridge is depended' do
let!(:trigger_job) { create(:ci_bridge, :strategy_depend, status: 'success') }

View File

@ -69,7 +69,7 @@ RSpec.describe Ci::CreatePipelineService do
end
it 'contains both errors and warnings' do
error_message = 'build job: need test is not defined in prior stages'
error_message = 'build job: need test is not defined in current or prior stages'
warning_message = /jobs:test may allow multiple pipelines to run/
expect(pipeline.yaml_errors).to eq(error_message)

View File

@ -84,7 +84,7 @@ RSpec.describe Ci::CreatePipelineService do
it_behaves_like 'returns a non persisted pipeline'
it 'returns a pipeline with errors', :aggregate_failures do
error_message = 'build job: need test is not defined in prior stages'
error_message = 'build job: need test is not defined in current or prior stages'
expect(subject.error_messages.map(&:content)).to eq([error_message])
expect(subject.errors).not_to be_empty
@ -109,7 +109,7 @@ RSpec.describe Ci::CreatePipelineService do
it_behaves_like 'returns a non persisted pipeline'
it 'returns a pipeline with errors', :aggregate_failures do
error_message = "'test' job needs 'build' job, but it was not added to the pipeline"
error_message = "'test' job needs 'build' job, but 'build' is not in any previous stage"
expect(subject.error_messages.map(&:content)).to eq([error_message])
expect(subject.errors).not_to be_empty

View File

@ -257,7 +257,7 @@ RSpec.describe Ci::CreatePipelineService do
it 'returns error' do
expect(pipeline.yaml_errors)
.to eq("'test' job needs 'build' job, but it was not added to the pipeline")
.to eq("'test' job needs 'build' job, but 'build' is not in any previous stage")
end
context 'when need is optional' do

View File

@ -252,7 +252,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
end
it_behaves_like 'creation failure' do
let(:expected_error) { /test job: dependency generator is not defined in prior stages/ }
let(:expected_error) { /test job: dependency generator is not defined in current or prior stages/ }
end
end

View File

@ -1715,7 +1715,7 @@ RSpec.describe Ci::CreatePipelineService do
it 'contains the expected errors' do
expect(pipeline.builds).to be_empty
error_message = "'test_a' job needs 'build_a' job, but it was not added to the pipeline"
error_message = "'test_a' job needs 'build_a' job, but 'build_a' is not in any previous stage"
expect(pipeline.yaml_errors).to eq(error_message)
expect(pipeline.error_messages.map(&:content)).to contain_exactly(error_message)
expect(pipeline.errors[:base]).to contain_exactly(error_message)

View File

@ -0,0 +1,47 @@
config:
build:
stage: test
script: exit 0
test:
stage: test
script: exit 0
needs: [build]
deploy:
stage: test
script: exit 0
needs: [test]
init:
expect:
pipeline: pending
stages:
test: pending
jobs:
build: pending
test: created
deploy: created
transitions:
- event: success
jobs: [build]
expect:
pipeline: running
stages:
test: running
jobs:
build: success
test: pending
deploy: created
- event: success
jobs: [test]
expect:
pipeline: running
stages:
test: running
jobs:
build: success
test: success
deploy: pending

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ErrorTracking::CollectErrorService do
let_it_be(:project) { create(:project) }
let_it_be(:parsed_event) { Gitlab::Json.parse(fixture_file('error_tracking/parsed_event.json')) }
subject { described_class.new(project, nil, event: parsed_event) }
describe '#execute' do
it 'creates Error and creates ErrorEvent' do
expect { subject.execute }
.to change { ErrorTracking::Error.count }.by(1)
.and change { ErrorTracking::ErrorEvent.count }.by(1)
end
it 'updates Error and created ErrorEvent on second hit' do
subject.execute
expect { subject.execute }.not_to change { ErrorTracking::Error.count }
expect { subject.execute }.to change { ErrorTracking::ErrorEvent.count }.by(1)
end
it 'has correct values set' do
subject.execute
event = ErrorTracking::ErrorEvent.last
error = event.error
expect(error.name).to eq 'ActionView::MissingTemplate'
expect(error.description).to start_with 'Missing template posts/error2'
expect(error.actor).to eq 'PostsController#error2'
expect(error.platform).to eq 'ruby'
expect(error.last_seen_at).to eq '2021-07-08T12:59:16Z'
expect(event.description).to eq 'ActionView::MissingTemplate'
expect(event.occurred_at).to eq '2021-07-08T12:59:16Z'
expect(event.level).to eq 'error'
expect(event.environment).to eq 'development'
expect(event.payload).to eq parsed_event
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Members::Mailgun::ProcessWebhookService do
describe '#execute', :aggregate_failures do
let_it_be(:member) { create(:project_member, :invited) }
let(:raw_invite_token) { member.raw_invite_token }
let(:payload) { { 'user-variables' => { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => raw_invite_token } } }
subject(:service) { described_class.new(payload).execute }
it 'marks the member invite email success as false' do
expect(Gitlab::AppLogger).to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/).and_call_original
expect { service }.to change { member.reload.invite_email_success }.from(true).to(false)
end
context 'when member can not be found' do
let(:raw_invite_token) { '_foobar_' }
it 'does not change member status' do
expect(Gitlab::AppLogger).not_to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/)
expect { service }.not_to change { member.reload.invite_email_success }
end
end
context 'when invite token is not found in payload' do
let(:payload) { {} }
it 'does not change member status and logs an error' do
expect(Gitlab::AppLogger).not_to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/)
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
an_instance_of(described_class::ProcessWebhookServiceError))
expect { service }.not_to change { member.reload.invite_email_success }
end
end
end
end