GitLab wurde erfolgreich aktualisiert. Durch regelmäßige Updates bleibt das THM GitLab sicher. Danke für Ihre Geduld.

Commit 0aa6061d authored by Kamil Trzcinski's avatar Kamil Trzcinski

Implement when syntax in .gitlab-ci.yml

parent 3d763907
...@@ -22,6 +22,7 @@ v 8.1.0 (unreleased) ...@@ -22,6 +22,7 @@ v 8.1.0 (unreleased)
- Add first and last to pagination (Zeger-Jan van de Weg) - Add first and last to pagination (Zeger-Jan van de Weg)
- Added Commit Status API - Added Commit Status API
- Added Builds View - Added Builds View
- Added when to .gitlab-ci.yml
- Show CI status on commit page - Show CI status on commit page
- Added CI_BUILD_TAG, _STAGE, _NAME and _TRIGGERED to CI builds - Added CI_BUILD_TAG, _STAGE, _NAME and _TRIGGERED to CI builds
- Show CI status on Your projects page and Starred projects page - Show CI status on Your projects page and Starred projects page
......
...@@ -93,10 +93,7 @@ def retry(build) ...@@ -93,10 +93,7 @@ def retry(build)
Ci::WebHookService.new.build_end(build) Ci::WebHookService.new.build_end(build)
end end
if build.commit.should_create_next_builds?(build) build.commit.create_next_builds(build)
build.commit.create_next_builds(build.ref, build.tag, build.user, build.trigger_request)
end
project.execute_services(build) project.execute_services(build)
if project.coverage_enabled? if project.coverage_enabled?
......
...@@ -91,19 +91,28 @@ def stage ...@@ -91,19 +91,28 @@ def stage
def create_builds(ref, tag, user, trigger_request = nil) def create_builds(ref, tag, user, trigger_request = nil)
return unless config_processor return unless config_processor
config_processor.stages.any? do |stage| config_processor.stages.any? do |stage|
CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request).present? CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request, 'success').present?
end end
end end
def create_next_builds(ref, tag, user, trigger_request) def create_next_builds(build)
return unless config_processor return unless config_processor
stages = builds.where(ref: ref, tag: tag, trigger_request: trigger_request).group_by(&:stage) # don't create other builds if this one is retried
latest_builds = builds.similar(build).latest
return unless latest_builds.exists?(build.id)
config_processor.stages.any? do |stage| # get list of stages after this build
unless stages.include?(stage) next_stages = config_processor.stages.drop_while { |stage| stage != build.stage }
CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request).present? next_stages.delete(build.stage)
end
# get status for all prior builds
prior_builds = latest_builds.reject { |other_build| next_stages.include?(other_build.stage) }
status = Ci::Status.get_status(prior_builds)
# create builds for next stages based
next_stages.any? do |stage|
CreateBuildsService.new.execute(self, stage, build.ref, build.tag, build.user, build.trigger_request, status).present?
end end
end end
...@@ -132,24 +141,7 @@ def status ...@@ -132,24 +141,7 @@ def status
return 'failed' return 'failed'
end end
@status ||= begin @status ||= Ci::Status.get_status(latest_statuses)
latest = latest_statuses
latest.reject! { |status| status.try(&:allow_failure?) }
if latest.none?
'skipped'
elsif latest.all?(&:success?)
'success'
elsif latest.all?(&:pending?)
'pending'
elsif latest.any?(&:running?) || latest.any?(&:pending?)
'running'
elsif latest.all?(&:canceled?)
'canceled'
else
'failed'
end
end
end end
def pending? def pending?
...@@ -219,16 +211,6 @@ def update_committed! ...@@ -219,16 +211,6 @@ def update_committed!
update!(committed_at: DateTime.now) update!(committed_at: DateTime.now)
end end
def should_create_next_builds?(build)
# don't create other builds if this one is retried
other_builds = builds.similar(build).latest
return false unless other_builds.include?(build)
other_builds.all? do |build|
build.success? || build.ignored?
end
end
private private
def save_yaml_error(error) def save_yaml_error(error)
......
...@@ -28,7 +28,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -28,7 +28,7 @@ class CommitStatus < ActiveRecord::Base
end end
event :drop do event :drop do
transition running: :failed transition [:pending, :running] => :failed
end end
event :success do event :success do
......
module Ci module Ci
class CreateBuildsService class CreateBuildsService
def execute(commit, stage, ref, tag, user, trigger_request) def execute(commit, stage, ref, tag, user, trigger_request, status)
builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag) builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag)
# check when to create next build
builds_attrs = builds_attrs.select do |build_attrs|
case build_attrs[:when]
when 'on_success'
status == 'success'
when 'on_failure'
status == 'failed'
when 'always'
%w(success failed).include?(status)
end
end
builds_attrs.map do |build_attrs| builds_attrs.map do |build_attrs|
# don't create the same build twice # don't create the same build twice
unless commit.builds.find_by(ref: ref, tag: tag, trigger_request: trigger_request, name: build_attrs[:name]) unless commit.builds.find_by(ref: ref, tag: tag, trigger_request: trigger_request, name: build_attrs[:name])
......
...@@ -140,6 +140,7 @@ job_name: ...@@ -140,6 +140,7 @@ job_name:
| except | optional | Defines a list of git refs for which build is not created | | except | optional | Defines a list of git refs for which build is not created |
| tags | optional | Defines a list of tags which are used to select runner | | tags | optional | Defines a list of tags which are used to select runner |
| allow_failure | optional | Allow build to fail. Failed build doesn't contribute to commit status | | allow_failure | optional | Allow build to fail. Failed build doesn't contribute to commit status |
| when | optional | Define when to run build. Can be on_success, on_failure or always |
### script ### script
`script` is a shell script which is executed by runner. The shell script is prepended with `before_script`. `script` is a shell script which is executed by runner. The shell script is prepended with `before_script`.
...@@ -196,6 +197,54 @@ job: ...@@ -196,6 +197,54 @@ job:
The above specification will make sure that `job` is built by a runner that have `ruby` AND `postgres` tags defined. The above specification will make sure that `job` is built by a runner that have `ruby` AND `postgres` tags defined.
### when
`when` is used to implement jobs that are run in case of failure or despite the failure.
The `when` can be set to one of the following values:
1. `on_success` - execute build only when all builds from prior stages succeeded. This is default.
1. `on_failure` - execute build only when at least one of the build from prior stages failed.
1. `always` - execute build despite the status of builds from prior stages.
```
stages:
- build
- cleanup_build
- test
- deploy
- cleanup
build:
stage: build
script:
- make build
cleanup_build:
stage: cleanup_build
script:
- cleanup build when failed
when: on_failure
test:
stage: test
script:
- make test
deploy:
stage: deploy
script:
- make deploy
cleanup:
stage: cleanup
script:
- cleanup after builds
when: always
```
The above script will:
1. Execute `cleanup_build` only when the `build` failed,
2. Always execute `cleanup` as the last step in pipeline.
## Validate the .gitlab-ci.yml ## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint. Each instance of GitLab CI has an embedded debug tool called Lint.
You can find the link to the Lint in the project's settings page or use short url `/lint`. You can find the link to the Lint in the project's settings page or use short url `/lint`.
......
...@@ -5,7 +5,7 @@ class ValidationError < StandardError;end ...@@ -5,7 +5,7 @@ class ValidationError < StandardError;end
DEFAULT_STAGES = %w(build test deploy) DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test' DEFAULT_STAGE = 'test'
ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables] ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables]
ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage] ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when]
attr_reader :before_script, :image, :services, :variables attr_reader :before_script, :image, :services, :variables
...@@ -93,6 +93,7 @@ def build_job(name, job) ...@@ -93,6 +93,7 @@ def build_job(name, job)
only: job[:only], only: job[:only],
except: job[:except], except: job[:except],
allow_failure: job[:allow_failure] || false, allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success',
options: { options: {
image: job[:image] || @image, image: job[:image] || @image,
services: job[:services] || @services services: job[:services] || @services
...@@ -184,6 +185,10 @@ def validate_job!(name, job) ...@@ -184,6 +185,10 @@ def validate_job!(name, job)
if job[:allow_failure] && !job[:allow_failure].in?([true, false]) if job[:allow_failure] && !job[:allow_failure].in?([true, false])
raise ValidationError, "#{name}: allow_failure parameter should be an boolean" raise ValidationError, "#{name}: allow_failure parameter should be an boolean"
end end
if job[:when] && !job[:when].in?(%w(on_success on_failure always))
raise ValidationError, "#{name}: when should be on_success, on_failure or always"
end
end end
private private
......
module Ci
class Status
def self.get_status(statuses)
statuses.reject! { |status| status.try(&:allow_failure?) }
if statuses.none?
'skipped'
elsif statuses.all?(&:success?)
'success'
elsif statuses.all?(&:pending?)
'pending'
elsif statuses.any?(&:running?) || statuses.any?(&:pending?)
'running'
elsif statuses.all?(&:canceled?)
'canceled'
else
'failed'
end
end
end
end
\ No newline at end of file
...@@ -125,7 +125,8 @@ module Ci ...@@ -125,7 +125,8 @@ module Ci
image: "ruby:2.1", image: "ruby:2.1",
services: ["mysql"] services: ["mysql"]
}, },
allow_failure: false allow_failure: false,
when: "on_success"
}) })
end end
...@@ -152,7 +153,8 @@ module Ci ...@@ -152,7 +153,8 @@ module Ci
image: "ruby:2.5", image: "ruby:2.5",
services: ["postgresql"] services: ["postgresql"]
}, },
allow_failure: false allow_failure: false,
when: "on_success"
}) })
end end
end end
...@@ -174,6 +176,21 @@ module Ci ...@@ -174,6 +176,21 @@ module Ci
end end
end end
describe "When" do
%w(on_success on_failure always).each do |when_state|
it "returns #{when_state} when defined" do
config = YAML.dump({
rspec: { script: "rspec", when: when_state }
})
config_processor = GitlabCiYamlProcessor.new(config)
builds = config_processor.builds_for_stage_and_ref("test", "master")
expect(builds.size).to eq(1)
expect(builds.first[:when]).to eq(when_state)
end
end
end
describe "Error handling" do describe "Error handling" do
it "indicates that object is invalid" do it "indicates that object is invalid" do
expect{GitlabCiYamlProcessor.new("invalid_yaml\n!ccdvlf%612334@@@@")}.to raise_error(GitlabCiYamlProcessor::ValidationError) expect{GitlabCiYamlProcessor.new("invalid_yaml\n!ccdvlf%612334@@@@")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
...@@ -311,6 +328,13 @@ module Ci ...@@ -311,6 +328,13 @@ module Ci
GitlabCiYamlProcessor.new(config) GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings") end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings")
end end
it "returns errors if job when is not on_success, on_failure or always" do
config = YAML.dump({ rspec: { script: "test", when: false } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always")
end
end end
end end
end end
...@@ -161,28 +161,28 @@ ...@@ -161,28 +161,28 @@
end end
describe :create_builds do describe :create_builds do
let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project } let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
def create_builds(trigger_request = nil) def create_builds(trigger_request = nil)
commit.create_builds('master', false, nil, trigger_request) commit.create_builds('master', false, nil, trigger_request)
end end
def create_next_builds(trigger_request = nil) def create_next_builds
commit.create_next_builds('master', false, nil, trigger_request) commit.create_next_builds(commit.builds.order(:id).last)
end end
it 'creates builds' do it 'creates builds' do
expect(create_builds).to be_truthy expect(create_builds).to be_truthy
commit.builds.reload commit.builds.update_all(status: "success")
expect(commit.builds.size).to eq(2) expect(commit.builds.count(:all)).to eq(2)
expect(create_next_builds).to be_truthy expect(create_next_builds).to be_truthy
commit.builds.reload commit.builds.update_all(status: "success")
expect(commit.builds.size).to eq(4) expect(commit.builds.count(:all)).to eq(4)
expect(create_next_builds).to be_truthy expect(create_next_builds).to be_truthy
commit.builds.reload commit.builds.update_all(status: "success")
expect(commit.builds.size).to eq(5) expect(commit.builds.count(:all)).to eq(5)
expect(create_next_builds).to be_falsey expect(create_next_builds).to be_falsey
end end
...@@ -194,12 +194,12 @@ def create_develop_builds ...@@ -194,12 +194,12 @@ def create_develop_builds
it 'creates builds' do it 'creates builds' do
expect(create_builds).to be_truthy expect(create_builds).to be_truthy
commit.builds.reload commit.builds.update_all(status: "success")
expect(commit.builds.size).to eq(2) expect(commit.builds.count(:all)).to eq(2)
expect(create_develop_builds).to be_truthy expect(create_develop_builds).to be_truthy
commit.builds.reload commit.builds.update_all(status: "success")
expect(commit.builds.size).to eq(4) expect(commit.builds.count(:all)).to eq(4)
expect(commit.refs.size).to eq(2) expect(commit.refs.size).to eq(2)
expect(commit.builds.pluck(:name).uniq.size).to eq(2) expect(commit.builds.pluck(:name).uniq.size).to eq(2)
end end
...@@ -211,28 +211,24 @@ def create_develop_builds ...@@ -211,28 +211,24 @@ def create_develop_builds
it 'creates builds' do it 'creates builds' do
expect(create_builds(trigger_request)).to be_truthy expect(create_builds(trigger_request)).to be_truthy
commit.builds.reload expect(commit.builds.count(:all)).to eq(2)
expect(commit.builds.size).to eq(2)
end end
it 'rebuilds commit' do it 'rebuilds commit' do
expect(create_builds).to be_truthy expect(create_builds).to be_truthy
commit.builds.reload expect(commit.builds.count(:all)).to eq(2)
expect(commit.builds.size).to eq(2)
expect(create_builds(trigger_request)).to be_truthy expect(create_builds(trigger_request)).to be_truthy
commit.builds.reload expect(commit.builds.count(:all)).to eq(4)
expect(commit.builds.size).to eq(4)
end end
it 'creates next builds' do it 'creates next builds' do
expect(create_builds(trigger_request)).to be_truthy expect(create_builds(trigger_request)).to be_truthy
commit.builds.reload expect(commit.builds.count(:all)).to eq(2)
expect(commit.builds.size).to eq(2) commit.builds.update_all(status: "success")
expect(create_next_builds(trigger_request)).to be_truthy expect(create_next_builds).to be_truthy
commit.builds.reload expect(commit.builds.count(:all)).to eq(4)
expect(commit.builds.size).to eq(4)
end end
context 'for [ci skip]' do context 'for [ci skip]' do
...@@ -242,7 +238,7 @@ def create_develop_builds ...@@ -242,7 +238,7 @@ def create_develop_builds
it 'rebuilds commit' do it 'rebuilds commit' do
expect(commit.status).to eq('skipped') expect(commit.status).to eq('skipped')
expect(create_builds(trigger_request)).to be_truthy expect(create_builds).to be_truthy
# since everything in Ci::Commit is cached we need to fetch a new object # since everything in Ci::Commit is cached we need to fetch a new object
new_commit = Ci::Commit.find_by_id(commit.id) new_commit = Ci::Commit.find_by_id(commit.id)
...@@ -250,6 +246,129 @@ def create_develop_builds ...@@ -250,6 +246,129 @@ def create_develop_builds
end end
end end
end end
context 'properly creates builds "when" is defined' do
let(:yaml) {
{
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",
}
}
}
before do
stub_ci_commit_yaml_file(YAML.dump(yaml))
end
it 'properly creates builds' do
expect(create_builds).to be_truthy
expect(commit.builds.pluck(:name)).to contain_exactly('build')
expect(commit.builds.pluck(:status)).to contain_exactly('pending')
commit.builds.running_or_pending.each(&:success)
expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
commit.builds.running_or_pending.each(&:success)
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
commit.builds.running_or_pending.each(&:success)
expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
commit.builds.running_or_pending.each(&:success)
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
expect(commit.status).to eq('success')
end
it 'properly creates builds when test fails' do
expect(create_builds).to be_truthy
expect(commit.builds.pluck(:name)).to contain_exactly('build')
expect(commit.builds.pluck(:status)).to contain_exactly('pending')
commit.builds.running_or_pending.each(&:success)
expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
commit.builds.running_or_pending.each(&:drop)
expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
commit.builds.running_or_pending.each(&:success)
expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
commit.builds.running_or_pending.each(&:success)
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
expect(commit.status).to eq('failed')
end
it 'properly creates builds when test and test_failure fails' do
expect(create_builds).to be_truthy
expect(commit.builds.pluck(:name)).to contain_exactly('build')
expect(commit.builds.pluck(:status)).to contain_exactly('pending')
commit.builds.running_or_pending.each(&:success)
expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
commit.builds.running_or_pending.each(&:drop)