PageRenderTime 27ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/app/models/ci/pipeline.rb

https://gitlab.com/gitlab-group/gitlab-ce
Ruby | 389 lines | 293 code | 84 blank | 12 comment | 9 complexity | 217e8f964bdb0c695efb3d99e1b66b6d MD5 | raw file
  1. module Ci
  2. class Pipeline < ActiveRecord::Base
  3. extend Ci::Model
  4. include HasStatus
  5. include Importable
  6. include AfterCommitQueue
  7. self.table_name = 'ci_commits'
  8. belongs_to :project, foreign_key: :gl_project_id
  9. belongs_to :user
  10. has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
  11. has_many :builds, foreign_key: :commit_id
  12. has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
  13. validates_presence_of :sha, unless: :importing?
  14. validates_presence_of :ref, unless: :importing?
  15. validates_presence_of :status, unless: :importing?
  16. validate :valid_commit_sha, unless: :importing?
  17. after_create :keep_around_commits, unless: :importing?
  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] => :running
  25. end
  26. event :skip do
  27. transition any - [:skipped] => :skipped
  28. end
  29. event :drop do
  30. transition any - [:failed] => :failed
  31. end
  32. event :succeed do
  33. transition any - [:success] => :success
  34. end
  35. event :cancel do
  36. transition any - [:canceled] => :canceled
  37. end
  38. # IMPORTANT
  39. # Do not add any operations to this state_machine
  40. # Create a separate worker for each new operation
  41. before_transition [:created, :pending] => :running do |pipeline|
  42. pipeline.started_at = Time.now
  43. end
  44. before_transition any => [:success, :failed, :canceled] do |pipeline|
  45. pipeline.finished_at = Time.now
  46. pipeline.update_duration
  47. end
  48. after_transition [:created, :pending] => :running do |pipeline|
  49. pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
  50. end
  51. after_transition any => [:success] do |pipeline|
  52. pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
  53. end
  54. after_transition [:created, :pending, :running] => :success do |pipeline|
  55. pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
  56. end
  57. after_transition do |pipeline, transition|
  58. next if transition.loopback?
  59. pipeline.run_after_commit do
  60. PipelineHooksWorker.perform_async(id)
  61. end
  62. end
  63. after_transition any => [:success, :failed] do |pipeline|
  64. pipeline.run_after_commit do
  65. PipelineNotificationWorker.perform_async(pipeline.id)
  66. end
  67. end
  68. end
  69. # ref can't be HEAD or SHA, can only be branch/tag name
  70. scope :latest, ->(ref = nil) do
  71. max_id = unscope(:select)
  72. .select("max(#{quoted_table_name}.id)")
  73. .group(:ref, :sha)
  74. relation = ref ? where(ref: ref) : self
  75. relation.where(id: max_id)
  76. end
  77. def self.latest_status(ref = nil)
  78. latest(ref).status
  79. end
  80. def self.latest_successful_for(ref)
  81. success.latest(ref).order(id: :desc).first
  82. end
  83. def self.truncate_sha(sha)
  84. sha[0...8]
  85. end
  86. def self.total_duration
  87. where.not(duration: nil).sum(:duration)
  88. end
  89. def stage(name)
  90. stage = Ci::Stage.new(self, name: name)
  91. stage unless stage.statuses_count.zero?
  92. end
  93. def stages_count
  94. statuses.select(:stage).distinct.count
  95. end
  96. def stages_name
  97. statuses.order(:stage_idx).distinct.
  98. pluck(:stage, :stage_idx).map(&:first)
  99. end
  100. def stages
  101. # TODO, this needs refactoring, see gitlab-ce#26481.
  102. stages_query = statuses
  103. .group('stage').select(:stage).order('max(stage_idx)')
  104. status_sql = statuses.latest.where('stage=sg.stage').status_sql
  105. warnings_sql = statuses.latest.select('COUNT(*) > 0')
  106. .where('stage=sg.stage').failed_but_allowed.to_sql
  107. stages_with_statuses = CommitStatus.from(stages_query, :sg)
  108. .pluck('sg.stage', status_sql, "(#{warnings_sql})")
  109. stages_with_statuses.map do |stage|
  110. Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
  111. end
  112. end
  113. def artifacts
  114. builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
  115. end
  116. def project_id
  117. project.id
  118. end
  119. # For now the only user who participates is the user who triggered
  120. def participants(_current_user = nil)
  121. Array(user)
  122. end
  123. def valid_commit_sha
  124. if self.sha == Gitlab::Git::BLANK_SHA
  125. self.errors.add(:sha, " cant be 00000000 (branch removal)")
  126. end
  127. end
  128. def git_author_name
  129. commit.try(:author_name)
  130. end
  131. def git_author_email
  132. commit.try(:author_email)
  133. end
  134. def git_commit_message
  135. commit.try(:message)
  136. end
  137. def git_commit_title
  138. commit.try(:title)
  139. end
  140. def short_sha
  141. Ci::Pipeline.truncate_sha(sha)
  142. end
  143. def commit
  144. @commit ||= project.commit(sha)
  145. rescue
  146. nil
  147. end
  148. def branch?
  149. !tag?
  150. end
  151. def manual_actions
  152. builds.latest.manual_actions.includes(project: [:namespace])
  153. end
  154. def stuck?
  155. builds.pending.any?(&:stuck?)
  156. end
  157. def retryable?
  158. builds.latest.failed_or_canceled.any?(&:retryable?)
  159. end
  160. def cancelable?
  161. statuses.cancelable.any?
  162. end
  163. def cancel_running
  164. Gitlab::OptimisticLocking.retry_lock(
  165. statuses.cancelable) do |cancelable|
  166. cancelable.each(&:cancel)
  167. end
  168. end
  169. def retry_failed(user)
  170. Gitlab::OptimisticLocking.retry_lock(
  171. builds.latest.failed_or_canceled) do |failed_or_canceled|
  172. failed_or_canceled.select(&:retryable?).each do |build|
  173. Ci::Build.retry(build, user)
  174. end
  175. end
  176. end
  177. def mark_as_processable_after_stage(stage_idx)
  178. builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
  179. end
  180. def latest?
  181. return false unless ref
  182. commit = project.commit(ref)
  183. return false unless commit
  184. commit.sha == sha
  185. end
  186. def triggered?
  187. trigger_requests.any?
  188. end
  189. def retried
  190. @retried ||= (statuses.order(id: :desc) - statuses.latest)
  191. end
  192. def coverage
  193. coverage_array = statuses.latest.map(&:coverage).compact
  194. if coverage_array.size >= 1
  195. '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
  196. end
  197. end
  198. def config_builds_attributes
  199. return [] unless config_processor
  200. config_processor.
  201. builds_for_ref(ref, tag?, trigger_requests.first).
  202. sort_by { |build| build[:stage_idx] }
  203. end
  204. def has_warnings?
  205. builds.latest.failed_but_allowed.any?
  206. end
  207. def config_processor
  208. return nil unless ci_yaml_file
  209. return @config_processor if defined?(@config_processor)
  210. @config_processor ||= begin
  211. Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
  212. rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
  213. self.yaml_errors = e.message
  214. nil
  215. rescue
  216. self.yaml_errors = 'Undefined error'
  217. nil
  218. end
  219. end
  220. def ci_yaml_file
  221. return @ci_yaml_file if defined?(@ci_yaml_file)
  222. @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
  223. end
  224. def has_yaml_errors?
  225. yaml_errors.present?
  226. end
  227. def environments
  228. builds.where.not(environment: nil).success.pluck(:environment).uniq
  229. end
  230. # Manually set the notes for a Ci::Pipeline
  231. # There is no ActiveRecord relation between Ci::Pipeline and notes
  232. # as they are related to a commit sha. This method helps importing
  233. # them using the +Gitlab::ImportExport::RelationFactory+ class.
  234. def notes=(notes)
  235. notes.each do |note|
  236. note[:id] = nil
  237. note[:commit_id] = sha
  238. note[:noteable_id] = self['id']
  239. note.save!
  240. end
  241. end
  242. def notes
  243. Note.for_commit_id(sha)
  244. end
  245. def process!
  246. Ci::ProcessPipelineService.new(project, user).execute(self)
  247. end
  248. def update_status
  249. Gitlab::OptimisticLocking.retry_lock(self) do
  250. case latest_builds_status
  251. when 'pending' then enqueue
  252. when 'running' then run
  253. when 'success' then succeed
  254. when 'failed' then drop
  255. when 'canceled' then cancel
  256. when 'skipped' then skip
  257. end
  258. end
  259. end
  260. def predefined_variables
  261. [
  262. { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
  263. ]
  264. end
  265. def queued_duration
  266. return unless started_at
  267. seconds = (started_at - created_at).to_i
  268. seconds unless seconds.zero?
  269. end
  270. def update_duration
  271. return unless started_at
  272. self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
  273. end
  274. def execute_hooks
  275. data = pipeline_data
  276. project.execute_hooks(data, :pipeline_hooks)
  277. project.execute_services(data, :pipeline_hooks)
  278. end
  279. # Merge requests for which the current pipeline is running against
  280. # the merge request's latest commit.
  281. def merge_requests
  282. @merge_requests ||= project.merge_requests
  283. .where(source_branch: self.ref)
  284. .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
  285. end
  286. def detailed_status(current_user)
  287. Gitlab::Ci::Status::Pipeline::Factory
  288. .new(self, current_user)
  289. .fabricate!
  290. end
  291. private
  292. def pipeline_data
  293. Gitlab::DataBuilder::Pipeline.build(self)
  294. end
  295. def latest_builds_status
  296. return 'failed' unless yaml_errors.blank?
  297. statuses.latest.status || 'skipped'
  298. end
  299. def keep_around_commits
  300. return unless project
  301. project.repository.keep_around(self.sha)
  302. project.repository.keep_around(self.before_sha)
  303. end
  304. end
  305. end