diff options
author | Rémy Coutable <remy@rymai.me> | 2016-08-11 17:27:42 +0300 |
---|---|---|
committer | Rémy Coutable <remy@rymai.me> | 2016-08-11 17:27:42 +0300 |
commit | 7e9b41896d8c2ffae36152401f35479b39297e78 (patch) | |
tree | f488221fceef5952f9d3495cff0026ed67169484 /spec | |
parent | a081b842f9b4e0205cf08656403e6bd9bf0d367f (diff) | |
parent | 39203f1adfc6fee3eca50f0cab99ffc597865200 (diff) |
Merge branch 'refactor-builds-creation-service' into 'master'
Refactor pipeline creation service
## What does this MR do?
This refactors GitLab CI build processing: all builds for pipeline are pre-created when a pipeline object is created.
The builds are created with a new introduced status `created`.
The builds are then automatically promoted to `pending` when a previous stage do succeed.
This significantly simplifies pipeline processing code solving a lot of problems of lazily initialisation of previous approach (builds were created on-demand).
## Why was this MR needed?
The previous mechanism had a lot of flows (shown in related issues) in how it work, but also in code design. Removing cross model-service-library dependencies.
The current approach moves a build creation to single place `CreatePipelineService` and removes a dynamic dependency on `config_processor` significantly simplifying a build creation and pipeline processing. Pipeline processing is implemented in `ProcessPipelineService`.
This also allows to easily extend GitLab with Manual Actions which is part of 8.10 direction issue.
## Migration problem
~~This MR removes the a on-demand creation of builds in pipelines.
Pipelines that are running and are in mid-stage (some stages started, but not all) will not be fully evaluated after application restart.
This happens, because the code responsible for on-demand creation is removed.
There's no easy way to migrate existing pipelines, other than doing offline migration and putting pipeline processing in migration code (which seems to be a really bad idea).~~
To support old pipelines I added a lazy initialization of builds if none is found.
## What are the relevant issue numbers?
Fixes: https://gitlab.com/gitlab-org/gitlab-ce/issues/12839
Solves: https://gitlab.com/gitlab-org/gitlab-ce/issues/18644 https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/issues/289
Allows to easily implement: https://gitlab.com/gitlab-org/gitlab-ce/issues/17010
## Does this MR meet the acceptance criteria?
- [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added
- [x] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
- [x] API support added
- Tests
- [x] Added for this feature/bug
- [ ] All builds are passing
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [x] Branch has no merge conflicts with `master` (if you do - rebase it please)
- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
See merge request !5295
Diffstat (limited to 'spec')
24 files changed, 746 insertions, 699 deletions
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 1b32d560b16..0c93bbdfe26 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -7,6 +7,7 @@ FactoryGirl.define do stage_idx 0 ref 'master' tag false + status 'pending' created_at 'Di 29. Okt 09:50:00 CET 2013' started_at 'Di 29. Okt 09:51:28 CET 2013' finished_at 'Di 29. Okt 09:53:28 CET 2013' @@ -45,6 +46,10 @@ FactoryGirl.define do status 'pending' end + trait :created do + status 'created' + end + trait :manual do status 'skipped' self.when 'manual' diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index a039bef6f3c..04d66020c87 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -18,7 +18,9 @@ FactoryGirl.define do factory :ci_empty_pipeline, class: Ci::Pipeline do + ref 'master' sha '97de212e80737a608d939f648d959671fb0a0142' + status 'pending' project factory: :empty_project diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 1e5c479616c..995f2080f10 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -7,6 +7,30 @@ FactoryGirl.define do started_at 'Tue, 26 Jan 2016 08:21:42 +0100' finished_at 'Tue, 26 Jan 2016 08:23:42 +0100' + trait :success do + status 'success' + end + + trait :failed do + status 'failed' + end + + trait :canceled do + status 'canceled' + end + + trait :running do + status 'running' + end + + trait :pending do + status 'pending' + end + + trait :created do + status 'created' + end + after(:build) do |build, evaluator| build.project = build.pipeline.project end diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index f676200ecf3..4d5d4aa121a 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -29,12 +29,16 @@ feature 'Merge request created from fork' do include WaitForAjax given(:pipeline) do - create(:ci_pipeline_with_two_job, project: fork_project, - sha: merge_request.diff_head_sha, - ref: merge_request.source_branch) + create(:ci_pipeline, + project: fork_project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch) end - background { pipeline.create_builds(user) } + background do + create(:ci_build, pipeline: pipeline, name: 'rspec') + create(:ci_build, pipeline: pipeline, name: 'spinach') + end scenario 'user visits a pipelines page', js: true do visit_merge_request(merge_request) diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index eace76c370f..f88b8f8e60b 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -33,7 +33,10 @@ describe "Pipelines" do context 'cancelable pipeline' do let!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') } - before { visit namespace_project_pipelines_path(project.namespace, project) } + before do + pipeline.reload_status! + visit namespace_project_pipelines_path(project.namespace, project) + end it { expect(page).to have_link('Cancel') } it { expect(page).to have_selector('.ci-running') } @@ -49,7 +52,10 @@ describe "Pipelines" do context 'retryable pipelines' do let!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') } - before { visit namespace_project_pipelines_path(project.namespace, project) } + before do + pipeline.reload_status! + visit namespace_project_pipelines_path(project.namespace, project) + end it { expect(page).to have_link('Retry') } it { expect(page).to have_selector('.ci-failed') } @@ -80,7 +86,10 @@ describe "Pipelines" do context 'when running' do let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } - before { visit namespace_project_pipelines_path(project.namespace, project) } + before do + pipeline.reload_status! + visit namespace_project_pipelines_path(project.namespace, project) + end it 'is not cancelable' do expect(page).not_to have_link('Cancel') @@ -92,9 +101,12 @@ describe "Pipelines" do end context 'when failed' do - let!(:running) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') } + let!(:failed) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') } - before { visit namespace_project_pipelines_path(project.namespace, project) } + before do + pipeline.reload_status! + visit namespace_project_pipelines_path(project.namespace, project) + end it 'is not retryable' do expect(page).not_to have_link('Retry') @@ -211,7 +223,7 @@ describe "Pipelines" do context 'for invalid commit' do before do - fill_in('Create for', with: 'invalid reference') + fill_in('Create for', with: 'invalid-reference') click_on 'Create pipeline' end diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb index 034ea098193..2cd6b00dad6 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/ci/charts_spec.rb @@ -5,6 +5,7 @@ describe Ci::Charts, lib: true do before do @pipeline = FactoryGirl.create(:ci_pipeline) FactoryGirl.create(:ci_build, pipeline: @pipeline) + @pipeline.reload_status! end it 'returns build times in minutes' do diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 85374b8761d..be51d942af7 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -19,7 +19,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ stage: "test", stage_idx: 1, - name: :rspec, + name: "rspec", commands: "pwd\nrspec", tag_list: [], options: {}, @@ -433,7 +433,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ stage: "test", stage_idx: 1, - name: :rspec, + name: "rspec", commands: "pwd\nrspec", tag_list: [], options: { @@ -461,7 +461,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ stage: "test", stage_idx: 1, - name: :rspec, + name: "rspec", commands: "pwd\nrspec", tag_list: [], options: { @@ -700,7 +700,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ stage: "test", stage_idx: 1, - name: :rspec, + name: "rspec", commands: "pwd\nrspec", tag_list: [], options: { @@ -837,7 +837,7 @@ module Ci expect(subject.first).to eq({ stage: "test", stage_idx: 1, - name: :normal_job, + name: "normal_job", commands: "test", tag_list: [], options: {}, @@ -882,7 +882,7 @@ module Ci expect(subject.first).to eq({ stage: "build", stage_idx: 0, - name: :job1, + name: "job1", commands: "execute-script-for-job", tag_list: [], options: {}, @@ -894,7 +894,7 @@ module Ci expect(subject.second).to eq({ stage: "build", stage_idx: 0, - name: :job2, + name: "job2", commands: "execute-script-for-job", tag_list: [], options: {}, diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index ef9d9e7fef4..bb8144d5122 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -96,9 +96,10 @@ describe Gitlab::Badge::Build do end def create_build(project, sha, branch) - pipeline = create(:ci_pipeline, project: project, - sha: sha, - ref: branch) + pipeline = create(:ci_empty_pipeline, + project: project, + sha: sha, + ref: branch) create(:ci_build, pipeline: pipeline, stage: 'notify') end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 9ecc9aac84b..60a221eba50 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -764,6 +764,53 @@ describe Ci::Build, models: true do end end + describe '#when' do + subject { build.when } + + context 'if is undefined' do + before do + build.when = nil + end + + context 'use from gitlab-ci.yml' do + before do + stub_ci_pipeline_yaml_file(config) + end + + context 'if config is not found' do + let(:config) { nil } + + it { is_expected.to eq('on_success') } + end + + context 'if config does not have a questioned job' do + let(:config) do + YAML.dump({ + test_other: { + script: 'Hello World' + } + }) + end + + it { is_expected.to eq('on_success') } + end + + context 'if config has when' do + let(:config) do + YAML.dump({ + test: { + script: 'Hello World', + when: 'always' + } + }) + end + + it { is_expected.to eq('always') } + end + end + end + end + describe '#retryable?' do context 'when build is running' do before do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index ccee591cf7a..fdb579ab45c 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -38,9 +38,6 @@ describe Ci::Pipeline, models: true do it { expect(pipeline.sha).to start_with(subject) } end - describe '#create_next_builds' do - end - describe '#retried' do subject { pipeline.retried } @@ -54,304 +51,20 @@ describe Ci::Pipeline, models: true do end end - describe '#create_builds' do - let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project, ref: 'master', tag: false } - - def create_builds(trigger_request = nil) - pipeline.create_builds(nil, trigger_request) - end - - def create_next_builds - pipeline.create_next_builds(pipeline.builds.order(:id).last) - end - - it 'creates builds' do - expect(create_builds).to be_truthy - pipeline.builds.update_all(status: "success") - expect(pipeline.builds.count(:all)).to eq(2) - - expect(create_next_builds).to be_truthy - pipeline.builds.update_all(status: "success") - expect(pipeline.builds.count(:all)).to eq(4) - - expect(create_next_builds).to be_truthy - pipeline.builds.update_all(status: "success") - expect(pipeline.builds.count(:all)).to eq(5) - - expect(create_next_builds).to be_falsey - end - - context 'custom stage with first job allowed to fail' do - let(:yaml) do - { - stages: ['clean', 'test'], - clean_job: { - stage: 'clean', - allow_failure: true, - script: 'BUILD', - }, - test_job: { - stage: 'test', - script: 'TEST', - }, - } - end - - before do - stub_ci_pipeline_yaml_file(YAML.dump(yaml)) - create_builds - end - - it 'properly schedules builds' do - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:drop) - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending', 'failed') - end - end - - context 'properly creates builds when "when" is defined' do - let(:yaml) do - { - stages: ["build", "test", "test_failure", "deploy", "cleanup"], - build: { - stage: "build", - script: "BUILD", - }, - test: { - stage: "test", - script: "TEST", - }, - test_failure: { - stage: "test_failure", - script: "ON test failure", - when: "on_failure", - }, - deploy: { - stage: "deploy", - script: "PUBLISH", - }, - cleanup: { - stage: "cleanup", - script: "TIDY UP", - when: "always", - } - } - end - - before do - stub_ci_pipeline_yaml_file(YAML.dump(yaml)) - end - - context 'when builds are successful' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(pipeline.builds.pluck(:name)).to contain_exactly('build') - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') - pipeline.reload - expect(pipeline.status).to eq('success') - end - end - - context 'when test job fails' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(pipeline.builds.pluck(:name)).to contain_exactly('build') - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end - end - - context 'when test and test_failure jobs fail' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(pipeline.builds.pluck(:name)).to contain_exactly('build') - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end - end - - context 'when deploy job fails' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(pipeline.builds.pluck(:name)).to contain_exactly('build') - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - pipeline.builds.running_or_pending.each(&:drop) - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') - pipeline.reload - expect(pipeline.status).to eq('failed') - end - end - - context 'when build is canceled in the second stage' do - it 'does not schedule builds after build has been canceled' do - expect(create_builds).to be_truthy - expect(pipeline.builds.pluck(:name)).to contain_exactly('build') - expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') - pipeline.builds.running_or_pending.each(&:success) - - expect(pipeline.builds.running_or_pending).not_to be_empty - - expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') - pipeline.builds.running_or_pending.each(&:cancel) - - expect(pipeline.builds.running_or_pending).to be_empty - expect(pipeline.reload.status).to eq('canceled') - end - end - - context 'when listing manual actions' do - let(:yaml) do - { - stages: ["build", "test", "staging", "production", "cleanup"], - build: { - stage: "build", - script: "BUILD", - }, - test: { - stage: "test", - script: "TEST", - }, - staging: { - stage: "staging", - script: "PUBLISH", - }, - production: { - stage: "production", - script: "PUBLISH", - when: "manual", - }, - cleanup: { - stage: "cleanup", - script: "TIDY UP", - when: "always", - }, - clear_cache: { - stage: "cleanup", - script: "CLEAR CACHE", - when: "manual", - } - } - end - - it 'returns only for skipped builds' do - # currently all builds are created - expect(create_builds).to be_truthy - expect(manual_actions).to be_empty - - # succeed stage build - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_empty - - # succeed stage test - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_empty - - # succeed stage staging and skip stage production - pipeline.builds.running_or_pending.each(&:success) - expect(manual_actions).to be_many # production and clear cache - - # succeed stage cleanup - pipeline.builds.running_or_pending.each(&:success) - - # after processing a pipeline we should have 6 builds, 5 succeeded - expect(pipeline.builds.count).to eq(6) - expect(pipeline.builds.success.count).to eq(4) - end - - def manual_actions - pipeline.manual_actions - end - end - end - - context 'when no builds created' do - let(:pipeline) { build(:ci_pipeline) } - - before do - stub_ci_pipeline_yaml_file(YAML.dump(before_script: ['ls'])) - end - - it 'returns false' do - expect(pipeline.create_builds(nil)).to be_falsey - expect(pipeline).not_to be_persisted - end - end - end - describe "#finished_at" do let(:pipeline) { FactoryGirl.create :ci_pipeline } it "returns finished_at of latest build" do build = FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 60 FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 120 + pipeline.reload_status! expect(pipeline.finished_at.to_i).to eq(build.finished_at.to_i) end it "returns nil if there is no finished build" do FactoryGirl.create :ci_not_started_build, pipeline: pipeline + pipeline.reload_status! expect(pipeline.finished_at).to be_nil end @@ -359,7 +72,7 @@ describe Ci::Pipeline, models: true do describe "coverage" do let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" } - let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } + let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project } it "calculates average when there are two builds with coverage" do FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline @@ -426,31 +139,30 @@ describe Ci::Pipeline, models: true do end end - describe '#update_state' do - it 'executes update_state after touching object' do - expect(pipeline).to receive(:update_state).and_return(true) - pipeline.touch - end + describe '#reload_status!' do + let(:pipeline) { create :ci_empty_pipeline, project: project } context 'dependent objects' do - let(:commit_status) { build :commit_status, pipeline: pipeline } + let(:commit_status) { create :commit_status, :pending, pipeline: pipeline } + + it 'executes reload_status! after succeeding dependent object' do + expect(pipeline).to receive(:reload_status!).and_return(true) - it 'executes update_state after saving dependent object' do - expect(pipeline).to receive(:update_state).and_return(true) - commit_status.save + commit_status.success end end - context 'update state' do + context 'updates' do let(:current) { Time.now.change(usec: 0) } - let(:build) { FactoryGirl.create :ci_build, :success, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 } + let(:build) { FactoryGirl.create :ci_build, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 } before do build + pipeline.reload_status! end [:status, :started_at, :finished_at, :duration].each do |param| - it "update #{param}" do + it "#{param}" do expect(pipeline.send(param)).to eq(build.send(param)) end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index bf438b26690..1b383219eb9 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -291,7 +291,8 @@ describe HipchatService, models: true do end context 'build events' do - let(:build) { create(:ci_build) } + let(:pipeline) { create(:ci_empty_pipeline) } + let(:build) { create(:ci_build, pipeline: pipeline) } let(:data) { Gitlab::BuildDataBuilder.build(build) } context 'for failed' do diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 966d302dfd3..a4cdd8f3140 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -238,6 +238,10 @@ describe API::API, api: true do it { expect(response.headers).to include(download_headers) } end + before do + pipeline.reload_status! + end + context 'with regular branch' do before do pipeline.update(ref: 'master', diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 4379fcb3c1e..7ca75d77673 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -89,16 +89,29 @@ describe API::API, api: true do it "returns nil for commit without CI" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response).to have_http_status(200) expect(json_response['status']).to be_nil end it "returns status for CI" do pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master') + pipeline.update(status: 'success') + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response).to have_http_status(200) expect(json_response['status']).to eq(pipeline.status) end + + it "returns status for CI when pipeline is created" do + project.ensure_pipeline(project.repository.commit.sha, 'master') + + get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['status']).to be_nil + end end context "unauthorized user" do diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 5702682fc7d..82bba1ce8a4 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -50,7 +50,8 @@ describe API::API do post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') expect(response).to have_http_status(201) pipeline.builds.reload - expect(pipeline.builds.size).to eq(2) + expect(pipeline.builds.pending.size).to eq(2) + expect(pipeline.builds.size).to eq(5) end it 'returns bad request with no builds created if there\'s no commit for that ref' do diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 05b309096cb..ca7932dc5da 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -6,112 +6,102 @@ describe Ci::API::API do let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) } let(:project) { FactoryGirl.create(:empty_project) } - before do - stub_ci_pipeline_to_return_yaml_file - end - describe "Builds API for runners" do - let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") } - let(:shared_project) { FactoryGirl.create(:empty_project, name: "SharedProject") } + let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') } before do - FactoryGirl.create :ci_runner_project, project: project, runner: runner + project.runners << runner end describe "POST /builds/register" do - it "starts a build" do - pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - pipeline.create_builds(nil) - build = pipeline.builds.first + let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + it "starts a build" do + register_builds info: { platform: :darwin } expect(response).to have_http_status(201) expect(json_response['sha']).to eq(build.sha) expect(runner.reload.platform).to eq("darwin") + expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] }) + expect(json_response["variables"]).to include( + { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, + { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, + { "key" => "DB_NAME", "value" => "postgres", "public" => true } + ) end - it "returns 404 error if no pending build found" do - post ci_api("/builds/register"), token: runner.token - - expect(response).to have_http_status(404) - end - - it "returns 404 error if no builds for specific runner" do - pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project) - FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending') + context 'when builds are finished' do + before do + build.success + end - post ci_api("/builds/register"), token: runner.token + it "returns 404 error if no builds for specific runner" do + register_builds - expect(response).to have_http_status(404) + expect(response).to have_http_status(404) + end end - it "returns 404 error if no builds for shared runner" do - pipeline = FactoryGirl.create(:ci_pipeline, project: project) - FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending') + context 'for other project with builds' do + before do + build.success + create(:ci_build, :pending) + end - post ci_api("/builds/register"), token: shared_runner.token + it "returns 404 error if no builds for shared runner" do + register_builds - expect(response).to have_http_status(404) + expect(response).to have_http_status(404) + end end - it "returns options" do - pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - pipeline.create_builds(nil) + context 'for shared runner' do + let(:shared_runner) { create(:ci_runner, token: "SharedRunner") } - post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + it "should return 404 error if no builds for shared runner" do + register_builds shared_runner.token - expect(response).to have_http_status(201) - expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] }) + expect(response).to have_http_status(404) + end end - it "returns variables" do - pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - pipeline.create_builds(nil) - project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") - - post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + context 'for triggered build' do + before do + trigger = create(:ci_trigger, project: project) + create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [build], trigger: trigger) + project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") + end - expect(response).to have_http_status(201) - expect(json_response["variables"]).to include( - { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, - { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, - { "key" => "DB_NAME", "value" => "postgres", "public" => true }, - { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false } - ) + it "returns variables for triggers" do + register_builds info: { platform: :darwin } + + expect(response).to have_http_status(201) + expect(json_response["variables"]).to include( + { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, + { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, + { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true }, + { "key" => "DB_NAME", "value" => "postgres", "public" => true }, + { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, + { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false }, + ) + end end - it "returns variables for triggers" do - trigger = FactoryGirl.create(:ci_trigger, project: project) - pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - - trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) - pipeline.create_builds(nil, trigger_request) - project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") - - post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } - - expect(response).to have_http_status(201) - expect(json_response["variables"]).to include( - { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, - { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, - { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true }, - { "key" => "DB_NAME", "value" => "postgres", "public" => true }, - { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, - { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false } - ) - end + context 'with multiple builds' do + before do + build.success + end - it "returns dependent builds" do - pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - pipeline.create_builds(nil, nil) - pipeline.builds.where(stage: 'test').each(&:success) + let!(:test_build) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } - post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + it "returns dependent builds" do + register_builds info: { platform: :darwin } - expect(response).to have_http_status(201) - expect(json_response["depends_on_builds"].count).to eq(2) - expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec") + expect(response).to have_http_status(201) + expect(json_response["id"]).to eq(test_build.id) + expect(json_response["depends_on_builds"].count).to eq(1) + expect(json_response["depends_on_builds"][0]).to include('id' => build.id, 'name' => 'spinach') + end end %w(name version revision platform architecture).each do |param| @@ -121,8 +111,9 @@ describe Ci::API::API do subject { runner.read_attribute(param.to_sym) } it do - post ci_api("/builds/register"), token: runner.token, info: { param => value } - expect(response).to have_http_status(404) + register_builds info: { param => value } + + expect(response).to have_http_status(201) runner.reload is_expected.to eq(value) end @@ -131,8 +122,7 @@ describe Ci::API::API do context 'when build has no tags' do before do - pipeline = create(:ci_pipeline, project: project) - create(:ci_build, pipeline: pipeline, tags: []) + build.update(tags: []) end context 'when runner is allowed to pick untagged builds' do @@ -154,17 +144,15 @@ describe Ci::API::API do expect(response).to have_http_status 404 end end + end - def register_builds - post ci_api("/builds/register"), token: runner.token, - info: { platform: :darwin } - end + def register_builds(token = runner.token, **params) + post ci_api("/builds/register"), params.merge(token: token) end end describe "PUT /builds/:id" do - let(:pipeline) {create(:ci_pipeline, project: project)} - let(:build) { create(:ci_build, :trace, pipeline: pipeline, runner_id: runner.id) } + let(:build) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) } before do build.run! @@ -189,7 +177,7 @@ describe Ci::API::API do end describe 'PATCH /builds/:id/trace.txt' do - let(:build) { create(:ci_build, :trace, runner_id: runner.id) } + let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) } let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } } let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } @@ -237,8 +225,7 @@ describe Ci::API::API do context "Artifacts" do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build) { create(:ci_build, pipeline: pipeline, runner_id: runner.id) } + let(:build) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) } let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") } let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index 3312bd11669..0a0f979f57d 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -42,7 +42,8 @@ describe Ci::API::API do post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options expect(response).to have_http_status(201) pipeline.builds.reload - expect(pipeline.builds.size).to eq(2) + expect(pipeline.builds.pending.size).to eq(2) + expect(pipeline.builds.size).to eq(5) end it 'returns bad request with no builds created if there\'s no commit for that ref' do diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb deleted file mode 100644 index 8b0becd83d3..00000000000 --- a/spec/services/ci/create_builds_service_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' - -describe Ci::CreateBuildsService, services: true do - let(:pipeline) { create(:ci_pipeline, ref: 'master') } - let(:user) { create(:user) } - - describe '#execute' do - # Using stubbed .gitlab-ci.yml created in commit factory - # - - subject do - described_class.new(pipeline).execute('test', user, status, nil) - end - - context 'next builds available' do - let(:status) { 'success' } - - it { is_expected.to be_an_instance_of Array } - it { is_expected.to all(be_an_instance_of Ci::Build) } - - it 'does not persist created builds' do - expect(subject.first).not_to be_persisted - end - end - - context 'builds skipped' do - let(:status) { 'skipped' } - - it { is_expected.to be_empty } - end - end -end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb new file mode 100644 index 00000000000..4aadd009f3e --- /dev/null +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -0,0 +1,214 @@ +require 'spec_helper' + +describe Ci::CreatePipelineService, services: true do + let(:project) { FactoryGirl.create(:project) } + let(:user) { create(:admin) } + + before do + stub_ci_pipeline_to_return_yaml_file + end + + describe '#execute' do + def execute(params) + described_class.new(project, user, params).execute + end + + context 'valid params' do + let(:pipeline) do + execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: "Message" }]) + end + + it { expect(pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(pipeline).to be_valid } + it { expect(pipeline).to be_persisted } + it { expect(pipeline).to eq(project.pipelines.last) } + it { expect(pipeline).to have_attributes(user: user) } + it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } + end + + context "skip tag if there is no build for it" do + it "creates commit if there is appropriate job" do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: "Message" }]) + expect(result).to be_persisted + 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) + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: "Message" }]) + + expect(result).to be_persisted + end + end + + it 'skips creating pipeline for refs without .gitlab-ci.yml' do + stub_ci_pipeline_yaml_file(nil) + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'Message' }]) + + expect(result).not_to be_persisted + expect(Ci::Pipeline.count).to eq(0) + end + + it 'fails commits if yaml is invalid' do + message = 'message' + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } + stub_ci_pipeline_yaml_file('invalid: file: file') + commits = [{ message: message }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + 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 + + context 'when commit contains a [ci skip] directive' do + let(:message) { "some message[ci skip]" } + let(:messageFlip) { "some message[skip ci]" } + let(:capMessage) { "some message[CI SKIP]" } + let(:capMessageFlip) { "some message[SKIP CI]" } + + before do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } + end + + it "skips builds creation if there is [ci skip] tag in commit message" do + commits = [{ message: message }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "skips builds creation if there is [skip ci] tag in commit message" do + commits = [{ message: messageFlip }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "skips builds creation if there is [CI SKIP] tag in commit message" do + commits = [{ message: capMessage }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "skips builds creation if there is [SKIP CI] tag in commit message" do + commits = [{ message: capMessageFlip }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + end + + it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } + + commits = [{ message: "some message" }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.builds.first.name).to eq("rspec") + end + + it "fails builds creation if there is [ci skip] tag in commit message and yaml is invalid" do + stub_ci_pipeline_yaml_file('invalid: file: fiile') + commits = [{ message: message }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + 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 + + it "creates commit with failed status if yaml is invalid" do + stub_ci_pipeline_yaml_file('invalid: file') + commits = [{ message: "some message" }] + pipeline = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: commits) + + expect(pipeline).to be_persisted + expect(pipeline.status).to eq("failed") + expect(pipeline.builds.any?).to be false + 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 + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some msg' }]) + + 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 + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some msg' }]) + + expect(result).to be_persisted + expect(result.manual_actions).not_to be_empty + end + end + end +end diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index b72e0bd3dbe..d8c443d29d5 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreateTriggerRequestService, services: true do - let(:service) { Ci::CreateTriggerRequestService.new } + let(:service) { described_class.new } let(:project) { create(:project) } let(:trigger) { create(:ci_trigger, project: project) } @@ -27,8 +27,7 @@ describe Ci::CreateTriggerRequestService, services: true do subject { service.execute(project, trigger, 'master') } before do - stub_ci_pipeline_yaml_file('{}') - FactoryGirl.create :ci_pipeline, project: project + stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }') end it { expect(subject).to be_nil } diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index 3a3e3efe709..259062406c7 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,8 +5,8 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:commit) { project.ensure_pipeline(commit_sha, 'master') } - let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) } + let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') } + let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) } describe '#execute' do before { build } @@ -14,6 +14,7 @@ module Ci context 'branch name' do before { allow(project).to receive(:commit).and_return(OpenStruct.new(sha: commit_sha)) } before { build.run! } + before { pipeline.reload_status! } let(:image) { service.execute(project, ref: 'master') } it { expect(image).to be_kind_of(OpenStruct) } @@ -31,6 +32,7 @@ module Ci context 'commit sha' do before { build.run! } + before { pipeline.reload_status! } let(:image) { service.execute(project, sha: build.sha) } it { expect(image).to be_kind_of(OpenStruct) } diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb new file mode 100644 index 00000000000..ad8c2485888 --- /dev/null +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -0,0 +1,288 @@ +require 'spec_helper' + +describe Ci::ProcessPipelineService, services: true do + let(:pipeline) { create(:ci_pipeline, ref: 'master') } + let(:user) { create(:user) } + let(:all_builds) { pipeline.builds } + let(:builds) { all_builds.where.not(status: [:created, :skipped]) } + let(:config) { nil } + + before do + allow(pipeline).to receive(:ci_yaml_file).and_return(config) + end + + describe '#execute' do + def create_builds + described_class.new(pipeline.project, user).execute(pipeline) + end + + def succeed_pending + builds.pending.update_all(status: 'success') + end + + context 'start queuing next builds' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2) + end + + it 'processes a pipeline' do + expect(create_builds).to be_truthy + succeed_pending + expect(builds.success.count).to eq(2) + + expect(create_builds).to be_truthy + succeed_pending + expect(builds.success.count).to eq(4) + + expect(create_builds).to be_truthy + succeed_pending + expect(builds.success.count).to eq(5) + + expect(create_builds).to be_falsey + end + + it 'does not process pipeline if existing stage is running' do + expect(create_builds).to be_truthy + expect(builds.pending.count).to eq(2) + + expect(create_builds).to be_falsey + expect(builds.pending.count).to eq(2) + end + end + + context 'custom stage with first job allowed to fail' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true) + create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true) + end + + it 'automatically triggers a next stage when build finishes' do + expect(create_builds).to be_truthy + expect(builds.pluck(:status)).to contain_exactly('pending') + + pipeline.builds.running_or_pending.each(&:drop) + expect(builds.pluck(:status)).to contain_exactly('failed', 'pending') + end + end + + context 'properly creates builds when "when" is defined' do + before do + create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1) + create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure') + create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3) + create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual') + create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always') + create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual') + end + + context 'when builds are successful' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') + pipeline.reload + expect(pipeline.status).to eq('success') + end + end + + context 'when test job fails' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when test and test_failure jobs fail' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when deploy job fails' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when build is canceled in the second stage' do + it 'does not schedule builds after build has been canceled' do + expect(create_builds).to be_truthy + expect(builds.pluck(:name)).to contain_exactly('build') + expect(builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(builds.running_or_pending).not_to be_empty + + expect(builds.pluck(:name)).to contain_exactly('build', 'test') + expect(builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:cancel) + + expect(builds.running_or_pending).to be_empty + expect(pipeline.reload.status).to eq('canceled') + end + end + + context 'when listing manual actions' do + it 'returns only for skipped builds' do + # currently all builds are created + expect(create_builds).to be_truthy + expect(manual_actions).to be_empty + + # succeed stage build + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_empty + + # succeed stage test + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_one # production + + # succeed stage deploy + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_many # production and clear cache + end + + def manual_actions + pipeline.manual_actions + end + end + end + + context 'creates a builds from .gitlab-ci.yml' do + let(:config) do + YAML.dump({ + rspec: { + stage: 'test', + script: 'rspec' + }, + rubocop: { + stage: 'test', + script: 'rubocop' + }, + deploy: { + stage: 'deploy', + script: 'deploy' + } + }) + end + + # Using stubbed .gitlab-ci.yml created in commit factory + # + + before do + stub_ci_pipeline_yaml_file(config) + create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0) + create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0) + end + + it 'when processing a pipeline' do + # Currently we have two builds with state created + expect(builds.count).to eq(0) + expect(all_builds.count).to eq(2) + + # Create builds will mark the created as pending + expect(create_builds).to be_truthy + expect(builds.count).to eq(2) + expect(all_builds.count).to eq(2) + + # When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml + # We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy) + succeed_pending + expect(create_builds).to be_truthy + expect(builds.success.count).to eq(2) + expect(builds.pending.count).to eq(2) + expect(all_builds.count).to eq(5) + + # When we succeed the 2 pending from stage test, + # We will queue a deploy stage, no new builds will be created + succeed_pending + expect(create_builds).to be_truthy + expect(builds.pending.count).to eq(1) + expect(builds.success.count).to eq(4) + expect(all_builds.count).to eq(5) + + # When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created + succeed_pending + expect(create_builds).to be_falsey + expect(builds.success.count).to eq(5) + expect(all_builds.count).to eq(5) + end + end + end +end diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb deleted file mode 100644 index d4c5e584421..00000000000 --- a/spec/services/create_commit_builds_service_spec.rb +++ /dev/null @@ -1,241 +0,0 @@ -require 'spec_helper' - -describe CreateCommitBuildsService, services: true do - let(:service) { CreateCommitBuildsService.new } - let(:project) { FactoryGirl.create(:empty_project) } - let(:user) { create(:user) } - - before do - stub_ci_pipeline_to_return_yaml_file - end - - describe '#execute' do - context 'valid params' do - let(:pipeline) do - service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: [{ message: "Message" }] - ) - end - - it { expect(pipeline).to be_kind_of(Ci::Pipeline) } - it { expect(pipeline).to be_valid } - it { expect(pipeline).to be_persisted } - it { expect(pipeline).to eq(project.pipelines.last) } - it { expect(pipeline).to have_attributes(user: user) } - it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } - end - - context "skip tag if there is no build for it" do - it "creates commit if there is appropriate job" do - result = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: [{ message: "Message" }] - ) - expect(result).to be_persisted - 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: ["0_1"] } }) - stub_ci_pipeline_yaml_file(config) - - result = service.execute(project, user, - ref: 'refs/heads/0_1', - before: '00000000', - after: '31das312', - commits: [{ message: "Message" }] - ) - expect(result).to be_persisted - end - end - - it 'skips creating pipeline for refs without .gitlab-ci.yml' do - stub_ci_pipeline_yaml_file(nil) - result = service.execute(project, user, - ref: 'refs/heads/0_1', - before: '00000000', - after: '31das312', - commits: [{ message: 'Message' }] - ) - expect(result).to be_falsey - expect(Ci::Pipeline.count).to eq(0) - end - - it 'fails commits if yaml is invalid' do - message = 'message' - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } - stub_ci_pipeline_yaml_file('invalid: file: file') - commits = [{ message: message }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - 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 - - context 'when commit contains a [ci skip] directive' do - let(:message) { "some message[ci skip]" } - let(:messageFlip) { "some message[skip ci]" } - let(:capMessage) { "some message[CI SKIP]" } - let(:capMessageFlip) { "some message[SKIP CI]" } - - before do - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } - end - - it "skips builds creation if there is [ci skip] tag in commit message" do - commits = [{ message: message }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "skips builds creation if there is [skip ci] tag in commit message" do - commits = [{ message: messageFlip }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "skips builds creation if there is [CI SKIP] tag in commit message" do - commits = [{ message: capMessage }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "skips builds creation if there is [SKIP CI] tag in commit message" do - commits = [{ message: capMessageFlip }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - end - - it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do - allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } - - commits = [{ message: "some message" }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.builds.first.name).to eq("staging") - end - - it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do - stub_ci_pipeline_yaml_file('invalid: file: fiile') - commits = [{ message: message }] - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(pipeline).to be_persisted - expect(pipeline.builds.any?).to be false - expect(pipeline.status).to eq("skipped") - expect(pipeline.yaml_errors).to be_nil - end - end - - it "skips build creation if there are already builds" do - allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml } - - commits = [{ message: "message" }] - pipeline = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(pipeline).to be_persisted - expect(pipeline.builds.count(:all)).to eq(2) - - pipeline = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(pipeline).to be_persisted - expect(pipeline.builds.count(:all)).to eq(2) - end - - it "creates commit with failed status if yaml is invalid" do - stub_ci_pipeline_yaml_file('invalid: file') - - commits = [{ message: "some message" }] - - pipeline = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(pipeline).to be_persisted - expect(pipeline.status).to eq("failed") - expect(pipeline.builds.any?).to be false - 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 - result = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: [{ message: 'some msg' }]) - - expect(result).to be_falsey - expect(Ci::Build.all).to be_empty - expect(Ci::Pipeline.count).to eq(0) - end - end - end -end diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 4da8146e3d6..520e906b21f 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -110,19 +110,15 @@ describe MergeRequests::MergeWhenBuildSucceedsService do context 'properly handles multiple stages' do let(:ref) { mr_merge_if_green_enabled.source_branch } - let(:build) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') } - let(:test) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') } + let!(:build) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') } + let!(:test) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') } + let(:pipeline) { create(:ci_empty_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project) } before do # This behavior of MergeRequest: we instantiate a new object allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do Ci::Pipeline.find(pipeline.id) end - - # We create test after the build - allow(pipeline).to receive(:create_next_builds).and_wrap_original do - test - end end it "doesn't merge if some stages failed" do diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 7f803a06902..1d2cf7acddd 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -53,7 +53,13 @@ describe PostReceive do subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) } context "creates a Ci::Pipeline for every change" do - before { stub_ci_pipeline_to_return_yaml_file } + before do + allow_any_instance_of(Ci::CreatePipelineService).to receive(:commit) do + OpenStruct.new(id: '123456') + end + allow_any_instance_of(Ci::CreatePipelineService).to receive(:branch?).and_return(true) + stub_ci_pipeline_to_return_yaml_file + end it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) } end |