Commit 1b1cf299 authored by Matija Čupić's avatar Matija Čupić

Implement subscription triggering mechanism

Implements the mechanism that triggers pipelines based on project
subscriptions.
parent 49e558e9
......@@ -227,3 +227,19 @@ Some features are not implemented yet. For example, support for environments.
- `only` and `except`
- `when` (only with `on_success`, `on_failure`, and `always` values)
- `extends`
## Trigger a pipeline when an upstream project is rebuilt
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9045) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.8.
You can trigger a pipeline in your project whenever a pipeline finishes for a new
tag in a different project:
1. Go to the project's **Settings > CI / CD** page, and expand the **Pipeline subscriptions** section.
1. Enter the path to the project you want to subscribe to.
1. Click subscribe.
Any pipelines that complete successfully for new tags in the subscribed project
will now trigger a pipeline on the current project's default branch. The maximum
number of upstream pipeline subscriptions is 2, for both the upstream and
downstream projects.
......@@ -4,17 +4,18 @@ class Projects::SubscriptionsController < Projects::ApplicationController
include ::Gitlab::Utils::StrongMemoize
before_action :authorize_admin_project!
before_action :authorize_read_upstream_project!, only: [:create]
before_action :feature_ci_project_subscriptions!
before_action :authorize_upstream_project!, only: [:create]
before_action :check_subscription_count!, only: [:create]
def create
subscription = project.upstream_project_subscriptions.create(upstream_project: upstream_project)
flash[:notice] = if subscription.persisted?
_('Subscription successfully created.')
else
_('This project path either does not exist or is private.')
end
if subscription.persisted?
flash[:notice] = _('Subscription successfully created.')
else
flash[:alert] = _('Subscription creation failed because the specified project is not public.')
end
redirect_to project_settings_ci_cd_path(project)
end
......@@ -41,11 +42,21 @@ class Projects::SubscriptionsController < Projects::ApplicationController
project.upstream_project_subscriptions.find(params[:id])
end
def authorize_read_upstream_project!
render_404 unless can?(current_user, :read_project, upstream_project)
end
def feature_ci_project_subscriptions!
render_404 unless project.feature_available?(:ci_project_subscriptions)
end
def authorize_upstream_project!
return if can?(current_user, :developer_access, upstream_project)
flash[:warning] = _('This project path either does not exist or you do not have access.')
redirect_to project_settings_ci_cd_path(project)
end
def check_subscription_count!
return if project.upstream_project_subscriptions.count < 2
flash[:warning] = _('Subscription limit reached.')
redirect_to project_settings_ci_cd_path(project)
end
end
......@@ -65,9 +65,22 @@ module EE
::Ci::PipelineBridgeStatusWorker.perform_async(pipeline.id)
end
end
after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
next unless pipeline.triggers_subscriptions?
pipeline.run_after_commit do
::Ci::TriggerDownstreamSubscriptionsWorker.perform_async(pipeline.id)
end
end
end
end
def triggers_subscriptions?
# Currently we trigger subscriptions only for tags.
tag? && project_has_subscriptions?
end
def retryable?
!merge_train_pipeline? && super
end
......@@ -145,6 +158,12 @@ module EE
private
def project_has_subscriptions?
return false unless ::Feature.enabled?(:ci_project_subscriptions, project)
project.downstream_projects.any?
end
def merge_train_ref?
::MergeRequest.merge_train_ref?(ref)
end
......
# frozen_string_literal: true
module Ci
class TriggerDownstreamSubscriptionService < ::BaseService
def execute(pipeline)
pipeline.project.downstream_projects.each do |downstream_project|
::Ci::CreatePipelineService.new(downstream_project, pipeline.user, ref: downstream_project.default_branch).execute(:pipeline)
end
end
end
end
......@@ -9,10 +9,10 @@
%p
= _("Set up pipeline subscriptions for this project.")
%p
- default_branch_docs = link_to(_("default branch"), help_page_path('user/project/repository/branches/index.md', anchor: 'default-branch'))
= _("Subscriptions allow successfully completed pipelines on the %{default_branch_docs} of the subscribed project to trigger a new pipeline on the default branch of this project.").html_safe % { default_branch_docs: default_branch_docs }
- default_branch_docs = link_to(_("default branch"), help_page_path('user/project/repository/branches', anchor: 'default-branch'))
= _("A subscription will trigger a new pipeline on the default branch of this project when a pipeline successfully completes for a new tag on the %{default_branch_docs} of the subscribed project.").html_safe % { default_branch_docs: default_branch_docs }
%p
= _("There is a limit of 100 subscriptions from or to a project.")
= _("There is a limit of 2 subscriptions from or to a project.")
.settings-content
= render 'projects/settings/subscriptions/index'
......@@ -402,6 +402,13 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: pipeline_default:ci_trigger_downstream_subscriptions
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :default
:resource_boundary: :cpu
:weight: 3
:idempotent:
- :name: security_scans:store_security_reports
:feature_category: :static_application_security_testing
:has_external_dependencies:
......
# frozen_string_literal: true
module Ci
class TriggerDownstreamSubscriptionsWorker # rubocop:disable Scalability/IdempotentWorker
include ::ApplicationWorker
include ::PipelineQueue
worker_resource_boundary :cpu
def perform(pipeline_id)
::Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
::Ci::TriggerDownstreamSubscriptionService
.new(pipeline.project, pipeline.user)
.execute(pipeline)
end
end
end
end
......@@ -19,7 +19,6 @@ describe Projects::SubscriptionsController do
context 'when user is authorized' do
before do
project.add_maintainer(user)
upstream_project.add_developer(user)
end
context 'when feature is available' do
......@@ -27,29 +26,77 @@ describe Projects::SubscriptionsController do
stub_licensed_features(ci_project_subscriptions: true)
end
context 'when project is public' do
it 'creates a new subscription' do
expect { post_create }.to change { project.upstream_project_subscriptions.count }.from(0).to(1)
context 'when user is developer in upstream project' do
before do
upstream_project.add_developer(user)
end
it 'sets the flash' do
post_create
context 'when project is public' do
context 'when subscription count is below the limit' do
it 'creates a new subscription' do
expect { post_create }.to change { project.upstream_project_subscriptions.count }.from(0).to(1)
end
expect(response).to set_flash[:notice].to('Subscription successfully created.')
end
it 'sets the flash' do
post_create
it 'redirects to ci_cd settings' do
post_create
expect(response).to set_flash[:notice].to('Subscription successfully created.')
end
expect(response).to redirect_to project_settings_ci_cd_path(project)
it 'redirects to ci_cd settings' do
post_create
expect(response).to redirect_to project_settings_ci_cd_path(project)
end
end
context 'when subscription count is on the limit' do
before do
create_list(:ci_subscriptions_project, 2, downstream_project: project)
end
it 'does not create a new subscription' do
expect { post_create }.not_to change { project.upstream_project_subscriptions.count }.from(2)
end
it 'sets the flash' do
post_create
expect(response).to set_flash[:warning].to('Subscription limit reached.')
end
it 'redirects to ci_cd settings' do
post_create
expect(response).to redirect_to project_settings_ci_cd_path(project)
end
end
end
end
context 'when project is not public' do
before do
upstream_project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
context 'when project is not public' do
before do
upstream_project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it 'does not create a new subscription' do
expect { post_create }.not_to change { project.upstream_project_subscriptions.count }.from(0)
end
it 'sets the flash' do
post_create
expect(response).to set_flash[:alert].to('Subscription creation failed because the specified project is not public.')
end
it 'redirects to ci_cd settings' do
post_create
expect(response).to redirect_to project_settings_ci_cd_path(project)
end
end
end
context 'when user is not developer in upstream project' do
it 'does not create a new subscription' do
expect { post_create }.not_to change { project.upstream_project_subscriptions.count }.from(0)
end
......@@ -57,7 +104,7 @@ describe Projects::SubscriptionsController do
it 'sets the flash' do
post_create
expect(response).to set_flash[:notice].to('This project path either does not exist or is private.')
expect(response).to set_flash[:warning].to('This project path either does not exist or you do not have access.')
end
it 'redirects to ci_cd settings' do
......
......@@ -385,6 +385,44 @@ describe Ci::Pipeline do
pipeline.cancel!
end
end
context 'when pipeline project has downstream subscriptions' do
let(:pipeline) { create(:ci_empty_pipeline, project: create(:project, :public)) }
before do
pipeline.project.downstream_projects << create(:project)
end
context 'when pipeline runs on a tag' do
before do
pipeline.update(tag: true)
end
context 'when feature is not available' do
before do
stub_feature_flags(ci_project_subscriptions: false)
end
it 'does not schedule the trigger downstream subscriptions worker' do
expect(::Ci::TriggerDownstreamSubscriptionsWorker).not_to receive(:perform_async)
pipeline.succeed!
end
end
context 'when feature is available' do
before do
stub_feature_flags(ci_project_subscriptions: true)
end
it 'schedules the trigger downstream subscriptions worker' do
expect(::Ci::TriggerDownstreamSubscriptionsWorker).to receive(:perform_async)
pipeline.succeed!
end
end
end
end
end
describe '#latest_merge_request_pipeline?' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::TriggerDownstreamSubscriptionService do
describe '#execute' do
subject(:execute) { described_class.new(pipeline.project, pipeline.user).execute(pipeline) }
let(:upstream_project) { create(:project, :public) }
let(:pipeline) { create(:ci_pipeline, project: upstream_project, user: create(:user)) }
context 'when pipeline project has downstream projects' do
before do
create(:project, :repository, upstream_projects: [upstream_project])
end
it 'calls the create pipeline service' do
service_double = instance_double(::Ci::CreatePipelineService)
expect(service_double).to receive(:execute)
expect(::Ci::CreatePipelineService).to receive(:new).with(
an_instance_of(Project), an_instance_of(User), ref: 'master'
).and_return(service_double)
execute
end
end
context 'when pipeline project does not have downstream projects' do
it 'does not call the create pipeline service' do
expect(::Ci::CreatePipelineService).not_to receive(:new)
execute
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::TriggerDownstreamSubscriptionsWorker do
describe '#perform' do
subject(:perform) { described_class.new.perform(pipeline_id) }
context 'when pipeline exists' do
let(:pipeline_id) { create(:ci_pipeline, user: create(:user)).id }
it 'calls the trigger downstream pipeline service' do
expect(::Ci::TriggerDownstreamSubscriptionService).to receive_message_chain(:new, :execute)
perform
end
end
context 'when pipeline does not exist' do
let(:pipeline_id) { 1234 }
it 'does nothing' do
expect(::Ci::TriggerDownstreamSubscriptionService).not_to receive(:new)
perform
end
end
end
end
......@@ -861,6 +861,9 @@ msgstr ""
msgid "A secure token that identifies an external storage request."
msgstr ""
msgid "A subscription will trigger a new pipeline on the default branch of this project when a pipeline successfully completes for a new tag on the %{default_branch_docs} of the subscribed project."
msgstr ""
msgid "A user with write access to the source branch selected this option"
msgstr ""
......@@ -18807,9 +18810,15 @@ msgstr ""
msgid "Subscription"
msgstr ""
msgid "Subscription creation failed because the specified project is not public."
msgstr ""
msgid "Subscription deletion failed."
msgstr ""
msgid "Subscription limit reached."
msgstr ""
msgid "Subscription successfully applied to \"%{group_name}\""
msgstr ""
......@@ -18891,9 +18900,6 @@ msgstr ""
msgid "Subscriptions"
msgstr ""
msgid "Subscriptions allow successfully completed pipelines on the %{default_branch_docs} of the subscribed project to trigger a new pipeline on the default branch of this project."
msgstr ""
msgid "Subtracted"
msgstr ""
......@@ -19732,7 +19738,7 @@ msgstr ""
msgid "There are no unstaged changes"
msgstr ""
msgid "There is a limit of 100 subscriptions from or to a project."
msgid "There is a limit of 2 subscriptions from or to a project."
msgstr ""
msgid "There is already a repository with that name on disk"
......@@ -20197,7 +20203,7 @@ msgstr ""
msgid "This project is archived and cannot be commented on."
msgstr ""
msgid "This project path either does not exist or is private."
msgid "This project path either does not exist or you do not have access."
msgstr ""
msgid "This project will be removed on %{date}"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment