gitlab-org--gitlab-foss/app/models/ci/build.rb

640 lines
19 KiB
Ruby
Raw Normal View History

2015-08-25 21:42:46 -04:00
module Ci
2015-10-06 06:01:16 -04:00
class Build < CommitStatus
prepend ArtifactMigratable
include TokenAuthenticatable
include AfterCommitQueue
2018-02-26 14:14:00 -05:00
include ObjectStorage::BackgroundMove
include Presentable
2017-09-01 04:54:07 -04:00
include Importable
MissingDependenciesError = Class.new(StandardError)
belongs_to :project, inverse_of: :builds
belongs_to :runner
belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User'
2015-08-25 21:42:46 -04:00
has_many :deployments, as: :deployable
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
2017-09-25 12:54:08 -04:00
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
2017-11-02 14:38:25 -04:00
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
2017-12-03 06:02:11 -05:00
has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :build_metadata, class_name: 'Ci::BuildMetadata'
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
@persisted_environment ||= Environment.find_by(
name: expanded_environment_name,
2017-03-17 19:06:11 -04:00
project: project
)
end
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
2015-08-25 21:42:46 -04:00
2017-02-22 17:35:08 -05:00
delegate :name, to: :project, prefix: true
2015-08-25 21:42:46 -04:00
validates :coverage, numericality: true, allow_blank: true
2017-02-21 19:40:04 -05:00
validates :ref, presence: true
2015-08-25 21:42:46 -04:00
scope :unstarted, ->() { where(runner_id: nil) }
2015-10-02 07:46:38 -04:00
scope :ignore_failures, ->() { where(allow_failure: false) }
scope :with_artifacts_archive, ->() do
2017-12-03 06:04:16 -05:00
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end
scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
2016-07-07 06:56:02 -04:00
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) }
2015-08-25 21:42:46 -04:00
scope :matches_tag_ids, -> (tag_ids) do
matcher = ::ActsAsTaggableOn::Tagging
.where(taggable_type: CommitStatus)
.where(context: 'tags')
.where('taggable_id = ci_builds.id')
.where.not(tag_id: tag_ids).select('1')
where("NOT EXISTS (?)", matcher)
end
scope :with_any_tags, -> do
matcher = ::ActsAsTaggableOn::Tagging
.where(taggable_type: CommitStatus)
.where(context: 'tags')
.where('taggable_id = ci_builds.id').select('1')
where("EXISTS (?)", matcher)
end
mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file
mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata
2015-08-25 21:42:46 -04:00
acts_as_taggable
add_authentication_token_field :token
before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
before_destroy { unscoped_project }
after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
after_commit :update_project_statistics_after_save, on: [:create, :update]
after_commit :update_project_statistics, on: :destroy
2015-08-25 21:42:46 -04:00
class << self
# This is needed for url_for to work,
# as the controller is JobsController
def model_name
ActiveModel::Name.new(self, nil, 'job')
end
2015-08-25 21:42:46 -04:00
def first_pending
pending.unstarted.order('created_at ASC').first
end
def retry(build, current_user)
Ci::RetryBuildService
.new(build.project, current_user)
.execute(build)
2015-08-25 21:42:46 -04:00
end
end
state_machine :status do
event :actionize do
transition created: :manual
2017-02-28 10:48:39 -05:00
end
after_transition any => [:pending] do |build|
build.run_after_commit do
2016-12-15 16:29:47 -05:00
BuildQueueWorker.perform_async(id)
end
end
after_transition pending: :running do |build|
build.run_after_commit do
BuildHooksWorker.perform_async(id)
end
end
after_transition any => [:success, :failed, :canceled] do |build|
build.run_after_commit do
BuildFinishedWorker.perform_async(id)
end
2015-08-25 21:42:46 -04:00
end
2016-06-10 17:36:54 -04:00
after_transition any => [:success] do |build|
build.run_after_commit do
BuildSuccessWorker.perform_async(id)
2016-06-10 17:36:54 -04:00
end
end
before_transition any => [:failed] do |build|
next unless build.project
next if build.retries_max.zero?
if build.retries_count < build.retries_max
begin
Ci::Build.retry(build, build.user)
rescue Gitlab::Access::AccessDeniedError => ex
Rails.logger.error "Unable to auto-retry job #{build.id}: #{ex}"
end
end
end
2017-09-03 10:35:37 -04:00
before_transition any => [:running] do |build|
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
2017-09-03 10:35:37 -04:00
end
2018-02-28 15:36:01 -05:00
before_transition pending: :running do |build|
build.metadata.save_timeout_state!
2018-02-28 15:36:01 -05:00
end
2015-08-25 21:42:46 -04:00
end
def metadata
self.build_metadata ||= Ci::BuildMetadata.new
end
def detailed_status(current_user)
Gitlab::Ci::Status::Build::Factory
.new(self, current_user)
.fabricate!
2016-12-08 03:51:36 -05:00
end
def other_actions
pipeline.manual_actions.where.not(name: name)
end
2016-07-16 12:39:58 -04:00
def playable?
action? && (manual? || complete?)
2017-02-28 10:48:39 -05:00
end
def action?
self.when == 'manual'
end
def play(current_user)
Ci::PlayBuildService
.new(project, current_user)
.execute(self)
2016-07-16 12:39:58 -04:00
end
2016-12-08 04:40:56 -05:00
def cancelable?
active?
end
def retryable?
2017-04-06 08:31:38 -04:00
success? || failed? || canceled?
end
def retries_count
pipeline.builds.retried.where(name: self.name).count
end
def retries_max
self.options.fetch(:retry, 0).to_i
end
def latest?
!retried?
2016-03-31 13:51:28 -04:00
end
def expanded_environment_name
ExpandVariables.expand(environment, simple_variables) if environment
end
2016-11-17 06:08:28 -05:00
def has_environment?
environment.present?
2016-11-17 06:08:28 -05:00
end
def starts_environment?
2016-11-17 06:08:28 -05:00
has_environment? && self.environment_action == 'start'
end
def stops_environment?
2016-11-17 06:08:28 -05:00
has_environment? && self.environment_action == 'stop'
end
def environment_action
self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options
end
def outdated_deployment?
success? && !last_deployment.try(:last?)
end
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
# Return builds from previous stages
latest_builds.where('stage_idx < ?', stage_idx)
end
2015-08-25 21:42:46 -04:00
def timeout
metadata.used_timeout
end
def triggered_by?(current_user)
2017-11-06 08:20:44 -05:00
user == current_user
end
2016-12-13 14:21:30 -05:00
# A slugified version of the build ref, suitable for inclusion in URLs and
# domain names. Rules:
#
# * Lowercased
# * Anything not matching [a-z0-9-] is replaced with a -
# * Maximum length is 63 bytes
2017-07-05 08:00:22 -04:00
# * First/Last Character is not a hyphen
2016-12-13 14:21:30 -05:00
def ref_slug
2017-08-07 14:00:11 -04:00
Gitlab::Utils.slugify(ref.to_s)
2016-12-13 14:21:30 -05:00
end
# Variables whose value does not depend on environment
def simple_variables
variables(environment: nil)
end
# All variables, including those dependent on environment, which could
# contain unexpanded variables.
def variables(environment: persisted_environment)
collection = Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables)
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runner
variables.concat(project.deployment_variables(environment: environment)) if has_environment?
variables.concat(yaml_variables)
variables.concat(user_variables)
2018-03-13 09:00:14 -04:00
variables.concat(project.group.secret_variables_for(ref, project)) if project.group
variables.concat(secret_variables(environment: environment))
variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
variables.concat(persisted_environment_variables) if environment
end
collection.to_runner_variables
end
def features
{ trace_sections: true }
end
def merge_request
return @merge_request if defined?(@merge_request)
2017-05-29 03:58:20 -04:00
@merge_request ||=
begin
Use latest_merge_request_diff association Compared to the merge_request_diff association: 1. It's simpler to query. The query uses a foreign key to the merge_request_diffs table, so no ordering is necessary. 2. It's faster for preloading. The merge_request_diff association has to load every diff for the MRs in the set, then discard all but the most recent for each. This association means that Rails can just query for N diffs from N MRs. 3. It's more complicated to update. This is a bidirectional foreign key, so we need to update two tables when adding a diff record. This also means we need to handle this as a special case when importing a GitLab project. There is some juggling with this association in the merge request model: * `MergeRequest#latest_merge_request_diff` is _always_ the latest diff. * `MergeRequest#merge_request_diff` reuses `MergeRequest#latest_merge_request_diff` unless: * Arguments are passed. These are typically to force-reload the association. * It doesn't exist. That means we might be trying to implicitly create a diff. This only seems to happen in specs. * The association is already loaded. This is important for the reasons explained in the comment, which I'll reiterate here: if we a) load a non-latest diff, then b) get its `merge_request`, then c) get that MR's `merge_request_diff`, we should get the diff we loaded in c), even though that's not the latest diff. Basically, `MergeRequest#merge_request_diff` is the latest diff in most cases, but not quite all.
2017-11-15 12:22:18 -05:00
merge_requests = MergeRequest.includes(:latest_merge_request_diff)
.where(source_branch: ref,
source_project: pipeline.project)
2017-05-29 03:58:20 -04:00
.reorder(iid: :desc)
merge_requests.find do |merge_request|
merge_request.commit_shas.include?(pipeline.sha)
end
end
end
2015-08-25 21:42:46 -04:00
def repo_url
2016-09-16 06:46:33 -04:00
auth = "gitlab-ci-token:#{ensure_token!}@"
2018-01-27 00:35:53 -05:00
project.http_url_to_repo.sub(%r{^https?://}) do |prefix|
2015-12-04 06:55:23 -05:00
prefix + auth
end
2015-08-25 21:42:46 -04:00
end
def allow_git_fetch
2015-12-04 06:55:23 -05:00
project.build_allow_git_fetch
2015-08-25 21:42:46 -04:00
end
def update_coverage
coverage = trace.extract_coverage(coverage_regex)
update_attributes(coverage: coverage) if coverage.present?
2015-08-25 21:42:46 -04:00
end
2017-09-25 12:54:08 -04:00
def parse_trace_sections!
ExtractSectionsFromBuildTraceService.new(project, user).execute(self)
2017-09-25 12:54:08 -04:00
end
def trace
Gitlab::Ci::Trace.new(self)
end
def has_trace?
trace.exist?
2016-08-25 07:53:20 -04:00
end
def trace=(data)
raise NotImplementedError
2016-03-31 07:24:14 -04:00
end
def old_trace
read_attribute(:trace)
2016-03-29 09:34:18 -04:00
end
def erase_old_trace!
update_column(:trace, nil)
2015-08-25 21:42:46 -04:00
end
def needs_touch?
Time.now - updated_at > 15.minutes.to_i
end
2016-05-18 18:43:00 -04:00
def valid_token?(token)
self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
def has_tags?
tag_list.any?
end
def any_runners_online?
project.any_runners? { |runner| runner.active? && runner.online? && runner.can_pick?(self) }
end
def stuck?
pending? && !any_runners_online?
end
def execute_hooks
return unless project
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :job_hooks)
project.execute_services(build_data.dup, :job_hooks)
PagesService.new(build_data).execute
2016-06-01 10:50:32 -04:00
project.running_or_pending_build_count(force: true)
end
def browsable_artifacts?
artifacts_metadata?
end
def artifacts_metadata_entry(path, **options)
artifacts_metadata.use_file do |metadata_path|
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
metadata_path,
path,
**options)
metadata.to_entry
end
end
2016-05-18 16:21:51 -04:00
def erase_artifacts!
remove_artifacts_file!
remove_artifacts_metadata!
save
2016-05-18 16:21:51 -04:00
end
def erase(opts = {})
return false unless erasable?
2016-05-18 16:21:51 -04:00
erase_artifacts!
erase_trace!
update_erased!(opts[:erased_by])
end
def erasable?
complete? && (artifacts? || has_trace?)
end
def erased?
!self.erased_at.nil?
end
2016-05-18 16:21:51 -04:00
def artifacts_expired?
artifacts_expire_at && artifacts_expire_at < Time.now
2016-05-18 16:21:51 -04:00
end
def artifacts_expire_in
artifacts_expire_at - Time.now if artifacts_expire_at
end
def artifacts_expire_in=(value)
2016-06-10 15:45:06 -04:00
self.artifacts_expire_at =
if value
ChronicDuration.parse(value)&.seconds&.from_now
2016-06-10 15:45:06 -04:00
end
end
def has_expiring_artifacts?
2017-05-29 03:58:20 -04:00
artifacts_expire_at.present? && artifacts_expire_at > Time.now
end
2016-06-08 11:18:54 -04:00
def keep_artifacts!
2016-05-18 16:21:51 -04:00
self.update(artifacts_expire_at: nil)
2017-12-03 06:02:11 -05:00
self.job_artifacts.update_all(expire_at: nil)
2016-05-18 16:21:51 -04:00
end
def coverage_regex
super || project.try(:build_coverage_regex)
end
def when
read_attribute(:when) || build_attributes_from_config[:when] || 'on_success'
end
def yaml_variables
read_attribute(:yaml_variables) || build_attributes_from_config[:yaml_variables] || []
end
def user_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables if user.blank?
variables.append(key: 'GITLAB_USER_ID', value: user.id.to_s)
variables.append(key: 'GITLAB_USER_EMAIL', value: user.email)
variables.append(key: 'GITLAB_USER_LOGIN', value: user.username)
variables.append(key: 'GITLAB_USER_NAME', value: user.name)
end
end
def secret_variables(environment: persisted_environment)
project.secret_variables_for(ref: ref, environment: environment)
.map(&:to_runner_variable)
end
2017-02-15 19:05:44 -05:00
def steps
2017-03-07 06:30:34 -05:00
[Gitlab::Ci::Build::Step.from_commands(self),
Gitlab::Ci::Build::Step.from_after_script(self)].compact
2017-02-15 19:05:44 -05:00
end
def image
Gitlab::Ci::Build::Image.from_image(self)
2017-02-15 19:05:44 -05:00
end
def services
Gitlab::Ci::Build::Image.from_services(self)
2017-02-15 19:05:44 -05:00
end
def artifacts
[options[:artifacts]]
2017-02-15 19:05:44 -05:00
end
def cache
2018-01-04 13:43:31 -05:00
cache = options[:cache]
if cache && project.jobs_cache_index
cache = cache.merge(
key: "#{cache[:key]}-#{project.jobs_cache_index}")
2017-12-22 17:06:15 -05:00
end
2018-01-04 13:43:31 -05:00
[cache]
2017-02-15 19:05:44 -05:00
end
2016-11-20 14:43:50 -05:00
def credentials
Gitlab::Ci::Build::Credentials::Factory.new(self).create!
2016-11-20 14:43:50 -05:00
end
def dependencies
return [] if empty_dependencies?
depended_jobs = depends_on_builds
2017-03-18 19:35:17 -04:00
return depended_jobs unless options[:dependencies].present?
2017-03-18 19:35:17 -04:00
depended_jobs.select do |job|
options[:dependencies].include?(job.name)
end
end
def empty_dependencies?
options[:dependencies]&.empty?
end
def validates_dependencies!
dependencies.each do |dependency|
raise MissingDependenciesError unless dependency.valid_dependency?
end
end
2017-12-02 02:23:19 -05:00
def valid_dependency?
return false if artifacts_expired?
return false if erased?
true
end
def hide_secrets(trace)
return unless trace
trace = trace.dup
Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
Gitlab::Ci::MaskSecret.mask!(trace, token)
trace
end
2017-09-05 11:10:57 -04:00
def serializable_hash(options = {})
2017-09-06 04:16:11 -04:00
super(options).merge(when: read_attribute(:when))
2017-09-05 11:10:57 -04:00
end
private
def update_artifacts_size
2017-12-03 10:21:59 -05:00
self.artifacts_size = legacy_artifacts_file&.size
end
def erase_trace!
trace.erase!
end
def update_erased!(user = nil)
2016-06-08 11:18:54 -04:00
self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
end
def unscoped_project
2017-03-17 19:06:11 -04:00
@unscoped_project ||= Project.unscoped.find_by(id: project_id)
end
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
variables.append(key: 'GITLAB_CI', value: 'true')
variables.append(key: 'GITLAB_FEATURES', value: project.namespace.features.join(','))
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
variables.append(key: 'CI_JOB_ID', value: id.to_s)
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
variables.append(key: 'CI_JOB_TOKEN', value: token, public: false)
variables.append(key: 'CI_COMMIT_SHA', value: sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
variables.concat(legacy_variables)
end
end
def persisted_environment_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless persisted_environment
2017-05-25 10:14:22 -04:00
variables.concat(persisted_environment.predefined_variables)
2017-05-25 10:14:22 -04:00
# Here we're passing unexpanded environment_url for runner to expand,
# and we need to make sure that CI_ENVIRONMENT_NAME and
# CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
end
end
def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_BUILD_ID', value: id.to_s)
variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
variables.append(key: 'CI_BUILD_REF', value: sha)
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
variables.append(key: 'CI_BUILD_REF_SLUG', value: ref_slug)
variables.append(key: 'CI_BUILD_NAME', value: name)
variables.append(key: 'CI_BUILD_STAGE', value: stage)
variables.append(key: "CI_BUILD_TAG", value: ref) if tag?
variables.append(key: "CI_BUILD_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_BUILD_MANUAL", value: 'true') if action?
end
end
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
def build_attributes_from_config
return {} unless pipeline.config_processor
pipeline.config_processor.build_attributes(name)
end
2016-09-19 06:38:03 -04:00
def update_project_statistics
return unless project
ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
end
def update_project_statistics_after_save
if previous_changes.include?('artifacts_size')
update_project_statistics
end
end
2015-08-25 21:42:46 -04:00
end
end