gitlab-org--gitlab-foss/app/models/ci/pipeline.rb
Rémy Coutable 4a2a127b5c Merge branch 'artifacts-from-ref-and-build-name-api' into 'master'
API for downloading latest successful build

## What does this MR do?

Implement parts of #4255, particularly the API.

## Are there points in the code the reviewer needs to double check?

I still made it that `ref` could be either branch, tag, or even SHA with:

``` ruby
# ref can't be HEAD, can only be branch/tag name or SHA
scope :latest_successful_for, ->(ref) do
  table = quoted_table_name
  # TODO: Use `where(ref: ref).or(sha: ref)` in Rails 5
  where("#{table}.ref = ? OR #{table}.sha = ?", ref, ref).
    success.order(id: :desc)
end
```

Because the reasons I put in:

* https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13165543
* https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5142#note_13165921

But if you still think that it's not good to do it this way, I'll drop it and let's think about the other way to satisfy the requirement specified in https://gitlab.com/gitlab-org/gitlab-ce/issues/4255#note_13101233 It could be `status=any` or `sha=DEADBEAF`

## What are the relevant issue numbers?

Part of #4255

## Does this MR meet the acceptance criteria?

- [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added
- [ ] [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
- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [ ] 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 !5347
2016-07-21 09:01:47 +00:00

250 lines
6.3 KiB
Ruby

module Ci
class Pipeline < ActiveRecord::Base
extend Ci::Model
include Statuseable
self.table_name = 'ci_commits'
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
belongs_to :user
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
validates_presence_of :sha
validates_presence_of :status
validate :valid_commit_sha
# Invalidate object and save if when touched
after_touch :update_state
after_save :keep_around_commits
# ref can't be HEAD or SHA, can only be branch/tag name
scope :latest_successful_for, ->(ref = default_branch) do
where(ref: ref).success.order(id: :desc).limit(1)
end
def self.truncate_sha(sha)
sha[0...8]
end
def self.stages
# We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
CommitStatus.where(pipeline: pluck(:id)).stages
end
def project_id
project.id
end
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
end
end
def git_author_name
commit.try(:author_name)
end
def git_author_email
commit.try(:author_email)
end
def git_commit_message
commit.try(:message)
end
def git_commit_title
commit.try(:title)
end
def short_sha
Ci::Pipeline.truncate_sha(sha)
end
def commit
@commit ||= project.commit(sha)
rescue
nil
end
def branch?
!tag?
end
def manual_actions
builds.latest.manual_actions
end
def retryable?
builds.latest.any? do |build|
build.failed? && build.retryable?
end
end
def cancelable?
builds.running_or_pending.any?
end
def cancel_running
builds.running_or_pending.each(&:cancel)
end
def retry_failed(user)
builds.latest.failed.select(&:retryable?).each do |build|
Ci::Build.retry(build, user)
end
end
def latest?
return false unless ref
commit = project.commit(ref)
return false unless commit
commit.sha == sha
end
def triggered?
trigger_requests.any?
end
def create_builds(user, trigger_request = nil)
##
# We persist pipeline only if there are builds available
#
return unless config_processor
build_builds_for_stages(config_processor.stages, user,
'success', trigger_request) && save
end
def create_next_builds(build)
return unless config_processor
# don't create other builds if this one is retried
latest_builds = builds.latest
return unless latest_builds.exists?(build.id)
# get list of stages after this build
next_stages = config_processor.stages.drop_while { |stage| stage != build.stage }
next_stages.delete(build.stage)
# get status for all prior builds
prior_builds = latest_builds.where.not(stage: next_stages)
prior_status = prior_builds.status
# build builds for next stage that has builds available
# and save pipeline if we have builds
build_builds_for_stages(next_stages, build.user, prior_status,
build.trigger_request) && save
end
def retried
@retried ||= (statuses.order(id: :desc) - statuses.latest)
end
def coverage
coverage_array = statuses.latest.map(&:coverage).compact
if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
end
def has_warnings?
builds.latest.ignored.any?
end
def config_processor
return nil unless ci_yaml_file
return @config_processor if defined?(@config_processor)
@config_processor ||= begin
Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
self.yaml_errors = e.message
nil
rescue
self.yaml_errors = 'Undefined error'
nil
end
end
def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file)
@ci_yaml_file ||= begin
blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
blob.load_all_data!(project.repository)
blob.data
rescue
nil
end
end
def skip_ci?
git_commit_message =~ /\[(ci skip|skip ci)\]/i if git_commit_message
end
def environments
builds.where.not(environment: nil).success.pluck(:environment).uniq
end
# Manually set the notes for a Ci::Pipeline
# There is no ActiveRecord relation between Ci::Pipeline and notes
# as they are related to a commit sha. This method helps importing
# them using the +Gitlab::ImportExport::RelationFactory+ class.
def notes=(notes)
notes.each do |note|
note[:id] = nil
note[:commit_id] = sha
note[:noteable_id] = self['id']
note.save!
end
end
def notes
Note.for_commit_id(sha)
end
def predefined_variables
[
{ key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
]
end
private
def build_builds_for_stages(stages, user, status, trigger_request)
##
# Note that `Array#any?` implements a short circuit evaluation, so we
# build builds only for the first stage that has builds available.
#
stages.any? do |stage|
CreateBuildsService.new(self).
execute(stage, user, status, trigger_request).
any?(&:active?)
end
end
def update_state
statuses.reload
self.status = if yaml_errors.blank?
statuses.latest.status || 'skipped'
else
'failed'
end
self.started_at = statuses.started_at
self.finished_at = statuses.finished_at
self.duration = statuses.latest.duration
save
end
def keep_around_commits
return unless project
project.repository.keep_around(self.sha)
project.repository.keep_around(self.before_sha)
end
end
end