buildkite-builder 1.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/CHANGELOG.md +0 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +13 -0
  7. data/Gemfile.lock +56 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +44 -0
  10. data/Rakefile +6 -0
  11. data/bin/buildkite-builder +6 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/buildkite-builder.gemspec +28 -0
  15. data/lib/buildkite-builder.rb +14 -0
  16. data/lib/buildkite/builder.rb +54 -0
  17. data/lib/buildkite/builder/commands.rb +49 -0
  18. data/lib/buildkite/builder/commands/abstract.rb +64 -0
  19. data/lib/buildkite/builder/commands/files.rb +25 -0
  20. data/lib/buildkite/builder/commands/preview.rb +29 -0
  21. data/lib/buildkite/builder/definition.rb +24 -0
  22. data/lib/buildkite/builder/file_resolver.rb +59 -0
  23. data/lib/buildkite/builder/github.rb +71 -0
  24. data/lib/buildkite/builder/loaders.rb +12 -0
  25. data/lib/buildkite/builder/loaders/abstract.rb +40 -0
  26. data/lib/buildkite/builder/loaders/manifests.rb +23 -0
  27. data/lib/buildkite/builder/loaders/processors.rb +37 -0
  28. data/lib/buildkite/builder/loaders/templates.rb +25 -0
  29. data/lib/buildkite/builder/logging_utils.rb +24 -0
  30. data/lib/buildkite/builder/manifest.rb +89 -0
  31. data/lib/buildkite/builder/manifest/rule.rb +51 -0
  32. data/lib/buildkite/builder/processors.rb +9 -0
  33. data/lib/buildkite/builder/processors/abstract.rb +76 -0
  34. data/lib/buildkite/builder/rainbow.rb +9 -0
  35. data/lib/buildkite/builder/runner.rb +114 -0
  36. data/lib/buildkite/env.rb +52 -0
  37. data/lib/buildkite/pipelines.rb +13 -0
  38. data/lib/buildkite/pipelines/api.rb +119 -0
  39. data/lib/buildkite/pipelines/attributes.rb +137 -0
  40. data/lib/buildkite/pipelines/command.rb +59 -0
  41. data/lib/buildkite/pipelines/helpers.rb +43 -0
  42. data/lib/buildkite/pipelines/helpers/block.rb +18 -0
  43. data/lib/buildkite/pipelines/helpers/command.rb +21 -0
  44. data/lib/buildkite/pipelines/helpers/depends_on.rb +13 -0
  45. data/lib/buildkite/pipelines/helpers/key.rb +13 -0
  46. data/lib/buildkite/pipelines/helpers/label.rb +18 -0
  47. data/lib/buildkite/pipelines/helpers/plugins.rb +24 -0
  48. data/lib/buildkite/pipelines/helpers/retry.rb +20 -0
  49. data/lib/buildkite/pipelines/helpers/skip.rb +15 -0
  50. data/lib/buildkite/pipelines/helpers/soft_fail.rb +15 -0
  51. data/lib/buildkite/pipelines/helpers/timeout_in_minutes.rb +17 -0
  52. data/lib/buildkite/pipelines/pipeline.rb +129 -0
  53. data/lib/buildkite/pipelines/plugin.rb +23 -0
  54. data/lib/buildkite/pipelines/steps.rb +15 -0
  55. data/lib/buildkite/pipelines/steps/abstract.rb +26 -0
  56. data/lib/buildkite/pipelines/steps/block.rb +20 -0
  57. data/lib/buildkite/pipelines/steps/command.rb +30 -0
  58. data/lib/buildkite/pipelines/steps/input.rb +20 -0
  59. data/lib/buildkite/pipelines/steps/skip.rb +28 -0
  60. data/lib/buildkite/pipelines/steps/trigger.rb +22 -0
  61. data/lib/buildkite/pipelines/steps/wait.rb +18 -0
  62. data/lib/vendor/rainbow/Changelog.md +101 -0
  63. data/lib/vendor/rainbow/Gemfile +30 -0
  64. data/lib/vendor/rainbow/LICENSE +20 -0
  65. data/lib/vendor/rainbow/README.markdown +225 -0
  66. data/lib/vendor/rainbow/Rakefile +11 -0
  67. data/lib/vendor/rainbow/lib/rainbow.rb +13 -0
  68. data/lib/vendor/rainbow/lib/rainbow/color.rb +150 -0
  69. data/lib/vendor/rainbow/lib/rainbow/ext/string.rb +64 -0
  70. data/lib/vendor/rainbow/lib/rainbow/global.rb +25 -0
  71. data/lib/vendor/rainbow/lib/rainbow/null_presenter.rb +100 -0
  72. data/lib/vendor/rainbow/lib/rainbow/presenter.rb +144 -0
  73. data/lib/vendor/rainbow/lib/rainbow/refinement.rb +14 -0
  74. data/lib/vendor/rainbow/lib/rainbow/string_utils.rb +22 -0
  75. data/lib/vendor/rainbow/lib/rainbow/version.rb +5 -0
  76. data/lib/vendor/rainbow/lib/rainbow/wrapper.rb +22 -0
  77. data/lib/vendor/rainbow/lib/rainbow/x11_color_names.rb +153 -0
  78. data/lib/vendor/rainbow/rainbow.gemspec +23 -0
  79. metadata +126 -0
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'pathname'
5
+
6
+ module Buildkite
7
+ module Builder
8
+ class Manifest::Rule
9
+ GLOB_OPTIONS = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
10
+
11
+ attr_reader :exclude
12
+ attr_reader :glob
13
+
14
+ def initialize(root, pattern)
15
+ @root = Pathname.new(root)
16
+ @exclude = false
17
+ @glob = @root
18
+
19
+ if pattern[0] == '!'
20
+ @exclude = true
21
+ pattern = pattern[1..-1]
22
+ end
23
+
24
+ if pattern.start_with?('/')
25
+ pattern = pattern[1..-1]
26
+ else
27
+ @glob = @glob.join('**')
28
+ end
29
+
30
+ @glob = @glob.join(pattern).to_s
31
+ end
32
+
33
+ def files
34
+ @files ||= begin
35
+ matched = Dir.glob(glob, GLOB_OPTIONS)
36
+ matched.map! { |file| Pathname.new(file) }
37
+ matched.reject!(&:directory?)
38
+ matched.map! { |file| file.relative_path_from(Builder.root) }
39
+ SortedSet.new(matched)
40
+ end
41
+ end
42
+
43
+ def match?(file)
44
+ file = Pathname.new(file)
45
+ file = @root.join(file) unless file.absolute?
46
+
47
+ File.fnmatch?(glob, file.to_s, GLOB_OPTIONS)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buildkite
4
+ module Builder
5
+ module Processors
6
+ autoload :Abstract, File.expand_path('processors/abstract', __dir__)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buildkite
4
+ module Builder
5
+ module Processors
6
+ class Abstract
7
+ include LoggingUtils
8
+ using Rainbow
9
+
10
+ def self.process(runner)
11
+ new(runner).run
12
+ end
13
+
14
+ def initialize(runner)
15
+ @runner = runner
16
+ end
17
+
18
+ def run
19
+ _log_run { process }
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :runner
25
+
26
+ def process
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def log
31
+ runner.log
32
+ end
33
+
34
+ def pipeline
35
+ runner.pipeline
36
+ end
37
+
38
+ def buildkite
39
+ @buildkite ||= begin
40
+ unless Buildkite.env
41
+ raise 'Must be in Buildkite environment to access the Buildkite API'
42
+ end
43
+
44
+ Buildkite::Pipelines::Api.new(Buildkite.env.api_token)
45
+ end
46
+ end
47
+
48
+ def pipeline_steps(*types)
49
+ steps = pipeline.steps
50
+ types = types.flatten
51
+ steps = steps.select { |step| types.include?(step.class.to_sym) } if types.any?
52
+ steps
53
+ end
54
+
55
+ def _log_run
56
+ log.info "\nProcessing ".color(:dimgray) + self.class.name.color(:springgreen)
57
+
58
+ results = benchmark('└──'.color(:springgreen) + ' Finished in %s'.color(:dimgray)) do
59
+ formatter = log.formatter
60
+ log.formatter = proc do |_severity, _datetime, _progname, msg|
61
+ '│'.color(:springgreen) + " #{msg}\n"
62
+ end
63
+
64
+ begin
65
+ yield
66
+ ensure
67
+ log.formatter = formatter
68
+ end
69
+ end
70
+
71
+ log.info results
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../vendor/rainbow/lib/rainbow/refinement'
4
+
5
+ module Buildkite
6
+ module Builder
7
+ Rainbow = ::Rainbow
8
+ end
9
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+ require 'pathname'
5
+ require 'tempfile'
6
+
7
+ module Buildkite
8
+ module Builder
9
+ class Runner
10
+ include Definition::Helper
11
+ include LoggingUtils
12
+ using Rainbow
13
+
14
+ PIPELINES_PATH = Pathname.new('.buildkite/pipelines').freeze
15
+ PIPELINE_DEFINITION_PATH = Pathname.new('pipeline.rb').freeze
16
+
17
+ attr_reader :options
18
+
19
+ # This entrypoint is for running on CI. It expects certain environment variables to
20
+ # be set.
21
+ def self.run
22
+ new(
23
+ upload: true,
24
+ pipeline: Buildkite.env.pipeline_slug
25
+ ).run
26
+ end
27
+
28
+ def initialize(**options)
29
+ @options = {
30
+ verbose: true,
31
+ }.merge(options)
32
+ end
33
+
34
+ def run
35
+ log.info "#{'+++ ' if Buildkite.env}🧰 " + 'Buildkite-builder'.color(:springgreen) + " ─ #{@options[:pipeline].yellow}"
36
+
37
+ results = benchmark("\nDone (%s)".color(:springgreen)) do
38
+ load_manifests
39
+ load_templates
40
+ load_processors
41
+ load_pipeline
42
+ run_processors
43
+ end
44
+ log.info results
45
+
46
+ upload! if options[:upload]
47
+ # Always return the pipeline.
48
+
49
+ pipeline
50
+ end
51
+
52
+ def pipeline
53
+ @pipeline ||= Buildkite::Pipelines::Pipeline.new
54
+ end
55
+
56
+ def pipeline_definition
57
+ @pipeline_definition ||= begin
58
+ expected = Definition::Pipeline
59
+ load_definition(Buildkite::Builder.root.join(".buildkite/pipelines/#{options[:pipeline]}").join(PIPELINE_DEFINITION_PATH), expected)
60
+ end
61
+ end
62
+
63
+ def log
64
+ @log ||= begin
65
+ Logger.new(options[:verbose] ? $stdout : StringIO.new).tap do |lgr|
66
+ lgr.formatter = proc do |_severity, _datetime, _progname, msg|
67
+ "#{msg}\n"
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def upload!
76
+ Tempfile.create(['pipeline', '.yml']) do |file|
77
+ file.sync = true
78
+ file.write(pipeline.to_yaml)
79
+
80
+ log.info '+++ :paperclip: Uploading artifact'
81
+ Buildkite::Pipelines::Command.artifact!(:upload, file.path)
82
+ log.info '+++ :pipeline: Uploading pipeline'
83
+ Buildkite::Pipelines::Command.pipeline!(:upload, file.path)
84
+ end
85
+ end
86
+
87
+ def load_manifests
88
+ Loaders::Manifests.load(options[:pipeline]).each do |name, asset|
89
+ Manifest[name] = asset
90
+ end
91
+ end
92
+
93
+ def load_templates
94
+ Loaders::Templates.load(options[:pipeline]).each do |name, asset|
95
+ pipeline.template(name, &asset)
96
+ end
97
+ end
98
+
99
+ def load_processors
100
+ Loaders::Processors.load(options[:pipeline])
101
+ end
102
+
103
+ def run_processors
104
+ pipeline.processors.each do |processor|
105
+ processor.process(self)
106
+ end
107
+ end
108
+
109
+ def load_pipeline
110
+ pipeline.instance_eval(&pipeline_definition)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buildkite
4
+ class Env
5
+ BUILDKITE = 'BUILDKITE'
6
+ PREFIX = "#{BUILDKITE}_"
7
+
8
+ module Fallback
9
+ def method_missing(method_name, *_args, &_block) # rubocop:disable Style/MethodMissingSuper
10
+ env_name = "#{PREFIX}#{method_name.to_s.gsub(/\?\z/, '').upcase}"
11
+
12
+ if method_name.to_s.end_with?('?')
13
+ @env.key?(env_name)
14
+ elsif @env.key?(env_name)
15
+ @env.fetch(env_name)
16
+ else
17
+ raise NoMethodError, "undefined method #{method_name} for #{self} because ENV[\"#{env_name}\"] is not defined"
18
+ end
19
+ end
20
+
21
+ def respond_to_missing?(method_name, include_private = false)
22
+ method_name.to_s.end_with?('?') || @env.key?("#{PREFIX}#{method_name.upcase}") || super
23
+ end
24
+ end
25
+ include Fallback
26
+
27
+ def self.load(env)
28
+ new(env) if env[BUILDKITE]
29
+ end
30
+
31
+ def initialize(env)
32
+ @env = env
33
+ end
34
+
35
+ def default_branch?
36
+ pipeline_default_branch == branch
37
+ end
38
+
39
+ def pull_request
40
+ super == 'false' ? false : Integer(super)
41
+ end
42
+
43
+ # Integer methods
44
+ %w(
45
+ build_number
46
+ ).each do |meth|
47
+ define_method(meth) do
48
+ Integer(super())
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buildkite
4
+ module Pipelines
5
+ autoload :Api, File.expand_path('pipelines/api', __dir__)
6
+ autoload :Attributes, File.expand_path('pipelines/attributes', __dir__)
7
+ autoload :Command, File.expand_path('pipelines/command', __dir__)
8
+ autoload :Helpers, File.expand_path('pipelines/helpers', __dir__)
9
+ autoload :Pipeline, File.expand_path('pipelines/pipeline', __dir__)
10
+ autoload :Plugin, File.expand_path('pipelines/plugin', __dir__)
11
+ autoload :Steps, File.expand_path('pipelines/steps', __dir__)
12
+ end
13
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'json'
6
+
7
+ module Buildkite
8
+ module Pipelines
9
+ class Api
10
+ BASE_URI = URI('https://api.buildkite.com/v2').freeze
11
+ URI_PARTS = {
12
+ organization: 'organizations',
13
+ pipeline: 'pipelines',
14
+ pipelines: 'pipelines',
15
+ build: 'builds',
16
+ builds: 'builds',
17
+ access_token: 'access-token',
18
+ }.freeze
19
+
20
+ def initialize(token)
21
+ @token = token
22
+ end
23
+
24
+ def get_access_token
25
+ uri = uri_for(access_token: nil)
26
+ JSON.parse(get_request(uri).body)
27
+ end
28
+
29
+ def list_pipelines(organization)
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def get_pipeline(organization, pipeline)
34
+ response = get_request(
35
+ uri_for(
36
+ organization: organization,
37
+ pipeline: pipeline
38
+ )
39
+ )
40
+ if response.is_a?(Net::HTTPSuccess)
41
+ JSON.parse(response.body)
42
+ end
43
+ end
44
+
45
+ def create_pipeline(organization, params)
46
+ uri = uri_for(
47
+ organization: organization,
48
+ pipelines: nil
49
+ )
50
+ JSON.parse(post_request(uri, params).body)
51
+ end
52
+
53
+ def get_pipeline_builds(organization, pipeline, **params)
54
+ uri = uri_for(params.merge(
55
+ organization: organization,
56
+ pipeline: pipeline,
57
+ builds: nil
58
+ ))
59
+ JSON.parse(get_request(uri).body)
60
+ end
61
+
62
+ def get_build(organization, pipeline, build)
63
+ uri = uri_for(
64
+ organization: organization,
65
+ pipeline: pipeline,
66
+ build: build
67
+ )
68
+ JSON.parse(get_request(uri).body)
69
+ end
70
+
71
+ def create_build(organization, pipeline)
72
+ raise NotImplementedError
73
+ end
74
+
75
+ def cancel_build(organization, pipeline, build)
76
+ raise NotImplementedError
77
+ end
78
+
79
+ def rebuild_build(organization, pipeline, build)
80
+ raise NotImplementedError
81
+ end
82
+
83
+ private
84
+
85
+ def get_request(uri)
86
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
87
+ request = prepare_request(Net::HTTP::Get.new(uri))
88
+ http.request(request)
89
+ end
90
+ end
91
+
92
+ def post_request(uri, data)
93
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
94
+ request = prepare_request(Net::HTTP::Post.new(uri))
95
+ request.content_type = 'application/json'
96
+ request.body = data.to_json
97
+ http.request(request)
98
+ end
99
+ end
100
+
101
+ def uri_for(options)
102
+ uri_parts = URI_PARTS.each_with_object([]) do |(resource, path), parts|
103
+ if options.key?(resource)
104
+ parts << [path, options.delete(resource)].compact
105
+ end
106
+ end
107
+ uri = URI(uri_parts.flatten.unshift(BASE_URI).join('/'))
108
+ uri.query = URI.encode_www_form(options) if options.any?
109
+ uri
110
+ end
111
+
112
+ def prepare_request(request)
113
+ request['Authorization'] = "Bearer #{@token}"
114
+ request['Accept'] = 'application/json'
115
+ request
116
+ end
117
+ end
118
+ end
119
+ end