jenkins_pipeline_builder 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +65 -0
  3. data/LICENSE +1 -1
  4. data/README.md +64 -2
  5. data/Rakefile +13 -11
  6. data/bin/generate +2 -4
  7. data/jenkins_pipeline_builder.gemspec +14 -11
  8. data/lib/jenkins_pipeline_builder.rb +2 -1
  9. data/lib/jenkins_pipeline_builder/builders.rb +89 -33
  10. data/lib/jenkins_pipeline_builder/cli/base.rb +21 -29
  11. data/lib/jenkins_pipeline_builder/cli/helper.rb +12 -15
  12. data/lib/jenkins_pipeline_builder/cli/pipeline.rb +6 -1
  13. data/lib/jenkins_pipeline_builder/cli/view.rb +2 -2
  14. data/lib/jenkins_pipeline_builder/compiler.rb +58 -56
  15. data/lib/jenkins_pipeline_builder/generator.rb +362 -172
  16. data/lib/jenkins_pipeline_builder/job_builder.rb +48 -45
  17. data/lib/jenkins_pipeline_builder/module_registry.rb +4 -6
  18. data/lib/jenkins_pipeline_builder/publishers.rb +53 -38
  19. data/lib/jenkins_pipeline_builder/pull_request.rb +156 -0
  20. data/lib/jenkins_pipeline_builder/triggers.rb +24 -25
  21. data/lib/jenkins_pipeline_builder/utils.rb +13 -7
  22. data/lib/jenkins_pipeline_builder/version.rb +2 -2
  23. data/lib/jenkins_pipeline_builder/view.rb +120 -98
  24. data/lib/jenkins_pipeline_builder/wrappers.rb +44 -44
  25. data/lib/jenkins_pipeline_builder/xml_helper.rb +4 -4
  26. data/spec/func_tests/spec_helper.rb +2 -2
  27. data/spec/func_tests/view_spec.rb +6 -6
  28. data/spec/unit_tests/compiler_spec.rb +7 -7
  29. data/spec/unit_tests/fixtures/files/Job-Gem-Build.xml +2 -2
  30. data/spec/unit_tests/fixtures/files/Job-Gem-Build.yaml +1 -0
  31. data/spec/unit_tests/fixtures/files/concurrent_build.xml +17 -0
  32. data/spec/unit_tests/fixtures/files/concurrent_build.yaml +4 -0
  33. data/spec/unit_tests/fixtures/files/downstream_blocking.xml +19 -0
  34. data/spec/unit_tests/fixtures/files/downstream_blocking.yaml +15 -0
  35. data/spec/unit_tests/fixtures/files/groovy_postbuild.xml +29 -0
  36. data/spec/unit_tests/fixtures/files/groovy_postbuild.yaml +9 -0
  37. data/spec/unit_tests/generator_spec.rb +30 -25
  38. data/spec/unit_tests/module_registry_spec.rb +9 -9
  39. data/spec/unit_tests/resolve_dependencies_spec.rb +108 -89
  40. data/spec/unit_tests/spec_helper.rb +1 -1
  41. metadata +62 -4
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2014 Igor Moochnick
2
+ # Copyright (c) 2014 Constant Contact
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -21,49 +21,41 @@
21
21
  #
22
22
 
23
23
  require 'thor'
24
- #require "#{File.dirname(__FILE__)}/pipeline.rb"
25
24
 
26
25
  module JenkinsPipelineBuilder
27
26
  module CLI
28
27
  class Base < Thor
28
+ class_option :username, aliases: '-u', desc: 'Name of Jenkins user'
29
+ class_option :password, aliases: '-p', desc: 'Password of Jenkins user'
30
+ class_option :password_base64, aliases: '-b', desc: 'Base 64 encoded password of Jenkins user'
31
+ class_option :server_ip, aliases: '-s', desc: 'Jenkins server IP address'
32
+ class_option :server_port, aliases: '-o', desc: 'Jenkins port'
33
+ class_option :creds_file, aliases: '-c', desc: 'Credentials file for communicating with Jenkins server'
34
+ class_option :debug, type: :boolean, aliases: '-d', desc: 'Run in debug mode (no Jenkins changes)', default: false
29
35
 
30
- class_option :username, :aliases => "-u", :desc => "Name of Jenkins user"
31
- class_option :password, :aliases => "-p",
32
- :desc => "Password of Jenkins user"
33
- class_option :password_base64, :aliases => "-b",
34
- :desc => "Base 64 encoded password of Jenkins user"
35
- class_option :server_ip, :aliases => "-s",
36
- :desc => "Jenkins server IP address"
37
- class_option :server_port, :aliases => "-o", :desc => "Jenkins port"
38
- class_option :creds_file, :aliases => "-c",
39
- :desc => "Credentials file for communicating with Jenkins server"
40
- class_option :debug, :type => :boolean, :aliases => "-d", :desc => "Run in debug mode (no Jenkins changes)",
41
- :default => false
36
+ map '-v' => :version
42
37
 
43
-
44
- map "-v" => :version
45
-
46
- desc "version", "Shows current version"
38
+ desc 'version', 'Shows current version'
47
39
  # CLI command that returns the version of Jenkins API Client
48
40
  def version
49
41
  puts JenkinsPipelineBuilder::VERSION
50
42
  end
51
43
 
52
- # Register the CLI::Pipeline class as "pipeline" subcommand to CLI
44
+ # Register the CLI::Pipeline class as 'pipeline' subcommand to CLI
53
45
  register(
54
- CLI::Pipeline,
55
- 'pipeline',
56
- 'pipeline [subcommand]',
57
- 'Provides functions to access pipeline functions of the Jenkins CI server'
46
+ CLI::Pipeline,
47
+ 'pipeline',
48
+ 'pipeline [subcommand]',
49
+ 'Provides functions to access pipeline functions of the Jenkins CI server'
58
50
  )
59
51
 
60
- # Register the CLI::Job class as "view" subcommand to CLI
52
+ # Register the CLI::Job class as 'view' subcommand to CLI
61
53
  register(
62
- CLI::View,
63
- 'view',
64
- 'view [subcommand]',
65
- 'Provides functions to access view interface of Jenkins CI server'
54
+ CLI::View,
55
+ 'view',
56
+ 'view [subcommand]',
57
+ 'Provides functions to access view interface of Jenkins CI server'
66
58
  )
67
59
  end
68
60
  end
69
- end
61
+ end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2014 Igor Moochnick
2
+ # Copyright (c) 2014 Constant Contact
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -22,7 +22,11 @@
22
22
 
23
23
  require 'fileutils'
24
24
  require 'yaml'
25
+ require 'json'
25
26
  require 'jenkins_api_client'
27
+ require 'open-uri'
28
+ require 'zlib'
29
+ require 'archive/tar/minitar'
26
30
 
27
31
  module JenkinsPipelineBuilder
28
32
  module CLI
@@ -40,29 +44,22 @@ module JenkinsPipelineBuilder
40
44
  (options[:password] || options[:password_base64])
41
45
  creds = options
42
46
  elsif options[:creds_file]
43
- creds = YAML.load_file(
44
- File.expand_path(options[:creds_file])
45
- #File.expand_path(options[:creds_file], __FILE__)
46
- #options[:creds_file]
47
- )
47
+ creds = YAML.load_file(File.expand_path(options[:creds_file]))
48
48
  elsif File.exist?("#{ENV['HOME']}/.jenkins_api_client/login.yml")
49
49
  creds = YAML.load_file(
50
- File.expand_path(
51
- "#{ENV['HOME']}/.jenkins_api_client/login.yml", __FILE__
52
- )
50
+ File.expand_path("#{ENV['HOME']}/.jenkins_api_client/login.yml", __FILE__)
53
51
  )
54
52
  else
55
- msg = "Credentials are not set. Please pass them as parameters or"
56
- msg << " set them in the default credentials file"
53
+ msg = 'Credentials are not set. Please pass them as parameters or'
54
+ msg << ' set them in the default credentials file'
57
55
  puts msg
58
56
  exit 1
59
57
  end
60
58
 
61
- #creds[:log_level] = Logger::DEBUG
62
59
  client = JenkinsApi::Client.new(creds)
63
- generator = JenkinsPipelineBuilder::Generator.new(options, client)
64
- generator.debug = options[:debug] #== 'debug'
65
- return generator
60
+ generator = JenkinsPipelineBuilder::Generator.new(client)
61
+ generator.debug = options[:debug]
62
+ generator
66
63
  end
67
64
  end
68
65
  end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2014 Igor Moochnick
2
+ # Copyright (c) 2014 Constant Contact
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -35,6 +35,11 @@ module JenkinsPipelineBuilder
35
35
  def bootstrap(path, project_name = nil)
36
36
  Helper.setup(parent_options).bootstrap(path, project_name)
37
37
  end
38
+
39
+ desc 'pull_request Path', 'Generates jenkins jobs based on a git pull request.'
40
+ def pull_request(path, project_name = nil)
41
+ Helper.setup(parent_options).pull_request(path, project_name)
42
+ end
38
43
  end
39
44
  end
40
45
  end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2014 Igor Moochnick
2
+ # Copyright (c) 2014 Constant Contact
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -37,4 +37,4 @@ module JenkinsPipelineBuilder
37
37
  end
38
38
  end
39
39
  end
40
- end
40
+ end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2014 Igor Moochnick
2
+ # Copyright (c) 2014 Constant Contact
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -22,17 +22,27 @@
22
22
 
23
23
  module JenkinsPipelineBuilder
24
24
  class Compiler
25
- def self.resolve_value(value, settings)
25
+ def self.resolve_value(value, settings, job_collection)
26
26
  settings = settings.with_indifferent_access
27
27
  value_s = value.to_s.clone
28
- vars = value_s.scan(/{{([^{}]+)}}/).flatten
28
+ # First we try to do job name correction
29
+ vars = value_s.scan(/{{job@(.*)}}/).flatten
30
+ if vars.count > 0
31
+ vars.select! do |var|
32
+ var_val = job_collection[var.to_s]
33
+ value_s.gsub!("{{job@#{var}}}", var_val[:value][:name]) unless var_val.nil?
34
+ var_val.nil?
35
+ end
36
+ end
37
+ # Then we look for normal values to replace
38
+ vars = value_s.scan(/{{([^{}@]+)}}/).flatten
29
39
  vars.select! do |var|
30
40
  var_val = settings[var]
31
- value_s.gsub!("{{#{var.to_s}}}", var_val) unless var_val.nil?
41
+ value_s.gsub!("{{#{var}}}", var_val) unless var_val.nil?
32
42
  var_val.nil?
33
43
  end
34
44
  return nil if vars.count != 0
35
- return value_s
45
+ value_s
36
46
  end
37
47
 
38
48
  def self.get_settings_bag(item_bag, settings_bag = {})
@@ -42,73 +52,65 @@ module JenkinsPipelineBuilder
42
52
  item.keys.each do |k|
43
53
  val = item[k]
44
54
  if val.kind_of? String
45
- new_value = resolve_value(val, settings_bag)
55
+ new_value = resolve_value(val, settings_bag, {})
46
56
  return nil if new_value.nil?
47
57
  bag[k] = new_value
48
58
  end
49
59
  end
50
60
  my_settings_bag = settings_bag.clone
51
- return my_settings_bag.merge(bag)
61
+ my_settings_bag.merge(bag)
52
62
  end
53
63
 
54
- def self.compile(item, settings = {})
64
+ def self.compile(item, settings = {}, job_collection = {})
55
65
  errors = {}
56
66
  case item
57
- when String
58
- new_value = resolve_value(item, settings)
59
- if new_value.nil?
60
- errors[item] = "Failed to resolve #{item}"
67
+ when String
68
+ new_value = resolve_value(item, settings, job_collection)
69
+ errors[item] = "Failed to resolve #{item}" if new_value.nil?
70
+ return false, errors unless errors.empty?
71
+ return true, new_value
72
+ when Hash
73
+ result = {}
74
+ item.each do |key, value|
75
+ if value.nil?
76
+ errors[key] = "key: #{key} has a nil value, this is likely a yaml syntax error. Skipping children and siblings"
77
+ break
61
78
  end
62
- unless errors.empty?
63
- return false, errors
79
+ success, payload = compile(value, settings, job_collection)
80
+ unless success
81
+ errors.merge!(payload)
82
+ next
64
83
  end
65
- return true, new_value
66
- when Hash
67
- result = {}
68
- item.each do |key, value|
69
- if value.nil?
70
- errors[key] = "key: #{key} has a nil value, this is likely a yaml syntax error. Skipping children and siblings"
71
- break
72
- end
73
- success, payload = compile(value, settings)
74
- unless success
75
- errors.merge!(payload)
76
- next
77
- end
78
- if payload.nil?
79
- errors[key] = "Failed to resolve:\n===>key: #{key}\n\n===>value: #{value}\n\n===>of: #{item}"
80
- next
81
- end
82
- result[key] = payload
84
+ if payload.nil?
85
+ errors[key] = "Failed to resolve:\n===>key: #{key}\n\n===>value: #{value}\n\n===>of: #{item}"
86
+ next
83
87
  end
84
- unless errors.empty?
85
- return false, errors
88
+ result[key] = payload
89
+ end
90
+ return false, errors unless errors.empty?
91
+ return true, result
92
+ when Array
93
+ result = []
94
+ item.each do |value|
95
+ if value.nil?
96
+ errors[item] = "found a nil value when processing following array:\n #{item.inspect}"
97
+ break
86
98
  end
87
- return true, result
88
- when Array
89
- result = []
90
- item.each do |value|
91
- if value.nil?
92
- errors[item] = "found a nil value when processing following array:\n #{item.inspect}"
93
- break
94
- end
95
- success, payload = compile(value, settings)
96
- unless success
97
- errors.merge!(payload)
98
- next
99
- end
100
- if payload.nil?
101
- errors[value] = "Failed to resolve:\n===>item #{value}\n\n===>of list: #{item}"
102
- next
103
- end
104
- result << payload
99
+ success, payload = compile(value, settings, job_collection)
100
+ unless success
101
+ errors.merge!(payload)
102
+ next
105
103
  end
106
- unless errors.empty?
107
- return false, errors
104
+ if payload.nil?
105
+ errors[value] = "Failed to resolve:\n===>item #{value}\n\n===>of list: #{item}"
106
+ next
108
107
  end
109
- return true, result
108
+ result << payload
109
+ end
110
+ return false, errors unless errors.empty?
111
+ return true, result
110
112
  end
111
- return true, item
113
+ [true, item]
112
114
  end
113
115
  end
114
116
  end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2014 Igor Moochnick
2
+ # Copyright (c) 2014 Constant Contact
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -35,84 +35,119 @@ module JenkinsPipelineBuilder
35
35
  #
36
36
  # @raise [ArgumentError] when required options are not provided.
37
37
  #
38
- def initialize(args, client)
38
+ def initialize(client)
39
39
  @client = client
40
40
  @logger = @client.logger
41
- #@logger.level = (@debug) ? Logger::DEBUG : Logger::INFO;
41
+ # @logger.level = (@debug) ? Logger::DEBUG : Logger::INFO;
42
42
  @job_templates = {}
43
43
  @job_collection = {}
44
-
45
- @module_registry = ModuleRegistry.new ({
46
- job: {
47
- description: JobBuilder.method(:change_description),
48
- scm_params: JobBuilder.method(:apply_scm_params),
49
- hipchat: JobBuilder.method(:hipchat_notifier),
50
- parameters: JobBuilder.method(:build_parameters),
51
- priority: JobBuilder.method(:use_specific_priority),
52
- discard_old: JobBuilder.method(:discard_old_param),
53
- throttle: JobBuilder.method(:throttle_job),
54
- prepare_environment: JobBuilder.method(:prepare_environment),
55
- builders: {
56
- registry: {
57
- multi_job: Builders.method(:build_multijob),
58
- inject_vars_file: Builders.method(:build_environment_vars_injector),
59
- shell_command: Builders.method(:build_shell_command),
60
- maven3: Builders.method(:build_maven3),
61
- remote_job: Builders.method(:start_remote_job)
62
- },
63
- method:
64
- lambda { |registry, params, n_xml| @module_registry.run_registry_on_path('//builders', registry, params, n_xml) }
65
- },
66
- publishers: {
67
- registry: {
68
- git: Publishers.method(:push_to_git),
69
- hipchat: Publishers.method(:push_to_hipchat),
70
- description_setter: Publishers.method(:description_setter),
71
- downstream: Publishers.method(:push_to_projects),
72
- junit_result: Publishers.method(:publish_junit),
73
- coverage_result: Publishers.method(:publish_rcov),
74
- post_build_script: Publishers.method(:post_build_script)
75
- },
76
- method:
77
- lambda { |registry, params, n_xml| @module_registry.run_registry_on_path('//publishers', registry, params, n_xml) }
78
- },
79
- wrappers: {
80
- registry: {
81
- timestamp: Wrappers.method(:console_timestamp),
82
- ansicolor: Wrappers.method(:ansicolor),
83
- artifactory: Wrappers.method(:publish_to_artifactory),
84
- rvm: Wrappers.method(:run_with_rvm),
85
- rvm05: Wrappers.method(:run_with_rvm05),
86
- inject_env_var: Wrappers.method(:inject_env_vars),
87
- inject_passwords: Wrappers.method(:inject_passwords),
88
- maven3artifactory: Wrappers.method(:artifactory_maven3_configurator)
89
- },
90
- method:
91
- lambda { |registry, params, n_xml| @module_registry.run_registry_on_path('//buildWrappers', registry, params, n_xml) }
92
- },
93
- triggers: {
94
- registry: {
95
- git_push: Triggers.method(:enable_git_push),
96
- scm_polling: Triggers.method(:enable_scm_polling),
97
- periodic_build: Triggers.method(:enable_periodic_build),
98
- upstream: Triggers.method(:enable_upstream_check)
99
- },
100
- method:
101
- lambda { |registry, params, n_xml| @module_registry.run_registry_on_path('//triggers', registry, params, n_xml) }
102
- }
44
+ @extensions = {}
45
+ @remote_depends = {}
46
+
47
+ @module_registry = ModuleRegistry.new(
48
+ job: {
49
+ description: JobBuilder.method(:change_description),
50
+ scm_params: JobBuilder.method(:apply_scm_params),
51
+ hipchat: JobBuilder.method(:hipchat_notifier),
52
+ parameters: JobBuilder.method(:build_parameters),
53
+ priority: JobBuilder.method(:use_specific_priority),
54
+ discard_old: JobBuilder.method(:discard_old_param),
55
+ throttle: JobBuilder.method(:throttle_job),
56
+ prepare_environment: JobBuilder.method(:prepare_environment),
57
+ concurrent_build: JobBuilder.method(:concurrent_build),
58
+ builders: {
59
+ registry: {
60
+ multi_job: Builders.method(:build_multijob),
61
+ inject_vars_file: Builders.method(:build_environment_vars_injector),
62
+ shell_command: Builders.method(:build_shell_command),
63
+ maven3: Builders.method(:build_maven3),
64
+ blocking_downstream: Builders.method(:blocking_downstream),
65
+ remote_job: Builders.method(:start_remote_job)
66
+ },
67
+ method:
68
+ ->(registry, params, n_xml) { @module_registry.run_registry_on_path('//builders', registry, params, n_xml) }
69
+ },
70
+ publishers: {
71
+ registry: {
72
+ git: Publishers.method(:push_to_git),
73
+ hipchat: Publishers.method(:push_to_hipchat),
74
+ description_setter: Publishers.method(:description_setter),
75
+ downstream: Publishers.method(:push_to_projects),
76
+ junit_result: Publishers.method(:publish_junit),
77
+ coverage_result: Publishers.method(:publish_rcov),
78
+ post_build_script: Publishers.method(:post_build_script),
79
+ groovy_postbuild: Publishers.method(:groovy_postbuild)
80
+ },
81
+ method:
82
+ ->(registry, params, n_xml) { @module_registry.run_registry_on_path('//publishers', registry, params, n_xml) }
83
+ },
84
+ wrappers: {
85
+ registry: {
86
+ timestamp: Wrappers.method(:console_timestamp),
87
+ ansicolor: Wrappers.method(:ansicolor),
88
+ artifactory: Wrappers.method(:publish_to_artifactory),
89
+ rvm: Wrappers.method(:run_with_rvm),
90
+ rvm05: Wrappers.method(:run_with_rvm05),
91
+ inject_env_var: Wrappers.method(:inject_env_vars),
92
+ inject_passwords: Wrappers.method(:inject_passwords),
93
+ maven3artifactory: Wrappers.method(:artifactory_maven3_configurator)
94
+ },
95
+ method:
96
+ ->(registry, params, n_xml) { @module_registry.run_registry_on_path('//buildWrappers', registry, params, n_xml) }
97
+ },
98
+ triggers: {
99
+ registry: {
100
+ git_push: Triggers.method(:enable_git_push),
101
+ scm_polling: Triggers.method(:enable_scm_polling),
102
+ periodic_build: Triggers.method(:enable_periodic_build),
103
+ upstream: Triggers.method(:enable_upstream_check)
104
+ },
105
+ method:
106
+ ->(registry, params, n_xml) { @module_registry.run_registry_on_path('//triggers', registry, params, n_xml) }
103
107
  }
104
- })
108
+ }
109
+ )
110
+ end
111
+
112
+ def load_extensions(path)
113
+ path = "#{path}/extensions"
114
+ path = File.expand_path(path, Dir.getwd)
115
+ if File.directory?(path)
116
+ @logger.info "Loading extensions from folder #{path}"
117
+ Dir[File.join(path, '/*.yaml'), File.join(path, '/*.yml')].each do |file|
118
+ @logger.info "Loading file #{file}"
119
+ yaml = YAML.load_file(file)
120
+ yaml.each do |ext|
121
+ Utils.symbolize_keys_deep!(ext)
122
+ ext = ext[:extension]
123
+ name = ext[:name]
124
+ type = ext[:type]
125
+ function = ext[:function]
126
+ fail "Duplicate extension with name '#{name}' was detected." if @extensions.key?(name)
127
+ @extensions[name.to_s] = { name: name.to_s, type: type, function: function }
128
+ end
129
+ end
130
+ end
131
+ @extensions.each_value do |ext|
132
+ name = ext[:name].to_sym
133
+ registry = @module_registry.registry[:job]
134
+ function = eval "Proc.new {|params,xml| #{ext[:function]} }"
135
+ type = ext[:type].downcase.pluralize.to_sym if ext[:type]
136
+ if type
137
+ registry[type][:registry][name] = function
138
+ else
139
+ registry[name] = function
140
+ end
141
+ end
105
142
  end
106
143
 
107
- attr_accessor :client
108
144
  def debug=(value)
109
145
  @debug = value
110
- @logger.level = (value) ? Logger::DEBUG : Logger::INFO;
111
- end
112
- def debug
113
- @debug
146
+ @logger.level = (value) ? Logger::DEBUG : Logger::INFO
114
147
  end
115
- # TODO: WTF?
148
+
149
+ attr_reader :debug
150
+ attr_accessor :client
116
151
  attr_accessor :no_files
117
152
  attr_accessor :job_collection
118
153
 
@@ -124,12 +159,12 @@ module JenkinsPipelineBuilder
124
159
  JenkinsPipelineBuilder::View.new(self)
125
160
  end
126
161
 
127
- def load_collection_from_path(path, recursively = false)
128
- path = File.expand_path(path, relative_to=Dir.getwd)
162
+ def load_collection_from_path(path, recursively = false, remote = false)
163
+ path = File.expand_path(path, Dir.getwd)
129
164
  if File.directory?(path)
130
165
  @logger.info "Generating from folder #{path}"
131
166
  Dir[File.join(path, '/*.yaml'), File.join(path, '/*.yml')].each do |file|
132
- if File.directory?(file)
167
+ if File.directory?(file) # TODO: This doesn't work unless the folder contains .yml or .yaml at the end
133
168
  if recursively
134
169
  load_collection_from_path(File.join(path, file), recursively)
135
170
  else
@@ -138,23 +173,35 @@ module JenkinsPipelineBuilder
138
173
  end
139
174
  @logger.info "Loading file #{file}"
140
175
  yaml = YAML.load_file(file)
141
- load_job_collection(yaml)
176
+ load_job_collection(yaml, remote)
142
177
  end
143
178
  else
144
179
  @logger.info "Loading file #{path}"
145
180
  yaml = YAML.load_file(path)
146
- load_job_collection(yaml)
181
+ load_job_collection(yaml, remote)
147
182
  end
148
183
  end
149
184
 
150
- def load_job_collection(yaml)
185
+ def load_job_collection(yaml, remote = false)
151
186
  yaml.each do |section|
152
187
  Utils.symbolize_keys_deep!(section)
153
188
  key = section.keys.first
154
189
  value = section[key]
190
+ if key == :dependencies
191
+ @logger.info 'Resolving Dependencies for remote project'
192
+ return load_remote_yaml(value)
193
+ end
194
+
155
195
  name = value[:name]
156
- raise "Duplicate item with name '#{name}' was detected." if @job_collection.has_key?(name)
157
- @job_collection[name.to_s] = { name: name.to_s, type: key, value: value }
196
+ if @job_collection.key?(name)
197
+ if remote
198
+ @logger.info "Duplicate item with name '#{name}' was detected from the remote folder."
199
+ else
200
+ fail "Duplicate item with name '#{name}' was detected."
201
+ end
202
+ else
203
+ @job_collection[name.to_s] = { name: name.to_s, type: key, value: value }
204
+ end
158
205
  end
159
206
  end
160
207
 
@@ -162,33 +209,116 @@ module JenkinsPipelineBuilder
162
209
  @job_collection[name.to_s]
163
210
  end
164
211
 
165
- def resolve_project(project)
166
- defaults = get_item('global')
167
- settings = defaults.nil? ? {} : defaults[:value] || {}
212
+ def load_template(path, template)
213
+ # If we specify what folder the yaml is in, load that
214
+ if template[:folder]
215
+ path = File.join(path, template[:folder])
216
+ else
217
+ path = File.join(path, template[:name]) unless template[:name] == 'default'
218
+ # If we are looking for the newest version or no version was set
219
+ if (template[:version].nil? || template[:version] == 'newest') && File.directory?(path)
220
+ folders = Dir.entries(path)
221
+ highest = '0' # Default to v1
222
+ folders.each do |f|
223
+ highest = f if f > highest # Note: to_i returns any integers in the folder name
224
+ end
225
+ template[:version] = highest unless highest == 0
226
+ end
227
+ path = File.join(path, template[:version]) unless template[:version].nil?
228
+ path = File.join(path, 'pipeline')
229
+ end
168
230
 
169
- project[:settings] = Compiler.get_settings_bag(project, settings) unless project[:settings]
170
- project_body = project[:value]
231
+ if File.directory?(path)
232
+ @logger.info "Loading from #{path}"
233
+ load_collection_from_path(path, false, true)
234
+ true
235
+ else
236
+ false
237
+ end
238
+ end
239
+
240
+ def download_yaml(url, file)
241
+ @remote_depends[url] = file
242
+ @logger.info "Downloading #{url} to #{file}.tar"
243
+ open("#{file}.tar", 'w') do |local_file|
244
+ open(url) do |remote_file|
245
+ local_file.write(Zlib::GzipReader.new(remote_file).read)
246
+ end
247
+ end
171
248
 
172
- # Process jobs
173
- jobs = project_body[:jobs] || []
249
+ # Extract Tar.gz to 'remote' folder
250
+ @logger.info "Unpacking #{file}.tar to #{file} folder"
251
+ Archive::Tar::Minitar.unpack("#{file}.tar", file)
252
+ end
253
+
254
+ def load_remote_yaml(dependencies)
255
+ ### Load remote YAML
256
+ # Download Tar.gz
257
+ dependencies.each do |source|
258
+ source = source[:source]
259
+ url = source[:url]
260
+ file = "remote-#{@remote_depends.length}"
261
+ if @remote_depends[url]
262
+ file = @remote_depends[url]
263
+ else
264
+ download_yaml(url, file)
265
+ end
266
+
267
+ path = File.expand_path(file, Dir.getwd)
268
+ # Load templates recursively
269
+ unless source[:templates]
270
+ @logger.info 'No specific template specified'
271
+ # Try to load the folder or the pipeline folder
272
+ path = File.join(path, 'pipeline') if Dir.entries(path).include? 'pipeline'
273
+ return load_collection_from_path(path)
274
+ end
275
+
276
+ load_templates(source[:templates])
277
+ end
278
+ end
279
+
280
+ def load_templates(templates)
281
+ templates.each do |template|
282
+ version = template[:version] || 'newest'
283
+ @logger.info "Loading #{template[:name]} at version #{version}"
284
+ # Move into the remote folder and look for the template folder
285
+ remote = Dir.entries(path)
286
+ if remote.include? template[:name]
287
+ # We found the template name, load this path
288
+ @logger.info 'We found the template!'
289
+ load_template(path, template)
290
+ else
291
+ # Many cases we must dig one layer deep
292
+ remote.each do |file|
293
+ load_template(File.join(path, file), template)
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ def cleanup_temp_remote
300
+ @remote_depends.each_value do |file|
301
+ FileUtils.rm_r file
302
+ FileUtils.rm_r "#{file}.tar"
303
+ end
304
+ end
305
+
306
+ def prepare_jobs(jobs)
174
307
  jobs.map! do |job|
175
308
  job.kind_of?(String) ? { job.to_sym => {} } : job
176
309
  end
177
- errors = {}
178
- @logger.info project
310
+ end
311
+
312
+ def process_job_changes(jobs)
179
313
  jobs.each do |job|
180
314
  job_id = job.keys.first
181
- settings = project[:settings].clone.merge(job[job_id])
182
- success, payload = resolve_job_by_name(job_id, settings)
183
- if success
184
- job[:result] = payload
185
- else
186
- errors[job_id] = payload
187
- end
315
+ j = get_item(job_id)
316
+ Utils.hash_merge!(j, job[job_id])
317
+ j[:value][:name] = j[:job_name] if j[:job_name]
188
318
  end
319
+ end
189
320
 
190
- # Process views
191
- views = project_body[:views] || []
321
+ def process_views(views, project, errors = {})
192
322
  views.map! do |view|
193
323
  view.kind_of?(String) ? { view.to_sym => {} } : view
194
324
  end
@@ -203,27 +333,55 @@ module JenkinsPipelineBuilder
203
333
  errors[view_id] = payload
204
334
  end
205
335
  end
336
+ errors
337
+ end
206
338
 
207
- errors.each do |k,v|
339
+ def process_jobs(jobs, project, errors = {})
340
+ jobs.each do |job|
341
+ job_id = job.keys.first
342
+ settings = project[:settings].clone.merge(job[job_id])
343
+ success, payload = resolve_job_by_name(job_id, settings)
344
+ if success
345
+ job[:result] = payload
346
+ else
347
+ errors[job_id] = payload
348
+ end
349
+ end
350
+ errors
351
+ end
352
+
353
+ def resolve_project(project)
354
+ defaults = get_item('global')
355
+ settings = defaults.nil? ? {} : defaults[:value] || {}
356
+
357
+ project[:settings] = Compiler.get_settings_bag(project, settings) unless project[:settings]
358
+ project_body = project[:value]
359
+
360
+ jobs = prepare_jobs(project_body[:jobs]) if project_body[:jobs]
361
+ @logger.info project
362
+ process_job_changes(jobs)
363
+ errors = process_jobs(jobs, project)
364
+ errors = process_view(project_body[:views], project, errors) if project_body[:views]
365
+
366
+ errors.each do |k, v|
208
367
  puts "Encountered errors processing: #{k}:"
209
368
  v.each do |key, error|
210
369
  puts " key: #{key} had the following error:"
211
370
  puts " #{error.inspect}"
212
371
  end
213
372
  end
214
- return false, "Encountered errors exiting" unless errors.empty?
373
+ return false, 'Encountered errors exiting' unless errors.empty?
215
374
 
216
- return true, project
375
+ [true, project]
217
376
  end
218
377
 
219
378
  def resolve_job_by_name(name, settings = {})
220
379
  job = get_item(name)
221
- raise "Failed to locate job by name '#{name}'" if job.nil?
380
+ fail "Failed to locate job by name '#{name}'" if job.nil?
222
381
  job_value = job[:value]
223
382
  @logger.debug "Compiling job #{name}"
224
-
225
- success, payload = Compiler.compile(job_value, settings)
226
- return success, payload
383
+ success, payload = Compiler.compile(job_value, settings, @job_collection)
384
+ [success, payload]
227
385
  end
228
386
 
229
387
  def projects
@@ -231,7 +389,7 @@ module JenkinsPipelineBuilder
231
389
  @job_collection.values.each do |item|
232
390
  result << item if item[:type] == :project
233
391
  end
234
- return result
392
+ result
235
393
  end
236
394
 
237
395
  def jobs
@@ -239,64 +397,96 @@ module JenkinsPipelineBuilder
239
397
  @job_collection.values.each do |item|
240
398
  result << item if item[:type] == :job
241
399
  end
242
- return result
400
+ result
401
+ end
402
+
403
+ def publish_project(project_name, errors = {})
404
+ projects.each do |project|
405
+ next if project_name && project[:name] == project_name
406
+ success, payload = resolve_project(project)
407
+ if success
408
+ puts 'successfully resolved project'
409
+ compiled_project = payload
410
+ else
411
+ puts payload
412
+ return false
413
+ end
414
+
415
+ if compiled_project[:value][:jobs]
416
+ errors = publish_jobs(compiled_project[:value][:jobs])
417
+ end
418
+ if compiled_project[:value][:views]
419
+ compiled_project[:value][:views].each do |v|
420
+ compiled_view = v[:result]
421
+ view.create(compiled_view)
422
+ end
423
+ end
424
+ end
425
+ errors
426
+ end
427
+
428
+ def publish_jobs(jobs, errors = {})
429
+ jobs.each do |i|
430
+ puts "Processing #{i}"
431
+ job = i[:result]
432
+ fail "Result is empty for #{i}" if job.nil?
433
+ success, payload = compile_job_to_xml(job)
434
+ if success
435
+ create_or_update(job, payload)
436
+ else
437
+ errors[job[:name]] = payload
438
+ end
439
+ end
440
+ errors
243
441
  end
244
442
 
245
443
  def bootstrap(path, project_name)
246
444
  @logger.info "Bootstrapping pipeline from path #{path}"
247
445
  load_collection_from_path(path)
248
-
446
+ cleanup_temp_remote
447
+ load_extensions(path)
249
448
  errors = {}
250
449
  # Publish all the jobs if the projects are not found
251
450
  if projects.count == 0
252
- jobs.each do |i|
253
- job = i[:value]
254
- success, payload = compile_job_to_xml(job)
255
- if success
256
- create_or_update(job, payload)
257
- else
258
- errors[job[:name]] = payload
259
- end
260
- end
451
+ errors = publish_jobs(jobs)
261
452
  else
262
- projects.each do |project|
263
- success, payload = resolve_project(project)
264
- if payload[:value][:name] == project_name || project_name == nil # If we specify a project name, only use that project
265
- if success
266
- puts 'successfully resolved project'
267
- compiled_project = payload
268
- else
269
- puts payload
270
- return false
271
- end
272
-
273
- if compiled_project[:value][:jobs]
274
- compiled_project[:value][:jobs].each do |i|
275
- puts "Processing #{i}"
276
- job = i[:result]
277
- fail "Result is empty for #{i}" if job.nil?
278
- success, payload = compile_job_to_xml(job)
279
- if success
280
- create_or_update(job, payload)
281
- else
282
- errors[job[:name]] = payload
283
- end
284
- end
285
- end
453
+ errors = publish_project(project_name)
454
+ end
455
+ errors.each do |k, v|
456
+ @logger.error "Encountered errors compiling: #{k}:"
457
+ @logger.error v
458
+ end
459
+ end
286
460
 
287
- if compiled_project[:value][:views]
288
- compiled_project[:value][:views].each do |v|
289
- _view = v[:result]
290
- view.create(_view)
291
- end
461
+ def pull_request(path, project_name)
462
+ @logger.info "Pull Request Generator Running from path #{path}"
463
+ load_collection_from_path(path)
464
+ cleanup_temp_remote
465
+ load_extensions(path)
466
+ jobs = {}
467
+ projects.each do |project|
468
+ if project[:name] == project_name || project_name.nil?
469
+ project_body = project[:value]
470
+ project_jobs = project_body[:jobs] || []
471
+ @logger.info "Using Project #{project}"
472
+ pull_job = nil
473
+ project_jobs.each do |job|
474
+ job = @job_collection[job.to_s]
475
+ pull_job = job if job[:value][:job_type] == 'pull_request_generator'
476
+ end
477
+ fail 'No Pull Request Found for Project' unless pull_job
478
+ pull_jobs = pull_job[:value][:jobs] || []
479
+ pull_jobs.each do |job|
480
+ if job.is_a? String
481
+ jobs[job.to_s] = @job_collection[job.to_s]
482
+ else
483
+ jobs[job.keys[0].to_s] = @job_collection[job.keys[0].to_s]
292
484
  end
293
485
  end
486
+ pull = JenkinsPipelineBuilder::PullRequestGenerator.new(self)
487
+ pull.run(project, jobs, pull_job)
294
488
  end
295
489
  end
296
- errors.each do |k,v|
297
- @logger.error "Encountered errors compiling: #{k}:"
298
- @logger.error v
299
- end
300
490
  end
301
491
 
302
492
  def dump(job_name)
@@ -323,40 +513,40 @@ module JenkinsPipelineBuilder
323
513
  end
324
514
 
325
515
  def compile_job_to_xml(job)
326
- raise 'Job name is not specified' unless job[:name]
516
+ fail 'Job name is not specified' unless job[:name]
327
517
 
328
518
  @logger.info "Creating Yaml Job #{job}"
329
519
  job[:job_type] = 'free_style' unless job[:job_type]
330
520
  case job[:job_type]
331
- when 'job_dsl'
332
- xml = compile_freestyle_job_to_xml(job)
333
- payload = update_job_dsl(job, xml)
334
- when 'multi_project'
335
- xml = compile_freestyle_job_to_xml(job)
336
- payload = adjust_multi_project xml
337
- when 'build_flow'
338
- xml = compile_freestyle_job_to_xml(job)
339
- payload = add_job_dsl(job, xml)
340
- when 'free_style'
341
- payload = compile_freestyle_job_to_xml job
342
- else
343
- return false, "Job type: #{job[:job_type]} is not one of job_dsl, multi_project, build_flow or free_style"
521
+ when 'job_dsl'
522
+ xml = compile_freestyle_job_to_xml(job)
523
+ payload = update_job_dsl(job, xml)
524
+ when 'multi_project'
525
+ xml = compile_freestyle_job_to_xml(job)
526
+ payload = adjust_multi_project xml
527
+ when 'build_flow'
528
+ xml = compile_freestyle_job_to_xml(job)
529
+ payload = add_job_dsl(job, xml)
530
+ when 'free_style', 'pull_request_generator'
531
+ payload = compile_freestyle_job_to_xml job
532
+ else
533
+ return false, "Job type: #{job[:job_type]} is not one of job_dsl, multi_project, build_flow or free_style"
344
534
  end
345
535
 
346
- return true, payload
536
+ [true, payload]
347
537
  end
348
538
 
349
539
  def adjust_multi_project(xml)
350
540
  n_xml = Nokogiri::XML(xml)
351
- root = n_xml.root()
541
+ root = n_xml.root
352
542
  root.name = 'com.tikal.jenkins.plugins.multijob.MultiJobProject'
353
543
  n_xml.to_xml
354
544
  end
355
545
 
356
546
  def compile_freestyle_job_to_xml(params)
357
- if params.has_key?(:template)
547
+ if params.key?(:template)
358
548
  template_name = params[:template]
359
- raise "Job template '#{template_name}' can't be resolved." unless @job_templates.has_key?(template_name)
549
+ fail "Job template '#{template_name}' can't be resolved." unless @job_templates.key?(template_name)
360
550
  params.delete(:template)
361
551
  template = @job_templates[template_name]
362
552
  puts "Template found: #{template}"
@@ -373,7 +563,7 @@ module JenkinsPipelineBuilder
373
563
  end
374
564
 
375
565
  def add_job_dsl(job, xml)
376
- n_xml = Nokogiri::XML(xml)
566
+ n_xml = Nokogiri::XML(xml)
377
567
  n_xml.root.name = 'com.cloudbees.plugins.flow.BuildFlow'
378
568
  Nokogiri::XML::Builder.with(n_xml.root) do |xml|
379
569
  xml.dsl job[:build_flow]
@@ -392,25 +582,25 @@ module JenkinsPipelineBuilder
392
582
  end
393
583
 
394
584
  def generate_job_dsl_body(params)
395
- @logger.info "Generating pipeline"
585
+ @logger.info 'Generating pipeline'
396
586
 
397
587
  xml = @client.job.build_freestyle_config(params)
398
588
 
399
589
  n_xml = Nokogiri::XML(xml)
400
590
  if n_xml.xpath('//javaposse.jobdsl.plugin.ExecuteDslScripts').empty?
401
- p_xml = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |b_xml|
591
+ p_xml = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |b_xml|
402
592
  build_job_dsl(params, b_xml)
403
593
  end
404
594
 
405
- n_xml.xpath('//builders').first.add_child("\r\n" + p_xml.doc.root.to_xml(:indent => 4) + "\r\n")
595
+ n_xml.xpath('//builders').first.add_child("\r\n" + p_xml.doc.root.to_xml(indent: 4) + "\r\n")
406
596
  xml = n_xml.to_xml
407
597
  end
408
598
  xml
409
599
  end
410
600
 
411
601
  def build_job_dsl(job, xml)
412
- xml.send('javaposse.jobdsl.plugin.ExecuteDslScripts') {
413
- if job.has_key?(:job_dsl)
602
+ xml.send('javaposse.jobdsl.plugin.ExecuteDslScripts') do
603
+ if job.key?(:job_dsl)
414
604
  xml.scriptText job[:job_dsl]
415
605
  xml.usingScriptText true
416
606
  else
@@ -419,7 +609,7 @@ module JenkinsPipelineBuilder
419
609
  end
420
610
  xml.ignoreExisting false
421
611
  xml.removedJobAction 'IGNORE'
422
- }
612
+ end
423
613
  end
424
614
  end
425
615
  end