PageRenderTime 53ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/app/models/ci/pipeline.rb

https://gitlab.com/vilhelmen/gitlab-ce
Ruby | 321 lines | 246 code | 67 blank | 8 comment | 9 complexity | 5147211c906e250a9d50a673832642cf MD5 | raw file
  1. module Ci
  2. class Pipeline < ActiveRecord::Base
  3. extend Ci::Model
  4. include HasStatus
  5. include Importable
  6. self.table_name = 'ci_commits'
  7. belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
  8. belongs_to :user
  9. has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
  10. has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
  11. has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
  12. validates_presence_of :sha, unless: :importing?
  13. validates_presence_of :ref, unless: :importing?
  14. validates_presence_of :status, unless: :importing?
  15. validate :valid_commit_sha, unless: :importing?
  16. after_save :keep_around_commits, unless: :importing?
  17. delegate :stages, to: :statuses
  18. state_machine :status, initial: :created do
  19. event :enqueue do
  20. transition created: :pending
  21. transition [:success, :failed, :canceled, :skipped] => :running
  22. end
  23. event :run do
  24. transition any => :running
  25. end
  26. event :skip do
  27. transition any => :skipped
  28. end
  29. event :drop do
  30. transition any => :failed
  31. end
  32. event :succeed do
  33. transition any => :success
  34. end
  35. event :cancel do
  36. transition any => :canceled
  37. end
  38. before_transition [:created, :pending] => :running do |pipeline|
  39. pipeline.started_at = Time.now
  40. end
  41. before_transition any => [:success, :failed, :canceled] do |pipeline|
  42. pipeline.finished_at = Time.now
  43. end
  44. after_transition [:created, :pending] => :running do |pipeline|
  45. MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
  46. update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
  47. end
  48. after_transition any => [:success] do |pipeline|
  49. MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
  50. update_all(latest_build_finished_at: pipeline.finished_at)
  51. end
  52. before_transition do |pipeline|
  53. pipeline.update_duration
  54. end
  55. after_transition do |pipeline, transition|
  56. pipeline.execute_hooks unless transition.loopback?
  57. end
  58. end
  59. # ref can't be HEAD or SHA, can only be branch/tag name
  60. def self.latest_successful_for(ref)
  61. where(ref: ref).order(id: :desc).success.first
  62. end
  63. def self.truncate_sha(sha)
  64. sha[0...8]
  65. end
  66. def self.stages
  67. # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
  68. CommitStatus.where(pipeline: pluck(:id)).stages
  69. end
  70. def self.total_duration
  71. where.not(duration: nil).sum(:duration)
  72. end
  73. def stages_with_latest_statuses
  74. statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
  75. end
  76. def project_id
  77. project.id
  78. end
  79. def valid_commit_sha
  80. if self.sha == Gitlab::Git::BLANK_SHA
  81. self.errors.add(:sha, " cant be 00000000 (branch removal)")
  82. end
  83. end
  84. def git_author_name
  85. commit.try(:author_name)
  86. end
  87. def git_author_email
  88. commit.try(:author_email)
  89. end
  90. def git_commit_message
  91. commit.try(:message)
  92. end
  93. def git_commit_title
  94. commit.try(:title)
  95. end
  96. def short_sha
  97. Ci::Pipeline.truncate_sha(sha)
  98. end
  99. def commit
  100. @commit ||= project.commit(sha)
  101. rescue
  102. nil
  103. end
  104. def branch?
  105. !tag?
  106. end
  107. def manual_actions
  108. builds.latest.manual_actions
  109. end
  110. def retryable?
  111. builds.latest.any? do |build|
  112. build.failed? && build.retryable?
  113. end
  114. end
  115. def cancelable?
  116. builds.running_or_pending.any?
  117. end
  118. def cancel_running
  119. builds.running_or_pending.each(&:cancel)
  120. end
  121. def retry_failed(user)
  122. builds.latest.failed.select(&:retryable?).each do |build|
  123. Ci::Build.retry(build, user)
  124. end
  125. end
  126. def mark_as_processable_after_stage(stage_idx)
  127. builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
  128. end
  129. def latest?
  130. return false unless ref
  131. commit = project.commit(ref)
  132. return false unless commit
  133. commit.sha == sha
  134. end
  135. def triggered?
  136. trigger_requests.any?
  137. end
  138. def retried
  139. @retried ||= (statuses.order(id: :desc) - statuses.latest)
  140. end
  141. def coverage
  142. coverage_array = statuses.latest.map(&:coverage).compact
  143. if coverage_array.size >= 1
  144. '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
  145. end
  146. end
  147. def config_builds_attributes
  148. return [] unless config_processor
  149. config_processor.
  150. builds_for_ref(ref, tag?, trigger_requests.first).
  151. sort_by { |build| build[:stage_idx] }
  152. end
  153. def has_warnings?
  154. builds.latest.failed_but_allowed.any?
  155. end
  156. def config_processor
  157. return nil unless ci_yaml_file
  158. return @config_processor if defined?(@config_processor)
  159. @config_processor ||= begin
  160. Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
  161. rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
  162. self.yaml_errors = e.message
  163. nil
  164. rescue
  165. self.yaml_errors = 'Undefined error'
  166. nil
  167. end
  168. end
  169. def ci_yaml_file
  170. return @ci_yaml_file if defined?(@ci_yaml_file)
  171. @ci_yaml_file ||= begin
  172. blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
  173. blob.load_all_data!(project.repository)
  174. blob.data
  175. rescue
  176. nil
  177. end
  178. end
  179. def environments
  180. builds.where.not(environment: nil).success.pluck(:environment).uniq
  181. end
  182. # Manually set the notes for a Ci::Pipeline
  183. # There is no ActiveRecord relation between Ci::Pipeline and notes
  184. # as they are related to a commit sha. This method helps importing
  185. # them using the +Gitlab::ImportExport::RelationFactory+ class.
  186. def notes=(notes)
  187. notes.each do |note|
  188. note[:id] = nil
  189. note[:commit_id] = sha
  190. note[:noteable_id] = self['id']
  191. note.save!
  192. end
  193. end
  194. def notes
  195. Note.for_commit_id(sha)
  196. end
  197. def process!
  198. Ci::ProcessPipelineService.new(project, user).execute(self)
  199. end
  200. def update_status
  201. with_lock do
  202. case latest_builds_status
  203. when 'pending' then enqueue
  204. when 'running' then run
  205. when 'success' then succeed
  206. when 'failed' then drop
  207. when 'canceled' then cancel
  208. when 'skipped' then skip
  209. end
  210. end
  211. end
  212. def predefined_variables
  213. [
  214. { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
  215. ]
  216. end
  217. def queued_duration
  218. return unless started_at
  219. seconds = (started_at - created_at).to_i
  220. seconds unless seconds.zero?
  221. end
  222. def update_duration
  223. return unless started_at
  224. self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
  225. end
  226. def execute_hooks
  227. data = pipeline_data
  228. project.execute_hooks(data, :pipeline_hooks)
  229. project.execute_services(data, :pipeline_hooks)
  230. end
  231. # Merge requests for which the current pipeline is running against
  232. # the merge request's latest commit.
  233. def merge_requests
  234. @merge_requests ||=
  235. begin
  236. project.merge_requests.where(source_branch: self.ref).
  237. select { |merge_request| merge_request.pipeline.try(:id) == self.id }
  238. end
  239. end
  240. private
  241. def pipeline_data
  242. Gitlab::DataBuilder::Pipeline.build(self)
  243. end
  244. def latest_builds_status
  245. return 'failed' unless yaml_errors.blank?
  246. statuses.latest.status || 'skipped'
  247. end
  248. def keep_around_commits
  249. return unless project
  250. project.repository.keep_around(self.sha)
  251. project.repository.keep_around(self.before_sha)
  252. end
  253. end
  254. end