diff options
author | Kamil Trzcinski <ayufan@ayufan.eu> | 2015-05-17 13:36:11 +0200 |
---|---|---|
committer | Valery Sizov <vsv2711@gmail.com> | 2015-05-22 16:34:26 +0300 |
commit | 4785e35d9477219997a0af7fee505558dceb449f (patch) | |
tree | 2fa3c8e0d4ca15c68eaa9af4180cca91b35b923f | |
parent | 2bab322eab1bba3f38ee01e5dbc5e61dffdbee1a (diff) | |
download | gitlab-ci-4785e35d9477219997a0af7fee505558dceb449f.tar.gz |
Travis-CI integration
45 files changed, 1153 insertions, 74 deletions
@@ -8,6 +8,7 @@ ci.db config/application.yml config/database.yml config/resque.yml +config/travis.yml config/unicorn.rb config/initializers/smtp_settings.rb coverage/* @@ -12,6 +12,9 @@ gem 'rails', '4.1.9' gem 'activerecord-deprecated_finders' gem 'activerecord-session_store' gem "nested_form" +gem 'addressable' +gem 'travis-build', github: 'travis-ci/travis-build' +gem 'travis-model-build-config', github: 'ayufan/travis-model-build-config' # tag runners gem 'acts-as-taggable-on', '~> 3.4' @@ -88,6 +91,8 @@ gem "paranoia", "~> 2.0" # Colored output to console gem "colored" +gem 'sshkey' +gem 'coder' group :development do gem 'brakeman', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 76bd8fd..5230184 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,16 @@ +GIT + remote: git://github.com/ayufan/travis-model-build-config.git + revision: b2618553985f2968480bd1e5c97a40954cf2989a + specs: + travis-model-build-config (0.0.2) + activesupport (~> 4.0) + +GIT + remote: git://github.com/travis-ci/travis-build.git + revision: 3ef77bcecbdfeacddaa3f5db9fb50e334e6f5b04 + specs: + travis-build (0.0.1) + GEM remote: https://rubygems.org/ specs: @@ -71,6 +84,7 @@ GEM timers (~> 1.1.0) chronic (0.10.2) cliver (0.3.2) + coder (0.4.0) coderay (1.1.0) coercible (1.0.0) descendants_tracker (~> 0.0.1) @@ -331,6 +345,7 @@ GEM actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) + sshkey (1.6.1) stamp (0.5.0) state_machine (1.2.0) temple (0.6.7) @@ -338,7 +353,7 @@ GEM tins (~> 0.8) terminal-table (1.4.5) thor (0.19.1) - thread_safe (0.3.4) + thread_safe (0.3.5) tilt (1.4.1) timers (1.1.0) tins (0.13.1) @@ -375,11 +390,13 @@ DEPENDENCIES activerecord-deprecated_finders activerecord-session_store acts-as-taggable-on (~> 3.4) + addressable annotate bootstrap-sass (~> 3.0) brakeman byebug capybara + coder coffee-rails (~> 4.0.0) colored coveralls @@ -428,8 +445,11 @@ DEPENDENCIES slack-notifier (~> 1.0.0) slim spring-commands-rspec + sshkey stamp state_machine + travis-build! + travis-model-build-config! turbolinks uglifier (>= 1.0.3) unicorn (~> 4.8.2) diff --git a/app/assets/javascripts/build.coffee b/app/assets/javascripts/build.coffee index b21b34d..450d202 100644 --- a/app/assets/javascripts/build.coffee +++ b/app/assets/javascripts/build.coffee @@ -39,3 +39,11 @@ class Build $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") @Build = Build + +@Fold_Section = (foldName) -> + section = document.getElementById(foldName) + if section + if section.classList.contains("open") + section.classList.remove("open") + else + section.classList.add("open") diff --git a/app/assets/stylesheets/sections/builds.scss b/app/assets/stylesheets/sections/builds.scss index 535d656..28a884a 100644 --- a/app/assets/stylesheets/sections/builds.scss +++ b/app/assets/stylesheets/sections/builds.scss @@ -1,16 +1,17 @@ pre.trace { background: #111111; color: #fff; - font-family: $monospace_font; + font-family: 'monospace'; white-space: pre; white-space: pre-wrap; /* css-3 */ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ white-space: -pre-wrap; /* Opera 4-6 */ white-space: -o-pre-wrap; /* Opera 7 */ word-wrap: break-word; /* Internet Explorer 5.5+ */ + position: relative; overflow: auto; - overflow-y: hidden; font-size: 12px; + counter-reset: line-numbering; .icon-refresh { font-size: 24px; @@ -18,6 +19,83 @@ pre.trace { } } +pre.trace p a::before { + content: counter(line-numbering); + counter-increment: line-numbering; + padding-right: 1em; +} + +pre.trace p a { + display: inline-block; + text-align: right; + min-width: 40px; + margin-left: -33px; + cursor: pointer; + text-decoration: none; + color: #666; +} + +pre.trace p { + position: relative; + padding: 0 15px 0 55px; + margin: 0; + min-height: 16px; +} + +pre.trace .fold { + position: relative; + height: 16px; + overflow: hidden; + cursor: pointer; +} + +pre.trace .fold p:first-of-type { + background: #333 url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAiIHdpZHRoPSIxMCI+CjxwYXRoIGQ9Im0wLjUsMS41LDQuNSw3LDQuNS03eiIgc3Ryb2tlPSIjNTU1IiBzdHJva2Utd2lkdGg9IjAuNSIgZmlsbD0iIzY2NiIvPgo8L3N2Zz4KCg==') no-repeat 8px 3px +} + +pre.trace .fold:not(.open) p:first-of-type { + visibility: visible; + height: auto; + min-height: 16px; + background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAiIHdpZHRoPSIxMCI+CjxwYXRoIGQ9Ik0yLDksOSw1LDIsMXoiIHN0cm9rZT0iIzU1NSIgc3Ryb2tlLXdpZHRoPSIwLjUiIGZpbGw9IiM2NjYiLz4KPC9zdmc+Cgo=') +} + +pre.trace .fold p:first-of-type { + padding-right: 190px; +} + +pre.trace .fold.open { + height: auto; +} + +pre.trace .fold .fold-name { + position: absolute; + z-index: 1; + display: block; + top: 2px; + right: 85px; + padding: 0px 7px 0px 7px; + line-height: 10px; + font-size: 10px; + background-color: #666; + border-radius: 6px; + color: #bbb; +} + +pre.trace .duration { + position: absolute; + z-index: 1; + display: block; + top: 2px; + right: 12px; + padding: 0px 7px 0px 7px; + line-height: 10px; + font-size: 10px; + background-color: #666; + border-radius: 6px; + color: #bbb; +} + .autoscroll-container { position: fixed; bottom: 10px; @@ -64,3 +142,7 @@ pre.trace { left: 4px; } } + +.build-variant { + width: 50%; +} diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ad5b120..bd38474 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -123,6 +123,9 @@ class ProjectsController < ApplicationController params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build, :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :skip_refs, :email_recipients, :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token, - { jobs_attributes: [:id, :name, :build_branches, :build_tags, :tag_list, :commands, :refs, :_destroy, :job_type] }) + { jobs_attributes: [:id, :name, :build_branches, :build_tags, :tag_list, :commands, :refs, :_destroy, :job_type] }, + :build_method, :ssh_key, :ssh_source, :ssh_refs, + { variables_attributes: [:id, :key, :value, :public] }, + ) end end diff --git a/app/controllers/variables_controller.rb b/app/controllers/variables_controller.rb new file mode 100644 index 0000000..6eb908e --- /dev/null +++ b/app/controllers/variables_controller.rb @@ -0,0 +1,17 @@ +class VariablesController < ApplicationController + before_filter :authenticate_user! + before_filter :project + before_filter :authorize_access_project! + before_filter :authorize_manage_project! + + layout 'project' + + def index + end + + private + + def project + @project ||= Project.find(params[:project_id]) + end +end diff --git a/app/models/build.rb b/app/models/build.rb index c9f1230..7026355 100644 --- a/app/models/build.rb +++ b/app/models/build.rb @@ -33,7 +33,7 @@ class Build < ActiveRecord::Base scope :pending, ->() { where(status: "pending") } scope :success, ->() { where(status: "success") } scope :failed, ->() { where(status: "failed") } - scope :unstarted, ->() { where(runner_id: nil) } + scope :unstarted, ->() { where(runner_id: nil).where.not(commands: nil) } acts_as_taggable @@ -64,17 +64,21 @@ class Build < ActiveRecord::Base def retry(build) new_build = Build.new(status: :pending) + new_build.job_id = build.job_id + new_build.commit_id = build.commit_id + new_build.ref = build.ref + new_build.origin_ref = build.origin_ref + new_build.project_id = build.project_id + new_build.build_method = build.build_method + new_build.save if build.job - new_build.commands = build.job.commands + new_build.commands = build.job.build_commands(build) new_build.tag_list = build.job.tag_list else new_build.commands = build.commands end - new_build.job_id = build.job_id - new_build.commit_id = build.commit_id - new_build.project_id = build.project_id new_build.save new_build end @@ -105,15 +109,23 @@ class Build < ActiveRecord::Base build.update_attributes finished_at: Time.now project = build.project + if build.job + build.job.build_end(build) + end + if project.web_hooks? WebHookService.new.build_end(build) end - if build.commit.success? && !(build.job && build.job.deploy?) - build.commit.create_deploy_builds(build.ref) + if project.can_have_jobs? + if build.commit.success? && !(build.job && build.job.deploy?) + build.commit.create_deploy_builds(build.ref, build.origin_ref) + end end - project.execute_services(build) + if project.can_have_services? + project.execute_services(build) + end if project.coverage_enabled? build.update_coverage @@ -130,6 +142,18 @@ class Build < ActiveRecord::Base delegate :sha, :short_sha, :before_sha, :ref, to: :commit, prefix: false + def build_id + project.builds.where("builds.id <= ?", id).count("builds.id") + end + + def build_commit_id + commit.builds.where("builds.id <= ?", id).count("builds.id") + end + + def build_name + "#{commit.commit_id}.#{build_commit_id}" + end + def trace_html html = Ansi2html::convert(trace) if trace.present? html ||= '' @@ -169,6 +193,10 @@ class Build < ActiveRecord::Base commit.project end + def repo_slug + repo_url.split('/').last(2).join('/').gsub(/\.git$/, '') + end + def project_id commit.project_id end @@ -182,7 +210,15 @@ class Build < ActiveRecord::Base end def allow_git_fetch - project.allow_git_fetch + project.can_git_fetch? && project.allow_git_fetch + end + + def wait + if started_at + started_at - created_at + else + Time.now - created_at + end end def update_coverage @@ -213,11 +249,23 @@ class Build < ActiveRecord::Base end end - def for_tag? - if job && job.build_tags - true + def ref + build_ref = read_attribute(:ref) + + if build_ref.present? + build_ref else - false + commit.ref end end + + def origin_ref=(value) + value = "refs/heads/#{value}" unless value.start_with?("refs/") + write_attribute(:origin_ref, value) + write_attribute(:ref, value.gsub(/\Arefs\/(tags|heads)\//, '')) + end + + def for_tag? + origin_ref.present? && origin_ref.start_with?("refs/tags/") + end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 6c876bd..80def46 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -19,9 +19,12 @@ class Commit < ActiveRecord::Base serialize :push_data - validates_presence_of :ref, :sha, :before_sha, :push_data + validates_presence_of :ref, :origin_ref, :sha, :before_sha, :push_data validate :valid_commit_sha + scope :heads, ->() { where("commits.origin_ref LIKE 'refs/heads/%'") } + scope :tags, ->() { where("commits.origin_ref LIKE 'refs/tags/%'") } + def self.truncate_sha(sha) sha[0...8] end @@ -92,25 +95,30 @@ class Commit < ActiveRecord::Base recipients.uniq end - def create_builds - project.jobs.where(build_branches: true).active.parallel.map do |job| - create_build_from_job(job) + def create_builds(origin_ref) + case project.build_method + when 'travis' + CreateTravisJobsService.new.execute(self, origin_ref) + else + project.jobs.where(build_branches: true).active.parallel.map do |job| + create_build_from_job(job, origin_ref) + end end end - def create_builds_for_tag(ref = '') - project.jobs.where(build_tags: true).active.parallel.map do |job| - create_build_from_job(job, ref) + def create_builds_for_tag(origin_ref) + case project.build_method + when 'travis' + CreateTravisJobsService.new.execute(self, origin_ref) + else + project.jobs.where(build_tags: true).active.parallel.map do |job| + create_build_from_job(job, origin_ref) + end end end - def create_build_from_job(job, ref = '') - build = builds.new(commands: job.commands) - build.tag_list = job.tag_list - build.project_id = project_id - build.job = job - build.save - build + def create_build_from_job(job, origin_ref) + job.execute(self, origin_ref) end def builds_without_retry @@ -127,10 +135,10 @@ class Commit < ActiveRecord::Base @retried_builds ||= (builds - builds_without_retry) end - def create_deploy_builds(ref) + def create_deploy_builds(ref, origin_ref) project.jobs.deploy.active.each do |job| if job.run_for_ref?(ref) - create_build_from_job(job) + create_build_from_job(job, origin_ref) end end end @@ -181,10 +189,25 @@ class Commit < ActiveRecord::Base @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i end + def started_at + @started_at ||= builds.order('started_at DESC').first.try(:started_at) + end + def finished_at @finished_at ||= builds.order('finished_at DESC').first.try(:finished_at) end + def real_duration + case status + when 'success' + when 'canceled' + when 'failed' + finished_at - started_at + else + nil + end + end + def coverage if project.coverage_enabled? && builds.size > 0 builds.last.coverage @@ -194,4 +217,47 @@ class Commit < ActiveRecord::Base def matrix? builds_without_retry.size > 1 end + + def commit_id + project.commits.where("id <= ?", id).count(:id) + end + + def head? + origin_ref.start_with?('refs/heads/') + end + + def tag? + origin_ref.start_with?('refs/tags/') + end + + def ref_type + return 'heads' if head? + return 'tags' if tag? + 'undefined' + end + + def tag_name + push_data[:tag_name] + end + + def tag_message + push_data[:tag_message] + end + + def prev_finished_commit + prev_commits = project.commits.where('id < ?', id) + prev_commits = prev_commits.where(origin_ref: origin_ref) if head? + prev_commits = prev_commits.tags if tag? + prev_commits.order(id: :desc).limit(10).each do |prev_commit| + if prev_commit.success? || prev_commit.failed? + return prev_commit + end + end + return nil + end + + def prev_status + prev_commit = prev_finished_commit + prev_commit ? prev_commit.status : 'success' + end end diff --git a/app/models/job.rb b/app/models/job.rb index 110c96d..9b99221 100644 --- a/app/models/job.rb +++ b/app/models/job.rb @@ -19,6 +19,10 @@ class Job < ActiveRecord::Base acts_as_paranoid + default_value_for :type, 'ShellJob' + + serialize :properties + belongs_to :project has_many :builds @@ -46,4 +50,41 @@ class Job < ActiveRecord::Base true end end + + after_initialize :initialize_properties + def initialize_properties + self.properties = {} if properties.nil? + end + + def title + # implement inside child + end + + def execute(commit, ref) + # implement inside child + end + + def build_end(build) + + end + + def build_commands(build) + commands + 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['#{arg}'] = value + end + } + end + end end diff --git a/app/models/job_models/shell_job.rb b/app/models/job_models/shell_job.rb new file mode 100644 index 0000000..82fbf72 --- /dev/null +++ b/app/models/job_models/shell_job.rb @@ -0,0 +1,20 @@ +class ShellJob < Job + def title + 'Shell' + end + + def type + 'shell' + end + + def execute(commit, origin_ref) + build = commit.builds.new(commands: commands) + build.build_method = 'shell' + build.tag_list = tag_list + build.project_id = commit.project_id + build.job = self + build.origin_ref = origin_ref + build.save + build + end +end
\ No newline at end of file diff --git a/app/models/job_models/travis_job.rb b/app/models/job_models/travis_job.rb new file mode 100644 index 0000000..0d12a52 --- /dev/null +++ b/app/models/job_models/travis_job.rb @@ -0,0 +1,244 @@ +require 'travis/model/build/config' +require 'travis/model/build/config/matrix' +require 'travis/build' +require 'uri' + +class TravisJob < Job + prop_accessor :config + prop_accessor :variables + prop_accessor :matrix_config + + after_initialize :init_values + before_save :update_values + + SCRIPT_HEADER = <<-eos +#!/bin/bash +cat > ~/build.script && chmod +x ~/build.script && exec ~/build.script +eos + + def title + 'Travis' + end + + def type + 'travis' + end + + def init_values + self.active = false + self.config ||= {} + self.variables ||= [] + self.matrix_config ||= {} + end + + def update_values + self.name = TravisJob.build_name(matrix_config) + end + + def commands + nil + end + + def build_commands(build) + commands + end + + def tag_list + tag_list = ['travis', config[:language] || 'ruby', config[:os] || 'linux'] + tag_list << "xcode-#{config[:xcode_version] || 'default'}" if config[:language] == 'objective-c' + tag_list + end + + def execute(commit, origin_ref) + build = commit.builds.new() + build.build_method = 'travis' + build.tag_list = tag_list + build.project_id = commit.project_id + build.job = self + build.origin_ref = origin_ref + build.save + build.update_attributes(commands: build_commands(build)) + build + end + + def build_end(build) + return unless build.commit + return unless build.commit.builds_without_retry.include?(build) + + fast_finish = matrix_config[:fast_finish] if fast_finish + + # cancel all builds + if fast_finish + build.commit.builds.each do |other_build| + other_build.cancel + end + end + + # notifications + execute_notifications(build, config[:notifications]) + end + + def self.build_name(matrix_config) + matrix_config.map { |k,v| v.to_s }.join(", ") + end + + def build_commands(build) + commit = build.commit + data = travis_config.dup + data[:config] = config + data[:env_vars] = (data[:env_vars] || []) + commit_variables(commit) + + data[:urls] = { + } + data[:repository] = { + source_url: build.repo_url, + github_id: build.project.id, + slug: build.repo_slug + } + data[:source] = { + id: commit.id.to_s, + number: commit.commit_id.to_s + } + data[:job] = { + id: build.id.to_s, + number: "#{build.build_name}", + branch: build.ref, + commit: commit.sha, + commit_range: "#{commit.short_before_sha}..#{commit.short_sha}", + pull_request: false, # not yet supported + secure_env_enabled: true, + tag: build.for_tag? ? build.ref : nil + } + + if project.ssh_key && (project.ssh_for_ref?(build.ref) || project.ssh_for_ref?(build.origin_ref)) + url = URI(project.gitlab_url) + data[:ssh_key] = { + value: project.ssh_key, + source: url.host + } + end + + # THIS DOESN'T WORK + # ::Travis::Build::Git.send(:define_method, :checkout) do + # disable_interactive_auth + # install_ssh_key + # + # if use_tarball? + # download_tarball + # else + # submodules + # end + # + # rm_key + # end + + script = ::Travis::Build.script(data) + SCRIPT_HEADER + script.compile(true) + end + + private + + def commit_variables(commit) + return {} unless commit.project + commit.project.variables.map do |variable| + { + name: variable.key, + value: variable.value, + public: variable.public + } + end + end + + def execute_notifications(build, notifications) + notifications ||= {} + email_notifications(build, notifications[:email]) + slack_notifications(build, notifications[:slack]) + end + + def should_execute_notification(status, prev_status, config) + config ||= {} + when_notify = nil + case status + when 'success' + when_notify = 'change' + when_notify ||= config[:on_success] + when 'failed' + when_notify = 'always' + when_notify ||= config[:on_failure] + else + return false + end + + case when_notify + when 'always' + return true + when 'never' + return false + when 'change' + return prev_status != status + else + return false + end + end + + def email_notifications(build, config) + begin + recipients = [] + notification_config = {} + + if config.is_a?(Hash) + notification_config = config + recipients = config[:recipients] + elsif config.is_a?(Array) + recipients = config + end + + return unless should_execute_notification(build.status, build.commit.prev_status, notification_config) + + mail = MailService.new + mail.project = build.project + mail.email_add_committer = recipients.empty? + mail.email_only_broken_builds = false + mail.email_recipients = recipients.join(" ") + mail.execute(build) + rescue => e + NewRelic::Agent.notice_error(e, request_params: config) + end + end + + def slack_notifications(build, config) + begin + rooms = [] + notification_config = {} + + if config.is_a?(Hash) + notification_config = config + rooms = config[:rooms] + elsif config.is_a?(Array) + rooms = config + elsif config.is_a?(String) + rooms = [config] + end + + return unless should_execute_notification(build.commit.status, build.commit.prev_status, notification_config) + + rooms.each do |room| + account_token, channel = room.split('#', 2) + account, token = account_token.split(':', 2) + + slack = SlackService.new + slack.project = build.project + slack.webhook = "https://#{account}.slack.com/services/hooks/travis?token=#{token}" + slack.channel = channel + slack.notify_only_broken_builds = false + slack.execute(build) + end + rescue => e + NewRelic::Agent.notice_error(e, request_params: config) + end + end + + def travis_config + CreateTravisJobsService.travis_config + end +end
\ No newline at end of file diff --git a/app/models/network.rb b/app/models/network.rb index 8431f75..093ce0b 100644 --- a/app/models/network.rb +++ b/app/models/network.rb @@ -102,6 +102,19 @@ class Network build_response(response) end + def raw_file_content(project_id, token, sha, filepath) + opts = { + headers: {"Content-Type" => "application/json"}, + } + + query = "projects/#{project_id}/repository/blobs/#{sha}?private_token=#{token}&filepath=#{filepath}" + + endpoint = File.join(url, API_PREFIX, query) + response = self.class.get(endpoint, opts) + + build_response(response) + end + private def url @@ -116,12 +129,15 @@ class Network def build_response(response) case response.code - when 200 + when 200, 201 response.parsed_response - when 401 + when 400 + raise response.parsed_response['message'] + when 401, 403 raise UnauthorizedError else nil end end + end diff --git a/app/models/project.rb b/app/models/project.rb index 9b015d2..5deec78 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -34,6 +34,8 @@ class Project < ActiveRecord::Base has_many :web_hooks, dependent: :destroy has_many :jobs, dependent: :destroy has_many :events, dependent: :destroy + has_many :variables, dependent: :destroy + has_many :travis_jobs, dependent: :destroy # Project services has_many :services, dependent: :destroy @@ -42,6 +44,7 @@ class Project < ActiveRecord::Base has_one :mail_service, dependent: :destroy accepts_nested_attributes_for :jobs, allow_destroy: true + accepts_nested_attributes_for :variables, allow_destroy: true # # Validations @@ -56,6 +59,7 @@ class Project < ActiveRecord::Base if: ->(project) { project.always_build.present? } validate :validate_jobs + validate :validate_ssh_key scope :public_only, ->() { where(public: true) } @@ -84,6 +88,7 @@ ls -la ssh_url_to_repo: project.ssh_url_to_repo, email_add_pusher: GitlabCi.config.gitlab_ci.add_pusher, email_only_broken_builds: GitlabCi.config.gitlab_ci.all_broken_builds, + build_method: GitlabCi.config.gitlab_ci.default_method } project = Project.new(params) @@ -136,6 +141,7 @@ ls -la def set_default_values self.token = SecureRandom.hex(15) if self.token.blank? + self.ssh_key = SSHKey.generate.private_key if self.ssh_key.blank? and self.ssh_key_was.blank? end def tracked_refs @@ -197,6 +203,8 @@ ls -la end def validate_jobs + return unless can_have_jobs? + remaining_jobs = jobs.reject(&:marked_for_destruction?) if remaining_jobs.empty? @@ -247,4 +255,67 @@ ls -la def setup_finished? commits.any? end + + def available_job_names + %w(shell travis) + end + + def create_job(job_name) + self.send "create_#{job_name}_job" if available_job_names.include?(job_name) + end + + def can_have_variables? + build_method == 'travis' + end + + def can_have_services? + build_method == 'shell' + end + + def can_have_jobs? + build_method == 'shell' + end + + def can_have_ssh_key? + build_method == 'travis' + end + + def can_git_fetch? + build_method == 'shell' + end + + def ssh_pub_key + ssh_openssl_pkey.ssh_public_key if ssh_openssl_pkey + end + + def ssh_fingerprint + ssh_openssl_pkey.fingerprint if ssh_openssl_pkey + end + + def validate_ssh_key + if ssh_key.blank? + self.ssh_key = ssh_key_was if self.ssh_key_changed? + return + end + + begin + SSHKey.new(ssh_key) + rescue => e + errors.add(:ssh_key, e.to_s) + return + end + end + + def ssh_for_ref?(ref_name) + ssh_refs.blank? || ssh_refs.split(",").map{|ref| ref.strip}.any? {|ref| File.fnmatch(ref, ref_name)} + end + + private + + def ssh_openssl_pkey + begin + @pkey ||= SSHKey.new(ssh_key_was) unless ssh_key.blank? + rescue + end + end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index c393bd4..63fe447 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -19,6 +19,8 @@ class SlackService < Service default_value_for :notify_only_broken_builds, true + attr_accessor :channel + def title 'Slack' end @@ -75,6 +77,7 @@ class SlackService < Service fallback: message.fallback, attachments: message.attachments ) + options[:channel] = channel if channel SlackNotifierWorker.perform_async(webhook, message.pretext, options) end diff --git a/app/models/variable.rb b/app/models/variable.rb new file mode 100644 index 0000000..b6e7741 --- /dev/null +++ b/app/models/variable.rb @@ -0,0 +1,3 @@ +class Variable < ActiveRecord::Base + belongs_to :project +end
\ No newline at end of file diff --git a/app/services/create_commit_service.rb b/app/services/create_commit_service.rb index 646b7c5..5ea81a9 100644 --- a/app/services/create_commit_service.rb +++ b/app/services/create_commit_service.rb @@ -33,12 +33,15 @@ class CreateCommitService unless commit data = { ref: ref, + origin_ref: origin_ref, + ref_message: params[:message], sha: sha, before_sha: before_sha, push_data: { before: before_sha, after: sha, ref: ref, + message: params[:message], user_name: params[:user_name], user_email: params[:user_email], repository: params[:repository], @@ -51,9 +54,14 @@ class CreateCommitService end if origin_ref.start_with?('refs/tags/') - commit.create_builds_for_tag(ref) + commit.create_builds_for_tag(origin_ref) else - commit.create_builds + commit.create_builds(origin_ref) + end + + if commit.builds.none? + commit.destroy + commit = nil end if commit.builds.empty? diff --git a/app/services/create_travis_jobs_service.rb b/app/services/create_travis_jobs_service.rb new file mode 100644 index 0000000..5f6c222 --- /dev/null +++ b/app/services/create_travis_jobs_service.rb @@ -0,0 +1,112 @@ +class CreateTravisJobsService + def execute(commit, origin_ref) + project = commit.project + + ref = origin_ref.gsub(/\Arefs\/(tags|heads)\//, '') + tag = origin_ref.start_with?('refs/tags/') + + build_config_params = load_config(project, ref, commit.sha, tag) + raise 'Missing .travis.yml' if build_config_params.nil? + + ActiveRecord::Base.transaction do + begin + generate_builds(build_config_params, commit.push_data) do |new_data, new_attributes| + new_job = find_or_create_job(project, new_attributes) + new_job.execute(commit, origin_ref) + end + + commit + rescue => e + raise ActiveRecord::Rollback + nil + end + end + end + + def find_job(project, new_attributes) + project.travis_jobs.where(name: TravisJob.build_name(new_attributes[:matrix_config])).find do |job| + job.variables == new_attributes[:env_vars] && + job.config == new_attributes[:config] && + job.matrix_config == new_attributes[:matrix_config] + end + end + + def find_or_create_job(project, new_attributes) + job = find_job(project, new_attributes) + + unless job + job = project.travis_jobs.new() + job.variables = new_attributes[:env_vars] || [] + job.config = new_attributes[:config] || {} + job.matrix_config = new_attributes[:matrix_config] || {} + job.save + end + + job + end + + def self.travis_config + @@travis_config ||= Extension.deep_symbolize_keys(YAML.load_file("#{Rails.root}/config/travis.yml")[Rails.env]) + end + + def travis_config + CreateTravisJobsService.travis_config + end + + private + + def load_config(project, ref, sha, is_tagged = false) + variant, version = ref.split('/', 2) + + config_file ||= load_config_file(project, sha, ".release_#{variant}.yml") if is_tagged && version + config_file ||= load_config_file(project, sha, ".travis_#{variant}.yml") if version + config_file ||= load_config_file(project, sha, '.release.yml') if is_tagged + config_file ||= load_config_file(project, sha, '.travis.yml') + + YAML.load(config_file) unless config_file.nil? + end + + def load_config_file(project, sha, file_name) + network.raw_file_content( + project.gitlab_id, GitlabCi.config.gitlab_server.private_token, sha, file_name) + end + + def generate_builds(build_config_params, data, &block) + raise 'block must be specified' unless block_given? + + build_config = ::Travis::Model::Build::Config.new(build_config_params, default_options).normalize + if branches = build_config[:branches] + if branches[:only] + return unless branches[:only].include? data[:ref] + elsif branches[:except] + return if branches[:except].include? data[:ref] + end + end + + # omit gh-pages unless specified in branches.only + return if data[:ref] == 'gh-pages' unless build_config[:branches] and build_config[:branches][:only] + + matrix_build = ::Travis::Model::Build::Config::Matrix.new(build_config, default_options) + matrix_build.expand.each do |expanded_config| + matrix_attributes = expanded_config.select do |key, value| + matrix_build.send(:expand_keys).include? key and build_config[key].is_a?(Array) + end + + matrix_build_data = { + config: expanded_config, + matrix_config: matrix_attributes, + env_vars: [] + } + + block.call(data.dup, matrix_build_data) + end + end + + def network + @@network ||= Network.new + end + + def default_options + {multi_os: true, dist_group_expansion: true} + end +end
\ No newline at end of file diff --git a/app/views/builds/show.html.haml b/app/views/builds/show.html.haml index afca8ce..dafc8ef 100644 --- a/app/views/builds/show.html.haml +++ b/app/views/builds/show.html.haml @@ -2,7 +2,7 @@ = link_to @project.name, @project @ = @commit.short_sha - - if current_user && current_user.can_manage_project?(@project.gitlab_id) + - if current_user && current_user.can_manage_project?(@project.gitlab_id) && @project.can_have_jobs? .pull-right = link_to project_jobs_path(@project), class: "btn btn-default btn-small" do %i.icon-edit.icon-white diff --git a/app/views/commits/show.html.haml b/app/views/commits/show.html.haml index 4464106..de69c71 100644 --- a/app/views/commits/show.html.haml +++ b/app/views/commits/show.html.haml @@ -2,7 +2,7 @@ = @project.name @ #{gitlab_commit_link(@project, @commit.sha)} - - if current_user && current_user.can_manage_project?(@project.gitlab_id) + - if current_user && current_user.can_manage_project?(@project.gitlab_id) && @project.can_have_jobs? .pull-right = link_to project_jobs_path(@project), class: "btn btn-default btn-small" do %i.icon-edit.icon-white diff --git a/app/views/jobs/_deploy_job_edit.html.haml b/app/views/jobs/_deploy_job_edit.html.haml index 5765784..c774293 100644 --- a/app/views/jobs/_deploy_job_edit.html.haml +++ b/app/views/jobs/_deploy_job_edit.html.haml @@ -8,7 +8,7 @@ %li= msg = f.fields_for :jobs do |job_form| - - if job_form.object.job_type == "deploy" || job_form.object.new_record? + - if (job_form.object.job_type == "deploy" || job_form.object.new_record?) && job_form.object.active? = job_form.hidden_field :job_type, value: "deploy" .form-group diff --git a/app/views/jobs/_edit.html.haml b/app/views/jobs/_edit.html.haml index d748b22..319a1e0 100644 --- a/app/views/jobs/_edit.html.haml +++ b/app/views/jobs/_edit.html.haml @@ -8,7 +8,7 @@ %li= msg = f.fields_for :jobs do |job_form| - - if job_form.object.job_type == "parallel" || job_form.object.new_record? + - if (job_form.object.job_type == "parallel" || job_form.object.new_record?) && job_form.object.active? .form-group = job_form.label :name, 'Name', class: 'control-label' .col-sm-10 diff --git a/app/views/jobs/_list.html.haml b/app/views/jobs/_list.html.haml index 440985b..c84f95a 100644 --- a/app/views/jobs/_list.html.haml +++ b/app/views/jobs/_list.html.haml @@ -8,12 +8,13 @@ %tbody - @project.jobs.each do |job| - %tr - %td= job.name - %td= check_box_tag nil, nil, job.build_branches, disabled: true - %td= check_box_tag nil, nil, job.build_tags, disabled: true - %td - - job.tag_list.each do |tag| - %span.label.label-primary - = tag + - if job.object.active? + %tr + %td= job.name + %td= check_box_tag nil, nil, job.build_branches, disabled: true + %td= check_box_tag nil, nil, job.build_tags, disabled: true + %td + - job.tag_list.each do |tag| + %span.label.label-primary + = tag diff --git a/app/views/layouts/_nav_project.html.haml b/app/views/layouts/_nav_project.html.haml index 7a8286e..223ae22 100644 --- a/app/views/layouts/_nav_project.html.haml +++ b/app/views/layouts/_nav_project.html.haml @@ -8,28 +8,35 @@ = link_to project_charts_path(@project) do %i.icon-bar-chart Charts + %li + %hr = nav_link path: ['runners#index', 'runners#show'] do = link_to project_runners_path(@project) do %i.icon-cog Runners - = nav_link path: ['jobs#index', 'jobs#deploy_jobs'] do - = link_to project_jobs_path(@project) do - %i.icon-code - Jobs + - if @project.can_have_jobs? + = nav_link path: ['jobs#index', 'jobs#deploy_jobs'] do + = link_to project_jobs_path(@project) do + %i.icon-code + Jobs + - if @project.can_have_variables? + = nav_link path: 'variables#index' do + = link_to project_variables_path(@project) do + %i.icon-code + Variables + - if @project.can_have_services? + = nav_link path: 'services#index' do + = link_to project_services_path(@project) do + %i.icon-gear + Services = nav_link path: 'web_hooks#index' do = link_to project_web_hooks_path(@project) do %i.icon-link Web Hooks - = nav_link path: 'services#index' do - = link_to project_services_path(@project) do - %i.icon-gear - Services = nav_link path: 'events#index' do = link_to project_events_path(@project) do %i.icon-book Events - %li - %hr = nav_link path: 'projects#edit' do = link_to edit_project_path(@project) do %i.icon-cogs diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml index af78db9..572b2a9 100644 --- a/app/views/projects/_form.html.haml +++ b/app/views/projects/_form.html.haml @@ -11,19 +11,37 @@ %legend Build settings .form-group = label_tag nil, class: 'control-label' do - Get code + Build method .col-sm-10 - %p Get recent application code using the following command: + %p Select the way project is built: .radio = label_tag do - = f.radio_button :allow_git_fetch, 'false' - %strong git clone - .light Slower but makes sure you have a clean dir before every build + = f.radio_button :build_method, 'shell' + %strong Shell Jobs + .light Scripts you want CI to run on each push to repository .radio = label_tag do - = f.radio_button :allow_git_fetch, 'true' - %strong git fetch - .light Faster + = f.radio_button :build_method, 'travis' + %strong Travis-CI + .light Use .travis.yml from project repository + + - if @project.can_git_fetch? + .form-group + = label_tag nil, class: 'control-label' do + Get code + .col-sm-10 + %p Get recent application code using the following command: + .radio + = label_tag do + = f.radio_button :allow_git_fetch, 'false' + %strong git clone + .light Slower but makes sure you have a clean dir before every build + .radio + = label_tag do + = f.radio_button :allow_git_fetch, 'true' + %strong git fetch + .light Faster + .form-group = f.label :timeout_in_minutes, 'Timeout', class: 'control-label' .col-sm-10 @@ -79,6 +97,28 @@ %code \d+\%$ + - if @project.can_have_ssh_key? + %fieldset + %legend SSH key + .form-group + = f.label :ssh_key, "Public key", class: 'control-label' + .col-sm-10 + = text_area_tag :ssh_pub_key, @project.ssh_pub_key, class: 'form-control', rows: 5, readonly: true + .form-group + = f.label :ssh_key, "Fingerprint", class: 'control-label' + .col-sm-10 + = text_field_tag :ssh_fingerprint, @project.ssh_fingerprint, class: 'form-control', readonly: true + .form-group + = f.label :ssh_key, "New private key", class: 'control-label' + .col-sm-10 + = f.text_area :ssh_key, value: (@project.ssh_key_changed? ? @project.ssh_key : ''), rows: 5, class: 'form-control' + .light Paste new key here in PEM format to replace current one + .form-group + = f.label :ssh_refs, "Refs", class: 'control-label' + .col-sm-10 + = f.text_field :ssh_refs, class: 'form-control', placeholder: 'branch1, branch2, feature/*' + .light + You can specify git references to which SSH key will be used CI builds. Accepts strings and glob pattern syntax %fieldset %legend Advanced settings diff --git a/app/views/variables/index.html.haml b/app/views/variables/index.html.haml new file mode 100644 index 0000000..49730e5 --- /dev/null +++ b/app/views/variables/index.html.haml @@ -0,0 +1,43 @@ +%p.slead + Define additional environment variables which will be used by project + +%h + += nested_form_for @project, html: { class: 'form-horizontal' } do |f| + - if @project.errors.any? + #error_explanation + %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" + .alert.alert-error + %ul + - @project.errors.full_messages.each do |msg| + %li= msg + + = f.fields_for :variables do |variable_form| + .form-group + = f.label :key, 'Key', class: 'control-label' + .col-sm-10 + = variable_form.text_field :key, class: 'form-control', placeholder: "PROJECT_VARIABLE" + + .form-group + = f.label :commands, 'Value', class: 'control-label' + .col-sm-10 + = variable_form.text_area :value, class: 'form-control', rows: 2, placeholder: "" + + .form-group + = f.label :public, 'Public mode', class: 'control-label' + .col-sm-10 + .checkbox + = f.label :public do + = variable_form.check_box :public + %span.light Anyone can see variable value + + = variable_form.link_to_remove "Remove this variable", class: 'btn btn-danger pull-right' + %hr + %p + .clearfix + = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right' + + .form-actions + // = f.hidden_field :return_to, value: request.original_url + = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url + diff --git a/config/application.rb b/config/application.rb index c101033..8798235 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,6 +12,7 @@ module GitlabCi # Custom directories with classes and modules you want to be autoloadable. config.autoload_paths += %W(#{config.root}/lib + #{config.root}/app/models/job_models #{config.root}/app/models/project_services) # Only load the plugins named here, in the order given (default is alphabetical). diff --git a/config/application.yml.example b/config/application.yml.example index 8f64c33..acdb54f 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -3,6 +3,7 @@ defaults: &defaults url: 'https://gitlab.example.com/' # Replace with your gitlab server url app_id: '' app_secret: '' + private_token: '' ## Gitlab CI settings gitlab_ci: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 6021b46..c9fb0c4 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -40,6 +40,7 @@ Settings.gitlab_ci['support_email'] ||= Settings.gitlab_ci.email_from Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil? Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil? Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) +Settings.gitlab_ci['default_method'] ||= 'shell' if Settings.gitlab_ci['default_method'].nil? # Compatibility with old config Settings['gitlab_server_urls'] ||= Settings['allowed_gitlab_urls'] diff --git a/config/routes.rb b/config/routes.rb index b92b523..9cbac61 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -68,6 +68,7 @@ Rails.application.routes.draw do end resources :events, only: [:index] + resources :variables, only: [:index] end resource :user_sessions do diff --git a/config/travis.yml.example b/config/travis.yml.example new file mode 100644 index 0000000..7ea0432 --- /dev/null +++ b/config/travis.yml.example @@ -0,0 +1,20 @@ +defaults: &defaults + skip_resolv_updates: true + skip_etc_hosts_fix: true + paranoid: false + hosts: + apt_cache: false + npm_cache: false + env_vars: + - name: MY_GLOBAL_VARIABLE + value: value + public: false + +development: + <<: *defaults + +test: + <<: *defaults + +production: + <<: *defaults diff --git a/db/migrate/20141206115152_add_type_to_job.rb b/db/migrate/20141206115152_add_type_to_job.rb new file mode 100644 index 0000000..52ebfae --- /dev/null +++ b/db/migrate/20141206115152_add_type_to_job.rb @@ -0,0 +1,5 @@ +class AddTypeToJob < ActiveRecord::Migration + def change + add_column :jobs, :type, :string, default: 'ShellJob' + end +end diff --git a/db/migrate/20141206120632_add_job_parameters.rb b/db/migrate/20141206120632_add_job_parameters.rb new file mode 100644 index 0000000..3f85e01 --- /dev/null +++ b/db/migrate/20141206120632_add_job_parameters.rb @@ -0,0 +1,5 @@ +class AddJobParameters < ActiveRecord::Migration + def change + add_column :jobs, :properties, :text + end +end diff --git a/db/migrate/20150120223924_add_build_method_to_projects.rb b/db/migrate/20150120223924_add_build_method_to_projects.rb new file mode 100644 index 0000000..ec8a320 --- /dev/null +++ b/db/migrate/20150120223924_add_build_method_to_projects.rb @@ -0,0 +1,5 @@ +class AddBuildMethodToProjects < ActiveRecord::Migration + def change + add_column :projects, :build_method, :string, default: 'shell', null: false + end +end diff --git a/db/migrate/20150121112130_create_variables.rb b/db/migrate/20150121112130_create_variables.rb new file mode 100644 index 0000000..a2f5415 --- /dev/null +++ b/db/migrate/20150121112130_create_variables.rb @@ -0,0 +1,13 @@ +class CreateVariables < ActiveRecord::Migration + def change + create_table :variables do |t| + t.integer :project_id, null: false + t.string :key + t.text :value + t.boolean :public, null: false, default: false + end + + add_index :variables, :project_id + add_index :variables, [:project_id, :key], unique: true + end +end diff --git a/db/migrate/20150121151447_add_params_to_builds.rb b/db/migrate/20150121151447_add_params_to_builds.rb new file mode 100644 index 0000000..e103754 --- /dev/null +++ b/db/migrate/20150121151447_add_params_to_builds.rb @@ -0,0 +1,5 @@ +class AddParamsToBuilds < ActiveRecord::Migration + def change + add_column :builds, :build_method, :string, default: 'shell', null: false + end +end diff --git a/db/migrate/20150204001034_add_ssh_key_to_projects.rb b/db/migrate/20150204001034_add_ssh_key_to_projects.rb new file mode 100644 index 0000000..5802d84 --- /dev/null +++ b/db/migrate/20150204001034_add_ssh_key_to_projects.rb @@ -0,0 +1,7 @@ +class AddSshKeyToProjects < ActiveRecord::Migration + def change + add_column :projects, :ssh_key, :text + add_column :projects, :ssh_key_source, :text + add_column :projects, :ssh_refs, :text + end +end diff --git a/db/migrate/20150306135340_add_origin_ref_to_builds.rb b/db/migrate/20150306135340_add_origin_ref_to_builds.rb new file mode 100644 index 0000000..4e69f6f --- /dev/null +++ b/db/migrate/20150306135340_add_origin_ref_to_builds.rb @@ -0,0 +1,5 @@ +class AddOriginRefToBuilds < ActiveRecord::Migration + def change + add_column :builds, :origin_ref, :string + end +end diff --git a/db/migrate/20150306135342_add_ref_message_to_commits.rb b/db/migrate/20150306135342_add_ref_message_to_commits.rb new file mode 100644 index 0000000..64524c6 --- /dev/null +++ b/db/migrate/20150306135342_add_ref_message_to_commits.rb @@ -0,0 +1,5 @@ +class AddRefMessageToCommits < ActiveRecord::Migration + def change + add_column :commits, :ref_message, :text + end +end diff --git a/db/migrate/20150306135343_add_origin_ref_to_commits.rb b/db/migrate/20150306135343_add_origin_ref_to_commits.rb new file mode 100644 index 0000000..4bfebf7 --- /dev/null +++ b/db/migrate/20150306135343_add_origin_ref_to_commits.rb @@ -0,0 +1,5 @@ +class AddOriginRefToCommits < ActiveRecord::Migration + def change + add_column :commits, :origin_ref, :string + end +end diff --git a/db/migrate/20150306135345_migrate_ref_type.rb b/db/migrate/20150306135345_migrate_ref_type.rb new file mode 100644 index 0000000..f0294fb --- /dev/null +++ b/db/migrate/20150306135345_migrate_ref_type.rb @@ -0,0 +1,6 @@ +class MigrateRefType < ActiveRecord::Migration + def change + execute "UPDATE builds SET origin_ref=CONCAT('refs/heads/', ref) WHERE origin_ref IS NULL" + execute "UPDATE commits SET origin_ref=CONCAT('refs/heads/', ref) WHERE origin_ref IS NULL" + end +end diff --git a/lib/ansi2html.rb b/lib/ansi2html.rb index b85c85a..7dac6b0 100644 --- a/lib/ansi2html.rb +++ b/lib/ansi2html.rb @@ -85,19 +85,47 @@ module Ansi2html def convert(ansi) @out = "" + @line = "" + @line_clear = false + @line_duration = nil + @line_fold = nil @n_open_tags = 0 + @n_open_folds = [] + @n_lines = 0 reset() - s = StringScanner.new(ansi.gsub("<", "<")) + s = StringScanner.new(ansi) while(!s.eos?) if s.scan(/\e([@-_])(.*?)([@-~])/) handle_sequence(s) + elsif s.scan(/\r?\n/) + finish_line() + begin_line() + elsif s.scan(/\r/) + @line_clear = true + elsif s.scan(/</) + append_text("<") + elsif s.scan(/travis_fold:start:([^\r]*)\r/) + open_new_fold(s[1]) + elsif s.scan(/travis_fold:end:([^\r]*)\r/) + close_open_fold() + elsif s.scan(/travis_time:start:([^\r]*)\r/) + unless @line.empty? + finish_line() + begin_line() + end + @line_duration = "id-time-#{s[1]}" + elsif s.scan(/travis_time:(end|finish):([^:]*):start=([^,]*),finish=([^,]*),duration=([^\r]*)\r/) + line_duration = s[5].to_i / 1000.0 / 1000.0 / 1000.0 + line_duration = beautify_duration(line_duration) + @out << "<script>var duration=document.getElementById('id-time-#{s[2]}'); if(duration) { duration.innerHTML = duration.title = '#{line_duration}'; }</script>" + @line_duration = nil else - @out << s.scan(/./m) + append_text(s.scan(/./m)) end end - close_open_tags() + close_open_folds() @out end @@ -119,7 +147,41 @@ module Ansi2html end evaluate_command_stack(commands) + append_style() + end + + def begin_line() + @line = "" + @line_duration = nil + @n_open_tags = 0 + append_style() + end + + def finish_line() + @line_clear = false + close_open_tags() + @n_lines += 1 + if @line_fold + @out << %{<p onclick="javascript:Fold_Section('#{@line_fold}')">} + @line_fold = nil + else + @out << %{<p>} + end + @out << %{<a></a>} + @out << %{<span id="#{@line_duration}" class="duration" title="duration">duration</span>} unless @line_duration.nil? + @out << %{<span id="id-1-#{@n_lines}">#{@line}</span>} + @out << %{</p>} + end + + def append_text(text) + if @line_clear + @line = "" + @line_clear = false + end + @line << text + end + def append_style() css_classes = [] unless @fg_color.nil? @@ -151,17 +213,46 @@ module Ansi2html end def open_new_tag(css_classes) - @out << %{<span class="#{css_classes.join(' ')}">} + append_text(%{<span class="#{css_classes.join(' ')}">}) @n_open_tags += 1 end def close_open_tags while @n_open_tags > 0 - @out << %{</span>} + append_text(%{</span>}) @n_open_tags -= 1 end end + def open_new_fold(fold_name) + finish_line + @line_fold = "fold-start-" + ('a'..'z').to_a.shuffle.first(8).join + @out << %{<div id="#{@line_fold}" class="fold-start fold">} + @out << %{<span class="fold-name">#{fold_name}</span>} + @n_open_folds << @line_fold + begin_line + end + + def close_open_fold + unless @n_open_folds.empty? + finish_line + @out << "</div>" + @n_open_folds.pop + begin_line + end + end + + def close_open_folds + finish_line unless @line.empty? + + # close section and make it open + while @n_open_folds.size > 0 + fold_name = @n_open_folds.pop + @out << "</div>" + @out << "<script>var fold = document.getElementById('#{fold_name}'); if(fold) { fold.classList.add(\"open\"); }</script>" + end + end + def reset @fg_color = nil @bg_color = nil @@ -218,5 +309,9 @@ module Ansi2html def get_color_class(segments) [segments].flatten.compact.join('-') end + + def beautify_duration(duration) + "#{duration.round(2)}s" + end end end diff --git a/lib/extension.rb b/lib/extension.rb new file mode 100644 index 0000000..f291697 --- /dev/null +++ b/lib/extension.rb @@ -0,0 +1,40 @@ +class Extension + def self.deep_merge(hash, other_hash, &block) + deep_merge!(hash.dup, other_hash, &block) + end + + # Same as +deep_merge+, but modifies +self+. + def self.deep_merge!(hash, other_hash, &block) + hash.merge!(other_hash) do |k, old, new| + if block + block.call(k, old, new) + elsif old.is_a?(Array) && new.is_a?(Array) + old + new + elsif old.is_a?(Hash) && new.is_a?(Hash) + deep_merge(old, new) + else + new + end + end + end + + + + def self.deep_symbolize_keys(hash) + hash.inject({}) { |result, (key, value)| + result[(key.to_sym rescue key) || key] = case value + when Array + value.map { |value| value.is_a?(Hash) ? Extension.deep_symbolize_keys(value) : value } + when Hash + Extension.deep_symbolize_keys(value) + else + value + end + result + } + end + + def self.deep_symbolize_keys!(hash) + hash.replace(hash.deep_symbolize_keys) + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 75d5610..e218273 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -135,7 +135,7 @@ describe Commit do FactoryGirl.create :job, job_type: :deploy, project: project project.reload - commit.create_deploy_builds(commit.ref) + commit.create_deploy_builds(commit.ref, commit.origin_ref) commit.builds.reload commit.builds.size.should == 1 diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 2960388..4a82318 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -18,7 +18,7 @@ describe API::API do it "should start a build" do commit = FactoryGirl.create(:commit, project: project) job = FactoryGirl.create :job, project: project - build = commit.create_builds.first + build = commit.create_builds(commit.origin_ref).first post api("/builds/register"), token: runner.token, info: {platform: :darwin} |