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

619 lines
18 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
include Presentable
2017-09-01 04:54:07 -04:00
include Importable
MissingDependenciesError = Class.new(StandardError)
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
# 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, ->() 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'))
end
scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
2016-06-08 11:18:54 -04:00
scope :with_expired_artifacts, ->() { with_artifacts.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) }
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
Ci::Build.retry(build, build.user)
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
2015-08-25 21:42:46 -04:00
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
2015-12-04 06:55:23 -05:00
project.build_timeout
2015-08-25 21:42:46 -04:00
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)
2016-07-20 07:17:21 -04:00
variables = predefined_variables
variables += project.predefined_variables
variables += pipeline.predefined_variables
variables += runner.predefined_variables if runner
variables += project.container_registry_variables
variables += project.deployment_variables if has_environment?
2017-09-06 12:57:07 -04:00
variables += project.auto_devops_variables
variables += yaml_variables
variables += user_variables
Basic BE change Fix static-snalysis Move the precedence of group secure variable before project secure variable. Allow project_id to be null. Separate Ci::VariableProject and Ci::VariableGroup Add the forgotton files Add migration file to update type of ci_variables Fix form_for fpr VariableProject Fix test Change the table structure according to the yorik advice Add necessary migration files. Remove unnecessary migration spec. Revert safe_model_attributes.yml Fix models Fix spec Avoid self.variable. Use becomes for correct routing. Use unique index on group_id and key Add null: false for t.timestamps Fix schema version Rename VariableProject and VariableGroup to ProjectVariable and GroupVariable Rename the rest of them Add the rest of files Basic BE change Fix static-snalysis Move the precedence of group secure variable before project secure variable. Allow project_id to be null. Separate Ci::VariableProject and Ci::VariableGroup Add the forgotton files Add migration file to update type of ci_variables Fix form_for fpr VariableProject Fix test Change the table structure according to the yorik advice Add necessary migration files. Remove unnecessary migration spec. Revert safe_model_attributes.yml Fix models Fix spec Avoid self.variable. Use becomes for correct routing. Use unique index on group_id and key Add null: false for t.timestamps Fix schema version Rename VariableProject and VariableGroup to ProjectVariable and GroupVariable Rename the rest of them Add the rest of files Implement CURD Rename codes related to VariableGroup and VariableProject FE part Remove unneccesary changes Make Fe code up-to-date Add protected flag to migration file Protected group variables essential package Update schema Improve doc Fix logic and spec for models Fix logic and spec for controllers Fix logic and spec for views(pre feature) Add feature spec Fixed bugs. placeholder. reveal button. doc. Add changelog Remove unnecessary comment godfat nice catches Improve secret_variables_for arctecture Fix spec Fix StaticAnlysys & path_regex spec Revert "Improve secret_variables_for arctecture" This reverts commit c3216ca212322ecf6ca534cb12ce75811a4e77f1. Use ayufan suggestion for secret_variables_for Use find instead of find_by Fix spec message for variable is invalid Fix spec remove variable.group_id = group.id godffat spec nitpicks Use include Gitlab::Routing.url_helpers for presenter spec
2017-05-03 14:51:55 -04:00
variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
variables += secret_variables(environment: environment)
2017-06-28 08:29:31 -04:00
variables += trigger_request.user_variables if trigger_request
variables += pipeline.variables.map(&:to_runner_variable)
2017-07-01 02:23:09 -04:00
variables += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule
variables += persisted_environment_variables if environment
2015-08-25 21:42:46 -04:00
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!}@"
2015-12-04 06:55:23 -05:00
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
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!
write_attribute(:trace, nil)
save
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 artifacts_metadata_entry(path, **options)
metadata = Gitlab::Ci::Build::Artifacts::Metadata.new(
artifacts_metadata.path,
path,
**options)
metadata.to_entry
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
return [] if user.blank?
[
{ key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
{ key: 'GITLAB_USER_EMAIL', value: user.email, public: true },
{ key: 'GITLAB_USER_LOGIN', value: user.username, public: true },
{ key: 'GITLAB_USER_NAME', value: user.name, public: true }
]
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
2016-07-20 07:17:21 -04:00
variables = [
{ key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', 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 },
{ key: 'CI_JOB_ID', value: id.to_s, public: true },
{ key: 'CI_JOB_NAME', value: name, public: true },
{ key: 'CI_JOB_STAGE', value: stage, public: true },
{ key: 'CI_JOB_TOKEN', value: token, public: false },
2017-03-07 11:37:05 -05:00
{ key: 'CI_COMMIT_SHA', value: sha, public: true },
{ key: 'CI_COMMIT_REF_NAME', value: ref, public: true },
{ key: 'CI_COMMIT_REF_SLUG', value: ref_slug, public: true },
{ key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER, public: true },
{ key: 'CI_REGISTRY_PASSWORD', value: token, public: false },
{ key: 'CI_REPOSITORY_URL', value: repo_url, public: false }
]
variables << { key: "CI_COMMIT_TAG", value: ref, public: true } if tag?
variables << { key: "CI_PIPELINE_TRIGGERED", value: 'true', public: true } if trigger_request
variables << { key: "CI_JOB_MANUAL", value: 'true', public: true } if action?
variables.concat(legacy_variables)
end
def persisted_environment_variables
return [] unless persisted_environment
2017-05-25 10:14:22 -04:00
variables = persisted_environment.predefined_variables
# 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 << { key: 'CI_ENVIRONMENT_URL', value: environment_url, public: true } if environment_url
2017-05-25 10:14:22 -04:00
variables
end
def legacy_variables
variables = [
2016-07-20 07:17:21 -04:00
{ 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 },
2016-12-13 14:21:30 -05:00
{ key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true },
2016-07-20 07:17:21 -04:00
{ key: 'CI_BUILD_NAME', value: name, public: true },
{ key: 'CI_BUILD_STAGE', value: stage, public: true }
2016-07-20 07:17:21 -04:00
]
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 action?
variables
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