summaryrefslogtreecommitdiff
path: root/app/models/ci
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/ci')
-rw-r--r--app/models/ci/application_setting.rb27
-rw-r--r--app/models/ci/build.rb285
-rw-r--r--app/models/ci/commit.rb267
-rw-r--r--app/models/ci/event.rb27
-rw-r--r--app/models/ci/project.rb225
-rw-r--r--app/models/ci/project_status.rb47
-rw-r--r--app/models/ci/runner.rb80
-rw-r--r--app/models/ci/runner_project.rb21
-rw-r--r--app/models/ci/service.rb105
-rw-r--r--app/models/ci/trigger.rb39
-rw-r--r--app/models/ci/trigger_request.rb23
-rw-r--r--app/models/ci/variable.rb25
-rw-r--r--app/models/ci/web_hook.rb44
13 files changed, 1215 insertions, 0 deletions
diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb
new file mode 100644
index 00000000000..0cf496f7d81
--- /dev/null
+++ b/app/models/ci/application_setting.rb
@@ -0,0 +1,27 @@
+# == Schema Information
+#
+# Table name: application_settings
+#
+# id :integer not null, primary key
+# all_broken_builds :boolean
+# add_pusher :boolean
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class ApplicationSetting < ActiveRecord::Base
+ extend Ci::Model
+
+ def self.current
+ Ci::ApplicationSetting.last
+ end
+
+ def self.create_from_defaults
+ create(
+ all_broken_builds: Settings.gitlab_ci['all_broken_builds'],
+ add_pusher: Settings.gitlab_ci['add_pusher'],
+ )
+ end
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
new file mode 100644
index 00000000000..8096d4fa5ae
--- /dev/null
+++ b/app/models/ci/build.rb
@@ -0,0 +1,285 @@
+# == Schema Information
+#
+# Table name: builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# commit_id :integer
+# coverage :float
+# commands :text
+# job_id :integer
+# name :string(255)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# deploy :boolean default(FALSE)
+# trigger_request_id :integer
+#
+
+module Ci
+ class Build < ActiveRecord::Base
+ extend Ci::Model
+
+ LAZY_ATTRIBUTES = ['trace']
+
+ belongs_to :commit, class_name: 'Ci::Commit'
+ belongs_to :project, class_name: 'Ci::Project'
+ belongs_to :runner, class_name: 'Ci::Runner'
+ belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
+
+ serialize :options
+
+ validates :commit, presence: true
+ validates :status, presence: true
+ validates :coverage, numericality: true, allow_blank: true
+
+ scope :running, ->() { where(status: "running") }
+ scope :pending, ->() { where(status: "pending") }
+ scope :success, ->() { where(status: "success") }
+ scope :failed, ->() { where(status: "failed") }
+ scope :unstarted, ->() { where(runner_id: nil) }
+ scope :running_or_pending, ->() { where(status:[:running, :pending]) }
+
+ acts_as_taggable
+
+ # To prevent db load megabytes of data from trace
+ default_scope -> { select(Ci::Build.columns_without_lazy) }
+
+ class << self
+ def columns_without_lazy
+ (column_names - LAZY_ATTRIBUTES).map do |column_name|
+ "#{table_name}.#{column_name}"
+ end
+ end
+
+ def last_month
+ where('created_at > ?', Date.today - 1.month)
+ end
+
+ 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.save
+ end
+
+ def retry(build)
+ new_build = Ci::Build.new(status: :pending)
+ new_build.options = build.options
+ new_build.commands = build.commands
+ new_build.tag_list = build.tag_list
+ new_build.commit_id = build.commit_id
+ new_build.project_id = build.project_id
+ new_build.name = build.name
+ new_build.allow_failure = build.allow_failure
+ new_build.stage = build.stage
+ new_build.trigger_request = build.trigger_request
+ new_build.save
+ new_build
+ end
+ end
+
+ state_machine :status, initial: :pending do
+ event :run do
+ transition pending: :running
+ end
+
+ event :drop do
+ transition running: :failed
+ end
+
+ event :success do
+ transition running: :success
+ end
+
+ event :cancel do
+ transition [:pending, :running] => :canceled
+ end
+
+ after_transition pending: :running do |build, transition|
+ build.update_attributes started_at: Time.now
+ end
+
+ after_transition any => [:success, :failed, :canceled] do |build, transition|
+ build.update_attributes finished_at: Time.now
+ project = build.project
+
+ if project.web_hooks?
+ Ci::WebHookService.new.build_end(build)
+ end
+
+ if build.commit.success?
+ build.commit.create_next_builds(build.trigger_request)
+ end
+
+ project.execute_services(build)
+
+ if project.coverage_enabled?
+ build.update_coverage
+ end
+ end
+
+ state :pending, value: 'pending'
+ state :running, value: 'running'
+ state :failed, value: 'failed'
+ state :success, value: 'success'
+ state :canceled, value: 'canceled'
+ end
+
+ delegate :sha, :short_sha, :before_sha, :ref,
+ to: :commit, prefix: false
+
+ def trace_html
+ html = Ci::Ansi2html::convert(trace) if trace.present?
+ html ||= ''
+ end
+
+ def trace
+ if project && read_attribute(:trace).present?
+ read_attribute(:trace).gsub(project.token, 'xxxxxx')
+ end
+ end
+
+ def started?
+ !pending? && !canceled? && started_at
+ end
+
+ def active?
+ running? || pending?
+ end
+
+ def complete?
+ canceled? || success? || failed?
+ end
+
+ def ignored?
+ failed? && allow_failure?
+ end
+
+ def timeout
+ project.timeout
+ end
+
+ def variables
+ yaml_variables + project_variables + trigger_variables
+ end
+
+ def duration
+ if started_at && finished_at
+ finished_at - started_at
+ elsif started_at
+ Time.now - started_at
+ end
+ end
+
+ def project
+ commit.project
+ end
+
+ def project_id
+ commit.project_id
+ end
+
+ def project_name
+ project.name
+ end
+
+ def repo_url
+ project.repo_url_with_auth
+ end
+
+ def allow_git_fetch
+ project.allow_git_fetch
+ end
+
+ def update_coverage
+ coverage = extract_coverage(trace, project.coverage_regex)
+
+ if coverage.is_a? Numeric
+ update_attributes(coverage: coverage)
+ end
+ end
+
+ def extract_coverage(text, regex)
+ begin
+ matches = text.gsub(Regexp.new(regex)).to_a.last
+ coverage = matches.gsub(/\d+(\.\d+)?/).first
+
+ if coverage.present?
+ coverage.to_f
+ end
+ rescue => ex
+ # if bad regex or something goes wrong we dont want to interrupt transition
+ # so we just silentrly ignore error for now
+ end
+ end
+
+ def trace
+ if File.exist?(path_to_trace)
+ File.read(path_to_trace)
+ else
+ # backward compatibility
+ read_attribute :trace
+ end
+ end
+
+ def trace=(trace)
+ unless Dir.exists? dir_to_trace
+ FileUtils.mkdir_p dir_to_trace
+ end
+
+ File.write(path_to_trace, trace)
+ 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
+
+ private
+
+ def yaml_variables
+ if commit.config_processor
+ commit.config_processor.variables.map do |key, value|
+ { key: key, value: value, public: true }
+ end
+ else
+ []
+ end
+ end
+
+ def project_variables
+ project.variables.map do |variable|
+ { key: variable.key, value: variable.value, public: false }
+ end
+ end
+
+ def trigger_variables
+ if trigger_request && trigger_request.variables
+ trigger_request.variables.map do |key, value|
+ { key: key, value: value, public: false }
+ end
+ else
+ []
+ end
+ end
+ end
+end
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
new file mode 100644
index 00000000000..23cd47dfe37
--- /dev/null
+++ b/app/models/ci/commit.rb
@@ -0,0 +1,267 @@
+# == Schema Information
+#
+# Table name: commits
+#
+# id :integer not null, primary key
+# project_id :integer
+# ref :string(255)
+# sha :string(255)
+# before_sha :string(255)
+# push_data :text
+# created_at :datetime
+# updated_at :datetime
+# tag :boolean default(FALSE)
+# yaml_errors :text
+# committed_at :datetime
+#
+
+module Ci
+ class Commit < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :project, class_name: 'Ci::Project'
+ has_many :builds, dependent: :destroy, class_name: 'Ci::Build'
+ has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
+
+ serialize :push_data
+
+ validates_presence_of :ref, :sha, :before_sha, :push_data
+ validate :valid_commit_sha
+
+ def self.truncate_sha(sha)
+ sha[0...8]
+ end
+
+ def to_param
+ sha
+ end
+
+ def last_build
+ builds.order(:id).last
+ end
+
+ def retry
+ builds_without_retry.each do |build|
+ Ci::Build.retry(build)
+ end
+ end
+
+ def valid_commit_sha
+ if self.sha == Ci::Git::BLANK_SHA
+ self.errors.add(:sha, " cant be 00000000 (branch removal)")
+ end
+ end
+
+ def new_branch?
+ before_sha == Ci::Git::BLANK_SHA
+ end
+
+ def compare?
+ !new_branch?
+ end
+
+ def git_author_name
+ commit_data[:author][:name] if commit_data && commit_data[:author]
+ end
+
+ def git_author_email
+ commit_data[:author][:email] if commit_data && commit_data[:author]
+ end
+
+ def git_commit_message
+ commit_data[:message] if commit_data && commit_data[:message]
+ end
+
+ def short_before_sha
+ Ci::Commit.truncate_sha(before_sha)
+ end
+
+ def short_sha
+ Ci::Commit.truncate_sha(sha)
+ end
+
+ def commit_data
+ push_data[:commits].find do |commit|
+ commit[:id] == sha
+ end
+ rescue
+ nil
+ end
+
+ def project_recipients
+ recipients = project.email_recipients.split(' ')
+
+ if project.email_add_pusher? && push_data[:user_email].present?
+ recipients << push_data[:user_email]
+ end
+
+ recipients.uniq
+ end
+
+ def stage
+ return unless config_processor
+ stages = builds_without_retry.select(&:active?).map(&:stage)
+ config_processor.stages.find { |stage| stages.include? stage }
+ end
+
+ def create_builds_for_stage(stage, trigger_request)
+ return if skip_ci? && trigger_request.blank?
+ return unless config_processor
+
+ builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag)
+ builds_attrs.map do |build_attrs|
+ builds.create!({
+ project: project,
+ name: build_attrs[:name],
+ commands: build_attrs[:script],
+ tag_list: build_attrs[:tags],
+ options: build_attrs[:options],
+ allow_failure: build_attrs[:allow_failure],
+ stage: build_attrs[:stage],
+ trigger_request: trigger_request,
+ })
+ end
+ end
+
+ def create_next_builds(trigger_request)
+ return if skip_ci? && trigger_request.blank?
+ return unless config_processor
+
+ stages = builds.where(trigger_request: trigger_request).group_by(&:stage)
+
+ config_processor.stages.any? do |stage|
+ !stages.include?(stage) && create_builds_for_stage(stage, trigger_request).present?
+ end
+ end
+
+ def create_builds(trigger_request = nil)
+ return if skip_ci? && trigger_request.blank?
+ return unless config_processor
+
+ config_processor.stages.any? do |stage|
+ create_builds_for_stage(stage, trigger_request).present?
+ end
+ end
+
+ def builds_without_retry
+ @builds_without_retry ||=
+ begin
+ grouped_builds = builds.group_by(&:name)
+ grouped_builds.map do |name, builds|
+ builds.sort_by(&:id).last
+ end
+ end
+ end
+
+ def builds_without_retry_sorted
+ return builds_without_retry unless config_processor
+
+ stages = config_processor.stages
+ builds_without_retry.sort_by do |build|
+ [stages.index(build.stage) || -1, build.name || ""]
+ end
+ end
+
+ def retried_builds
+ @retried_builds ||= (builds.order(id: :desc) - builds_without_retry)
+ end
+
+ def status
+ if skip_ci?
+ return 'skipped'
+ elsif yaml_errors.present?
+ return 'failed'
+ elsif builds.none?
+ return 'skipped'
+ elsif success?
+ 'success'
+ elsif pending?
+ 'pending'
+ elsif running?
+ 'running'
+ elsif canceled?
+ 'canceled'
+ else
+ 'failed'
+ end
+ end
+
+ def pending?
+ builds_without_retry.all? do |build|
+ build.pending?
+ end
+ end
+
+ def running?
+ builds_without_retry.any? do |build|
+ build.running? || build.pending?
+ end
+ end
+
+ def success?
+ builds_without_retry.all? do |build|
+ build.success? || build.ignored?
+ end
+ end
+
+ def failed?
+ status == 'failed'
+ end
+
+ def canceled?
+ builds_without_retry.all? do |build|
+ build.canceled?
+ end
+ end
+
+ def duration
+ @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i
+ end
+
+ def finished_at
+ @finished_at ||= builds.order('finished_at DESC').first.try(:finished_at)
+ end
+
+ def coverage
+ if project.coverage_enabled?
+ coverage_array = builds_without_retry.map(&:coverage).compact
+ if coverage_array.size >= 1
+ '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
+ end
+ end
+ end
+
+ def matrix?
+ builds_without_retry.size > 1
+ end
+
+ def config_processor
+ @config_processor ||= Ci::GitlabCiYamlProcessor.new(push_data[:ci_yaml_file] || project.generated_yaml_config)
+ rescue Ci::GitlabCiYamlProcessor::ValidationError => e
+ save_yaml_error(e.message)
+ nil
+ rescue Exception => e
+ logger.error e.message + "\n" + e.backtrace.join("\n")
+ save_yaml_error("Undefined yaml error")
+ nil
+ end
+
+ def skip_ci?
+ return false if builds.any?
+ commits = push_data[:commits]
+ commits.present? && commits.last[:message] =~ /(\[ci skip\])/
+ end
+
+ def update_committed!
+ update!(committed_at: DateTime.now)
+ end
+
+ private
+
+ def save_yaml_error(error)
+ return if self.yaml_errors?
+ self.yaml_errors = error
+ save
+ end
+ end
+end
diff --git a/app/models/ci/event.rb b/app/models/ci/event.rb
new file mode 100644
index 00000000000..cac3a7a49c1
--- /dev/null
+++ b/app/models/ci/event.rb
@@ -0,0 +1,27 @@
+# == Schema Information
+#
+# Table name: events
+#
+# id :integer not null, primary key
+# project_id :integer
+# user_id :integer
+# is_admin :integer
+# description :text
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class Event < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates :description,
+ presence: true,
+ length: { in: 5..200 }
+
+ scope :admin, ->(){ where(is_admin: true) }
+ scope :project_wide, ->(){ where(is_admin: false) }
+ end
+end
diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb
new file mode 100644
index 00000000000..2cf1783616f
--- /dev/null
+++ b/app/models/ci/project.rb
@@ -0,0 +1,225 @@
+# == Schema Information
+#
+# Table name: projects
+#
+# id :integer not null, primary key
+# name :string(255) not null
+# timeout :integer default(3600), not null
+# created_at :datetime
+# updated_at :datetime
+# token :string(255)
+# default_ref :string(255)
+# path :string(255)
+# always_build :boolean default(FALSE), not null
+# polling_interval :integer
+# public :boolean default(FALSE), not null
+# ssh_url_to_repo :string(255)
+# gitlab_id :integer
+# allow_git_fetch :boolean default(TRUE), not null
+# email_recipients :string(255) default(""), not null
+# email_add_pusher :boolean default(TRUE), not null
+# email_only_broken_builds :boolean default(TRUE), not null
+# skip_refs :string(255)
+# coverage_regex :string(255)
+# shared_runners_enabled :boolean default(FALSE)
+# generated_yaml_config :text
+#
+
+module Ci
+ class Project < ActiveRecord::Base
+ extend Ci::Model
+
+ include Ci::ProjectStatus
+
+ belongs_to :gl_project, class_name: '::Project', foreign_key: :gitlab_id
+
+ has_many :commits, ->() { order(:committed_at) }, dependent: :destroy, class_name: 'Ci::Commit'
+ has_many :builds, through: :commits, dependent: :destroy, class_name: 'Ci::Build'
+ has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
+ has_many :runners, through: :runner_projects, class_name: 'Ci::Runner'
+ has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook'
+ has_many :events, dependent: :destroy, class_name: 'Ci::Event'
+ has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
+ has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
+
+ # Project services
+ has_many :services, dependent: :destroy, class_name: 'Ci::Service'
+ has_one :hip_chat_service, dependent: :destroy, class_name: 'Ci::HipChatService'
+ has_one :slack_service, dependent: :destroy, class_name: 'Ci::SlackService'
+ has_one :mail_service, dependent: :destroy, class_name: 'Ci::MailService'
+
+ accepts_nested_attributes_for :variables, allow_destroy: true
+
+ #
+ # Validations
+ #
+ validates_presence_of :name, :timeout, :token, :default_ref,
+ :path, :ssh_url_to_repo, :gitlab_id
+
+ validates_uniqueness_of :gitlab_id
+
+ validates :polling_interval,
+ presence: true,
+ if: ->(project) { project.always_build.present? }
+
+ scope :public_only, ->() { where(public: true) }
+
+ before_validation :set_default_values
+
+ class << self
+ include Ci::CurrentSettings
+
+ def base_build_script
+ <<-eos
+ git submodule update --init
+ ls -la
+ eos
+ end
+
+ def parse(project)
+ params = {
+ name: project.name_with_namespace,
+ gitlab_id: project.id,
+ path: project.path_with_namespace,
+ default_ref: project.default_branch || 'master',
+ ssh_url_to_repo: project.ssh_url_to_repo,
+ email_add_pusher: current_application_settings.add_pusher,
+ email_only_broken_builds: current_application_settings.all_broken_builds,
+ }
+
+ project = Ci::Project.new(params)
+ project.build_missing_services
+ project
+ end
+
+ # TODO: remove
+ def from_gitlab(user, scope = :owned, options)
+ opts = user.authenticate_options
+ opts.merge! options
+
+ raise 'Implement me of fix'
+ #projects = Ci::Network.new.projects(opts.compact, scope)
+
+ if projects
+ projects.map { |pr| OpenStruct.new(pr) }
+ else
+ []
+ end
+ end
+
+ def already_added?(project)
+ where(gitlab_id: project.id).any?
+ end
+
+ def unassigned(runner)
+ joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \
+ "AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}").
+ where('#{Ci::RunnerProject.table_name}.project_id' => nil)
+ end
+
+ def ordered_by_last_commit_date
+ last_commit_subquery = "(SELECT project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY project_id)"
+ joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.id = last_commit.project_id").
+ order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC")
+ end
+
+ def search(query)
+ where("LOWER(#{Ci::Project.table_name}.name) LIKE :query",
+ query: "%#{query.try(:downcase)}%")
+ end
+ end
+
+ def any_runners?
+ if runners.active.any?
+ return true
+ end
+
+ shared_runners_enabled && Ci::Runner.shared.active.any?
+ end
+
+ def set_default_values
+ self.token = SecureRandom.hex(15) if self.token.blank?
+ end
+
+ def tracked_refs
+ @tracked_refs ||= default_ref.split(",").map{|ref| ref.strip}
+ end
+
+ def valid_token? token
+ self.token && self.token == token
+ end
+
+ def no_running_builds?
+ # Get running builds not later than 3 days ago to ignore hangs
+ builds.running.where("updated_at > ?", 3.days.ago).empty?
+ end
+
+ def email_notification?
+ email_add_pusher || email_recipients.present?
+ end
+
+ def web_hooks?
+ web_hooks.any?
+ end
+
+ def services?
+ services.any?
+ end
+
+ def timeout_in_minutes
+ timeout / 60
+ end
+
+ def timeout_in_minutes=(value)
+ self.timeout = value.to_i * 60
+ end
+
+ def coverage_enabled?
+ coverage_regex.present?
+ end
+
+ # Build a clone-able repo url
+ # using http and basic auth
+ def repo_url_with_auth
+ auth = "gitlab-ci-token:#{token}@"
+ url = gitlab_url + ".git"
+ url.sub(/^https?:\/\//) do |prefix|
+ prefix + auth
+ end
+ end
+
+ def available_services_names
+ %w(slack mail hip_chat)
+ end
+
+ def build_missing_services
+ available_services_names.each do |service_name|
+ service = services.find { |service| service.to_param == service_name }
+
+ # If service is available but missing in db
+ # we should create an instance. Ex `create_gitlab_ci_service`
+ service = self.send :"create_#{service_name}_service" if service.nil?
+ end
+ end
+
+ def execute_services(data)
+ services.each do |service|
+
+ # Call service hook only if it is active
+ begin
+ service.execute(data) if service.active && service.can_execute?(data)
+ rescue => e
+ logger.error(e)
+ end
+ end
+ end
+
+ def gitlab_url
+ File.join(Gitlab.config.gitlab.url, path)
+ end
+
+ def setup_finished?
+ commits.any?
+ end
+ end
+end
diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb
new file mode 100644
index 00000000000..6d5cafe81a2
--- /dev/null
+++ b/app/models/ci/project_status.rb
@@ -0,0 +1,47 @@
+module Ci
+ module ProjectStatus
+ def status
+ last_commit.status if last_commit
+ end
+
+ def broken?
+ last_commit.failed? if last_commit
+ end
+
+ def success?
+ last_commit.success? if last_commit
+ end
+
+ def broken_or_success?
+ broken? || success?
+ end
+
+ def last_commit
+ @last_commit ||= commits.last if commits.any?
+ end
+
+ def last_commit_date
+ last_commit.try(:created_at)
+ end
+
+ def human_status
+ status
+ end
+
+ # only check for toggling build status within same ref.
+ def last_commit_changed_status?
+ ref = last_commit.ref
+ last_commits = commits.where(ref: ref).last(2)
+
+ if last_commits.size < 2
+ false
+ else
+ last_commits[0].status != last_commits[1].status
+ end
+ end
+
+ def last_commit_for_ref(ref)
+ commits.where(ref: ref).last
+ end
+ end
+end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
new file mode 100644
index 00000000000..1e9f78a3748
--- /dev/null
+++ b/app/models/ci/runner.rb
@@ -0,0 +1,80 @@
+# == Schema Information
+#
+# Table name: runners
+#
+# id :integer not null, primary key
+# token :string(255)
+# created_at :datetime
+# updated_at :datetime
+# description :string(255)
+# contacted_at :datetime
+# active :boolean default(TRUE), not null
+# is_shared :boolean default(FALSE)
+# name :string(255)
+# version :string(255)
+# revision :string(255)
+# platform :string(255)
+# architecture :string(255)
+#
+
+module Ci
+ class Runner < ActiveRecord::Base
+ extend Ci::Model
+
+ has_many :builds, class_name: 'Ci::Build'
+ has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
+ has_many :projects, through: :runner_projects, class_name: 'Ci::Project'
+
+ has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
+
+ before_validation :set_default_values
+
+ scope :specific, ->() { where(is_shared: false) }
+ scope :shared, ->() { where(is_shared: true) }
+ scope :active, ->() { where(active: true) }
+ scope :paused, ->() { where(active: false) }
+
+ acts_as_taggable
+
+ def self.search(query)
+ where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query',
+ query: "%#{query.try(:downcase)}%")
+ end
+
+ def set_default_values
+ self.token = SecureRandom.hex(15) if self.token.blank?
+ end
+
+ def assign_to(project, current_user = nil)
+ self.is_shared = false if shared?
+ self.save
+ project.runner_projects.create!(runner_id: self.id)
+ end
+
+ def display_name
+ return token unless !description.blank?
+
+ description
+ end
+
+ def shared?
+ is_shared
+ end
+
+ def belongs_to_one_project?
+ runner_projects.count == 1
+ end
+
+ def specific?
+ !shared?
+ end
+
+ def only_for?(project)
+ projects == [project]
+ end
+
+ def short_sha
+ token[0...10]
+ end
+ end
+end
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
new file mode 100644
index 00000000000..44453ee4b41
--- /dev/null
+++ b/app/models/ci/runner_project.rb
@@ -0,0 +1,21 @@
+# == Schema Information
+#
+# Table name: runner_projects
+#
+# id :integer not null, primary key
+# runner_id :integer not null
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class RunnerProject < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :runner, class_name: 'Ci::Runner'
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates_uniqueness_of :runner_id, scope: :project_id
+ end
+end
diff --git a/app/models/ci/service.rb b/app/models/ci/service.rb
new file mode 100644
index 00000000000..ed5e3f940b6
--- /dev/null
+++ b/app/models/ci/service.rb
@@ -0,0 +1,105 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+#
+
+# To add new service you should build a class inherited from Service
+# and implement a set of methods
+module Ci
+ class Service < ActiveRecord::Base
+ extend Ci::Model
+
+ serialize :properties, JSON
+
+ default_value_for :active, false
+
+ after_initialize :initialize_properties
+
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates :project_id, presence: true
+
+ def activated?
+ active
+ end
+
+ def category
+ :common
+ end
+
+ def initialize_properties
+ self.properties = {} if properties.nil?
+ end
+
+ def title
+ # implement inside child
+ end
+
+ def description
+ # implement inside child
+ end
+
+ def help
+ # implement inside child
+ end
+
+ def to_param
+ # implement inside child
+ end
+
+ def fields
+ # implement inside child
+ []
+ end
+
+ def can_test?
+ project.builds.any?
+ end
+
+ def can_execute?(build)
+ true
+ end
+
+ def execute(build)
+ # implement inside child
+ end
+
+ # Provide convenient accessor methods
+ # for each serialized property.
+ def self.prop_accessor(*args)
+ args.each do |arg|
+ class_eval %{
+ def #{arg}
+ (properties || {})['#{arg}']
+ end
+
+ def #{arg}=(value)
+ self.properties ||= {}
+ self.properties['#{arg}'] = value
+ end
+ }
+ end
+ end
+
+ def self.boolean_accessor(*args)
+ self.prop_accessor(*args)
+
+ args.each do |arg|
+ class_eval %{
+ def #{arg}?
+ ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg})
+ end
+ }
+ end
+ end
+ end
+end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
new file mode 100644
index 00000000000..fe224b7dc70
--- /dev/null
+++ b/app/models/ci/trigger.rb
@@ -0,0 +1,39 @@
+# == Schema Information
+#
+# Table name: triggers
+#
+# id :integer not null, primary key
+# token :string(255)
+# project_id :integer not null
+# deleted_at :datetime
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class Trigger < ActiveRecord::Base
+ extend Ci::Model
+
+ acts_as_paranoid
+
+ belongs_to :project, class_name: 'Ci::Project'
+ has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
+
+ validates_presence_of :token
+ validates_uniqueness_of :token
+
+ before_validation :set_default_values
+
+ def set_default_values
+ self.token = SecureRandom.hex(15) if self.token.blank?
+ end
+
+ def last_trigger_request
+ trigger_requests.last
+ end
+
+ def short_token
+ token[0...10]
+ end
+ end
+end
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
new file mode 100644
index 00000000000..29cd9553394
--- /dev/null
+++ b/app/models/ci/trigger_request.rb
@@ -0,0 +1,23 @@
+# == Schema Information
+#
+# Table name: trigger_requests
+#
+# id :integer not null, primary key
+# trigger_id :integer not null
+# variables :text
+# created_at :datetime
+# updated_at :datetime
+# commit_id :integer
+#
+
+module Ci
+ class TriggerRequest < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :trigger, class_name: 'Ci::Trigger'
+ belongs_to :commit, class_name: 'Ci::Commit'
+ has_many :builds, class_name: 'Ci::Build'
+
+ serialize :variables
+ end
+end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
new file mode 100644
index 00000000000..7a542802fa6
--- /dev/null
+++ b/app/models/ci/variable.rb
@@ -0,0 +1,25 @@
+# == Schema Information
+#
+# Table name: variables
+#
+# id :integer not null, primary key
+# project_id :integer not null
+# key :string(255)
+# value :text
+# encrypted_value :text
+# encrypted_value_salt :string(255)
+# encrypted_value_iv :string(255)
+#
+
+module Ci
+ class Variable < ActiveRecord::Base
+ extend Ci::Model
+
+ belongs_to :project, class_name: 'Ci::Project'
+
+ validates_presence_of :key
+ validates_uniqueness_of :key, scope: :project_id
+
+ attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
+ end
+end
diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb
new file mode 100644
index 00000000000..8f03b0625da
--- /dev/null
+++ b/app/models/ci/web_hook.rb
@@ -0,0 +1,44 @@
+# == Schema Information
+#
+# Table name: web_hooks
+#
+# id :integer not null, primary key
+# url :string(255) not null
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+module Ci
+ class WebHook < ActiveRecord::Base
+ extend Ci::Model
+
+ include HTTParty
+
+ belongs_to :project, class_name: 'Ci::Project'
+
+ # HTTParty timeout
+ default_timeout 10
+
+ validates :url, presence: true,
+ format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }
+
+ def execute(data)
+ parsed_url = URI.parse(url)
+ if parsed_url.userinfo.blank?
+ Ci::WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false)
+ else
+ post_url = url.gsub("#{parsed_url.userinfo}@", "")
+ auth = {
+ username: URI.decode(parsed_url.user),
+ password: URI.decode(parsed_url.password),
+ }
+ Ci::WebHook.post(post_url,
+ body: data.to_json,
+ headers: { "Content-Type" => "application/json" },
+ verify: false,
+ basic_auth: auth)
+ end
+ end
+ end
+end