Commit 6e3e285b authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'ee-detect-github-pull-requests' into 'master'

Run pipelines for GitHub pull requests

See merge request gitlab-org/gitlab-ee!15082
parents 8a79d495 fd450ddc
...@@ -23,6 +23,7 @@ module Ci ...@@ -23,6 +23,7 @@ module Ci
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
belongs_to :merge_request, class_name: 'MergeRequest' belongs_to :merge_request, class_name: 'MergeRequest'
belongs_to :external_pull_request
has_internal_id :iid, scope: :project, presence: false, init: ->(s) do has_internal_id :iid, scope: :project, presence: false, init: ->(s) do
s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count
...@@ -64,6 +65,11 @@ module Ci ...@@ -64,6 +65,11 @@ module Ci
validates :merge_request, presence: { if: :merge_request_event? } validates :merge_request, presence: { if: :merge_request_event? }
validates :merge_request, absence: { unless: :merge_request_event? } validates :merge_request, absence: { unless: :merge_request_event? }
validates :tag, inclusion: { in: [false], if: :merge_request_event? } validates :tag, inclusion: { in: [false], if: :merge_request_event? }
validates :external_pull_request, presence: { if: :external_pull_request_event? }
validates :external_pull_request, absence: { unless: :external_pull_request_event? }
validates :tag, inclusion: { in: [false], if: :external_pull_request_event? }
validates :status, presence: { unless: :importing? } validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing? validate :valid_commit_sha, unless: :importing?
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
...@@ -683,6 +689,10 @@ module Ci ...@@ -683,6 +689,10 @@ module Ci
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s) variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s)
variables.concat(merge_request.predefined_variables) variables.concat(merge_request.predefined_variables)
end end
if external_pull_request_event? && external_pull_request
variables.concat(external_pull_request.predefined_variables)
end
end end
end end
......
...@@ -23,7 +23,8 @@ module Ci ...@@ -23,7 +23,8 @@ module Ci
api: 5, api: 5,
external: 6, external: 6,
chat: 8, chat: 8,
merge_request_event: 10 merge_request_event: 10,
external_pull_request_event: 11
} }
end end
......
# frozen_string_literal: true
# This model stores pull requests coming from external providers, such as
# GitHub, when GitLab project is set as CI/CD only and remote mirror.
#
# When setting up a remote mirror with GitHub we subscribe to push and
# pull_request webhook events. When a pull request is opened on GitHub,
# a webhook is sent out, we create or update the status of the pull
# request locally.
#
# When the mirror is updated and changes are pushed to branches we check
# if there are open pull requests for the source and target branch.
# If so, we create pipelines for external pull requests.
class ExternalPullRequest < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include ShaAttribute
belongs_to :project
sha_attribute :source_sha
sha_attribute :target_sha
validates :source_branch, presence: true
validates :target_branch, presence: true
validates :source_sha, presence: true
validates :target_sha, presence: true
validates :source_repository, presence: true
validates :target_repository, presence: true
validates :status, presence: true
enum status: {
open: 1,
closed: 2
}
# We currently don't support pull requests from fork, so
# we are going to return an error to the webhook
validate :not_from_fork
scope :by_source_branch, ->(branch) { where(source_branch: branch) }
scope :by_source_repository, -> (repository) { where(source_repository: repository) }
def self.create_or_update_from_params(params)
find_params = params.slice(:project_id, :source_branch, :target_branch)
safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request|
yield(pull_request) if block_given?
end
end
def actual_branch_head?
actual_source_branch_sha == source_sha
end
def from_fork?
source_repository != target_repository
end
def source_ref
Gitlab::Git::BRANCH_REF_PREFIX + source_branch
end
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch)
end
end
private
def actual_source_branch_sha
project.commit(source_ref)&.sha
end
def not_from_fork
if from_fork?
errors.add(:base, 'Pull requests from fork are not supported')
end
end
def self.safe_find_or_initialize_and_update(find:, update:)
safe_ensure_unique(retries: 1) do
model = find_or_initialize_by(find)
if model.update(update)
yield(model) if block_given?
end
model
end
end
end
...@@ -291,6 +291,8 @@ class Project < ApplicationRecord ...@@ -291,6 +291,8 @@ class Project < ApplicationRecord
has_many :remote_mirrors, inverse_of: :project has_many :remote_mirrors, inverse_of: :project
has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage' has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
has_many :external_pull_requests, inverse_of: :project
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data accepts_nested_attributes_for :import_data
......
...@@ -18,7 +18,8 @@ module Ci ...@@ -18,7 +18,8 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::Activity, Gitlab::Ci::Pipeline::Chain::Limit::Activity,
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, **options, &block) # rubocop: disable Metrics/ParameterLists
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, **options, &block)
@pipeline = Ci::Pipeline.new @pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new( command = Gitlab::Ci::Pipeline::Chain::Command.new(
...@@ -32,6 +33,7 @@ module Ci ...@@ -32,6 +33,7 @@ module Ci
trigger_request: trigger_request, trigger_request: trigger_request,
schedule: schedule, schedule: schedule,
merge_request: merge_request, merge_request: merge_request,
external_pull_request: external_pull_request,
ignore_skip_ci: ignore_skip_ci, ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors, save_incompleted: save_on_errors,
seeds_block: block, seeds_block: block,
...@@ -62,6 +64,7 @@ module Ci ...@@ -62,6 +64,7 @@ module Ci
pipeline pipeline
end end
# rubocop: enable Metrics/ParameterLists
def execute!(*args, &block) def execute!(*args, &block)
execute(*args, &block).tap do |pipeline| execute(*args, &block).tap do |pipeline|
......
# frozen_string_literal: true
# This service is responsible for creating a pipeline for a given
# ExternalPullRequest coming from other providers such as GitHub.
module ExternalPullRequests
class CreatePipelineService < BaseService
def execute(pull_request)
return unless pull_request.open? && pull_request.actual_branch_head?
create_pipeline_for(pull_request)
end
private
def create_pipeline_for(pull_request)
Ci::CreatePipelineService.new(project, current_user, create_params(pull_request))
.execute(:external_pull_request_event, external_pull_request: pull_request)
end
def create_params(pull_request)
{
ref: pull_request.source_ref,
source_sha: pull_request.source_sha,
target_sha: pull_request.target_sha
}
end
end
end
...@@ -160,6 +160,7 @@ ...@@ -160,6 +160,7 @@
- repository_import - repository_import
- repository_remove_remote - repository_remove_remote
- system_hook_push - system_hook_push
- update_external_pull_requests
- update_merge_requests - update_merge_requests
- update_project_statistics - update_project_statistics
- upload_checksum - upload_checksum
......
# frozen_string_literal: true
class UpdateExternalPullRequestsWorker
include ApplicationWorker
def perform(project_id, user_id, ref)
project = Project.find_by_id(project_id)
return unless project
user = User.find_by_id(user_id)
return unless user
branch = Gitlab::Git.branch_name(ref)
return unless branch
external_pull_requests = project.external_pull_requests
.by_source_repository(project.import_source)
.by_source_branch(branch)
external_pull_requests.find_each do |pull_request|
ExternalPullRequests::CreatePipelineService.new(project, user)
.execute(pull_request)
end
end
end
...@@ -115,3 +115,4 @@ ...@@ -115,3 +115,4 @@
- [export_csv, 1] - [export_csv, 1]
- [incident_management, 2] - [incident_management, 2]
- [jira_connect, 1] - [jira_connect, 1]
- [update_external_pull_requests, 3]
# frozen_string_literal: true
class CreateExternalPullRequests < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX = 'index_external_pull_requests_on_project_and_branches'
def change
create_table :external_pull_requests do |t|
t.timestamps_with_timezone null: false
t.references :project, null: false, foreign_key: { on_delete: :cascade }, index: false
t.integer :pull_request_iid, null: false
t.integer :status, null: false, limit: 2
t.string :source_branch, null: false, limit: 255
t.string :target_branch, null: false, limit: 255
t.string :source_repository, null: false, limit: 255
t.string :target_repository, null: false, limit: 255
t.binary :source_sha, null: false
t.binary :target_sha, null: false
t.index [:project_id, :source_branch, :target_branch], unique: true, name: INDEX
end
end
end
# frozen_string_literal: true
class AddExternalPullRequestIdToCiPipelines < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
add_column :ci_pipelines, :external_pull_request_id, :bigint
end
def down
remove_column :ci_pipelines, :external_pull_request_id
end
end
# frozen_string_literal: true
class AddIndexToCiPipelinesExternalPullRequest < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_pipelines, :external_pull_request_id, where: 'external_pull_request_id IS NOT NULL'
end
def down
remove_concurrent_index :ci_pipelines, :external_pull_request_id
end
end
# frozen_string_literal: true
class AddForeignKeyToCiPipelinesExternalPullRequest < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key :ci_pipelines, :external_pull_requests, column: :external_pull_request_id, on_delete: :nullify
end
def down
remove_foreign_key :ci_pipelines, :external_pull_requests
end
end
...@@ -754,7 +754,9 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do ...@@ -754,7 +754,9 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.integer "merge_request_id" t.integer "merge_request_id"
t.binary "source_sha" t.binary "source_sha"
t.binary "target_sha" t.binary "target_sha"
t.bigint "external_pull_request_id"
t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id" t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id"
t.index ["external_pull_request_id"], name: "index_ci_pipelines_on_external_pull_request_id", where: "(external_pull_request_id IS NOT NULL)"
t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", where: "(merge_request_id IS NOT NULL)" t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", where: "(merge_request_id IS NOT NULL)"
t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id" t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id"
t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)" t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)"
...@@ -1323,6 +1325,21 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do ...@@ -1323,6 +1325,21 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id" t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id"
end end
create_table "external_pull_requests", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.bigint "project_id", null: false
t.integer "pull_request_iid", null: false
t.integer "status", limit: 2, null: false
t.string "source_branch", limit: 255, null: false
t.string "target_branch", limit: 255, null: false
t.string "source_repository", limit: 255, null: false
t.string "target_repository", limit: 255, null: false
t.binary "source_sha", null: false
t.binary "target_sha", null: false
t.index ["project_id", "source_branch", "target_branch"], name: "index_external_pull_requests_on_project_and_branches", unique: true
end
create_table "feature_gates", id: :serial, force: :cascade do |t| create_table "feature_gates", id: :serial, force: :cascade do |t|
t.string "feature_key", null: false t.string "feature_key", null: false
t.string "key", null: false t.string "key", null: false
...@@ -3785,6 +3802,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do ...@@ -3785,6 +3802,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
add_foreign_key "ci_pipeline_variables", "ci_pipelines", column: "pipeline_id", name: "fk_f29c5f4380", on_delete: :cascade add_foreign_key "ci_pipeline_variables", "ci_pipelines", column: "pipeline_id", name: "fk_f29c5f4380", on_delete: :cascade
add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
add_foreign_key "ci_pipelines", "external_pull_requests", name: "fk_190998ef09", on_delete: :nullify
add_foreign_key "ci_pipelines", "merge_requests", name: "fk_a23be95014", on_delete: :cascade add_foreign_key "ci_pipelines", "merge_requests", name: "fk_a23be95014", on_delete: :cascade
add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade
add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade
...@@ -3849,6 +3867,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do ...@@ -3849,6 +3867,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
add_foreign_key "events", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "events", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "events", "projects", on_delete: :cascade add_foreign_key "events", "projects", on_delete: :cascade
add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
add_foreign_key "external_pull_requests", "projects", on_delete: :cascade
add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade
add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify
add_foreign_key "fork_network_members", "projects", on_delete: :cascade add_foreign_key "fork_network_members", "projects", on_delete: :cascade
......
...@@ -8,6 +8,7 @@ module EE ...@@ -8,6 +8,7 @@ module EE
override :execute override :execute
def execute def execute
enqueue_elasticsearch_indexing enqueue_elasticsearch_indexing
enqueue_update_external_pull_requests
super super
end end
...@@ -24,6 +25,17 @@ module EE ...@@ -24,6 +25,17 @@ module EE
) )
end end
def enqueue_update_external_pull_requests
return unless project.mirror?
return unless params.fetch(:create_pipelines, true)
UpdateExternalPullRequestsWorker.perform_async(
project.id,
current_user.id,
params[:ref]
)
end
def should_index_commits? def should_index_commits?
return false unless default_branch? return false unless default_branch?
return false unless project.use_elasticsearch? return false unless project.use_elasticsearch?
......
# frozen_string_literal: true
class ProcessGithubPullRequestEventService < ::BaseService
# All possible statuses available here:
# https://developer.github.com/v3/activity/events/types/#pullrequestevent
GITHUB_ACTIONS_TO_STATUS = {
'opened' => :open,
'reopened' => :open,
'edited' => :open,
'closed' => :closed
}.freeze
def execute(webhook_params)
return unless project.mirror?
params = params_from_webhook(webhook_params)
return unless params[:status]
# Save pull request info for later. when mirror update will run
# and the pipeline is created for all newly pushed branches.
# At that point we will be able to reference it back if a pull request
# was created.
ExternalPullRequest.create_or_update_from_params(params) do |pr|
ExternalPullRequests::CreatePipelineService.new(project, current_user).execute(pr)
end
end
private
def params_from_webhook(params)
{
project_id: project.id,
pull_request_iid: params.dig(:pull_request, :id),
source_branch: params.dig(:pull_request, :head, :ref),
source_sha: params.dig(:pull_request, :head, :sha),
source_repository: params.dig(:pull_request, :head, :repo, :full_name),
target_branch: params.dig(:pull_request, :base, :ref),
target_sha: params.dig(:pull_request, :base, :sha),
target_repository: params.dig(:pull_request, :base, :repo, :full_name),
status: GITHUB_ACTIONS_TO_STATUS[params[:action]]
}
end
end
...@@ -23,7 +23,7 @@ class CreateGithubWebhookWorker ...@@ -23,7 +23,7 @@ class CreateGithubWebhookWorker
insecure_ssl: 1 insecure_ssl: 1
}, },
{ {
events: ['push'], events: %w[push pull_request],
active: true active: true
} }
) )
......
---
title: 'Enable "only/except: external_pull_request" with GitHub integration when a pull request is open for the given ref'
merge_request: 15082
author:
type: added
...@@ -45,6 +45,16 @@ module API ...@@ -45,6 +45,16 @@ module API
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
# pull_request params
optional :action, type: String, desc: 'Pull Request action'
optional 'pull_request.id', type: Integer, desc: 'Pull request ID'
optional 'pull_request.head.ref', type: String, desc: 'Source branch'
optional 'pull_request.head.sha', type: String, desc: 'Source sha'
optional 'pull_request.head.repo.full_name', type: String, desc: 'Source repository'
optional 'pull_request.base.ref', type: String, desc: 'Target branch'
optional 'pull_request.base.sha', type: String, desc: 'Target sha'
optional 'pull_request.base.repo.full_name', type: String, desc: 'Target repository'
end end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Triggers a pull mirror operation' desc 'Triggers a pull mirror operation'
...@@ -53,7 +63,15 @@ module API ...@@ -53,7 +63,15 @@ module API
break render_api_error!('The project is not mirrored', 400) unless project.mirror? break render_api_error!('The project is not mirrored', 400) unless project.mirror?
project.import_state.force_import_job! if params[:pull_request]
if external_pull_request = ProcessGithubPullRequestEventService.new(project, current_user).execute(params)
render_validation_error!(external_pull_request)
else
render_api_error!('The pull request event is not processable', 422)
end
else
project.import_state.force_import_job!
end
status 200 status 200
end end
......
...@@ -4,7 +4,7 @@ require 'spec_helper' ...@@ -4,7 +4,7 @@ require 'spec_helper'
describe API::ProjectMirror do describe API::ProjectMirror do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) } let(:project) { create(:project, :repository, namespace: user.namespace) }
describe 'POST /projects/:id/mirror/pull' do describe 'POST /projects/:id/mirror/pull' do
context 'when the project is not mirrored' do context 'when the project is not mirrored' do
...@@ -22,63 +22,132 @@ describe API::ProjectMirror do ...@@ -22,63 +22,132 @@ describe API::ProjectMirror do
allow_any_instance_of(Projects::UpdateMirrorService).to receive(:execute).and_return(status: :success) allow_any_instance_of(Projects::UpdateMirrorService).to receive(:execute).and_return(status: :success)
end end
context 'when import state is' do context 'when it receives a "push" event' do
def project_in_state(state) context 'when import state is' do
project = create(:project, :repository, namespace: user.namespace) def project_in_state(state)
import_state = create(:import_state, :mirror, state, project: project) project = create(:project, :repository, namespace: user.namespace)
import_state.update(next_execution_timestamp: 10.minutes.from_now) import_state = create(:import_state, :mirror, state, project: project)
import_state.update(next_execution_timestamp: 10.minutes.from_now)
project project
end end
it 'none it triggers the pull mirroring operation' do it 'none it triggers the pull mirroring operation' do
project = project_in_state(:none) project = project_in_state(:none)
expect(UpdateAllMirrorsWorker).to receive(:perform_async).once expect(UpdateAllMirrorsWorker).to receive(:perform_async).once
post api("/projects/#{project.id}/mirror/pull", user) post api("/projects/#{project.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
it 'failed it triggers the pull mirroring operation' do it 'failed it triggers the pull mirroring operation' do
project = project_in_state(:failed) project = project_in_state(:failed)
expect(UpdateAllMirrorsWorker).to receive(:perform_async).once expect(UpdateAllMirrorsWorker).to receive(:perform_async).once
post api("/projects/#{project.id}/mirror/pull", user) post api("/projects/#{project.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
it 'finished it triggers the pull mirroring operation' do it 'finished it triggers the pull mirroring operation' do
project = project_in_state(:finished) project = project_in_state(:finished)
expect(UpdateAllMirrorsWorker).to receive(:perform_async).once expect(UpdateAllMirrorsWorker).to receive(:perform_async).once
post api("/projects/#{project.id}/mirror/pull", user) post api("/projects/#{project.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end
it 'scheduled does not trigger the pull mirroring operation and returns 200' do
project = project_in_state(:scheduled)
expect(UpdateAllMirrorsWorker).not_to receive(:perform_async)
post api("/projects/#{project.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(200)
end
it 'started does not trigger the pull mirroring operation and returns 200' do
project = project_in_state(:started)
expect(UpdateAllMirrorsWorker).not_to receive(:perform_async)
post api("/projects/#{project.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(200)
end
end
end
context 'when it receives a "pull_request" event' do
let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService) }
let(:branch) { project.repository.branches.first }
let(:source_branch) { branch.name }
let(:source_sha) { branch.target }
let(:action) { 'opened' }
let(:params) do
{
pull_request: {
id: 123,
head: {
ref: source_branch,
sha: source_sha,
repo: { full_name: 'the-repo' }
},
base: {
ref: 'master',
sha: 'a09386439ca39abe575675ffd4b89ae824fec22f',
repo: { full_name: 'the-repo' }
}
},
action: action
}
end end
it 'scheduled does not trigger the pull mirroring operation and returns 200' do before do
project = project_in_state(:scheduled) create(:import_state, :mirror, :finished, project: project)
end
expect(UpdateAllMirrorsWorker).not_to receive(:perform_async) it 'triggers a pipeline for pull request' do
pipeline_params = {
ref: Gitlab::Git::BRANCH_REF_PREFIX + branch.name,
source_sha: branch.target,
target_sha: 'a09386439ca39abe575675ffd4b89ae824fec22f'
}
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, pipeline_params).and_return(create_pipeline_service)
expect(create_pipeline_service).to receive(:execute).with(:external_pull_request_event, any_args)
post api("/projects/#{project.id}/mirror/pull", user) post api("/projects/#{project.id}/mirror/pull", user), params: params
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end end
it 'started does not trigger the pull mirroring operation and returns 200' do context 'when any param is missing' do
project = project_in_state(:started) let(:source_sha) { nil }
expect(UpdateAllMirrorsWorker).not_to receive(:perform_async) it 'returns the error message' do
post api("/projects/#{project.id}/mirror/pull", user), params: params
post api("/projects/#{project.id}/mirror/pull", user) expect(response).to have_gitlab_http_status(400)
end
end
expect(response).to have_gitlab_http_status(200) context 'when action is not supported' do
let(:action) { 'assigned' }
it 'ignores it and return success status' do
expect(Ci::CreatePipelineService).not_to receive(:new)
post api("/projects/#{project.id}/mirror/pull", user), params: params
expect(response).to have_gitlab_http_status(422)
end
end end
end end
......
...@@ -9,8 +9,12 @@ describe Git::BranchPushService do ...@@ -9,8 +9,12 @@ describe Git::BranchPushService do
let(:newrev) { sample_commit.id } let(:newrev) { sample_commit.id }
let(:ref) { 'refs/heads/master' } let(:ref) { 'refs/heads/master' }
let(:params) do
{ oldrev: oldrev, newrev: newrev, ref: ref }
end
subject do subject do
described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) described_class.new(project, user, params)
end end
context 'with pull project' do context 'with pull project' do
...@@ -107,6 +111,38 @@ describe Git::BranchPushService do ...@@ -107,6 +111,38 @@ describe Git::BranchPushService do
end end
end end
end end
context 'External pull requests' do
it 'runs UpdateExternalPullRequestsWorker' do
expect(UpdateExternalPullRequestsWorker).to receive(:perform_async).with(project.id, user.id, ref)
subject.execute
end
context 'when project is not mirror' do
before do
allow(project).to receive(:mirror?).and_return(false)
end
it 'does nothing' do
expect(UpdateExternalPullRequestsWorker).not_to receive(:perform_async)
subject.execute
end
end
context 'when param skips pipeline creation' do
before do
params[:create_pipelines] = false
end
it 'does nothing' do
expect(UpdateExternalPullRequestsWorker).not_to receive(:perform_async)
subject.execute
end
end
end
end end
context 'Jira Connect hooks' do context 'Jira Connect hooks' do
......
# frozen_string_literal: true
require 'spec_helper'
describe ProcessGithubPullRequestEventService do
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:action) { 'opened' }
let(:params) do
{
pull_request: {
id: 123,
head: {
ref: source_branch,
sha: source_sha,
repo: { full_name: 'the-repo' }
},
base: {
ref: 'the-target-branch',
sha: 'a09386439ca39abe575675ffd4b89ae824fec22f',
repo: { full_name: 'the-repo' }
}
},
action: action
}
end
subject { described_class.new(project, user) }
describe '#execute' do
context 'when project is not a mirror' do
let(:source_branch) { double }
let(:source_sha) { double }
it 'does nothing' do
expect(subject.execute(params)).to be_nil
end
end
context 'when project is a mirror' do
before do
allow(project).to receive(:mirror?).and_return(true)
end
context 'when mirror update occurs before the pull request webhook' do
let(:branch) { project.repository.branches.first }
let(:source_branch) { branch.name }
let(:source_sha) { branch.target }
let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService) }
it 'creates a pipeline and saves pull request info' do
pipeline_params = {
ref: Gitlab::Git::BRANCH_REF_PREFIX + branch.name,
source_sha: branch.target,
target_sha: 'a09386439ca39abe575675ffd4b89ae824fec22f'
}
expect(Ci::CreatePipelineService).to receive(:new)
.with(project, user, pipeline_params)
.and_return(create_pipeline_service)
expect(create_pipeline_service).to receive(:execute)
.with(:external_pull_request_event, any_args)
expect { subject.execute(params) }.to change { ExternalPullRequest.count }.by(1)
end
end
context 'when the pull request webhook occurs before mirror update' do
let(:source_branch) { 'a-new-branch' }
let(:source_sha) { 'non-existent-sha' }
it 'only saves pull request info' do
expect(Ci::CreatePipelineService).not_to receive(:new)
expect { subject.execute(params) }.to change { ExternalPullRequest.count }.by(1)
pull_request = ExternalPullRequest.last
expect(pull_request).to be_persisted
expect(pull_request.project).to eq(project)
expect(pull_request.source_branch).to eq('a-new-branch')
expect(pull_request.source_repository).to eq('the-repo')
expect(pull_request.target_branch).to eq('the-target-branch')
expect(pull_request.target_repository).to eq('the-repo')
expect(pull_request.status).to eq('open')
end
end
context 'when pull request webhook action is "closed"' do
let(:source_branch) { 'a-new-branch' }
let(:source_sha) { 'non-existent-sha' }
let(:action) { 'closed' }
it 'only saves pull request info' do
expect(Ci::CreatePipelineService).not_to receive(:new)
expect { subject.execute(params) }.to change { ExternalPullRequest.count }.by(1)
pull_request = ExternalPullRequest.last
expect(pull_request).to be_persisted
expect(pull_request.project).to eq(project)
expect(pull_request.source_branch).to eq('a-new-branch')
expect(pull_request.source_repository).to eq('the-repo')
expect(pull_request.target_branch).to eq('the-target-branch')
expect(pull_request.target_repository).to eq('the-repo')
expect(pull_request.status).to eq('closed')
end
end
context 'when pull request webhook has unsupported action' do
let(:source_branch) { double }
let(:source_sha) { double }
let(:action) { 'assigned' }
it 'returns nil' do
expect(subject.execute(params)).to be_nil
end
end
context 'project is not a mirror' do
let(:source_branch) { double }
let(:source_sha) { double }
before do
allow(project).to receive(:mirror?).and_return(false)
end
it 'returns nil' do
expect(subject.execute(params)).to be_nil
end
end
context 'when pull request webhook any missing params' do
let(:source_branch) { nil }
let(:source_sha) { nil }
it 'returns a pull request with errors' do
expect(subject.execute(params).errors).not_to be_empty
end
end
end
end
end
...@@ -32,7 +32,7 @@ describe CreateGithubWebhookWorker do ...@@ -32,7 +32,7 @@ describe CreateGithubWebhookWorker do
insecure_ssl: 1 insecure_ssl: 1
}, },
{ {
events: ['push'], events: %w[push pull_request],
active: true active: true
} }
) )
......
...@@ -19,6 +19,7 @@ module Gitlab ...@@ -19,6 +19,7 @@ module Gitlab
user: @command.current_user, user: @command.current_user,
pipeline_schedule: @command.schedule, pipeline_schedule: @command.schedule,
merge_request: @command.merge_request, merge_request: @command.merge_request,
external_pull_request: @command.external_pull_request,
variables_attributes: Array(@command.variables_attributes) variables_attributes: Array(@command.variables_attributes)
) )
......
...@@ -7,7 +7,7 @@ module Gitlab ...@@ -7,7 +7,7 @@ module Gitlab
Command = Struct.new( Command = Struct.new(
:source, :project, :current_user, :source, :project, :current_user,
:origin_ref, :checkout_sha, :after_sha, :before_sha, :source_sha, :target_sha, :origin_ref, :checkout_sha, :after_sha, :before_sha, :source_sha, :target_sha,
:trigger_request, :schedule, :merge_request, :trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted, :ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options, :seeds_block, :variables_attributes, :push_options,
:chat_data, :allow_mirror_update :chat_data, :allow_mirror_update
......
...@@ -64,6 +64,8 @@ project_tree: ...@@ -64,6 +64,8 @@ project_tree:
- :push_event_payload - :push_event_payload
- stages: - stages:
- :statuses - :statuses
- :external_pull_request
- :external_pull_requests
- :auto_devops - :auto_devops
- :triggers - :triggers
- :pipeline_schedules - :pipeline_schedules
......
# frozen_string_literal: true
FactoryBot.define do
factory :external_pull_request do
sequence(:pull_request_iid)
project
source_branch 'feature'
source_repository 'the-repository'
source_sha '97de212e80737a608d939f648d959671fb0a0142'
target_branch 'master'
target_repository 'the-repository'
target_sha 'a09386439ca39abe575675ffd4b89ae824fec22f'
status :open
trait(:closed) { status 'closed' }
end
end
...@@ -84,6 +84,20 @@ describe Gitlab::Ci::Build::Policy::Refs do ...@@ -84,6 +84,20 @@ describe Gitlab::Ci::Build::Policy::Refs do
.not_to be_satisfied_by(pipeline) .not_to be_satisfied_by(pipeline)
end end
end end
context 'when source is external_pull_request_event' do
let(:pipeline) { build_stubbed(:ci_pipeline, source: :external_pull_request_event) }
it 'is satisfied with only: external_pull_request' do
expect(described_class.new(%w[external_pull_requests]))
.to be_satisfied_by(pipeline)
end
it 'is not satisfied with only: external_pull_request_event' do
expect(described_class.new(%w[external_pull_request_events]))
.not_to be_satisfied_by(pipeline)
end
end
end end
context 'when matching a ref by a regular expression' do context 'when matching a ref by a regular expression' do
......
...@@ -128,4 +128,38 @@ describe Gitlab::Ci::Pipeline::Chain::Build do ...@@ -128,4 +128,38 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
expect(pipeline.target_sha).to eq(merge_request.target_branch_sha) expect(pipeline.target_sha).to eq(merge_request.target_branch_sha)
end end
end end
context 'when pipeline is running for an external pull request' do
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
source: :external_pull_request_event,
origin_ref: 'feature',
checkout_sha: project.commit.id,
after_sha: nil,
before_sha: nil,
source_sha: external_pull_request.source_sha,
target_sha: external_pull_request.target_sha,
trigger_request: nil,
schedule: nil,
external_pull_request: external_pull_request,
project: project,
current_user: user)
end
let(:external_pull_request) { build(:external_pull_request, project: project) }
before do
step.perform!
end
it 'correctly indicated that this is an external pull request pipeline' do
expect(pipeline).to be_external_pull_request_event
expect(pipeline.external_pull_request).to eq(external_pull_request)
end
it 'correctly sets source sha and target sha to pipeline' do
expect(pipeline.source_sha).to eq(external_pull_request.source_sha)
expect(pipeline.target_sha).to eq(external_pull_request.target_sha)
end
end
end end
...@@ -127,6 +127,8 @@ merge_requests: ...@@ -127,6 +127,8 @@ merge_requests:
- blocks_as_blockee - blocks_as_blockee
- blocking_merge_requests - blocking_merge_requests
- blocked_merge_requests - blocked_merge_requests
external_pull_requests:
- project
merge_request_diff: merge_request_diff:
- merge_request - merge_request
- merge_request_diff_commits - merge_request_diff_commits
...@@ -156,6 +158,7 @@ ci_pipelines: ...@@ -156,6 +158,7 @@ ci_pipelines:
- pipeline_schedule - pipeline_schedule
- merge_requests_as_head_pipeline - merge_requests_as_head_pipeline
- merge_request - merge_request
- external_pull_request
- deployments - deployments
- environments - environments
- chat_data - chat_data
...@@ -403,6 +406,7 @@ project: ...@@ -403,6 +406,7 @@ project:
- merge_trains - merge_trains
- designs - designs
- project_aliases - project_aliases
- external_pull_requests
award_emoji: award_emoji:
- awardable - awardable
- user - user
......
...@@ -270,6 +270,7 @@ Ci::Pipeline: ...@@ -270,6 +270,7 @@ Ci::Pipeline:
- protected - protected
- iid - iid
- merge_request_id - merge_request_id
- external_pull_request_id
Ci::Stage: Ci::Stage:
- id - id
- name - name
...@@ -715,3 +716,16 @@ List: ...@@ -715,3 +716,16 @@ List:
- updated_at - updated_at
- milestone_id - milestone_id
- user_id - user_id
ExternalPullRequest:
- id
- created_at
- updated_at
- project_id
- pull_request_iid
- status
- source_branch
- target_branch
- source_repository
- target_repository
- source_sha
- target_sha
...@@ -20,6 +20,7 @@ describe Ci::Pipeline, :mailer do ...@@ -20,6 +20,7 @@ describe Ci::Pipeline, :mailer do
it { is_expected.to belong_to(:auto_canceled_by) } it { is_expected.to belong_to(:auto_canceled_by) }
it { is_expected.to belong_to(:pipeline_schedule) } it { is_expected.to belong_to(:pipeline_schedule) }
it { is_expected.to belong_to(:merge_request) } it { is_expected.to belong_to(:merge_request) }
it { is_expected.to belong_to(:external_pull_request) }
it { is_expected.to have_many(:statuses) } it { is_expected.to have_many(:statuses) }
it { is_expected.to have_many(:trigger_requests) } it { is_expected.to have_many(:trigger_requests) }
...@@ -885,6 +886,25 @@ describe Ci::Pipeline, :mailer do ...@@ -885,6 +886,25 @@ describe Ci::Pipeline, :mailer do
end end
end end
end end
context 'when source is external pull request' do
let(:pipeline) do
create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: pull_request)
end
let(:pull_request) { create(:external_pull_request, project: project) }
it 'exposes external pull request pipeline variables' do
expect(subject.to_hash)
.to include(
'CI_EXTERNAL_PULL_REQUEST_IID' => pull_request.pull_request_iid.to_s,
'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA' => pull_request.source_sha,
'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA' => pull_request.target_sha,
'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME' => pull_request.source_branch,
'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME' => pull_request.target_branch
)
end
end
end end
describe '#protected_ref?' do describe '#protected_ref?' do
......
# frozen_string_literal: true
require 'spec_helper'
describe ExternalPullRequest do
let(:project) { create(:project) }
let(:source_branch) { 'the-branch' }
let(:status) { :open }
it { is_expected.to belong_to(:project) }
shared_examples 'has errors on' do |attribute|
it "has errors for #{attribute}" do
expect(subject).not_to be_valid
expect(subject.errors[attribute]).not_to be_empty
end
end
describe 'validations' do
context 'when source branch not present' do
subject { build(:external_pull_request, source_branch: nil) }
it_behaves_like 'has errors on', :source_branch
end
context 'when status not present' do
subject { build(:external_pull_request, status: nil) }
it_behaves_like 'has errors on', :status
end
context 'when pull request is from a fork' do
subject { build(:external_pull_request, source_repository: 'the-fork', target_repository: 'the-target') }
it_behaves_like 'has errors on', :base
end
end
describe 'create_or_update_from_params' do
subject { described_class.create_or_update_from_params(params) }
context 'when pull request does not exist' do
context 'when params are correct' do
let(:params) do
{
project_id: project.id,
pull_request_iid: 123,
source_branch: 'feature',
target_branch: 'master',
source_repository: 'the-repository',
target_repository: 'the-repository',
source_sha: '97de212e80737a608d939f648d959671fb0a0142',
target_sha: 'a09386439ca39abe575675ffd4b89ae824fec22f',
status: :open
}
end
it 'saves the model successfully and returns it' do
expect(subject).to be_persisted
expect(subject).to be_valid
end
it 'yields the model' do
yielded_value = nil
result = described_class.create_or_update_from_params(params) do |pull_request|
yielded_value = pull_request
end
expect(result).to eq(yielded_value)
end
end
context 'when params are not correct' do
let(:params) do
{
pull_request_iid: 123,
source_branch: 'feature',
target_branch: 'master',
source_repository: 'the-repository',
target_repository: 'the-repository',
source_sha: nil,
target_sha: nil,
status: :open
}
end
it 'returns an invalid model' do
expect(subject).not_to be_persisted
expect(subject).not_to be_valid
end
end
end
context 'when pull request exists' do
let!(:pull_request) do
create(:external_pull_request,
project: project,
source_sha: '97de212e80737a608d939f648d959671fb0a0142')
end
context 'when params are correct' do
let(:params) do
{
pull_request_iid: pull_request.pull_request_iid,
source_branch: pull_request.source_branch,
target_branch: pull_request.target_branch,
source_repository: 'the-repository',
target_repository: 'the-repository',
source_sha: 'ce84140e8b878ce6e7c4d298c7202ff38170e3ac',
target_sha: pull_request.target_sha,
status: :open
}
end
it 'updates the model successfully and returns it' do
expect(subject).to be_valid
expect(subject.source_sha).to eq(params[:source_sha])
expect(pull_request.reload.source_sha).to eq(params[:source_sha])
end
end
context 'when params are not correct' do
let(:params) do
{
pull_request_iid: pull_request.pull_request_iid,
source_branch: pull_request.source_branch,
target_branch: pull_request.target_branch,
source_repository: 'the-repository',
target_repository: 'the-repository',
source_sha: nil,
target_sha: nil,
status: :open
}
end
it 'returns an invalid model' do
expect(subject).not_to be_valid
expect(pull_request.reload.source_sha).not_to be_nil
expect(pull_request.target_sha).not_to be_nil
end
end
end
end
describe '#open?' do
it 'returns true if status is open' do
pull_request = create(:external_pull_request, status: :open)
expect(pull_request).to be_open
end
it 'returns false if status is not open' do
pull_request = create(:external_pull_request, status: :closed)
expect(pull_request).not_to be_open
end
end
describe '#closed?' do
it 'returns true if status is closed' do
pull_request = build(:external_pull_request, status: :closed)
expect(pull_request).to be_closed
end
it 'returns false if status is not closed' do
pull_request = build(:external_pull_request, status: :open)
expect(pull_request).not_to be_closed
end
end
describe '#actual_branch_head?' do
let(:project) { create(:project, :repository) }
let(:branch) { project.repository.branches.first }
let(:source_branch) { branch.name }
let(:pull_request) do
create(:external_pull_request,
project: project,
source_branch: source_branch,
source_sha: source_sha)
end
context 'when source sha matches the head of the branch' do
let(:source_sha) { branch.target }
it 'returns true' do
expect(pull_request).to be_actual_branch_head
end
end
context 'when source sha does not match the head of the branch' do
let(:source_sha) { project.repository.commit('HEAD').sha }
it 'returns true' do
expect(pull_request).not_to be_actual_branch_head
end
end
end
describe '#from_fork?' do
it 'returns true if source_repository differs from target_repository' do
pull_request = build(:external_pull_request,
source_repository: 'repository-1',
target_repository: 'repository-2')
expect(pull_request).to be_from_fork
end
it 'returns false if source_repository is the same as target_repository' do
pull_request = build(:external_pull_request,
source_repository: 'repository-1',
target_repository: 'repository-1')
expect(pull_request).not_to be_from_fork
end
end
end
...@@ -99,6 +99,7 @@ describe Project do ...@@ -99,6 +99,7 @@ describe Project do
it { is_expected.to have_many(:project_deploy_tokens) } it { is_expected.to have_many(:project_deploy_tokens) }
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
it { is_expected.to have_many(:cycle_analytics_stages) } it { is_expected.to have_many(:cycle_analytics_stages) }
it { is_expected.to have_many(:external_pull_requests) }
it 'has an inverse relationship with merge requests' do it 'has an inverse relationship with merge requests' do
expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project) expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
......
...@@ -23,6 +23,7 @@ describe Ci::CreatePipelineService do ...@@ -23,6 +23,7 @@ describe Ci::CreatePipelineService do
trigger_request: nil, trigger_request: nil,
variables_attributes: nil, variables_attributes: nil,
merge_request: nil, merge_request: nil,
external_pull_request: nil,
push_options: nil, push_options: nil,
source_sha: nil, source_sha: nil,
target_sha: nil, target_sha: nil,
...@@ -36,8 +37,11 @@ describe Ci::CreatePipelineService do ...@@ -36,8 +37,11 @@ describe Ci::CreatePipelineService do
source_sha: source_sha, source_sha: source_sha,
target_sha: target_sha } target_sha: target_sha }
described_class.new(project, user, params).execute( described_class.new(project, user, params).execute(source,
source, save_on_errors: save_on_errors, trigger_request: trigger_request, merge_request: merge_request) save_on_errors: save_on_errors,
trigger_request: trigger_request,
merge_request: merge_request,
external_pull_request: external_pull_request)
end end
# rubocop:enable Metrics/ParameterLists # rubocop:enable Metrics/ParameterLists
...@@ -969,6 +973,152 @@ describe Ci::CreatePipelineService do ...@@ -969,6 +973,152 @@ describe Ci::CreatePipelineService do
end end
end end
describe 'Pipeline for external pull requests' do
let(:pipeline) do
execute_service(source: source,
external_pull_request: pull_request,
ref: ref_name,
source_sha: source_sha,
target_sha: target_sha)
end
before do
stub_ci_pipeline_yaml_file(YAML.dump(config))
end
let(:ref_name) { 'refs/heads/feature' }
let(:source_sha) { project.commit(ref_name).id }
let(:target_sha) { nil }
context 'when source is external pull request' do
let(:source) { :external_pull_request_event }
context 'when config has external_pull_requests keywords' do
let(:config) do
{
build: {
stage: 'build',
script: 'echo'
},
test: {
stage: 'test',
script: 'echo',
only: ['external_pull_requests']
},
pages: {
stage: 'deploy',
script: 'echo',
except: ['external_pull_requests']
}
}
end
context 'when external pull request is specified' do
let(:pull_request) { create(:external_pull_request, project: project, source_branch: 'feature', target_branch: 'master') }
let(:ref_name) { pull_request.source_ref }
it 'creates an external pull request pipeline' do
expect(pipeline).to be_persisted
expect(pipeline).to be_external_pull_request_event
expect(pipeline.external_pull_request).to eq(pull_request)
expect(pipeline.source_sha).to eq(source_sha)
expect(pipeline.builds.order(:stage_id)
.map(&:name))
.to eq(%w[build test])
end
context 'when ref is tag' do
let(:ref_name) { 'refs/tags/v1.1.0' }
it 'does not create an extrnal pull request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:tag]).to eq(["is not included in the list"])
end
end
context 'when pull request is created from fork' do
it 'does not create an external pull request pipeline'
end
context "when there are no matched jobs" do
let(:config) do
{
test: {
stage: 'test',
script: 'echo',
except: ['external_pull_requests']
}
}
end
it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base]).to eq(["No stages / jobs for this pipeline."])
end
end
end
context 'when external pull request is not specified' do
let(:pull_request) { nil }
it 'does not create an external pull request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:external_pull_request]).to eq(["can't be blank"])
end
end
end
context "when config does not have external_pull_requests keywords" do
let(:config) do
{
build: {
stage: 'build',
script: 'echo'
},
test: {
stage: 'test',
script: 'echo'
},
pages: {
stage: 'deploy',
script: 'echo'
}
}
end
context 'when external pull request is specified' do
let(:pull_request) do
create(:external_pull_request,
project: project,
source_branch: Gitlab::Git.ref_name(ref_name),
target_branch: 'master')
end
it 'creates an external pull request pipeline' do
expect(pipeline).to be_persisted
expect(pipeline).to be_external_pull_request_event
expect(pipeline.external_pull_request).to eq(pull_request)
expect(pipeline.source_sha).to eq(source_sha)
expect(pipeline.builds.order(:stage_id)
.map(&:name))
.to eq(%w[build test pages])
end
end
context 'when external pull request is not specified' do
let(:pull_request) { nil }
it 'does not create an external pull request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
.to eq(['Failed to build the pipeline!'])
end
end
end
end
end
describe 'Pipelines for merge requests' do describe 'Pipelines for merge requests' do
let(:pipeline) do let(:pipeline) do
execute_service(source: source, execute_service(source: source,
......
# frozen_string_literal: true
require 'spec_helper'
describe ExternalPullRequests::CreatePipelineService do
describe '#execute' do
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:pull_request) { create(:external_pull_request, project: project) }
before do
project.add_maintainer(user)
end
subject { described_class.new(project, user).execute(pull_request) }
context 'when pull request is open' do
before do
pull_request.update!(status: :open)
end
context 'when source sha is the head of the source branch' do
let(:source_branch) { project.repository.branches.last }
let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService) }
before do
pull_request.update!(source_branch: source_branch.name, source_sha: source_branch.target)
end
it 'creates a pipeline for external pull request' do
expect(subject).to be_valid
expect(subject).to be_persisted
expect(subject).to be_external_pull_request_event
expect(subject).to eq(project.ci_pipelines.last)
expect(subject.external_pull_request).to eq(pull_request)
expect(subject.user).to eq(user)
expect(subject.status).to eq('pending')
expect(subject.ref).to eq(pull_request.source_branch)
expect(subject.sha).to eq(pull_request.source_sha)
expect(subject.source_sha).to eq(pull_request.source_sha)
end
end
context 'when source sha is not the head of the source branch (force push upon rebase)' do
let(:source_branch) { project.repository.branches.first }
let(:commit) { project.repository.commits(source_branch.name, limit: 2).last }
before do
pull_request.update!(source_branch: source_branch.name, source_sha: commit.sha)
end
it 'does nothing' do
expect(Ci::CreatePipelineService).not_to receive(:new)
expect(subject).to be_nil
end
end
end
context 'when pull request is not opened' do
before do
pull_request.update!(status: :closed)
end
it 'does nothing' do
expect(Ci::CreatePipelineService).not_to receive(:new)
expect(subject).to be_nil
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe UpdateExternalPullRequestsWorker do
describe '#perform' do
set(:project) { create(:project, import_source: 'tanuki/repository') }
set(:user) { create(:user) }
let(:worker) { described_class.new }
before do
create(:external_pull_request,
project: project,
source_repository: project.import_source,
target_repository: project.import_source,
source_branch: 'feature-1',
target_branch: 'master')
create(:external_pull_request,
project: project,
source_repository: project.import_source,
target_repository: project.import_source,
source_branch: 'feature-1',
target_branch: 'develop')
end
subject { worker.perform(project.id, user.id, ref) }
context 'when ref is a branch' do
let(:ref) { 'refs/heads/feature-1' }
let(:create_pipeline_service) { instance_double(ExternalPullRequests::CreatePipelineService) }
it 'runs CreatePipelineService for each pull request matching the source branch and repository' do
expect(ExternalPullRequests::CreatePipelineService)
.to receive(:new)
.and_return(create_pipeline_service)
.twice
expect(create_pipeline_service).to receive(:execute).twice
subject
end
end
context 'when ref is not a branch' do
let(:ref) { 'refs/tags/v1.2.3' }
it 'does nothing' do
expect(ExternalPullRequests::CreatePipelineService).not_to receive(:new)
subject
end
end
end
end
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