create_pipeline_service_spec.rb 21.6 KB
Newer Older
1 2
require 'spec_helper'

3
describe Ci::CreatePipelineService do
B
Bob Van Landuyt 已提交
4 5
  include ProjectForksHelper

6
  set(:project) { create(:project, :repository) }
7
  let(:user) { create(:admin) }
8
  let(:ref_name) { 'refs/heads/master' }
9 10

  before do
11
    stub_repository_ci_yaml_file(sha: anything)
12 13 14
  end

  describe '#execute' do
15 16 17 18 19
    def execute_service(
      source: :push,
      after: project.commit.id,
      message: 'Message',
      ref: ref_name,
20
      trigger_request: nil,
21
      variables_attributes: nil)
22 23 24
      params = { ref: ref,
                 before: '00000000',
                 after: after,
25
                 commits: [{ message: message }],
26
                 variables_attributes: variables_attributes }
27

28 29
      described_class.new(project, user, params).execute(
        source, trigger_request: trigger_request)
30 31
    end

32 33 34
    context 'valid params' do
      let(:pipeline) { execute_service }

35 36 37 38 39 40
      let(:pipeline_on_previous_commit) do
        execute_service(
          after: previous_commit_sha_from_ref('master')
        )
      end

41 42 43
      it 'creates a pipeline' do
        expect(pipeline).to be_kind_of(Ci::Pipeline)
        expect(pipeline).to be_valid
44
        expect(pipeline).to be_persisted
45
        expect(pipeline).to be_push
46 47 48
        expect(pipeline).to eq(project.pipelines.last)
        expect(pipeline).to have_attributes(user: user)
        expect(pipeline).to have_attributes(status: 'pending')
49
        expect(pipeline.repository_source?).to be true
50 51
        expect(pipeline.builds.first).to be_kind_of(Ci::Build)
      end
52

53 54
      it 'increments the prometheus counter' do
        expect(Gitlab::Metrics).to receive(:counter)
B
Ben Kochie 已提交
55
          .with(:pipelines_created_total, "Counter of pipelines created")
56 57 58 59 60
          .and_call_original

        pipeline
      end

61
      context 'when merge requests already exist for this source branch' do
62 63 64 65 66 67
        let(:merge_request_1) do
          create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project)
        end
        let(:merge_request_2) do
          create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project)
        end
68

69 70 71 72 73 74 75 76 77 78 79 80
        context 'when related merge request is already merged' do
          let!(:merged_merge_request) do
            create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project, state: 'merged')
          end

          it 'does not schedule update head pipeline job' do
            expect(UpdateHeadPipelineForMergeRequestWorker).not_to receive(:perform_async).with(merged_merge_request.id)

            execute_service
          end
        end

81 82 83 84 85 86
        context 'when the head pipeline sha equals merge request sha' do
          it 'updates head pipeline of each merge request' do
            merge_request_1
            merge_request_2

            head_pipeline = execute_service
87

88 89 90 91 92 93
            expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
            expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
          end
        end

        context 'when the head pipeline sha does not equal merge request sha' do
94
          it 'does not update the head piepeline of MRs' do
95 96 97 98
            merge_request_1
            merge_request_2

            allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(true)
99

100
            expect { execute_service(after: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }.not_to raise_error
101 102 103 104 105 106

            last_pipeline = Ci::Pipeline.last

            expect(merge_request_1.reload.head_pipeline).not_to eq(last_pipeline)
            expect(merge_request_2.reload.head_pipeline).not_to eq(last_pipeline)
          end
107 108 109 110
        end

        context 'when there is no pipeline for source branch' do
          it "does not update merge request head pipeline" do
111 112 113
            merge_request = create(:merge_request, source_branch: 'feature',
                                                   target_branch: "branch_1",
                                                   source_project: project)
114

115
            head_pipeline = execute_service
116 117 118 119 120 121

            expect(merge_request.reload.head_pipeline).not_to eq(head_pipeline)
          end
        end

        context 'when merge request target project is different from source project' do
B
Bob Van Landuyt 已提交
122
          let!(:project) { fork_project(target_project, nil, repository: true) }
123
          let!(:target_project) { create(:project, :repository) }
124

125
          it 'updates head pipeline for merge request' do
126 127 128 129
            merge_request = create(:merge_request, source_branch: 'master',
                                                   target_branch: "branch_1",
                                                   source_project: project,
                                                   target_project: target_project)
130

131
            head_pipeline = execute_service
132 133 134 135

            expect(merge_request.reload.head_pipeline).to eq(head_pipeline)
          end
        end
136

137
        context 'when the pipeline is not the latest for the branch' do
138
          it 'does not update merge request head pipeline' do
139 140 141
            merge_request = create(:merge_request, source_branch: 'master',
                                                   target_branch: "branch_1",
                                                   source_project: project)
142

143
            allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(false)
144

145
            execute_service
146 147 148 149

            expect(merge_request.reload.head_pipeline).to be_nil
          end
        end
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167

        context 'when pipeline has errors' do
          before do
            stub_ci_pipeline_yaml_file('some invalid syntax')
          end

          it 'updates merge request head pipeline reference' do
            merge_request = create(:merge_request, source_branch: 'master',
                                                   target_branch: 'feature',
                                                   source_project: project)

            head_pipeline = execute_service

            expect(head_pipeline).to be_persisted
            expect(head_pipeline.yaml_errors).to be_present
            expect(merge_request.reload.head_pipeline).to eq head_pipeline
          end
        end
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187

        context 'when pipeline has been skipped' do
          before do
            allow_any_instance_of(Ci::Pipeline)
              .to receive(:git_commit_message)
              .and_return('some commit [ci skip]')
          end

          it 'updates merge request head pipeline' do
            merge_request = create(:merge_request, source_branch: 'master',
                                                   target_branch: 'feature',
                                                   source_project: project)

            head_pipeline = execute_service

            expect(head_pipeline).to be_skipped
            expect(head_pipeline).to be_persisted
            expect(merge_request.reload.head_pipeline).to eq head_pipeline
          end
        end
188 189
      end

190 191 192 193 194
      context 'auto-cancel enabled' do
        before do
          project.update(auto_cancel_pending_pipelines: 'enabled')
        end

195 196
        it 'does not cancel HEAD pipeline' do
          pipeline
R
Rydkin Maxim 已提交
197
          pipeline_on_previous_commit
198

R
Rydkin Maxim 已提交
199
          expect(pipeline.reload).to have_attributes(status: 'pending', auto_canceled_by_id: nil)
200
        end
201 202

        it 'auto cancel pending non-HEAD pipelines' do
R
Rydkin Maxim 已提交
203
          pipeline_on_previous_commit
204 205
          pipeline

R
Rydkin Maxim 已提交
206
          expect(pipeline_on_previous_commit.reload).to have_attributes(status: 'canceled', auto_canceled_by_id: pipeline.id)
207 208 209
        end

        it 'does not cancel running outdated pipelines' do
R
Rydkin Maxim 已提交
210
          pipeline_on_previous_commit.run
211 212
          execute_service

R
Rydkin Maxim 已提交
213
          expect(pipeline_on_previous_commit.reload).to have_attributes(status: 'running', auto_canceled_by_id: nil)
214 215 216
        end

        it 'cancel created outdated pipelines' do
R
Rydkin Maxim 已提交
217
          pipeline_on_previous_commit.update(status: 'created')
218 219
          pipeline

R
Rydkin Maxim 已提交
220
          expect(pipeline_on_previous_commit.reload).to have_attributes(status: 'canceled', auto_canceled_by_id: pipeline.id)
221 222 223 224 225 226 227 228 229
        end

        it 'does not cancel pipelines from the other branches' do
          pending_pipeline = execute_service(
            ref: 'refs/heads/feature',
            after: previous_commit_sha_from_ref('feature')
          )
          pipeline

230
          expect(pending_pipeline.reload).to have_attributes(status: 'pending', auto_canceled_by_id: nil)
231 232
        end
      end
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250

      context 'auto-cancel disabled' do
        before do
          project.update(auto_cancel_pending_pipelines: 'disabled')
        end

        it 'does not auto cancel pending non-HEAD pipelines' do
          pipeline_on_previous_commit
          pipeline

          expect(pipeline_on_previous_commit.reload)
            .to have_attributes(status: 'pending', auto_canceled_by_id: nil)
        end
      end

      def previous_commit_sha_from_ref(ref)
        project.commit(ref).parent.sha
      end
251 252
    end

253 254
    context "skip tag if there is no build for it" do
      it "creates commit if there is appropriate job" do
255
        expect(execute_service).to be_persisted
256 257 258 259 260 261
      end

      it "creates commit if there is no appropriate job but deploy job has right ref setting" do
        config = YAML.dump({ deploy: { script: "ls", only: ["master"] } })
        stub_ci_pipeline_yaml_file(config)

262
        expect(execute_service).to be_persisted
263 264 265 266 267 268
      end
    end

    it 'skips creating pipeline for refs without .gitlab-ci.yml' do
      stub_ci_pipeline_yaml_file(nil)

269
      expect(execute_service).not_to be_persisted
270 271 272
      expect(Ci::Pipeline.count).to eq(0)
    end

273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
    shared_examples 'a failed pipeline' do
      it 'creates failed pipeline' do
        stub_ci_pipeline_yaml_file(ci_yaml)

        pipeline = execute_service(message: message)

        expect(pipeline).to be_persisted
        expect(pipeline.builds.any?).to be false
        expect(pipeline.status).to eq('failed')
        expect(pipeline.yaml_errors).not_to be_nil
      end
    end

    context 'when yaml is invalid' do
      let(:ci_yaml) { 'invalid: file: fiile' }
      let(:message) { 'Message' }

      it_behaves_like 'a failed pipeline'

      context 'when receive git commit' do
        before do
          allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
        end

        it_behaves_like 'a failed pipeline'
      end
299 300 301 302
    end

    context 'when commit contains a [ci skip] directive' do
      let(:message) { "some message[ci skip]" }
303 304 305 306 307 308 309 310 311 312 313

      ci_messages = [
        "some message[ci skip]",
        "some message[skip ci]",
        "some message[CI SKIP]",
        "some message[SKIP CI]",
        "some message[ci_skip]",
        "some message[skip_ci]",
        "some message[ci-skip]",
        "some message[skip-ci]"
      ]
314 315 316 317 318

      before do
        allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
      end

319 320
      ci_messages.each do |ci_message|
        it "skips builds creation if the commit message is #{ci_message}" do
321
          pipeline = execute_service(message: ci_message)
322 323 324 325 326

          expect(pipeline).to be_persisted
          expect(pipeline.builds.any?).to be false
          expect(pipeline.status).to eq("skipped")
        end
327 328
      end

329
      shared_examples 'creating a pipeline' do
R
Rydkin Maxim 已提交
330
        it 'does not skip pipeline creation' do
331
          allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { commit_message }
332

333
          pipeline = execute_service(message: commit_message)
334

335 336 337
          expect(pipeline).to be_persisted
          expect(pipeline.builds.first.name).to eq("rspec")
        end
338 339
      end

340 341
      context 'when commit message does not contain [ci skip] nor [skip ci]' do
        let(:commit_message) { 'some message' }
342

343
        it_behaves_like 'creating a pipeline'
344 345
      end

346 347
      context 'when commit message is nil' do
        let(:commit_message) { nil }
348

349
        it_behaves_like 'creating a pipeline'
350 351
      end

352 353 354 355 356
      context 'when there is [ci skip] tag in commit message and yaml is invalid' do
        let(:ci_yaml) { 'invalid: file: fiile' }

        it_behaves_like 'a failed pipeline'
      end
357 358 359 360 361 362 363 364 365
    end

    context 'when there are no jobs for this pipeline' do
      before do
        config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
        stub_ci_pipeline_yaml_file(config)
      end

      it 'does not create a new pipeline' do
366
        result = execute_service
367 368 369 370 371 372 373 374 375 376 377 378 379 380

        expect(result).not_to be_persisted
        expect(Ci::Build.all).to be_empty
        expect(Ci::Pipeline.count).to eq(0)
      end
    end

    context 'with manual actions' do
      before do
        config = YAML.dump({ deploy: { script: 'ls', when: 'manual' } })
        stub_ci_pipeline_yaml_file(config)
      end

      it 'does not create a new pipeline' do
381
        result = execute_service
382 383 384 385 386

        expect(result).to be_persisted
        expect(result.manual_actions).not_to be_empty
      end
    end
387 388 389

    context 'with environment' do
      before do
K
Kamil Trzciński 已提交
390 391 392 393 394 395 396
        config = YAML.dump(
          deploy: {
            environment: { name: "review/$CI_COMMIT_REF_NAME" },
            script: 'ls',
            tags: ['hello']
          })

397 398 399
        stub_ci_pipeline_yaml_file(config)
      end

K
Kamil Trzciński 已提交
400
      it 'creates the environment with tags' do
401
        result = execute_service
402 403

        expect(result).to be_persisted
404
        expect(Environment.find_by(name: "review/master")).to be_present
K
Kamil Trzciński 已提交
405
        expect(result.builds.first.tag_list).to contain_exactly('hello')
K
Kamil Trzciński 已提交
406 407
        expect(result.builds.first.deployment).to be_persisted
        expect(result.builds.first.deployment.deployable).to be_a(Ci::Build)
K
Kamil Trzciński 已提交
408
      end
409 410 411 412 413 414 415
    end

    context 'with environment name including persisted variables' do
      before do
        config = YAML.dump(
          deploy: {
            environment: { name: "review/id1$CI_PIPELINE_ID/id2$CI_BUILD_ID" },
416 417
            script: 'ls'
          }
418 419 420 421 422 423 424 425 426 427
        )

        stub_ci_pipeline_yaml_file(config)
      end

      it 'skipps persisted variables in environment name' do
        result = execute_service

        expect(result).to be_persisted
        expect(Environment.find_by(name: "review/id1/id2")).to be_present
428 429
      end
    end
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444

    context 'when environment with invalid name' do
      before do
        config = YAML.dump(deploy: { environment: { name: 'name,with,commas' }, script: 'ls' })
        stub_ci_pipeline_yaml_file(config)
      end

      it 'does not create an environment' do
        expect do
          result = execute_service

          expect(result).to be_persisted
        end.not_to change { Environment.count }
      end
    end
445

446
    context 'when builds with auto-retries are configured' do
447 448 449 450 451 452 453 454 455 456 457 458 459
      context 'as an integer' do
        before do
          config = YAML.dump(rspec: { script: 'rspec', retry: 2 })
          stub_ci_pipeline_yaml_file(config)
        end

        it 'correctly creates builds with auto-retry value configured' do
          pipeline = execute_service

          expect(pipeline).to be_persisted
          expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
          expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['always']
        end
460 461
      end

462 463 464 465 466
      context 'as hash' do
        before do
          config = YAML.dump(rspec: { script: 'rspec', retry: { max: 2, when: 'runner_system_failure' } })
          stub_ci_pipeline_yaml_file(config)
        end
467

468 469 470 471 472 473 474
        it 'correctly creates builds with auto-retry value configured' do
          pipeline = execute_service

          expect(pipeline).to be_persisted
          expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
          expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['runner_system_failure']
        end
475 476
      end
    end
477

478 479 480 481 482 483 484 485 486 487 488 489 490 491
    shared_examples 'when ref is protected' do
      let(:user) { create(:user) }

      context 'when user is developer' do
        before do
          project.add_developer(user)
        end

        it 'does not create a pipeline' do
          expect(execute_service).not_to be_persisted
          expect(Ci::Pipeline.count).to eq(0)
        end
      end

492
      context 'when user is maintainer' do
S
Shinya Maeda 已提交
493 494
        let(:pipeline) { execute_service }

495
        before do
496
          project.add_maintainer(user)
497 498
        end

S
Shinya Maeda 已提交
499 500 501
        it 'creates a protected pipeline' do
          expect(pipeline).to be_persisted
          expect(pipeline).to be_protected
502 503 504
          expect(Ci::Pipeline.count).to eq(1)
        end
      end
505 506 507 508 509 510 511 512 513 514 515 516 517

      context 'when trigger belongs to no one' do
        let(:user) {}
        let(:trigger_request) { create(:ci_trigger_request) }

        it 'does not create a pipeline' do
          expect(execute_service(trigger_request: trigger_request))
            .not_to be_persisted
          expect(Ci::Pipeline.count).to eq(0)
        end
      end

      context 'when trigger belongs to a developer' do
S
Shinya Maeda 已提交
518 519 520
        let(:user) { create(:user) }
        let(:trigger) { create(:ci_trigger, owner: user) }
        let(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
521

S
Shinya Maeda 已提交
522 523
        before do
          project.add_developer(user)
524 525 526 527 528 529 530 531 532
        end

        it 'does not create a pipeline' do
          expect(execute_service(trigger_request: trigger_request))
            .not_to be_persisted
          expect(Ci::Pipeline.count).to eq(0)
        end
      end

533
      context 'when trigger belongs to a maintainer' do
S
Shinya Maeda 已提交
534 535 536
        let(:user) { create(:user) }
        let(:trigger) { create(:ci_trigger, owner: user) }
        let(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
537

S
Shinya Maeda 已提交
538
        before do
539
          project.add_maintainer(user)
540 541
        end

S
Shinya Maeda 已提交
542
        it 'creates a pipeline' do
543 544 545 546 547
          expect(execute_service(trigger_request: trigger_request))
            .to be_persisted
          expect(Ci::Pipeline.count).to eq(1)
        end
      end
548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566
    end

    context 'when ref is a protected branch' do
      before do
        create(:protected_branch, project: project, name: 'master')
      end

      it_behaves_like 'when ref is protected'
    end

    context 'when ref is a protected tag' do
      let(:ref_name) { 'refs/tags/v1.0.0' }

      before do
        create(:protected_tag, project: project, name: '*')
      end

      it_behaves_like 'when ref is protected'
    end
567 568 569 570

    context 'when ref is not protected' do
      context 'when trigger belongs to no one' do
        let(:user) {}
571 572
        let(:trigger) { create(:ci_trigger, owner: nil) }
        let(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
S
Shinya Maeda 已提交
573
        let(:pipeline) { execute_service(trigger_request: trigger_request) }
574

S
Shinya Maeda 已提交
575 576 577
        it 'creates an unprotected pipeline' do
          expect(pipeline).to be_persisted
          expect(pipeline).not_to be_protected
578
          expect(Ci::Pipeline.count).to eq(1)
579 580 581
        end
      end
    end
582

K
Kamil Trzcinski 已提交
583
    context 'when pipeline is running for a tag' do
584 585
      before do
        config = YAML.dump(test: { script: 'test', only: ['branches'] },
K
Kamil Trzcinski 已提交
586
                           deploy: { script: 'deploy', only: ['tags'] })
587 588 589 590 591

        stub_ci_pipeline_yaml_file(config)
      end

      it 'creates a tagged pipeline' do
K
Kamil Trzcinski 已提交
592
        pipeline = execute_service(ref: 'v1.0.0')
593 594 595 596

        expect(pipeline.tag?).to be true
      end
    end
597 598

    context 'when pipeline variables are specified' do
599
      let(:variables_attributes) do
600 601 602 603
        [{ key: 'first', secret_value: 'world' },
         { key: 'second', secret_value: 'second_world' }]
      end

604
      subject { execute_service(variables_attributes: variables_attributes) }
605 606

      it 'creates a pipeline with specified variables' do
M
Matija Čupić 已提交
607 608
        expect(subject.variables.map { |var| var.slice(:key, :secret_value) })
          .to eq variables_attributes.map(&:with_indifferent_access)
609 610
      end
    end
611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658

    context 'when pipeline has a job with environment' do
      let(:pipeline) { execute_service }

      before do
        stub_ci_pipeline_yaml_file(YAML.dump(config))
      end

      context 'when environment name is valid' do
        let(:config) do
          {
            review_app: {
              script: 'deploy',
              environment: {
                name: 'review/${CI_COMMIT_REF_NAME}',
                url: 'http://${CI_COMMIT_REF_SLUG}-staging.example.com'
              }
            }
          }
        end

        it 'has a job with environment' do
          expect(pipeline.builds.count).to eq(1)
          expect(pipeline.builds.first.persisted_environment.name).to eq('review/master')
          expect(pipeline.builds.first.deployment).to be_created
        end
      end

      context 'when environment name is invalid' do
        let(:config) do
          {
            'job:deploy-to-test-site': {
              script: 'deploy',
              environment: {
                name: '${CI_JOB_NAME}',
                url: 'https://$APP_URL'
              }
            }
          }
        end

        it 'has a job without environment' do
          expect(pipeline.builds.count).to eq(1)
          expect(pipeline.builds.first.persisted_environment).to be_nil
          expect(pipeline.builds.first.deployment).to be_nil
        end
      end
    end
659
  end
S
Shinya Maeda 已提交
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692

  describe '#execute!' do
    subject { service.execute!(*args) }

    let(:service) { described_class.new(project, user, ref: ref_name) }
    let(:args) { [:push] }

    context 'when user has a permission to create a pipeline' do
      let(:user) { create(:user) }

      before do
        project.add_developer(user)
      end

      it 'does not raise an error' do
        expect { subject }.not_to raise_error
      end

      it 'creates a pipeline' do
        expect { subject }.to change { Ci::Pipeline.count }.by(1)
      end
    end

    context 'when user does not have a permission to create a pipeline' do
      let(:user) { create(:user) }

      it 'raises an error' do
        expect { subject }
          .to raise_error(described_class::CreateError)
          .with_message('Insufficient permissions to create a new pipeline')
      end
    end
  end
693
end