summaryrefslogtreecommitdiff
path: root/lib/ci
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ci')
-rw-r--r--lib/ci/ansi2html.rb224
-rw-r--r--lib/ci/api/api.rb37
-rw-r--r--lib/ci/api/builds.rb53
-rw-r--r--lib/ci/api/commits.rb66
-rw-r--r--lib/ci/api/entities.rb44
-rw-r--r--lib/ci/api/forks.rb40
-rw-r--r--lib/ci/api/helpers.rb114
-rw-r--r--lib/ci/api/projects.rb209
-rw-r--r--lib/ci/api/runners.rb69
-rw-r--r--lib/ci/api/triggers.rb49
-rw-r--r--lib/ci/assets/.gitkeep0
-rw-r--r--lib/ci/backup/builds.rb32
-rw-r--r--lib/ci/backup/database.rb94
-rw-r--r--lib/ci/backup/manager.rb158
-rw-r--r--lib/ci/charts.rb71
-rw-r--r--lib/ci/current_settings.rb22
-rw-r--r--lib/ci/git.rb5
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb198
-rw-r--r--lib/ci/model.rb11
-rw-r--r--lib/ci/scheduler.rb16
-rw-r--r--lib/ci/static_model.rb49
-rw-r--r--lib/ci/version_info.rb52
22 files changed, 1613 insertions, 0 deletions
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
new file mode 100644
index 00000000000..ac6d667cf8d
--- /dev/null
+++ b/lib/ci/ansi2html.rb
@@ -0,0 +1,224 @@
+# ANSI color library
+#
+# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
+module Ci
+ module Ansi2html
+ # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
+ COLOR = {
+ 0 => 'black', # not that this is gray in the intense color table
+ 1 => 'red',
+ 2 => 'green',
+ 3 => 'yellow',
+ 4 => 'blue',
+ 5 => 'magenta',
+ 6 => 'cyan',
+ 7 => 'white', # not that this is gray in the dark (aka default) color table
+ }
+
+ STYLE_SWITCHES = {
+ bold: 0x01,
+ italic: 0x02,
+ underline: 0x04,
+ conceal: 0x08,
+ cross: 0x10,
+ }
+
+ def self.convert(ansi)
+ Converter.new().convert(ansi)
+ end
+
+ class Converter
+ def on_0(s) reset() end
+ def on_1(s) enable(STYLE_SWITCHES[:bold]) end
+ def on_3(s) enable(STYLE_SWITCHES[:italic]) end
+ def on_4(s) enable(STYLE_SWITCHES[:underline]) end
+ def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
+ def on_9(s) enable(STYLE_SWITCHES[:cross]) end
+
+ def on_21(s) disable(STYLE_SWITCHES[:bold]) end
+ def on_22(s) disable(STYLE_SWITCHES[:bold]) end
+ def on_23(s) disable(STYLE_SWITCHES[:italic]) end
+ def on_24(s) disable(STYLE_SWITCHES[:underline]) end
+ def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
+ def on_29(s) disable(STYLE_SWITCHES[:cross]) end
+
+ def on_30(s) set_fg_color(0) end
+ def on_31(s) set_fg_color(1) end
+ def on_32(s) set_fg_color(2) end
+ def on_33(s) set_fg_color(3) end
+ def on_34(s) set_fg_color(4) end
+ def on_35(s) set_fg_color(5) end
+ def on_36(s) set_fg_color(6) end
+ def on_37(s) set_fg_color(7) end
+ def on_38(s) set_fg_color_256(s) end
+ def on_39(s) set_fg_color(9) end
+
+ def on_40(s) set_bg_color(0) end
+ def on_41(s) set_bg_color(1) end
+ def on_42(s) set_bg_color(2) end
+ def on_43(s) set_bg_color(3) end
+ def on_44(s) set_bg_color(4) end
+ def on_45(s) set_bg_color(5) end
+ def on_46(s) set_bg_color(6) end
+ def on_47(s) set_bg_color(7) end
+ def on_48(s) set_bg_color_256(s) end
+ def on_49(s) set_bg_color(9) end
+
+ def on_90(s) set_fg_color(0, 'l') end
+ def on_91(s) set_fg_color(1, 'l') end
+ def on_92(s) set_fg_color(2, 'l') end
+ def on_93(s) set_fg_color(3, 'l') end
+ def on_94(s) set_fg_color(4, 'l') end
+ def on_95(s) set_fg_color(5, 'l') end
+ def on_96(s) set_fg_color(6, 'l') end
+ def on_97(s) set_fg_color(7, 'l') end
+ def on_99(s) set_fg_color(9, 'l') end
+
+ def on_100(s) set_bg_color(0, 'l') end
+ def on_101(s) set_bg_color(1, 'l') end
+ def on_102(s) set_bg_color(2, 'l') end
+ def on_103(s) set_bg_color(3, 'l') end
+ def on_104(s) set_bg_color(4, 'l') end
+ def on_105(s) set_bg_color(5, 'l') end
+ def on_106(s) set_bg_color(6, 'l') end
+ def on_107(s) set_bg_color(7, 'l') end
+ def on_109(s) set_bg_color(9, 'l') end
+
+ def convert(ansi)
+ @out = ""
+ @n_open_tags = 0
+ reset()
+
+ s = StringScanner.new(ansi.gsub("<", "&lt;"))
+ while(!s.eos?)
+ if s.scan(/\e([@-_])(.*?)([@-~])/)
+ handle_sequence(s)
+ else
+ @out << s.scan(/./m)
+ end
+ end
+
+ close_open_tags()
+ @out
+ end
+
+ def handle_sequence(s)
+ indicator = s[1]
+ commands = s[2].split ';'
+ terminator = s[3]
+
+ # We are only interested in color and text style changes - triggered by
+ # sequences starting with '\e[' and ending with 'm'. Any other control
+ # sequence gets stripped (including stuff like "delete last line")
+ return unless indicator == '[' and terminator == 'm'
+
+ close_open_tags()
+
+ if commands.empty?()
+ reset()
+ return
+ end
+
+ evaluate_command_stack(commands)
+
+ css_classes = []
+
+ unless @fg_color.nil?
+ fg_color = @fg_color
+ # Most terminals show bold colored text in the light color variant
+ # Let's mimic that here
+ if @style_mask & STYLE_SWITCHES[:bold] != 0
+ fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1')
+ end
+ css_classes << fg_color
+ end
+ css_classes << @bg_color unless @bg_color.nil?
+
+ STYLE_SWITCHES.each do |css_class, flag|
+ css_classes << "term-#{css_class}" if @style_mask & flag != 0
+ end
+
+ open_new_tag(css_classes) if css_classes.length > 0
+ end
+
+ def evaluate_command_stack(stack)
+ return unless command = stack.shift()
+
+ if self.respond_to?("on_#{command}", true)
+ self.send("on_#{command}", stack)
+ end
+
+ evaluate_command_stack(stack)
+ end
+
+ def open_new_tag(css_classes)
+ @out << %{<span class="#{css_classes.join(' ')}">}
+ @n_open_tags += 1
+ end
+
+ def close_open_tags
+ while @n_open_tags > 0
+ @out << %{</span>}
+ @n_open_tags -= 1
+ end
+ end
+
+ def reset
+ @fg_color = nil
+ @bg_color = nil
+ @style_mask = 0
+ end
+
+ def enable(flag)
+ @style_mask |= flag
+ end
+
+ def disable(flag)
+ @style_mask &= ~flag
+ end
+
+ def set_fg_color(color_index, prefix = nil)
+ @fg_color = get_term_color_class(color_index, ["fg", prefix])
+ end
+
+ def set_bg_color(color_index, prefix = nil)
+ @bg_color = get_term_color_class(color_index, ["bg", prefix])
+ end
+
+ def get_term_color_class(color_index, prefix)
+ color_name = COLOR[color_index]
+ return nil if color_name.nil?
+
+ get_color_class(["term", prefix, color_name])
+ end
+
+ def set_fg_color_256(command_stack)
+ css_class = get_xterm_color_class(command_stack, "fg")
+ @fg_color = css_class unless css_class.nil?
+ end
+
+ def set_bg_color_256(command_stack)
+ css_class = get_xterm_color_class(command_stack, "bg")
+ @bg_color = css_class unless css_class.nil?
+ end
+
+ def get_xterm_color_class(command_stack, prefix)
+ # the 38 and 48 commands have to be followed by "5" and the color index
+ return unless command_stack.length >= 2
+ return unless command_stack[0] == "5"
+
+ command_stack.shift() # ignore the "5" command
+ color_index = command_stack.shift().to_i
+
+ return unless color_index >= 0
+ return unless color_index <= 255
+
+ get_color_class(["xterm", prefix, color_index])
+ end
+
+ def get_color_class(segments)
+ [segments].flatten.compact.join('-')
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
new file mode 100644
index 00000000000..392fb548001
--- /dev/null
+++ b/lib/ci/api/api.rb
@@ -0,0 +1,37 @@
+Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file}
+
+module Ci
+ module API
+ class API < Grape::API
+ version 'v1', using: :path
+
+ rescue_from ActiveRecord::RecordNotFound do
+ rack_response({ 'message' => '404 Not found' }.to_json, 404)
+ end
+
+ rescue_from :all do |exception|
+ # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
+ # why is this not wrapped in something reusable?
+ trace = exception.backtrace
+
+ message = "\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << trace.join("\n ")
+
+ API.logger.add Logger::FATAL, message
+ rack_response({ 'message' => '500 Internal Server Error' }, 500)
+ end
+
+ format :json
+
+ helpers Helpers
+
+ mount Builds
+ mount Commits
+ mount Runners
+ mount Projects
+ mount Forks
+ mount Triggers
+ end
+ end
+end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
new file mode 100644
index 00000000000..83ca1e6481c
--- /dev/null
+++ b/lib/ci/api/builds.rb
@@ -0,0 +1,53 @@
+module Ci
+ module API
+ # Builds API
+ class Builds < Grape::API
+ resource :builds do
+ # Runs oldest pending build by runner - Runners only
+ #
+ # Parameters:
+ # token (required) - The uniq token of runner
+ #
+ # Example Request:
+ # POST /builds/register
+ post "register" do
+ authenticate_runner!
+ update_runner_last_contact
+ required_attributes! [:token]
+ not_found! unless current_runner.active?
+
+ build = Ci::RegisterBuildService.new.execute(current_runner)
+
+ if build
+ update_runner_info
+ present build, with: Entities::Build
+ else
+ not_found!
+ end
+ end
+
+ # Update an existing build - Runners only
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # state (optional) - The state of a build
+ # trace (optional) - The trace of a build
+ # Example Request:
+ # PUT /builds/:id
+ put ":id" do
+ authenticate_runner!
+ update_runner_last_contact
+ build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id])
+ build.update_attributes(trace: params[:trace]) if params[:trace]
+
+ case params[:state].to_s
+ when 'success'
+ build.success
+ when 'failed'
+ build.drop
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/commits.rb b/lib/ci/api/commits.rb
new file mode 100644
index 00000000000..bac463a5909
--- /dev/null
+++ b/lib/ci/api/commits.rb
@@ -0,0 +1,66 @@
+module Ci
+ module API
+ class Commits < Grape::API
+ resource :commits do
+ # Get list of commits per project
+ #
+ # Parameters:
+ # project_id (required) - The ID of a project
+ # project_token (requires) - Project token
+ # page (optional)
+ # per_page (optional) - items per request (default is 20)
+ #
+ get do
+ required_attributes! [:project_id, :project_token]
+ project = Ci::Project.find(params[:project_id])
+ authenticate_project_token!(project)
+
+ commits = project.commits.page(params[:page]).per(params[:per_page] || 20)
+ present commits, with: Entities::CommitWithBuilds
+ end
+
+ # Create a commit
+ #
+ # Parameters:
+ # project_id (required) - The ID of a project
+ # project_token (requires) - Project token
+ # data (required) - GitLab push data
+ #
+ # Sample GitLab push data:
+ # {
+ # "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
+ # "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ # "ref": "refs/heads/master",
+ # "commits": [
+ # {
+ # "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ # "message": "Update Catalan translation to e38cb41.",
+ # "timestamp": "2011-12-12T14:27:31+02:00",
+ # "url": "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ # "author": {
+ # "name": "Jordi Mallach",
+ # "email": "jordi@softcatala.org",
+ # }
+ # }, .... more commits
+ # ]
+ # }
+ #
+ # Example Request:
+ # POST /commits
+ post do
+ required_attributes! [:project_id, :data, :project_token]
+ project = Ci::Project.find(params[:project_id])
+ authenticate_project_token!(project)
+ commit = Ci::CreateCommitService.new.execute(project, params[:data])
+
+ if commit.persisted?
+ present commit, with: Entities::CommitWithBuilds
+ else
+ errors = commit.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
new file mode 100644
index 00000000000..2f0e9d36bc4
--- /dev/null
+++ b/lib/ci/api/entities.rb
@@ -0,0 +1,44 @@
+module Ci
+ module API
+ module Entities
+ class Commit < Grape::Entity
+ expose :id, :ref, :sha, :project_id, :before_sha, :created_at
+ expose :status, :finished_at, :duration
+ expose :git_commit_message, :git_author_name, :git_author_email
+ end
+
+ class CommitWithBuilds < Commit
+ expose :builds
+ end
+
+ class Build < Grape::Entity
+ expose :id, :commands, :path, :ref, :sha, :project_id, :repo_url,
+ :before_sha, :timeout, :allow_git_fetch, :project_name, :options
+
+ expose :variables
+ end
+
+ class Runner < Grape::Entity
+ expose :id, :token
+ end
+
+ class Project < Grape::Entity
+ expose :id, :name, :timeout, :token, :default_ref, :gitlab_url, :path,
+ :always_build, :polling_interval, :public, :ssh_url_to_repo, :gitlab_id
+ end
+
+ class RunnerProject < Grape::Entity
+ expose :id, :project_id, :runner_id
+ end
+
+ class WebHook < Grape::Entity
+ expose :id, :project_id, :url
+ end
+
+ class TriggerRequest < Grape::Entity
+ expose :id, :variables
+ expose :commit, using: Commit
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/forks.rb b/lib/ci/api/forks.rb
new file mode 100644
index 00000000000..4ce944df054
--- /dev/null
+++ b/lib/ci/api/forks.rb
@@ -0,0 +1,40 @@
+module Ci
+ module API
+ class Forks < Grape::API
+ resource :forks do
+ # Create a fork
+ #
+ # Parameters:
+ # project_id (required) - The ID of a project
+ # project_token (requires) - Project token
+ # private_token(required) - User private token
+ # data (required) - GitLab project data (name_with_namespace, web_url, default_branch, ssh_url_to_repo)
+ #
+ #
+ # Example Request:
+ # POST /forks
+ post do
+ required_attributes! [:project_id, :data, :project_token, :private_token]
+ project = Ci::Project.find_by!(gitlab_id: params[:project_id])
+ authenticate_project_token!(project)
+
+ user_session = Ci::UserSession.new
+ user = user_session.authenticate(private_token: params[:private_token])
+
+ fork = Ci::CreateProjectService.new.execute(
+ user,
+ params[:data],
+ Ci::RoutesHelper.ci_project_url(":project_id"),
+ project
+ )
+
+ if fork
+ present fork, with: Entities::Project
+ else
+ not_found!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
new file mode 100644
index 00000000000..3f58670fb49
--- /dev/null
+++ b/lib/ci/api/helpers.rb
@@ -0,0 +1,114 @@
+module Ci
+ module API
+ module Helpers
+ PRIVATE_TOKEN_PARAM = :private_token
+ PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
+ ACCESS_TOKEN_PARAM = :access_token
+ ACCESS_TOKEN_HEADER = "HTTP_ACCESS_TOKEN"
+ UPDATE_RUNNER_EVERY = 60
+
+ def current_user
+ @current_user ||= begin
+ options = {
+ access_token: (params[ACCESS_TOKEN_PARAM] || env[ACCESS_TOKEN_HEADER]),
+ private_token: (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]),
+ }
+ Ci::UserSession.new.authenticate(options.compact)
+ end
+ end
+
+ def current_runner
+ @runner ||= Ci::Runner.find_by_token(params[:token].to_s)
+ end
+
+ def authenticate!
+ forbidden! unless current_user
+ end
+
+ def authenticate_runners!
+ forbidden! unless params[:token] == GitlabCi::REGISTRATION_TOKEN
+ end
+
+ def authenticate_runner!
+ forbidden! unless current_runner
+ end
+
+ def authenticate_project_token!(project)
+ forbidden! unless project.valid_token?(params[:project_token])
+ end
+
+ def update_runner_last_contact
+ if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= UPDATE_RUNNER_EVERY
+ current_runner.update_attributes(contacted_at: Time.now)
+ end
+ end
+
+ def update_runner_info
+ return unless params["info"].present?
+ info = attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
+ current_runner.update(info)
+ end
+
+ # Checks the occurrences of required attributes, each attribute must be present in the params hash
+ # or a Bad Request error is invoked.
+ #
+ # Parameters:
+ # keys (required) - A hash consisting of keys that must be present
+ def required_attributes!(keys)
+ keys.each do |key|
+ bad_request!(key) unless params[key].present?
+ end
+ end
+
+ def attributes_for_keys(keys, custom_params = nil)
+ params_hash = custom_params || params
+ attrs = {}
+ keys.each do |key|
+ attrs[key] = params_hash[key] if params_hash[key].present?
+ end
+ attrs
+ end
+
+ # error helpers
+
+ def forbidden!
+ render_api_error!('403 Forbidden', 403)
+ end
+
+ def bad_request!(attribute)
+ message = ["400 (Bad request)"]
+ message << "\"" + attribute.to_s + "\" not given"
+ render_api_error!(message.join(' '), 400)
+ end
+
+ def not_found!(resource = nil)
+ message = ["404"]
+ message << resource if resource
+ message << "Not Found"
+ render_api_error!(message.join(' '), 404)
+ end
+
+ def unauthorized!
+ render_api_error!('401 Unauthorized', 401)
+ end
+
+ def not_allowed!
+ render_api_error!('Method Not Allowed', 405)
+ end
+
+ def render_api_error!(message, status)
+ error!({ 'message' => message }, status)
+ end
+
+ private
+
+ def abilities
+ @abilities ||= begin
+ abilities = Six.new
+ abilities << Ability
+ abilities
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/projects.rb b/lib/ci/api/projects.rb
new file mode 100644
index 00000000000..f9b4937c033
--- /dev/null
+++ b/lib/ci/api/projects.rb
@@ -0,0 +1,209 @@
+module Ci
+ module API
+ # Projects API
+ class Projects < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ # Register new webhook for project
+ #
+ # Parameters
+ # project_id (required) - The ID of a project
+ # web_hook (required) - WebHook URL
+ # Example Request
+ # POST /projects/:project_id/webhooks
+ post ":project_id/webhooks" do
+ required_attributes! [:web_hook]
+
+ project = Ci::Project.find(params[:project_id])
+
+ unauthorized! unless current_user.can_manage_project?(project.gitlab_id)
+
+ web_hook = project.web_hooks.new({ url: params[:web_hook] })
+
+ if web_hook.save
+ present web_hook, with: Entities::WebHook
+ else
+ errors = web_hook.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Retrieve all Gitlab CI projects that the user has access to
+ #
+ # Example Request:
+ # GET /projects
+ get do
+ gitlab_projects = Ci::Project.from_gitlab(
+ current_user, :authorized, { page: params[:page], per_page: params[:per_page], ci_enabled_first: true }
+ )
+ ids = gitlab_projects.map { |project| project.id }
+
+ projects = Ci::Project.where("gitlab_id IN (?)", ids).load
+ present projects, with: Entities::Project
+ end
+
+ # Retrieve all Gitlab CI projects that the user owns
+ #
+ # Example Request:
+ # GET /projects/owned
+ get "owned" do
+ gitlab_projects = Ci::Project.from_gitlab(
+ current_user, :owned, { page: params[:page], per_page: params[:per_page], ci_enabled_first: true }
+ )
+ ids = gitlab_projects.map { |project| project.id }
+
+ projects = Ci::Project.where("gitlab_id IN (?)", ids).load
+ present projects, with: Entities::Project
+ end
+
+ # Retrieve info for a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # GET /projects/:id
+ get ":id" do
+ project = Ci::Project.find(params[:id])
+
+ unauthorized! unless current_user.can_access_project?(project.gitlab_id)
+
+ present project, with: Entities::Project
+ end
+
+ # Create Gitlab CI project using Gitlab project info
+ #
+ # Parameters:
+ # name (required) - The name of the project
+ # gitlab_id (required) - The gitlab id of the project
+ # path (required) - The gitlab project path, ex. randx/six
+ # ssh_url_to_repo (required) - The gitlab ssh url to the repo
+ # default_ref - The branch to run against (defaults to `master`)
+ # Example Request:
+ # POST /projects
+ post do
+ required_attributes! [:name, :gitlab_id, :ssh_url_to_repo]
+
+ filtered_params = {
+ name: params[:name],
+ gitlab_id: params[:gitlab_id],
+ # we accept gitlab_url for backward compatibility for a while (added to 7.11)
+ path: params[:path] || params[:gitlab_url].sub(/.*\/(.*\/.*)$/, '\1'),
+ default_ref: params[:default_ref] || 'master',
+ ssh_url_to_repo: params[:ssh_url_to_repo]
+ }
+
+ project = Ci::Project.new(filtered_params)
+ project.build_missing_services
+
+ if project.save
+ present project, with: Entities::Project
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Update a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # name - The name of the project
+ # gitlab_id - The gitlab id of the project
+ # path - The gitlab project path, ex. randx/six
+ # ssh_url_to_repo - The gitlab ssh url to the repo
+ # default_ref - The branch to run against (defaults to `master`)
+ # Example Request:
+ # PUT /projects/:id
+ put ":id" do
+ project = Ci::Project.find(params[:id])
+
+ unauthorized! unless current_user.can_manage_project?(project.gitlab_id)
+
+ attrs = attributes_for_keys [:name, :gitlab_id, :path, :gitlab_url, :default_ref, :ssh_url_to_repo]
+
+ # we accept gitlab_url for backward compatibility for a while (added to 7.11)
+ if attrs[:gitlab_url] && !attrs[:path]
+ attrs[:path] = attrs[:gitlab_url].sub(/.*\/(.*\/.*)$/, '\1')
+ end
+
+ if project.update_attributes(attrs)
+ present project, with: Entities::Project
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Remove a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # DELETE /projects/:id
+ delete ":id" do
+ project = Ci::Project.find(params[:id])
+
+ unauthorized! unless current_user.can_manage_project?(project.gitlab_id)
+
+ project.destroy
+ end
+
+ # Link a Gitlab CI project to a runner
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # runner_id (required) - The ID of a runner
+ # Example Request:
+ # POST /projects/:id/runners/:runner_id
+ post ":id/runners/:runner_id" do
+ project = Ci::Project.find(params[:id])
+ runner = Ci::Runner.find(params[:runner_id])
+
+ unauthorized! unless current_user.can_manage_project?(project.gitlab_id)
+
+ options = {
+ project_id: project.id,
+ runner_id: runner.id
+ }
+
+ runner_project = Ci::RunnerProject.new(options)
+
+ if runner_project.save
+ present runner_project, with: Entities::RunnerProject
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Remove a Gitlab CI project from a runner
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # runner_id (required) - The ID of a runner
+ # Example Request:
+ # DELETE /projects/:id/runners/:runner_id
+ delete ":id/runners/:runner_id" do
+ project = Ci::Project.find(params[:id])
+ runner = Ci::Runner.find(params[:runner_id])
+
+ unauthorized! unless current_user.can_manage_project?(project.gitlab_id)
+
+ options = {
+ project_id: project.id,
+ runner_id: runner.id
+ }
+
+ runner_project = Ci::RunnerProject.find_by(options)
+
+ if runner_project.present?
+ runner_project.destroy
+ else
+ not_found!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
new file mode 100644
index 00000000000..1466fe4356e
--- /dev/null
+++ b/lib/ci/api/runners.rb
@@ -0,0 +1,69 @@
+module Ci
+ module API
+ # Runners API
+ class Runners < Grape::API
+ resource :runners do
+ # Get list of all available runners
+ #
+ # Example Request:
+ # GET /runners
+ get do
+ authenticate!
+ runners = Ci::Runner.all
+
+ present runners, with: Entities::Runner
+ end
+
+ # Delete runner
+ # Parameters:
+ # token (required) - The unique token of runner
+ #
+ # Example Request:
+ # GET /runners/delete
+ delete "delete" do
+ required_attributes! [:token]
+ authenticate_runner!
+ Ci::Runner.find_by_token(params[:token]).destroy
+ end
+
+ # Register a new runner
+ #
+ # Note: This is an "internal" API called when setting up
+ # runners, so it is authenticated differently.
+ #
+ # Parameters:
+ # token (required) - The unique token of runner
+ #
+ # Example Request:
+ # POST /runners/register
+ post "register" do
+ required_attributes! [:token]
+
+ runner =
+ if params[:token] == GitlabCi::REGISTRATION_TOKEN
+ # Create shared runner. Requires admin access
+ Ci::Runner.create(
+ description: params[:description],
+ tag_list: params[:tag_list],
+ is_shared: true
+ )
+ elsif project = Ci::Project.find_by(token: params[:token])
+ # Create a specific runner for project.
+ project.runners.create(
+ description: params[:description],
+ tag_list: params[:tag_list]
+ )
+ end
+
+ return forbidden! unless runner
+
+ if runner.id
+ present runner, with: Entities::Runner
+ else
+ not_found!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb
new file mode 100644
index 00000000000..40907d6db54
--- /dev/null
+++ b/lib/ci/api/triggers.rb
@@ -0,0 +1,49 @@
+module Ci
+ module API
+ # Build Trigger API
+ class Triggers < Grape::API
+ resource :projects do
+ # Trigger a GitLab CI project build
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # ref (required) - The name of project's branch or tag
+ # token (required) - The uniq token of trigger
+ # Example Request:
+ # POST /projects/:id/ref/:ref/trigger
+ post ":id/refs/:ref/trigger" do
+ required_attributes! [:token]
+
+ project = Ci::Project.find(params[:id])
+ trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ not_found! unless project && trigger
+ unauthorized! unless trigger.project == project
+
+ # validate variables
+ variables = params[:variables]
+ if variables
+ unless variables.is_a?(Hash)
+ render_api_error!('variables needs to be a hash', 400)
+ end
+
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
+ end
+
+ # convert variables from Mash to Hash
+ variables = variables.to_h
+ end
+
+ # create request and trigger builds
+ trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
+ if trigger_request
+ present trigger_request, with: Entities::TriggerRequest
+ else
+ errors = 'No builds created'
+ render_api_error!(errors, 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/assets/.gitkeep b/lib/ci/assets/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/lib/ci/assets/.gitkeep
diff --git a/lib/ci/backup/builds.rb b/lib/ci/backup/builds.rb
new file mode 100644
index 00000000000..832a5ab8fdc
--- /dev/null
+++ b/lib/ci/backup/builds.rb
@@ -0,0 +1,32 @@
+module Ci
+ module Backup
+ class Builds
+ attr_reader :app_builds_dir, :backup_builds_dir, :backup_dir
+
+ def initialize
+ @app_builds_dir = File.realpath(Rails.root.join('ci/builds'))
+ @backup_dir = GitlabCi.config.backup.path
+ @backup_builds_dir = File.join(GitlabCi.config.backup.path, 'ci/builds')
+ end
+
+ # Copy builds from builds directory to backup/builds
+ def dump
+ FileUtils.mkdir_p(backup_builds_dir)
+ FileUtils.cp_r(app_builds_dir, backup_dir)
+ end
+
+ def restore
+ backup_existing_builds_dir
+
+ FileUtils.cp_r(backup_builds_dir, app_builds_dir)
+ end
+
+ def backup_existing_builds_dir
+ timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}")
+ if File.exists?(app_builds_dir)
+ FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/backup/database.rb b/lib/ci/backup/database.rb
new file mode 100644
index 00000000000..f7fa3f1833a
--- /dev/null
+++ b/lib/ci/backup/database.rb
@@ -0,0 +1,94 @@
+require 'yaml'
+
+module Ci
+ module Backup
+ class Database
+ attr_reader :config, :db_dir
+
+ def initialize
+ @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env]
+ @db_dir = File.join(GitlabCi.config.backup.path, 'db')
+ FileUtils.mkdir_p(@db_dir) unless Dir.exists?(@db_dir)
+ end
+
+ def dump
+ success = case config["adapter"]
+ when /^mysql/ then
+ $progress.print "Dumping MySQL database #{config['database']} ... "
+ system('mysqldump', *mysql_args, config['database'], out: db_file_name)
+ when "postgresql" then
+ $progress.print "Dumping PostgreSQL database #{config['database']} ... "
+ pg_env
+ system('pg_dump', config['database'], out: db_file_name)
+ end
+ report_success(success)
+ abort 'Backup failed' unless success
+ end
+
+ def restore
+ success = case config["adapter"]
+ when /^mysql/ then
+ $progress.print "Restoring MySQL database #{config['database']} ... "
+ system('mysql', *mysql_args, config['database'], in: db_file_name)
+ when "postgresql" then
+ $progress.print "Restoring PostgreSQL database #{config['database']} ... "
+ # Drop all tables because PostgreSQL DB dumps do not contain DROP TABLE
+ # statements like MySQL.
+ drop_all_tables
+ drop_all_postgres_sequences
+ pg_env
+ system('psql', config['database'], '-f', db_file_name)
+ end
+ report_success(success)
+ abort 'Restore failed' unless success
+ end
+
+ protected
+
+ def db_file_name
+ File.join(db_dir, 'database.sql')
+ end
+
+ def mysql_args
+ args = {
+ 'host' => '--host',
+ 'port' => '--port',
+ 'socket' => '--socket',
+ 'username' => '--user',
+ 'encoding' => '--default-character-set',
+ 'password' => '--password'
+ }
+ args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact
+ end
+
+ def pg_env
+ ENV['PGUSER'] = config["username"] if config["username"]
+ ENV['PGHOST'] = config["host"] if config["host"]
+ ENV['PGPORT'] = config["port"].to_s if config["port"]
+ ENV['PGPASSWORD'] = config["password"].to_s if config["password"]
+ end
+
+ def report_success(success)
+ if success
+ $progress.puts '[DONE]'.green
+ else
+ $progress.puts '[FAILED]'.red
+ end
+ end
+
+ def drop_all_tables
+ connection = ActiveRecord::Base.connection
+ connection.tables.each do |table|
+ connection.drop_table(table)
+ end
+ end
+
+ def drop_all_postgres_sequences
+ connection = ActiveRecord::Base.connection
+ connection.execute("SELECT c.relname FROM pg_class c WHERE c.relkind = 'S';").each do |sequence|
+ connection.execute("DROP SEQUENCE #{sequence['relname']}")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/backup/manager.rb b/lib/ci/backup/manager.rb
new file mode 100644
index 00000000000..2e9d6df7139
--- /dev/null
+++ b/lib/ci/backup/manager.rb
@@ -0,0 +1,158 @@
+module Ci
+ module Backup
+ class Manager
+ def pack
+ # saving additional informations
+ s = {}
+ s[:db_version] = "#{ActiveRecord::Migrator.current_version}"
+ s[:backup_created_at] = Time.now
+ s[:gitlab_version] = GitlabCi::VERSION
+ s[:tar_version] = tar_version
+ tar_file = "#{s[:backup_created_at].to_i}_gitlab_ci_backup.tar.gz"
+
+ Dir.chdir(GitlabCi.config.backup.path) do
+ File.open("#{GitlabCi.config.backup.path}/backup_information.yml",
+ "w+") do |file|
+ file << s.to_yaml.gsub(/^---\n/,'')
+ end
+
+ FileUtils.chmod(0700, ["db", "builds"])
+
+ # create archive
+ $progress.print "Creating backup archive: #{tar_file} ... "
+ orig_umask = File.umask(0077)
+ if Kernel.system('tar', '-czf', tar_file, *backup_contents)
+ $progress.puts "done".green
+ else
+ puts "creating archive #{tar_file} failed".red
+ abort 'Backup failed'
+ end
+ File.umask(orig_umask)
+
+ upload(tar_file)
+ end
+ end
+
+ def upload(tar_file)
+ remote_directory = GitlabCi.config.backup.upload.remote_directory
+ $progress.print "Uploading backup archive to remote storage #{remote_directory} ... "
+
+ connection_settings = GitlabCi.config.backup.upload.connection
+ if connection_settings.blank?
+ $progress.puts "skipped".yellow
+ return
+ end
+
+ connection = ::Fog::Storage.new(connection_settings)
+ directory = connection.directories.get(remote_directory)
+
+ if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
+ multipart_chunk_size: GitlabCi.config.backup.upload.multipart_chunk_size)
+ $progress.puts "done".green
+ else
+ puts "uploading backup to #{remote_directory} failed".red
+ abort 'Backup failed'
+ end
+ end
+
+ def cleanup
+ $progress.print "Deleting tmp directories ... "
+
+ backup_contents.each do |dir|
+ next unless File.exist?(File.join(GitlabCi.config.backup.path, dir))
+
+ if FileUtils.rm_rf(File.join(GitlabCi.config.backup.path, dir))
+ $progress.puts "done".green
+ else
+ puts "deleting tmp directory '#{dir}' failed".red
+ abort 'Backup failed'
+ end
+ end
+ end
+
+ def remove_old
+ # delete backups
+ $progress.print "Deleting old backups ... "
+ keep_time = GitlabCi.config.backup.keep_time.to_i
+
+ if keep_time > 0
+ removed = 0
+
+ Dir.chdir(GitlabCi.config.backup.path) do
+ file_list = Dir.glob('*_gitlab_ci_backup.tar.gz')
+ file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_ci_backup.tar.gz/ }
+ file_list.sort.each do |timestamp|
+ if Time.at(timestamp) < (Time.now - keep_time)
+ if Kernel.system(*%W(rm #{timestamp}_gitlab_ci_backup.tar.gz))
+ removed += 1
+ end
+ end
+ end
+ end
+
+ $progress.puts "done. (#{removed} removed)".green
+ else
+ $progress.puts "skipping".yellow
+ end
+ end
+
+ def unpack
+ Dir.chdir(GitlabCi.config.backup.path)
+
+ # check for existing backups in the backup dir
+ file_list = Dir.glob("*_gitlab_ci_backup.tar.gz").each.map { |f| f.split(/_/).first.to_i }
+ puts "no backups found" if file_list.count == 0
+
+ if file_list.count > 1 && ENV["BACKUP"].nil?
+ puts "Found more than one backup, please specify which one you want to restore:"
+ puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup"
+ exit 1
+ end
+
+ tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_ci_backup.tar.gz") : File.join(ENV["BACKUP"] + "_gitlab_ci_backup.tar.gz")
+
+ unless File.exists?(tar_file)
+ puts "The specified backup doesn't exist!"
+ exit 1
+ end
+
+ $progress.print "Unpacking backup ... "
+
+ unless Kernel.system(*%W(tar -xzf #{tar_file}))
+ puts "unpacking backup failed".red
+ exit 1
+ else
+ $progress.puts "done".green
+ end
+
+ ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
+
+ # restoring mismatching backups can lead to unexpected problems
+ if settings[:gitlab_version] != GitlabCi::VERSION
+ puts "GitLab CI version mismatch:".red
+ puts " Your current GitLab CI version (#{GitlabCi::VERSION}) differs from the GitLab CI version in the backup!".red
+ puts " Please switch to the following version and try again:".red
+ puts " version: #{settings[:gitlab_version]}".red
+ puts
+ puts "Hint: git checkout v#{settings[:gitlab_version]}"
+ exit 1
+ end
+ end
+
+ def tar_version
+ tar_version = `tar --version`
+ tar_version.force_encoding('locale').split("\n").first
+ end
+
+ private
+
+ def backup_contents
+ ["db", "builds", "backup_information.yml"]
+ end
+
+ def settings
+ @settings ||= YAML.load_file("backup_information.yml")
+ end
+ end
+ end
+end
diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb
new file mode 100644
index 00000000000..e50a7a59c27
--- /dev/null
+++ b/lib/ci/charts.rb
@@ -0,0 +1,71 @@
+module Ci
+ module Charts
+ class Chart
+ attr_reader :labels, :total, :success, :project, :build_times
+
+ def initialize(project)
+ @labels = []
+ @total = []
+ @success = []
+ @build_times = []
+ @project = project
+
+ collect
+ end
+
+
+ def push(from, to, format)
+ @labels << from.strftime(format)
+ @total << project.builds.
+ where('? > builds.created_at AND builds.created_at > ?', to, from).
+ count(:all)
+ @success << project.builds.
+ where('? > builds.created_at AND builds.created_at > ?', to, from).
+ success.count(:all)
+ end
+ end
+
+ class YearChart < Chart
+ def collect
+ 13.times do |i|
+ start_month = (Date.today.years_ago(1) + i.month).beginning_of_month
+ end_month = start_month.end_of_month
+
+ push(start_month, end_month, "%d %B %Y")
+ end
+ end
+ end
+
+ class MonthChart < Chart
+ def collect
+ 30.times do |i|
+ start_day = Date.today - 30.days + i.days
+ end_day = Date.today - 30.days + i.day + 1.day
+
+ push(start_day, end_day, "%d %B")
+ end
+ end
+ end
+
+ class WeekChart < Chart
+ def collect
+ 7.times do |i|
+ start_day = Date.today - 7.days + i.days
+ end_day = Date.today - 7.days + i.day + 1.day
+
+ push(start_day, end_day, "%d %B")
+ end
+ end
+ end
+
+ class BuildTime < Chart
+ def collect
+ commits = project.commits.joins(:builds).where('builds.finished_at is NOT NULL AND builds.started_at is NOT NULL').last(30)
+ commits.each do |commit|
+ @labels << commit.short_sha
+ @build_times << (commit.duration / 60)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/current_settings.rb b/lib/ci/current_settings.rb
new file mode 100644
index 00000000000..fd78b024970
--- /dev/null
+++ b/lib/ci/current_settings.rb
@@ -0,0 +1,22 @@
+module Ci
+ module CurrentSettings
+ def current_application_settings
+ key = :ci_current_application_settings
+
+ RequestStore.store[key] ||= begin
+ if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('ci_application_settings')
+ Ci::ApplicationSetting.current || Ci::ApplicationSetting.create_from_defaults
+ else
+ fake_application_settings
+ end
+ end
+ end
+
+ def fake_application_settings
+ OpenStruct.new(
+ all_broken_builds: Ci::Settings.gitlab_ci['all_broken_builds'],
+ add_pusher: Ci::Settings.gitlab_ci['add_pusher'],
+ )
+ end
+ end
+end
diff --git a/lib/ci/git.rb b/lib/ci/git.rb
new file mode 100644
index 00000000000..7acc3f38edb
--- /dev/null
+++ b/lib/ci/git.rb
@@ -0,0 +1,5 @@
+module Ci
+ module Git
+ BLANK_SHA = '0' * 40
+ end
+end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
new file mode 100644
index 00000000000..e625e790df8
--- /dev/null
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -0,0 +1,198 @@
+module Ci
+ class GitlabCiYamlProcessor
+ class ValidationError < StandardError;end
+
+ DEFAULT_STAGES = %w(build test deploy)
+ DEFAULT_STAGE = 'test'
+ ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables]
+ ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage]
+
+ attr_reader :before_script, :image, :services, :variables
+
+ def initialize(config)
+ @config = YAML.load(config)
+
+ unless @config.is_a? Hash
+ raise ValidationError, "YAML should be a hash"
+ end
+
+ @config = @config.deep_symbolize_keys
+
+ initial_parsing
+
+ validate!
+ end
+
+ def builds_for_stage_and_ref(stage, ref, tag = false)
+ builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag)}
+ end
+
+ def builds
+ @jobs.map do |name, job|
+ build_job(name, job)
+ end
+ end
+
+ def stages
+ @stages || DEFAULT_STAGES
+ end
+
+ private
+
+ def initial_parsing
+ @before_script = @config[:before_script] || []
+ @image = @config[:image]
+ @services = @config[:services]
+ @stages = @config[:stages] || @config[:types]
+ @variables = @config[:variables] || {}
+ @config.except!(*ALLOWED_YAML_KEYS)
+
+ # anything that doesn't have script is considered as unknown
+ @config.each do |name, param|
+ raise ValidationError, "Unknown parameter: #{name}" unless param.is_a?(Hash) && param.has_key?(:script)
+ end
+
+ unless @config.values.any?{|job| job.is_a?(Hash)}
+ raise ValidationError, "Please define at least one job"
+ end
+
+ @jobs = {}
+ @config.each do |key, job|
+ stage = job[:stage] || job[:type] || DEFAULT_STAGE
+ @jobs[key] = { stage: stage }.merge(job)
+ end
+ end
+
+ def process?(only_params, except_params, ref, tag)
+ return true if only_params.nil? && except_params.nil?
+
+ if only_params
+ return true if tag && only_params.include?("tags")
+ return true if !tag && only_params.include?("branches")
+
+ only_params.find do |pattern|
+ match_ref?(pattern, ref)
+ end
+ else
+ return false if tag && except_params.include?("tags")
+ return false if !tag && except_params.include?("branches")
+
+ except_params.each do |pattern|
+ return false if match_ref?(pattern, ref)
+ end
+ end
+ end
+
+ def build_job(name, job)
+ {
+ stage: job[:stage],
+ script: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}",
+ tags: job[:tags] || [],
+ name: name,
+ only: job[:only],
+ except: job[:except],
+ allow_failure: job[:allow_failure] || false,
+ options: {
+ image: job[:image] || @image,
+ services: job[:services] || @services
+ }.compact
+ }
+ end
+
+ def match_ref?(pattern, ref)
+ if pattern.first == "/" && pattern.last == "/"
+ Regexp.new(pattern[1...-1]) =~ ref
+ else
+ pattern == ref
+ end
+ end
+
+ def normalize_script(script)
+ if script.is_a? Array
+ script.join("\n")
+ else
+ script
+ end
+ end
+
+ def validate!
+ unless validate_array_of_strings(@before_script)
+ raise ValidationError, "before_script should be an array of strings"
+ end
+
+ unless @image.nil? || @image.is_a?(String)
+ raise ValidationError, "image should be a string"
+ end
+
+ unless @services.nil? || validate_array_of_strings(@services)
+ raise ValidationError, "services should be an array of strings"
+ end
+
+ unless @stages.nil? || validate_array_of_strings(@stages)
+ raise ValidationError, "stages should be an array of strings"
+ end
+
+ unless @variables.nil? || validate_variables(@variables)
+ raise ValidationError, "variables should be a map of key-valued strings"
+ end
+
+ @jobs.each do |name, job|
+ validate_job!("#{name} job", job)
+ end
+
+ true
+ end
+
+ def validate_job!(name, job)
+ job.keys.each do |key|
+ unless ALLOWED_JOB_KEYS.include? key
+ raise ValidationError, "#{name}: unknown parameter #{key}"
+ end
+ end
+
+ if !job[:script].is_a?(String) && !validate_array_of_strings(job[:script])
+ raise ValidationError, "#{name}: script should be a string or an array of a strings"
+ end
+
+ if job[:stage]
+ unless job[:stage].is_a?(String) && job[:stage].in?(stages)
+ raise ValidationError, "#{name}: stage parameter should be #{stages.join(", ")}"
+ end
+ end
+
+ if job[:image] && !job[:image].is_a?(String)
+ raise ValidationError, "#{name}: image should be a string"
+ end
+
+ if job[:services] && !validate_array_of_strings(job[:services])
+ raise ValidationError, "#{name}: services should be an array of strings"
+ end
+
+ if job[:tags] && !validate_array_of_strings(job[:tags])
+ raise ValidationError, "#{name}: tags parameter should be an array of strings"
+ end
+
+ if job[:only] && !validate_array_of_strings(job[:only])
+ raise ValidationError, "#{name}: only parameter should be an array of strings"
+ end
+
+ if job[:except] && !validate_array_of_strings(job[:except])
+ raise ValidationError, "#{name}: except parameter should be an array of strings"
+ end
+
+ if job[:allow_failure] && !job[:allow_failure].in?([true, false])
+ raise ValidationError, "#{name}: allow_failure parameter should be an boolean"
+ end
+ end
+
+ private
+
+ def validate_array_of_strings(values)
+ values.is_a?(Array) && values.all? {|tag| tag.is_a?(String)}
+ end
+
+ def validate_variables(variables)
+ variables.is_a?(Hash) && variables.all? {|key, value| key.is_a?(Symbol) && value.is_a?(String)}
+ end
+ end
+end
diff --git a/lib/ci/model.rb b/lib/ci/model.rb
new file mode 100644
index 00000000000..c42a0ad36db
--- /dev/null
+++ b/lib/ci/model.rb
@@ -0,0 +1,11 @@
+module Ci
+ module Model
+ def table_name_prefix
+ "ci_"
+ end
+
+ def model_name
+ @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
+ end
+ end
+end
diff --git a/lib/ci/scheduler.rb b/lib/ci/scheduler.rb
new file mode 100644
index 00000000000..ee0958f4be1
--- /dev/null
+++ b/lib/ci/scheduler.rb
@@ -0,0 +1,16 @@
+module Ci
+ class Scheduler
+ def perform
+ projects = Ci::Project.where(always_build: true).all
+ projects.each do |project|
+ last_commit = project.commits.last
+ next unless last_commit && last_commit.last_build
+
+ interval = project.polling_interval
+ if (last_commit.last_build.created_at + interval.hours) < Time.now
+ last_commit.retry
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/static_model.rb b/lib/ci/static_model.rb
new file mode 100644
index 00000000000..bb2bdbed495
--- /dev/null
+++ b/lib/ci/static_model.rb
@@ -0,0 +1,49 @@
+# Provides an ActiveRecord-like interface to a model whose data is not persisted to a database.
+module Ci
+ module StaticModel
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Used by ActiveRecord's polymorphic association to set object_id
+ def primary_key
+ 'id'
+ end
+
+ # Used by ActiveRecord's polymorphic association to set object_type
+ def base_class
+ self
+ end
+ end
+
+ # Used by AR for fetching attributes
+ #
+ # Pass it along if we respond to it.
+ def [](key)
+ send(key) if respond_to?(key)
+ end
+
+ def to_param
+ id
+ end
+
+ def new_record?
+ false
+ end
+
+ def persisted?
+ false
+ end
+
+ def destroyed?
+ false
+ end
+
+ def ==(other)
+ if other.is_a? ::Ci::StaticModel
+ id == other.id
+ else
+ super
+ end
+ end
+ end
+end
diff --git a/lib/ci/version_info.rb b/lib/ci/version_info.rb
new file mode 100644
index 00000000000..2a87c91db5e
--- /dev/null
+++ b/lib/ci/version_info.rb
@@ -0,0 +1,52 @@
+class VersionInfo
+ include Comparable
+
+ attr_reader :major, :minor, :patch
+
+ def self.parse(str)
+ if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/)
+ VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i)
+ else
+ VersionInfo.new
+ end
+ end
+
+ def initialize(major = 0, minor = 0, patch = 0)
+ @major = major
+ @minor = minor
+ @patch = patch
+ end
+
+ def <=>(other)
+ return unless other.is_a? VersionInfo
+ return unless valid? && other.valid?
+
+ if other.major < @major
+ 1
+ elsif @major < other.major
+ -1
+ elsif other.minor < @minor
+ 1
+ elsif @minor < other.minor
+ -1
+ elsif other.patch < @patch
+ 1
+ elsif @patch < other.patch
+ -1
+ else
+ 0
+ end
+ end
+
+ def to_s
+ if valid?
+ "%d.%d.%d" % [@major, @minor, @patch]
+ else
+ "Unknown"
+ end
+ end
+
+ def valid?
+ @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0
+ end
+end