Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e9112f3002
commit
1aa9cd3080
39 changed files with 1706 additions and 45 deletions
|
@ -40,7 +40,7 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
|
|||
project_pipelines_url(group.projects.first)
|
||||
when :trial, :trial_short
|
||||
'https://about.gitlab.com/free-trial/'
|
||||
when :team, :team_short
|
||||
when :team, :team_short, :invite_team
|
||||
group_group_members_url(group)
|
||||
when :admin_verify
|
||||
project_settings_ci_cd_path(group.projects.first, ci_runner_templates: true, anchor: 'js-runners-settings')
|
||||
|
@ -59,6 +59,11 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
|
|||
@track = params[:track]&.to_sym
|
||||
@series = params[:series]&.to_i
|
||||
|
||||
# There is only one email that will be sent for invite team track so series
|
||||
# should only have the value 0. Return early if track is invite team and
|
||||
# condition for series value is met
|
||||
return if @track == Namespaces::InviteTeamEmailService::TRACK && @series == 0
|
||||
|
||||
track_valid = @track.in?(Namespaces::InProductMarketingEmailsService::TRACKS.keys)
|
||||
return render_404 unless track_valid
|
||||
|
||||
|
|
|
@ -22,7 +22,8 @@ module Users
|
|||
experience: 4,
|
||||
team_short: 5,
|
||||
trial_short: 6,
|
||||
admin_verify: 7
|
||||
admin_verify: 7,
|
||||
invite_team: 8
|
||||
}, _suffix: true
|
||||
|
||||
scope :without_track_and_series, -> (track, series) do
|
||||
|
|
|
@ -57,7 +57,10 @@ module Groups
|
|||
end
|
||||
|
||||
def after_create_hook
|
||||
# overridden in EE
|
||||
if group.persisted? && group.root?
|
||||
delay = Namespaces::InviteTeamEmailService::DELIVERY_DELAY_IN_MINUTES
|
||||
Namespaces::InviteTeamEmailWorker.perform_in(delay, group.id, current_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_unallowed_params
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Namespaces
|
||||
class InProductMarketingEmailRecords
|
||||
attr_reader :records
|
||||
|
||||
def initialize
|
||||
@records = []
|
||||
end
|
||||
|
||||
def save!
|
||||
Users::InProductMarketingEmail.bulk_insert!(@records)
|
||||
@records = []
|
||||
end
|
||||
|
||||
def add(user, track, series)
|
||||
@records << Users::InProductMarketingEmail.new(
|
||||
user: user,
|
||||
track: track,
|
||||
series: series,
|
||||
created_at: Time.zone.now,
|
||||
updated_at: Time.zone.now
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -56,7 +56,7 @@ module Namespaces
|
|||
def initialize(track, interval)
|
||||
@track = track
|
||||
@interval = interval
|
||||
@in_product_marketing_email_records = []
|
||||
@sent_email_records = InProductMarketingEmailRecords.new
|
||||
end
|
||||
|
||||
def execute
|
||||
|
@ -71,17 +71,21 @@ module Namespaces
|
|||
|
||||
private
|
||||
|
||||
attr_reader :track, :interval, :in_product_marketing_email_records
|
||||
attr_reader :track, :interval, :sent_email_records
|
||||
|
||||
def send_email(user, group)
|
||||
NotificationService.new.in_product_marketing(user.id, group.id, track, series)
|
||||
end
|
||||
|
||||
def send_email_for_group(group)
|
||||
users_for_group(group).each do |user|
|
||||
if can_perform_action?(user, group)
|
||||
send_email(user, group)
|
||||
track_sent_email(user, track, series)
|
||||
sent_email_records.add(user, track, series)
|
||||
end
|
||||
end
|
||||
|
||||
save_tracked_emails!
|
||||
sent_email_records.save!
|
||||
end
|
||||
|
||||
def groups_for_track
|
||||
|
@ -126,10 +130,6 @@ module Namespaces
|
|||
end
|
||||
end
|
||||
|
||||
def send_email(user, group)
|
||||
NotificationService.new.in_product_marketing(user.id, group.id, track, series)
|
||||
end
|
||||
|
||||
def completed_actions
|
||||
TRACKS[track][:completed_actions]
|
||||
end
|
||||
|
@ -146,21 +146,6 @@ module Namespaces
|
|||
def series
|
||||
TRACKS[track][:interval_days].index(interval)
|
||||
end
|
||||
|
||||
def save_tracked_emails!
|
||||
Users::InProductMarketingEmail.bulk_insert!(in_product_marketing_email_records)
|
||||
@in_product_marketing_email_records = []
|
||||
end
|
||||
|
||||
def track_sent_email(user, track, series)
|
||||
in_product_marketing_email_records << Users::InProductMarketingEmail.new(
|
||||
user: user,
|
||||
track: track,
|
||||
series: series,
|
||||
created_at: Time.zone.now,
|
||||
updated_at: Time.zone.now
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
62
app/services/namespaces/invite_team_email_service.rb
Normal file
62
app/services/namespaces/invite_team_email_service.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Namespaces
|
||||
class InviteTeamEmailService
|
||||
include Gitlab::Experiment::Dsl
|
||||
|
||||
TRACK = :invite_team
|
||||
DELIVERY_DELAY_IN_MINUTES = 20.minutes
|
||||
|
||||
def self.send_email(user, group)
|
||||
new(user, group).execute
|
||||
end
|
||||
|
||||
def initialize(user, group)
|
||||
@group = group
|
||||
@user = user
|
||||
@sent_email_records = InProductMarketingEmailRecords.new
|
||||
end
|
||||
|
||||
def execute
|
||||
return unless user.email_opted_in?
|
||||
return unless group.root?
|
||||
return unless group.setup_for_company
|
||||
|
||||
# Exclude group if users other than the creator have already been
|
||||
# added/invited
|
||||
return unless group.member_count == 1
|
||||
|
||||
return if email_for_track_sent_to_user?
|
||||
|
||||
experiment(:invite_team_email, group: group) do |e|
|
||||
e.candidate do
|
||||
send_email(user, group)
|
||||
sent_email_records.add(user, track, series)
|
||||
sent_email_records.save!
|
||||
end
|
||||
|
||||
e.record!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :group, :sent_email_records
|
||||
|
||||
def send_email(user, group)
|
||||
NotificationService.new.in_product_marketing(user.id, group.id, track, series)
|
||||
end
|
||||
|
||||
def track
|
||||
TRACK
|
||||
end
|
||||
|
||||
def series
|
||||
0
|
||||
end
|
||||
|
||||
def email_for_track_sent_to_user?
|
||||
Users::InProductMarketingEmail.for_user_with_track_and_series(user, track, series).present?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2411,6 +2411,15 @@
|
|||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: namespaces_invite_team_email
|
||||
:worker_name: Namespaces::InviteTeamEmailWorker
|
||||
:feature_category: :experimentation_activation
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: namespaces_onboarding_issue_created
|
||||
:worker_name: Namespaces::OnboardingIssueCreatedWorker
|
||||
:feature_category: :onboarding
|
||||
|
|
22
app/workers/namespaces/invite_team_email_worker.rb
Normal file
22
app/workers/namespaces/invite_team_email_worker.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Namespaces
|
||||
class InviteTeamEmailWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
|
||||
data_consistency :always
|
||||
|
||||
feature_category :experimentation_activation
|
||||
urgency :low
|
||||
|
||||
def perform(group_id, user_id)
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
user = User.find_by(id: user_id)
|
||||
group = Group.find_by(id: group_id)
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
return unless user && group
|
||||
|
||||
Namespaces::InviteTeamEmailService.send_email(user, group)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340388
|
|||
milestone: '14.3'
|
||||
type: development
|
||||
group: group::dynamic analysis
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
8
config/feature_flags/experiment/invite_team_email.yml
Normal file
8
config/feature_flags/experiment/invite_team_email.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: invite_team_email
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72470
|
||||
rollout_issue_url:
|
||||
milestone: '14.5'
|
||||
type: experiment
|
||||
group: group::activation
|
||||
default_enabled: false
|
|
@ -255,6 +255,8 @@
|
|||
- 1
|
||||
- - namespaceless_project_destroy
|
||||
- 1
|
||||
- - namespaces_invite_team_email
|
||||
- 1
|
||||
- - namespaces_onboarding_issue_created
|
||||
- 1
|
||||
- - namespaces_onboarding_pipeline_created
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
response = ::Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService.new.execute
|
||||
response = Sidekiq::Worker.skipping_transaction_check do
|
||||
::Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService.new.execute
|
||||
end
|
||||
|
||||
if response[:status] == :success
|
||||
puts "Successfully created self monitoring project."
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexesToIssueStageEvents < Gitlab::Database::Migration[1.0]
|
||||
include Gitlab::Database::PartitioningMigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
GROUP_INDEX_NAME = 'index_issue_stage_events_group_duration'
|
||||
GROUP_IN_PROGRESS_INDEX_NAME = 'index_issue_stage_events_group_in_progress_duration'
|
||||
PROJECT_INDEX_NAME = 'index_issue_stage_events_project_duration'
|
||||
PROJECT_IN_PROGRESS_INDEX_NAME = 'index_issue_stage_events_project_in_progress_duration'
|
||||
|
||||
def up
|
||||
add_concurrent_partitioned_index :analytics_cycle_analytics_issue_stage_events,
|
||||
'stage_event_hash_id, group_id, end_event_timestamp, issue_id, start_event_timestamp',
|
||||
where: 'end_event_timestamp IS NOT NULL',
|
||||
name: GROUP_INDEX_NAME
|
||||
|
||||
add_concurrent_partitioned_index :analytics_cycle_analytics_issue_stage_events,
|
||||
'stage_event_hash_id, project_id, end_event_timestamp, issue_id, start_event_timestamp',
|
||||
where: 'end_event_timestamp IS NOT NULL',
|
||||
name: PROJECT_INDEX_NAME
|
||||
|
||||
add_concurrent_partitioned_index :analytics_cycle_analytics_issue_stage_events,
|
||||
'stage_event_hash_id, group_id, start_event_timestamp, issue_id',
|
||||
where: 'end_event_timestamp IS NULL AND state_id = 1',
|
||||
name: GROUP_IN_PROGRESS_INDEX_NAME
|
||||
|
||||
add_concurrent_partitioned_index :analytics_cycle_analytics_issue_stage_events,
|
||||
'stage_event_hash_id, project_id, start_event_timestamp, issue_id',
|
||||
where: 'end_event_timestamp IS NULL AND state_id = 1',
|
||||
name: PROJECT_IN_PROGRESS_INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_issue_stage_events, GROUP_INDEX_NAME
|
||||
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_issue_stage_events, PROJECT_INDEX_NAME
|
||||
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_issue_stage_events, GROUP_IN_PROGRESS_INDEX_NAME
|
||||
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_issue_stage_events, PROJECT_IN_PROGRESS_INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexesToMergeRequestStageEvents < Gitlab::Database::Migration[1.0]
|
||||
include Gitlab::Database::PartitioningMigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
GROUP_INDEX_NAME = 'index_merge_request_stage_events_group_duration'
|
||||
GROUP_IN_PROGRESS_INDEX_NAME = 'index_merge_request_stage_events_group_in_progress_duration'
|
||||
PROJECT_INDEX_NAME = 'index_merge_request_stage_events_project_duration'
|
||||
PROJECT_IN_PROGRESS_INDEX_NAME = 'index_merge_request_stage_events_project_in_progress_duration'
|
||||
|
||||
def up
|
||||
add_concurrent_partitioned_index :analytics_cycle_analytics_merge_request_stage_events,
|
||||
'stage_event_hash_id, group_id, end_event_timestamp, merge_request_id, start_event_timestamp',
|
||||
where: 'end_event_timestamp IS NOT NULL',
|
||||
name: GROUP_INDEX_NAME
|
||||
|
||||
add_concurrent_partitioned_index :analytics_cycle_analytics_merge_request_stage_events,
|
||||
'stage_event_hash_id, project_id, end_event_timestamp, merge_request_id, start_event_timestamp',
|
||||
where: 'end_event_timestamp IS NOT NULL',
|
||||
name: PROJECT_INDEX_NAME
|
||||
|
||||
add_concurrent_partitioned_index :analytics_cycle_analytics_merge_request_stage_events,
|
||||
'stage_event_hash_id, group_id, start_event_timestamp, merge_request_id',
|
||||
where: 'end_event_timestamp IS NULL AND state_id = 1',
|
||||
name: GROUP_IN_PROGRESS_INDEX_NAME
|
||||
|
||||
add_concurrent_partitioned_index :analytics_cycle_analytics_merge_request_stage_events,
|
||||
'stage_event_hash_id, project_id, start_event_timestamp, merge_request_id',
|
||||
where: 'end_event_timestamp IS NULL AND state_id = 1',
|
||||
name: PROJECT_IN_PROGRESS_INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_merge_request_stage_events, GROUP_INDEX_NAME
|
||||
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_merge_request_stage_events, PROJECT_INDEX_NAME
|
||||
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_merge_request_stage_events, GROUP_IN_PROGRESS_INDEX_NAME
|
||||
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_merge_request_stage_events, PROJECT_IN_PROGRESS_INDEX_NAME
|
||||
end
|
||||
end
|
1
db/schema_migrations/20211031152417
Normal file
1
db/schema_migrations/20211031152417
Normal file
|
@ -0,0 +1 @@
|
|||
f46a0dd662a80d38a4e8d3e6c4db05e61563a959b75d30a4c3724ae6afc2647f
|
1
db/schema_migrations/20211031154919
Normal file
1
db/schema_migrations/20211031154919
Normal file
|
@ -0,0 +1 @@
|
|||
08399bfbf62533c00dfe3ca3434f6be292ec768f053d3b1fde41d2d68de32fe7
|
1040
db/structure.sql
1040
db/structure.sql
File diff suppressed because it is too large
Load diff
|
@ -3864,15 +3864,11 @@ you can ensure that concurrent deployments never happen to the production enviro
|
|||
|
||||
Use `release` to create a [release](../../user/project/releases/index.md).
|
||||
|
||||
The release job must have access to the [`release-cli`](https://gitlab.com/gitlab-org/release-cli/-/tree/master/docs)
|
||||
and your runner must be using one of these executors:
|
||||
The release job must have access to the [`release-cli`](https://gitlab.com/gitlab-org/release-cli/-/tree/master/docs),
|
||||
which must be in the `$PATH`.
|
||||
|
||||
- If you use the [Docker executor](https://docs.gitlab.com/runner/executors/docker.html),
|
||||
your [`image:`](#image) must include the `release-cli`. You can use this image from the GitLab
|
||||
Container Registry: `registry.gitlab.com/gitlab-org/release-cli:latest`
|
||||
|
||||
- If you use the [Shell executor](https://docs.gitlab.com/runner/executors/shell.html), the server
|
||||
where the runner is registered must have the `release-cli` [installed](../../user/project/releases/release_cli.md).
|
||||
If you use the [Docker executor](https://docs.gitlab.com/runner/executors/docker.html),
|
||||
you can use this image from the GitLab Container Registry: `registry.gitlab.com/gitlab-org/release-cli:latest`
|
||||
|
||||
**Keyword type**: Job keyword. You can use it only as part of a job.
|
||||
|
||||
|
@ -3921,6 +3917,8 @@ This example creates a release:
|
|||
- The `release` section executes after the `script` keyword and before the `after_script`.
|
||||
- A release is created only if the job's main script succeeds.
|
||||
- If the release already exists, it is not updated and the job with the `release` keyword fails.
|
||||
- If you use the [Shell executor](https://docs.gitlab.com/runner/executors/shell.html) or similar,
|
||||
[install `release-cli`](../../user/project/releases/release_cli.md) on the server where the runner is registered.
|
||||
|
||||
**Related topics**:
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ For example, [a pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipelines/
|
|||
[`pages` job](https://gitlab.com/gitlab-org/gitlab-docs/-/jobs/1733948332).
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
graph LR
|
||||
A{{"Container registry on gitlab-docs project"}}
|
||||
B[["Scheduled pipeline<br>`pages` and<br>`pages:deploy` job"]]
|
||||
C([docs.gitlab.com])
|
||||
|
|
|
@ -8,7 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
GitLab uses [Akismet](https://akismet.com/) to prevent the creation of
|
||||
spam issues on public projects. Issues created through the web UI or the API can be submitted to
|
||||
Akismet for review.
|
||||
Akismet for review, and instance administrators can
|
||||
[mark snippets as spam](../user/snippets.md#mark-snippet-as-spam).
|
||||
|
||||
Detected spam is rejected, and an entry is added in the **Spam Log** section of the
|
||||
Admin page.
|
||||
|
|
|
@ -954,6 +954,11 @@ An on-demand scan can be run in active or passive mode:
|
|||
minimize the risk of accidental damage, running an active scan requires a [validated site
|
||||
profile](#site-profile-validation).
|
||||
|
||||
### View on-demand DAST scans
|
||||
|
||||
To view running and completed on-demand DAST scans for a project, go to
|
||||
**Security & Compliance > On-demand Scans** in the left sidebar.
|
||||
|
||||
### Run an on-demand DAST scan
|
||||
|
||||
Prerequisites:
|
||||
|
@ -987,6 +992,7 @@ To run an on-demand scan either at a scheduled date or frequency, read
|
|||
|
||||
1. From your project's home page, go to **Security & Compliance > On-demand Scans** in the left
|
||||
sidebar.
|
||||
1. Select **New DAST scan**.
|
||||
1. Complete the **Scan name** and **Description** fields.
|
||||
1. In GitLab 13.10 and later, select the desired branch from the **Branch** dropdown.
|
||||
1. In **Scanner profile**, select a scanner profile from the dropdown.
|
||||
|
@ -1023,6 +1029,7 @@ To schedule a scan:
|
|||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Security & Compliance > On-demand Scans**.
|
||||
1. Select **New DAST scan**.
|
||||
1. Complete the **Scan name** and **Description** text boxes.
|
||||
1. In GitLab 13.10 and later, from the **Branch** dropdown list, select the desired branch.
|
||||
1. In the **Scanner profile** section, from the dropdown list, select a scanner profile.
|
||||
|
|
|
@ -19,6 +19,7 @@ You can report a user through their:
|
|||
- [Profile](#reporting-abuse-through-a-users-profile)
|
||||
- [Comments](#reporting-abuse-through-a-users-comment)
|
||||
- [Issues and Merge requests](#reporting-abuse-through-a-users-issue-or-merge-request)
|
||||
- [Snippets](snippets.md#mark-snippet-as-spam)
|
||||
|
||||
## Reporting abuse through a user's profile
|
||||
|
||||
|
|
|
@ -211,6 +211,24 @@ snippet was created using the GitLab web interface the original line ending is W
|
|||
With snippets, you engage in a conversation about that piece of code,
|
||||
which can encourage user collaboration.
|
||||
|
||||
## Mark snippet as spam **(FREE SELF)**
|
||||
|
||||
Administrators on self-managed GitLab instances can mark snippets as spam.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be the administrator for your instance.
|
||||
- [Akismet](../integration/akismet.md) spam protection must be enabled on the instance.
|
||||
|
||||
To do this task:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Snippets**.
|
||||
1. Select the snippet you want to report as spam.
|
||||
1. Select **Submit as spam**.
|
||||
|
||||
GitLab forwards the spam to Akismet.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Snippet limitations
|
||||
|
|
42
lib/bulk_imports/common/pipelines/wiki_pipeline.rb
Normal file
42
lib/bulk_imports/common/pipelines/wiki_pipeline.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
module Common
|
||||
module Pipelines
|
||||
class WikiPipeline
|
||||
include Pipeline
|
||||
|
||||
def extract(*)
|
||||
BulkImports::Pipeline::ExtractedData.new(data: { url: url_from_parent_path(context.entity.source_full_path) })
|
||||
end
|
||||
|
||||
def transform(_, data)
|
||||
data&.slice(:url)
|
||||
end
|
||||
|
||||
def load(context, data)
|
||||
return unless context.portable.wiki
|
||||
|
||||
url = data[:url].sub("://", "://oauth2:#{context.configuration.access_token}@")
|
||||
|
||||
Gitlab::UrlBlocker.validate!(url, allow_local_network: allow_local_requests?, allow_localhost: allow_local_requests?)
|
||||
|
||||
context.portable.wiki.ensure_repository
|
||||
context.portable.wiki.repository.fetch_as_mirror(url)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def url_from_parent_path(parent_path)
|
||||
wiki_path = parent_path + ".wiki.git"
|
||||
root = context.configuration.url
|
||||
Gitlab::Utils.append_path(root, wiki_path)
|
||||
end
|
||||
|
||||
def allow_local_requests?
|
||||
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -35,6 +35,10 @@ module BulkImports
|
|||
pipeline: BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline,
|
||||
stage: 4
|
||||
},
|
||||
wiki: {
|
||||
pipeline: BulkImports::Common::Pipelines::WikiPipeline,
|
||||
stage: 5
|
||||
},
|
||||
uploads: {
|
||||
pipeline: BulkImports::Common::Pipelines::UploadsPipeline,
|
||||
stage: 5
|
||||
|
|
|
@ -7,7 +7,7 @@ module Gitlab
|
|||
UnknownTrackError = Class.new(StandardError)
|
||||
|
||||
def self.for(track)
|
||||
valid_tracks = [:invite_team, Namespaces::InProductMarketingEmailsService::TRACKS.keys].flatten
|
||||
valid_tracks = [Namespaces::InviteTeamEmailService::TRACK, Namespaces::InProductMarketingEmailsService::TRACKS.keys].flatten
|
||||
raise UnknownTrackError unless valid_tracks.include?(track)
|
||||
|
||||
"Gitlab::Email::Message::InProductMarketing::#{track.to_s.classify}".constantize
|
||||
|
|
|
@ -177,8 +177,6 @@ module Gitlab
|
|||
end
|
||||
|
||||
def validate_series!
|
||||
return unless series?
|
||||
|
||||
raise ArgumentError, "Only #{total_series} series available for this track." unless @series.between?(0, total_series - 1)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,6 +40,12 @@ module Gitlab
|
|||
def series?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_series!
|
||||
raise ArgumentError, "Only one email is sent for this track. Value of `series` should be 0." unless @series == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -829,7 +829,13 @@ module Gitlab
|
|||
|
||||
Users::InProductMarketingEmail.tracks.keys.each_with_object({}) do |track, result|
|
||||
# rubocop: enable UsageData/LargeTable:
|
||||
series_amount = Namespaces::InProductMarketingEmailsService::TRACKS[track.to_sym][:interval_days].count
|
||||
series_amount =
|
||||
if track.to_sym == Namespaces::InviteTeamEmailService::TRACK
|
||||
0
|
||||
else
|
||||
Namespaces::InProductMarketingEmailsService::TRACKS[track.to_sym][:interval_days].count
|
||||
end
|
||||
|
||||
0.upto(series_amount - 1).map do |series|
|
||||
# When there is an error with the query and it's not the Hash we expect, we return what we got from `count`.
|
||||
sent_count = sent_emails.is_a?(Hash) ? sent_emails.fetch([track, series], 0) : sent_emails
|
||||
|
|
25
spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb
Normal file
25
spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BulkImports::Common::Pipelines::WikiPipeline do
|
||||
describe '#run' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
|
||||
let_it_be(:parent) { create(:project) }
|
||||
|
||||
let_it_be(:entity) do
|
||||
create(
|
||||
:bulk_import_entity,
|
||||
:project_entity,
|
||||
bulk_import: bulk_import,
|
||||
source_full_path: 'source/full/path',
|
||||
destination_name: 'My Destination Wiki',
|
||||
destination_namespace: parent.full_path,
|
||||
project: parent
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'wiki pipeline imports a wiki for an entity'
|
||||
end
|
||||
end
|
|
@ -12,6 +12,7 @@ RSpec.describe BulkImports::Projects::Stage do
|
|||
[4, BulkImports::Common::Pipelines::BoardsPipeline],
|
||||
[4, BulkImports::Projects::Pipelines::MergeRequestsPipeline],
|
||||
[4, BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline],
|
||||
[5, BulkImports::Common::Pipelines::WikiPipeline],
|
||||
[5, BulkImports::Common::Pipelines::UploadsPipeline],
|
||||
[6, BulkImports::Common::Pipelines::EntityFinisher]
|
||||
]
|
||||
|
|
|
@ -6,7 +6,25 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::InviteTeam do
|
|||
let_it_be(:group) { build(:group) }
|
||||
let_it_be(:user) { build(:user) }
|
||||
|
||||
subject(:message) { described_class.new(group: group, user: user, series: 0) }
|
||||
let(:series) { 0 }
|
||||
|
||||
subject(:message) { described_class.new(group: group, user: user, series: series) }
|
||||
|
||||
describe 'initialize' do
|
||||
context 'when series is valid' do
|
||||
it 'does not raise error' do
|
||||
expect { subject }.not_to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when series is invalid' do
|
||||
let(:series) { 1 }
|
||||
|
||||
it 'raises error' do
|
||||
expect { subject }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'contains the correct message', :aggregate_failures do
|
||||
expect(message.subject_line).to eq 'Invite your teammates to GitLab'
|
||||
|
|
|
@ -21,7 +21,8 @@ RSpec.describe Users::InProductMarketingEmail, type: :model do
|
|||
|
||||
describe '.tracks' do
|
||||
it 'has an entry for every track' do
|
||||
expect(Namespaces::InProductMarketingEmailsService::TRACKS.keys).to match_array(described_class.tracks.keys.map(&:to_sym))
|
||||
tracks = [Namespaces::InviteTeamEmailService::TRACK, Namespaces::InProductMarketingEmailsService::TRACKS.keys].flatten
|
||||
expect(tracks).to match_array(described_class.tracks.keys.map(&:to_sym))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ RSpec.describe Groups::EmailCampaignsController do
|
|||
|
||||
describe 'track parameter' do
|
||||
context 'when valid' do
|
||||
where(track: Namespaces::InProductMarketingEmailsService::TRACKS.keys.without(:experience))
|
||||
where(track: [Namespaces::InProductMarketingEmailsService::TRACKS.keys.without(:experience), Namespaces::InviteTeamEmailService::TRACK].flatten)
|
||||
|
||||
with_them do
|
||||
it_behaves_like 'track and redirect'
|
||||
|
@ -117,6 +117,10 @@ RSpec.describe Groups::EmailCampaignsController do
|
|||
with_them do
|
||||
it_behaves_like 'track and redirect'
|
||||
end
|
||||
|
||||
it_behaves_like 'track and redirect' do
|
||||
let(:track) { Namespaces::InviteTeamEmailService::TRACK.to_s }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid' do
|
||||
|
@ -124,6 +128,10 @@ RSpec.describe Groups::EmailCampaignsController do
|
|||
|
||||
with_them do
|
||||
it_behaves_like 'no track and 404'
|
||||
|
||||
it_behaves_like 'no track and 404' do
|
||||
let(:track) { Namespaces::InviteTeamEmailService::TRACK.to_s }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -242,4 +242,41 @@ RSpec.describe Groups::CreateService, '#execute' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'invite team email' do
|
||||
let(:service) { described_class.new(user, group_params) }
|
||||
|
||||
before do
|
||||
allow(Namespaces::InviteTeamEmailWorker).to receive(:perform_in)
|
||||
end
|
||||
|
||||
it 'is sent' do
|
||||
group = service.execute
|
||||
delay = Namespaces::InviteTeamEmailService::DELIVERY_DELAY_IN_MINUTES
|
||||
expect(Namespaces::InviteTeamEmailWorker).to have_received(:perform_in).with(delay, group.id, user.id)
|
||||
end
|
||||
|
||||
context 'when group has not been persisted' do
|
||||
let(:service) { described_class.new(user, group_params.merge(name: '<script>alert("Attack!")</script>')) }
|
||||
|
||||
it 'not sent' do
|
||||
expect(Namespaces::InviteTeamEmailWorker).not_to receive(:perform_in)
|
||||
service.execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group is not root' do
|
||||
let(:parent_group) { create :group }
|
||||
let(:service) { described_class.new(user, group_params.merge(parent_id: parent_group.id)) }
|
||||
|
||||
before do
|
||||
parent_group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'not sent' do
|
||||
expect(Namespaces::InviteTeamEmailWorker).not_to receive(:perform_in)
|
||||
service.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Namespaces::InProductMarketingEmailRecords do
|
||||
let_it_be(:user) { create :user }
|
||||
|
||||
subject(:records) { described_class.new }
|
||||
|
||||
it 'initializes records' do
|
||||
expect(subject.records).to match_array []
|
||||
end
|
||||
|
||||
describe '#save!' do
|
||||
before do
|
||||
allow(Users::InProductMarketingEmail).to receive(:bulk_insert!)
|
||||
|
||||
records.add(user, :invite_team, 0)
|
||||
records.add(user, :create, 1)
|
||||
end
|
||||
|
||||
it 'bulk inserts added records' do
|
||||
expect(Users::InProductMarketingEmail).to receive(:bulk_insert!).with(records.records)
|
||||
records.save!
|
||||
end
|
||||
|
||||
it 'resets its records' do
|
||||
records.save!
|
||||
expect(records.records).to match_array []
|
||||
end
|
||||
end
|
||||
|
||||
describe '#add' do
|
||||
it 'adds a Users::InProductMarketingEmail record to its records' do
|
||||
freeze_time do
|
||||
records.add(user, :invite_team, 0)
|
||||
records.add(user, :create, 1)
|
||||
|
||||
first, second = records.records
|
||||
|
||||
expect(first).to be_a Users::InProductMarketingEmail
|
||||
expect(first.track.to_sym).to eq :invite_team
|
||||
expect(first.series).to eq 0
|
||||
expect(first.created_at).to eq Time.zone.now
|
||||
expect(first.updated_at).to eq Time.zone.now
|
||||
|
||||
expect(second).to be_a Users::InProductMarketingEmail
|
||||
expect(second.track.to_sym).to eq :create
|
||||
expect(second.series).to eq 1
|
||||
expect(second.created_at).to eq Time.zone.now
|
||||
expect(second.updated_at).to eq Time.zone.now
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
128
spec/services/namespaces/invite_team_email_service_spec.rb
Normal file
128
spec/services/namespaces/invite_team_email_service_spec.rb
Normal file
|
@ -0,0 +1,128 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Namespaces::InviteTeamEmailService do
|
||||
let_it_be(:user) { create(:user, email_opted_in: true) }
|
||||
|
||||
let(:track) { described_class::TRACK }
|
||||
let(:series) { 0 }
|
||||
|
||||
let(:setup_for_company) { true }
|
||||
let(:parent_group) { nil }
|
||||
let(:group) { create(:group, parent: parent_group) }
|
||||
|
||||
subject(:action) { described_class.send_email(user, group) }
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
allow(group).to receive(:setup_for_company).and_return(setup_for_company)
|
||||
allow(Notify).to receive(:in_product_marketing_email).and_return(double(deliver_later: nil))
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :send_invite_team_email do |*args|
|
||||
match do
|
||||
expect(Notify).to have_received(:in_product_marketing_email).with(*args).once
|
||||
end
|
||||
|
||||
match_when_negated do
|
||||
expect(Notify).not_to have_received(:in_product_marketing_email)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'unexperimented' do
|
||||
it { is_expected.not_to send_invite_team_email }
|
||||
|
||||
it 'does not record sent email' do
|
||||
expect { subject }.not_to change { Users::InProductMarketingEmail.count }
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'candidate' do
|
||||
it { is_expected.to send_invite_team_email(user.id, group.id, track, 0) }
|
||||
|
||||
it 'records sent email' do
|
||||
expect { subject }.to change { Users::InProductMarketingEmail.count }.by(1)
|
||||
|
||||
expect(
|
||||
Users::InProductMarketingEmail.where(
|
||||
user: user,
|
||||
track: track,
|
||||
series: 0
|
||||
)
|
||||
).to exist
|
||||
end
|
||||
|
||||
it_behaves_like 'tracks assignment and records the subject', :invite_team_email, :group do
|
||||
subject { group }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group is in control path' do
|
||||
before do
|
||||
stub_experiments(invite_team_email: :control)
|
||||
end
|
||||
|
||||
it { is_expected.not_to send_invite_team_email }
|
||||
|
||||
it 'does not record sent email' do
|
||||
expect { subject }.not_to change { Users::InProductMarketingEmail.count }
|
||||
end
|
||||
|
||||
it_behaves_like 'tracks assignment and records the subject', :invite_team_email, :group do
|
||||
subject { group }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group is in candidate path' do
|
||||
before do
|
||||
stub_experiments(invite_team_email: :candidate)
|
||||
end
|
||||
|
||||
it_behaves_like 'candidate'
|
||||
|
||||
context 'when the user has not opted into marketing emails' do
|
||||
let(:user) { create(:user, email_opted_in: false ) }
|
||||
|
||||
it_behaves_like 'unexperimented'
|
||||
end
|
||||
|
||||
context 'when group is not top level' do
|
||||
it_behaves_like 'unexperimented' do
|
||||
let(:parent_group) do
|
||||
create(:group).tap { |g| g.add_owner(user) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group is not set up for a company' do
|
||||
it_behaves_like 'unexperimented' do
|
||||
let(:setup_for_company) { nil }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when other users have already been added to the group' do
|
||||
before do
|
||||
group.add_developer(create(:user))
|
||||
end
|
||||
|
||||
it_behaves_like 'unexperimented'
|
||||
end
|
||||
|
||||
context 'when other users have already been invited to the group' do
|
||||
before do
|
||||
group.add_developer('not_a_user_yet@example.com')
|
||||
end
|
||||
|
||||
it_behaves_like 'unexperimented'
|
||||
end
|
||||
|
||||
context 'when the user already got sent the email' do
|
||||
before do
|
||||
create(:in_product_marketing_email, user: user, track: track, series: 0)
|
||||
end
|
||||
|
||||
it_behaves_like 'unexperimented'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do
|
||||
describe '#run' do
|
||||
let_it_be(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
|
||||
|
||||
let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
|
||||
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
|
||||
|
||||
let(:extracted_data) { BulkImports::Pipeline::ExtractedData.new(data: {}) }
|
||||
|
||||
context 'successfully imports wiki for an entity' do
|
||||
subject { described_class.new(context) }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
|
||||
allow(extractor).to receive(:extract).and_return(extracted_data)
|
||||
end
|
||||
end
|
||||
|
||||
it 'imports new wiki into destination project' do
|
||||
expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
|
||||
url = "https://oauth2:token@gitlab.example/#{entity.source_full_path}.wiki.git"
|
||||
expect(repository_service).to receive(:fetch_remote).with(url, any_args).and_return 0
|
||||
end
|
||||
|
||||
subject.run
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
27
spec/workers/namespaces/invite_team_email_worker_spec.rb
Normal file
27
spec/workers/namespaces/invite_team_email_worker_spec.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Namespaces::InviteTeamEmailWorker do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
it 'sends the email' do
|
||||
expect(Namespaces::InviteTeamEmailService).to receive(:send_email).with(user, group).once
|
||||
subject.perform(group.id, user.id)
|
||||
end
|
||||
|
||||
context 'when user id is non-existent' do
|
||||
it 'does not send the email' do
|
||||
expect(Namespaces::InviteTeamEmailService).not_to receive(:send_email)
|
||||
subject.perform(group.id, non_existing_record_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group id is non-existent' do
|
||||
it 'does not send the email' do
|
||||
expect(Namespaces::InviteTeamEmailService).not_to receive(:send_email)
|
||||
subject.perform(non_existing_record_id, user.id)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue