32e32fa0fd
See: 6280fd3777 (note_14766241)
We wanted to avoid accesses to the file system, because it seems reasonable that if the mounter column has some content it’s because there are an associate file. This is an speed boost when you access to a bunch of build instances to show their information.
The problem is that solution doesn’t work if you uses that method to detect the thing and you’re changing the uploaded file on instances that had that column empty. Until you don’t persist the instance the database column will be empty so we can have false negatives
504 lines
13 KiB
Ruby
504 lines
13 KiB
Ruby
module Ci
|
|
class Build < CommitStatus
|
|
include TokenAuthenticatable
|
|
|
|
belongs_to :runner, class_name: 'Ci::Runner'
|
|
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
|
|
belongs_to :erased_by, class_name: 'User'
|
|
|
|
serialize :options
|
|
serialize :yaml_variables
|
|
|
|
validates :coverage, numericality: true, allow_blank: true
|
|
validates_presence_of :ref
|
|
|
|
scope :unstarted, ->() { where(runner_id: nil) }
|
|
scope :ignore_failures, ->() { where(allow_failure: false) }
|
|
scope :with_artifacts, ->() { where.not(artifacts_file: [nil, '']) }
|
|
scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
|
|
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
|
|
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
|
|
scope :manual_actions, ->() { where(when: :manual).relevant }
|
|
|
|
mount_uploader :artifacts_file, ArtifactUploader
|
|
mount_uploader :artifacts_metadata, ArtifactUploader
|
|
|
|
acts_as_taggable
|
|
|
|
add_authentication_token_field :token
|
|
|
|
before_save :update_artifacts_size, if: :artifacts_file_changed?
|
|
before_save :ensure_token
|
|
before_destroy { project }
|
|
|
|
after_create :execute_hooks
|
|
|
|
class << self
|
|
def first_pending
|
|
pending.unstarted.order('created_at ASC').first
|
|
end
|
|
|
|
def create_from(build)
|
|
new_build = build.dup
|
|
new_build.status = 'pending'
|
|
new_build.runner_id = nil
|
|
new_build.trigger_request_id = nil
|
|
new_build.token = nil
|
|
new_build.save
|
|
end
|
|
|
|
def retry(build, user = nil)
|
|
new_build = Ci::Build.create(
|
|
ref: build.ref,
|
|
tag: build.tag,
|
|
options: build.options,
|
|
commands: build.commands,
|
|
tag_list: build.tag_list,
|
|
project: build.project,
|
|
pipeline: build.pipeline,
|
|
name: build.name,
|
|
allow_failure: build.allow_failure,
|
|
stage: build.stage,
|
|
stage_idx: build.stage_idx,
|
|
trigger_request: build.trigger_request,
|
|
yaml_variables: build.yaml_variables,
|
|
when: build.when,
|
|
user: user,
|
|
environment: build.environment,
|
|
status_event: 'enqueue'
|
|
)
|
|
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
|
|
build.pipeline.mark_as_processable_after_stage(build.stage_idx)
|
|
new_build
|
|
end
|
|
end
|
|
|
|
state_machine :status do
|
|
after_transition pending: :running do |build|
|
|
build.execute_hooks
|
|
end
|
|
|
|
after_transition any => [:success, :failed, :canceled] do |build|
|
|
build.update_coverage
|
|
build.execute_hooks
|
|
end
|
|
|
|
after_transition any => [:success] do |build|
|
|
if build.environment.present?
|
|
service = CreateDeploymentService.new(
|
|
build.project, build.user,
|
|
environment: build.environment,
|
|
sha: build.sha,
|
|
ref: build.ref,
|
|
tag: build.tag,
|
|
options: build.options.to_h[:environment],
|
|
variables: build.variables)
|
|
service.execute(build)
|
|
end
|
|
end
|
|
end
|
|
|
|
def manual?
|
|
self.when == 'manual'
|
|
end
|
|
|
|
def other_actions
|
|
pipeline.manual_actions.where.not(name: name)
|
|
end
|
|
|
|
def playable?
|
|
project.builds_enabled? && commands.present? && manual? && skipped?
|
|
end
|
|
|
|
def play(current_user = nil)
|
|
# Try to queue a current build
|
|
if self.enqueue
|
|
self.update(user: current_user)
|
|
self
|
|
else
|
|
# Otherwise we need to create a duplicate
|
|
Ci::Build.retry(self, current_user)
|
|
end
|
|
end
|
|
|
|
def retryable?
|
|
project.builds_enabled? && commands.present? && complete?
|
|
end
|
|
|
|
def retried?
|
|
!self.pipeline.statuses.latest.include?(self)
|
|
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
|
|
|
|
def trace_html
|
|
trace_with_state[:html] || ''
|
|
end
|
|
|
|
def trace_with_state(state = nil)
|
|
trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present?
|
|
trace_with_state || {}
|
|
end
|
|
|
|
def timeout
|
|
project.build_timeout
|
|
end
|
|
|
|
def variables
|
|
variables = predefined_variables
|
|
variables += project.predefined_variables
|
|
variables += pipeline.predefined_variables
|
|
variables += runner.predefined_variables if runner
|
|
variables += project.container_registry_variables
|
|
variables += yaml_variables
|
|
variables += user_variables
|
|
variables += project.secret_variables
|
|
variables += trigger_request.user_variables if trigger_request
|
|
variables
|
|
end
|
|
|
|
def merge_request
|
|
merge_requests = MergeRequest.includes(:merge_request_diff)
|
|
.where(source_branch: ref, source_project_id: pipeline.gl_project_id)
|
|
.reorder(iid: :asc)
|
|
|
|
merge_requests.find do |merge_request|
|
|
merge_request.commits.any? { |ci| ci.id == pipeline.sha }
|
|
end
|
|
end
|
|
|
|
def project_id
|
|
pipeline.project_id
|
|
end
|
|
|
|
def project_name
|
|
project.name
|
|
end
|
|
|
|
def repo_url
|
|
auth = "gitlab-ci-token:#{ensure_token!}@"
|
|
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
|
|
prefix + auth
|
|
end
|
|
end
|
|
|
|
def allow_git_fetch
|
|
project.build_allow_git_fetch
|
|
end
|
|
|
|
def update_coverage
|
|
return unless project
|
|
coverage_regex = project.build_coverage_regex
|
|
return unless coverage_regex
|
|
coverage = extract_coverage(trace, coverage_regex)
|
|
|
|
if coverage.is_a? Numeric
|
|
update_attributes(coverage: coverage)
|
|
end
|
|
end
|
|
|
|
def extract_coverage(text, regex)
|
|
begin
|
|
matches = text.scan(Regexp.new(regex)).last
|
|
matches = matches.last if matches.kind_of?(Array)
|
|
coverage = matches.gsub(/\d+(\.\d+)?/).first
|
|
|
|
if coverage.present?
|
|
coverage.to_f
|
|
end
|
|
rescue
|
|
# if bad regex or something goes wrong we dont want to interrupt transition
|
|
# so we just silentrly ignore error for now
|
|
end
|
|
end
|
|
|
|
def has_trace_file?
|
|
File.exist?(path_to_trace) || has_old_trace_file?
|
|
end
|
|
|
|
def has_trace?
|
|
raw_trace.present?
|
|
end
|
|
|
|
def raw_trace
|
|
if File.exist?(trace_file_path)
|
|
File.read(trace_file_path)
|
|
else
|
|
# backward compatibility
|
|
read_attribute :trace
|
|
end
|
|
end
|
|
|
|
##
|
|
# Deprecated
|
|
#
|
|
# This is a hotfix for CI build data integrity, see #4246
|
|
def has_old_trace_file?
|
|
project.ci_id && File.exist?(old_path_to_trace)
|
|
end
|
|
|
|
def trace
|
|
hide_secrets(raw_trace)
|
|
end
|
|
|
|
def trace_length
|
|
if raw_trace
|
|
raw_trace.bytesize
|
|
else
|
|
0
|
|
end
|
|
end
|
|
|
|
def trace=(trace)
|
|
recreate_trace_dir
|
|
trace = hide_secrets(trace)
|
|
File.write(path_to_trace, trace)
|
|
end
|
|
|
|
def recreate_trace_dir
|
|
unless Dir.exist?(dir_to_trace)
|
|
FileUtils.mkdir_p(dir_to_trace)
|
|
end
|
|
end
|
|
private :recreate_trace_dir
|
|
|
|
def append_trace(trace_part, offset)
|
|
recreate_trace_dir
|
|
|
|
trace_part = hide_secrets(trace_part)
|
|
|
|
File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
|
|
File.open(path_to_trace, 'ab') do |f|
|
|
f.write(trace_part)
|
|
end
|
|
end
|
|
|
|
def trace_file_path
|
|
if has_old_trace_file?
|
|
old_path_to_trace
|
|
else
|
|
path_to_trace
|
|
end
|
|
end
|
|
|
|
def dir_to_trace
|
|
File.join(
|
|
Settings.gitlab_ci.builds_path,
|
|
created_at.utc.strftime("%Y_%m"),
|
|
project.id.to_s
|
|
)
|
|
end
|
|
|
|
def path_to_trace
|
|
"#{dir_to_trace}/#{id}.log"
|
|
end
|
|
|
|
##
|
|
# Deprecated
|
|
#
|
|
# This is a hotfix for CI build data integrity, see #4246
|
|
# Should be removed in 8.4, after CI files migration has been done.
|
|
#
|
|
def old_dir_to_trace
|
|
File.join(
|
|
Settings.gitlab_ci.builds_path,
|
|
created_at.utc.strftime("%Y_%m"),
|
|
project.ci_id.to_s
|
|
)
|
|
end
|
|
|
|
##
|
|
# Deprecated
|
|
#
|
|
# This is a hotfix for CI build data integrity, see #4246
|
|
# Should be removed in 8.4, after CI files migration has been done.
|
|
#
|
|
def old_path_to_trace
|
|
"#{old_dir_to_trace}/#{id}.log"
|
|
end
|
|
|
|
##
|
|
# Deprecated
|
|
#
|
|
# This contains a hotfix for CI build data integrity, see #4246
|
|
#
|
|
# This method is used by `ArtifactUploader` to create a store_dir.
|
|
# Warning: Uploader uses it after AND before file has been stored.
|
|
#
|
|
# This method returns old path to artifacts only if it already exists.
|
|
#
|
|
def artifacts_path
|
|
old = File.join(created_at.utc.strftime('%Y_%m'),
|
|
project.ci_id.to_s,
|
|
id.to_s)
|
|
|
|
old_store = File.join(ArtifactUploader.artifacts_path, old)
|
|
return old if project.ci_id && File.directory?(old_store)
|
|
|
|
File.join(
|
|
created_at.utc.strftime('%Y_%m'),
|
|
project.id.to_s,
|
|
id.to_s
|
|
)
|
|
end
|
|
|
|
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, :build_hooks)
|
|
project.execute_services(build_data.dup, :build_hooks)
|
|
project.running_or_pending_build_count(force: true)
|
|
end
|
|
|
|
def artifacts?
|
|
!artifacts_expired? && artifacts_file.exists?
|
|
end
|
|
|
|
def artifacts_metadata?
|
|
artifacts? && artifacts_metadata.exists?
|
|
end
|
|
|
|
def artifacts_metadata_entry(path, **options)
|
|
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
|
|
artifacts_metadata.path,
|
|
path,
|
|
**options)
|
|
|
|
metadata.to_entry
|
|
end
|
|
|
|
def erase_artifacts!
|
|
remove_artifacts_file!
|
|
remove_artifacts_metadata!
|
|
save
|
|
end
|
|
|
|
def erase(opts = {})
|
|
return false unless erasable?
|
|
|
|
erase_artifacts!
|
|
erase_trace!
|
|
update_erased!(opts[:erased_by])
|
|
end
|
|
|
|
def erasable?
|
|
complete? && (artifacts? || has_trace?)
|
|
end
|
|
|
|
def erased?
|
|
!self.erased_at.nil?
|
|
end
|
|
|
|
def artifacts_expired?
|
|
artifacts_expire_at && artifacts_expire_at < Time.now
|
|
end
|
|
|
|
def artifacts_expire_in
|
|
artifacts_expire_at - Time.now if artifacts_expire_at
|
|
end
|
|
|
|
def artifacts_expire_in=(value)
|
|
self.artifacts_expire_at =
|
|
if value
|
|
Time.now + ChronicDuration.parse(value)
|
|
end
|
|
end
|
|
|
|
def keep_artifacts!
|
|
self.update(artifacts_expire_at: nil)
|
|
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
|
|
return [] if user.blank?
|
|
|
|
[
|
|
{ key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
|
|
{ key: 'GITLAB_USER_EMAIL', value: user.email, public: true }
|
|
]
|
|
end
|
|
|
|
private
|
|
|
|
def update_artifacts_size
|
|
self.artifacts_size = if artifacts_file.exists?
|
|
artifacts_file.size
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def erase_trace!
|
|
self.trace = nil
|
|
end
|
|
|
|
def update_erased!(user = nil)
|
|
self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
|
|
end
|
|
|
|
def predefined_variables
|
|
variables = [
|
|
{ key: 'CI', value: 'true', public: true },
|
|
{ key: 'GITLAB_CI', value: 'true', public: true },
|
|
{ key: 'CI_BUILD_ID', value: id.to_s, public: true },
|
|
{ key: 'CI_BUILD_TOKEN', value: token, public: false },
|
|
{ key: 'CI_BUILD_REF', value: sha, public: true },
|
|
{ key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true },
|
|
{ key: 'CI_BUILD_REF_NAME', value: ref, public: true },
|
|
{ key: 'CI_BUILD_NAME', value: name, public: true },
|
|
{ key: 'CI_BUILD_STAGE', value: stage, public: true },
|
|
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
|
|
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
|
|
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }
|
|
]
|
|
variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag?
|
|
variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request
|
|
variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if manual?
|
|
variables
|
|
end
|
|
|
|
def build_attributes_from_config
|
|
return {} unless pipeline.config_processor
|
|
|
|
pipeline.config_processor.build_attributes(name)
|
|
end
|
|
|
|
def hide_secrets(trace)
|
|
return unless trace
|
|
|
|
trace = trace.dup
|
|
Ci::MaskSecret.mask!(trace, project.runners_token) if project
|
|
Ci::MaskSecret.mask!(trace, token)
|
|
trace
|
|
end
|
|
end
|
|
end
|