buildkite-builder 1.0.0.beta.1

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 (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