jenkins_pipeline_builder 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +65 -0
- data/LICENSE +1 -1
- data/README.md +64 -2
- data/Rakefile +13 -11
- data/bin/generate +2 -4
- data/jenkins_pipeline_builder.gemspec +14 -11
- data/lib/jenkins_pipeline_builder.rb +2 -1
- data/lib/jenkins_pipeline_builder/builders.rb +89 -33
- data/lib/jenkins_pipeline_builder/cli/base.rb +21 -29
- data/lib/jenkins_pipeline_builder/cli/helper.rb +12 -15
- data/lib/jenkins_pipeline_builder/cli/pipeline.rb +6 -1
- data/lib/jenkins_pipeline_builder/cli/view.rb +2 -2
- data/lib/jenkins_pipeline_builder/compiler.rb +58 -56
- data/lib/jenkins_pipeline_builder/generator.rb +362 -172
- data/lib/jenkins_pipeline_builder/job_builder.rb +48 -45
- data/lib/jenkins_pipeline_builder/module_registry.rb +4 -6
- data/lib/jenkins_pipeline_builder/publishers.rb +53 -38
- data/lib/jenkins_pipeline_builder/pull_request.rb +156 -0
- data/lib/jenkins_pipeline_builder/triggers.rb +24 -25
- data/lib/jenkins_pipeline_builder/utils.rb +13 -7
- data/lib/jenkins_pipeline_builder/version.rb +2 -2
- data/lib/jenkins_pipeline_builder/view.rb +120 -98
- data/lib/jenkins_pipeline_builder/wrappers.rb +44 -44
- data/lib/jenkins_pipeline_builder/xml_helper.rb +4 -4
- data/spec/func_tests/spec_helper.rb +2 -2
- data/spec/func_tests/view_spec.rb +6 -6
- data/spec/unit_tests/compiler_spec.rb +7 -7
- data/spec/unit_tests/fixtures/files/Job-Gem-Build.xml +2 -2
- data/spec/unit_tests/fixtures/files/Job-Gem-Build.yaml +1 -0
- data/spec/unit_tests/fixtures/files/concurrent_build.xml +17 -0
- data/spec/unit_tests/fixtures/files/concurrent_build.yaml +4 -0
- data/spec/unit_tests/fixtures/files/downstream_blocking.xml +19 -0
- data/spec/unit_tests/fixtures/files/downstream_blocking.yaml +15 -0
- data/spec/unit_tests/fixtures/files/groovy_postbuild.xml +29 -0
- data/spec/unit_tests/fixtures/files/groovy_postbuild.yaml +9 -0
- data/spec/unit_tests/generator_spec.rb +30 -25
- data/spec/unit_tests/module_registry_spec.rb +9 -9
- data/spec/unit_tests/resolve_dependencies_spec.rb +108 -89
- data/spec/unit_tests/spec_helper.rb +1 -1
- metadata +62 -4
@@ -1,5 +1,5 @@
|
|
1
1
|
#
|
2
|
-
# Copyright (c) 2014
|
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
|
-
|
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
|
44
|
+
# Register the CLI::Pipeline class as 'pipeline' subcommand to CLI
|
53
45
|
register(
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
52
|
+
# Register the CLI::Job class as 'view' subcommand to CLI
|
61
53
|
register(
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
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
|
-
|
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 =
|
56
|
-
msg <<
|
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(
|
64
|
-
generator.debug = options[:debug]
|
65
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
63
|
-
|
79
|
+
success, payload = compile(value, settings, job_collection)
|
80
|
+
unless success
|
81
|
+
errors.merge!(payload)
|
82
|
+
next
|
64
83
|
end
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
85
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
107
|
-
|
104
|
+
if payload.nil?
|
105
|
+
errors[value] = "Failed to resolve:\n===>item #{value}\n\n===>of list: #{item}"
|
106
|
+
next
|
108
107
|
end
|
109
|
-
|
108
|
+
result << payload
|
109
|
+
end
|
110
|
+
return false, errors unless errors.empty?
|
111
|
+
return true, result
|
110
112
|
end
|
111
|
-
|
113
|
+
[true, item]
|
112
114
|
end
|
113
115
|
end
|
114
116
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
#
|
2
|
-
# Copyright (c) 2014
|
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(
|
38
|
+
def initialize(client)
|
39
39
|
@client = client
|
40
40
|
@logger = @client.logger
|
41
|
-
|
41
|
+
# @logger.level = (@debug) ? Logger::DEBUG : Logger::INFO;
|
42
42
|
@job_templates = {}
|
43
43
|
@job_collection = {}
|
44
|
-
|
45
|
-
@
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
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,
|
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
|
-
|
157
|
-
|
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
|
166
|
-
|
167
|
-
|
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
|
-
|
170
|
-
|
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
|
-
#
|
173
|
-
|
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
|
-
|
178
|
-
|
310
|
+
end
|
311
|
+
|
312
|
+
def process_job_changes(jobs)
|
179
313
|
jobs.each do |job|
|
180
314
|
job_id = job.keys.first
|
181
|
-
|
182
|
-
|
183
|
-
if
|
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
|
-
|
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
|
-
|
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,
|
373
|
+
return false, 'Encountered errors exiting' unless errors.empty?
|
215
374
|
|
216
|
-
|
375
|
+
[true, project]
|
217
376
|
end
|
218
377
|
|
219
378
|
def resolve_job_by_name(name, settings = {})
|
220
379
|
job = get_item(name)
|
221
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
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
|
-
|
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
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
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
|
-
|
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.
|
547
|
+
if params.key?(:template)
|
358
548
|
template_name = params[:template]
|
359
|
-
|
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
|
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
|
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(:
|
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(:
|
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.
|
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
|