4a2a127b5c
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
250 lines
6.3 KiB
Ruby
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
|