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,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buildkite
4
+ module Builder
5
+ module Commands
6
+ class Files < Abstract
7
+ private
8
+
9
+ self.description = 'Outputs files that match the specified manifest.'
10
+
11
+ def run
12
+ pipeline, manifest = ARGV.first.to_s.split('/')
13
+ if !pipeline || !manifest
14
+ raise 'You must specify a pipeline and a manifest (eg "mypipeline/mymanifest")'
15
+ end
16
+
17
+ manifests = Loaders::Manifests.load(pipeline)
18
+ manifests[manifest].files.each do |file|
19
+ puts file
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buildkite
4
+ module Builder
5
+ module Commands
6
+ class Preview < Abstract
7
+ private
8
+
9
+ self.description = 'Outputs the pipeline YAML.'
10
+
11
+ def run
12
+ unless pipeline
13
+ raise 'You must specify a pipeline'
14
+ end
15
+
16
+ puts Runner.new(pipeline: pipeline).run.to_yaml
17
+ end
18
+
19
+ def pipeline
20
+ @pipeline ||= ARGV.last || begin
21
+ if available_pipelines.one?
22
+ available_pipelines.first
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buildkite
4
+ module Builder
5
+ module Definition
6
+ module Helper
7
+ def load_definition(file, expected)
8
+ result = eval(file.read, TOPLEVEL_BINDING.dup, file.to_s) # rubocop:disable Security/Eval
9
+ unless result.is_a?(expected)
10
+ raise "#{file} must return a valid definition (#{expected}); got #{result.class}"
11
+ end
12
+
13
+ result
14
+ end
15
+ end
16
+
17
+ class Pipeline < Proc
18
+ end
19
+
20
+ class Template < Proc
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'set'
5
+
6
+ module Buildkite
7
+ module Builder
8
+ class FileResolver
9
+ @cache = true
10
+
11
+ attr_reader :modified_files
12
+
13
+ class << self
14
+ attr_accessor :cache
15
+
16
+ def resolve(reset = false)
17
+ @resolve = nil if !cache || reset
18
+ @resolve ||= new
19
+ end
20
+ end
21
+
22
+ def initialize
23
+ @modified_files = SortedSet.new(pull_request? ? files_from_pull_request : files_from_git)
24
+ end
25
+
26
+ private
27
+
28
+ def files_from_pull_request
29
+ Github.pull_request_files.map { |f| f.fetch('filename') }
30
+ end
31
+
32
+ def files_from_git
33
+ if Buildkite.env
34
+ changed_files = command("git diff-tree --no-commit-id --name-only -r #{Buildkite.env.commit}")
35
+ else
36
+ default_branch = command('git symbolic-ref refs/remotes/origin/HEAD').strip
37
+ changed_files = command("git diff --name-only #{default_branch}")
38
+ changed_files << command('git diff --name-only')
39
+ end
40
+
41
+ changed_files.split.uniq.sort
42
+ end
43
+
44
+ def pull_request?
45
+ Buildkite.env&.pull_request
46
+ end
47
+
48
+ def command(cmd)
49
+ output, status = Open3.capture2(*cmd.split)
50
+
51
+ if status.success?
52
+ output
53
+ else
54
+ raise "Command failed (exit #{status.exitstatus}): #{cmd}"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+
7
+ module Buildkite
8
+ module Builder
9
+ class Github
10
+ BASE_URI = URI('https://api.github.com').freeze
11
+ ACCEPT_HEADER = 'application/vnd.github.v3+json'
12
+ LINK_HEADER = 'link'
13
+ NEXT_LINK_REGEX = /<(?<uri>.+)>; rel="next"/.freeze
14
+ REPO_REGEX = /github\.com(?::|\/)(.*)\.git\z/.freeze
15
+ PER_PAGE = 100
16
+
17
+ def self.pull_request_files
18
+ new.pull_request_files
19
+ end
20
+
21
+ def initialize(env = ENV)
22
+ @env = env
23
+ end
24
+
25
+ def pull_request_files
26
+ files = []
27
+ next_uri = URI.join(BASE_URI, "repos/#{repo}/pulls/#{pull_request_number}/files?per_page=#{PER_PAGE}")
28
+
29
+ while next_uri
30
+ response = request(next_uri)
31
+ files.concat(JSON.parse(response.body))
32
+ next_uri = parse_next_uri(response)
33
+ end
34
+
35
+ files
36
+ end
37
+
38
+ private
39
+
40
+ def repo
41
+ Buildkite.env.repo[REPO_REGEX, 1]
42
+ end
43
+
44
+ def token
45
+ @env.fetch('GITHUB_API_TOKEN')
46
+ end
47
+
48
+ def pull_request_number
49
+ Buildkite.env.pull_request
50
+ end
51
+
52
+ def request(uri)
53
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
54
+ request = Net::HTTP::Get.new(uri)
55
+ request['Authorization'] = "token #{token}"
56
+ request['Accept'] = ACCEPT_HEADER
57
+
58
+ http.request(request)
59
+ end
60
+ end
61
+
62
+ def parse_next_uri(response)
63
+ links = response[LINK_HEADER]
64
+ return unless links
65
+
66
+ matches = links.match(NEXT_LINK_REGEX)
67
+ URI.parse(matches[:uri]) if matches
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buildkite
4
+ module Builder
5
+ module Loaders
6
+ autoload :Abstract, File.expand_path('loaders/abstract', __dir__)
7
+ autoload :Manifests, File.expand_path('loaders/manifests', __dir__)
8
+ autoload :Templates, File.expand_path('loaders/templates', __dir__)
9
+ autoload :Processors, File.expand_path('loaders/processors', __dir__)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buildkite
4
+ module Builder
5
+ module Loaders
6
+ class Abstract
7
+ attr_reader :assets
8
+ attr_reader :pipeline
9
+
10
+ def self.load(pipeline)
11
+ new(pipeline).assets
12
+ end
13
+
14
+ def initialize(pipeline)
15
+ @pipeline = pipeline
16
+ @assets = {}
17
+ load
18
+ end
19
+
20
+ private
21
+
22
+ def buildkite_path
23
+ Buildkite::Builder.root.join('.buildkite')
24
+ end
25
+
26
+ def pipeline_path
27
+ buildkite_path.join("pipelines/#{pipeline}")
28
+ end
29
+
30
+ def load
31
+ raise NotImplementedError
32
+ end
33
+
34
+ def add(name, asset)
35
+ @assets[name.to_s] = asset
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buildkite
4
+ module Builder
5
+ module Loaders
6
+ class Manifests < Abstract
7
+ MANIFESTS_PATH = Pathname.new('manifests').freeze
8
+
9
+ def load
10
+ return unless manifests_path.directory?
11
+
12
+ manifests_path.children.map do |file|
13
+ add(file.basename, Manifest.new(Buildkite::Builder.root, file.readlines))
14
+ end
15
+ end
16
+
17
+ def manifests_path
18
+ pipeline_path.join(MANIFESTS_PATH)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Buildkite
6
+ module Builder
7
+ module Loaders
8
+ class Processors < Abstract
9
+ PROCESSORS_PATH = Pathname.new('processors').freeze
10
+
11
+ def load
12
+ load_processors_from_path(global_processors_path)
13
+ load_processors_from_path(pipeline_processors_path)
14
+ end
15
+
16
+ private
17
+
18
+ def load_processors_from_path(path)
19
+ return unless path.directory?
20
+
21
+ path.children.map do |file|
22
+ required_status = require(file)
23
+ add(file.basename, { required: required_status })
24
+ end
25
+ end
26
+
27
+ def global_processors_path
28
+ buildkite_path.join(PROCESSORS_PATH)
29
+ end
30
+
31
+ def pipeline_processors_path
32
+ pipeline_path.join(PROCESSORS_PATH)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Buildkite
4
+ module Builder
5
+ module Loaders
6
+ class Templates < Abstract
7
+ include Definition::Helper
8
+
9
+ TEMPLATES_PATH = Pathname.new('templates').freeze
10
+
11
+ def load
12
+ return unless templates_path.directory?
13
+
14
+ templates_path.children.sort.each do |file|
15
+ add(file.basename('.rb'), load_definition(file, Definition::Template))
16
+ end
17
+ end
18
+
19
+ def templates_path
20
+ pipeline_path.join(TEMPLATES_PATH)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+
5
+ module Buildkite
6
+ module Builder
7
+ module LoggingUtils
8
+ def benchmark(output, &block)
9
+ time = Benchmark.realtime(&block)
10
+ output % [pluralize(time.round(2), 'second')]
11
+ end
12
+
13
+ def pluralize(count, singular, plural = nil)
14
+ if count == 1
15
+ "#{count} #{singular}"
16
+ elsif plural
17
+ "#{count} #{plural}"
18
+ else
19
+ "#{count} #{singular}s"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/md5'
4
+ require 'pathname'
5
+ require 'set'
6
+
7
+ module Buildkite
8
+ module Builder
9
+ class Manifest
10
+ autoload :Rule, File.expand_path('manifest/rule', __dir__)
11
+
12
+ class << self
13
+ def resolve(root, patterns)
14
+ new(root, Array(patterns)).modified?
15
+ end
16
+
17
+ def manifests
18
+ @manifests ||= {}
19
+ end
20
+
21
+ def [](name)
22
+ manifests[name.to_s]
23
+ end
24
+
25
+ def []=(name, manifest)
26
+ name = name.to_s
27
+ if manifests.key?(name)
28
+ raise ArgumentError, "manifest #{name} already exists"
29
+ end
30
+
31
+ manifests[name] = manifest
32
+ end
33
+ end
34
+
35
+ attr_reader :root
36
+
37
+ def initialize(root, patterns)
38
+ @root = Pathname.new(root)
39
+ @root = Buildkite::Builder.root.join(@root) unless @root.absolute?
40
+ @patterns = patterns.map(&:to_s)
41
+ end
42
+
43
+ def modified?
44
+ # DO NOT intersect FileResolver with manifest files. If the manifest is
45
+ # large, the operation can be expensive. It's always cheaper to loop
46
+ # through the changed files and compare them against the rules.
47
+ unless defined?(@modified)
48
+ @modified = FileResolver.resolve.modified_files.any? do |file|
49
+ file = Buildkite::Builder.root.join(file)
50
+ inclusion_rules.any? { |rule| rule.match?(file) } &&
51
+ exclusion_rules.none? { |rule| rule.match?(file) }
52
+ end
53
+ end
54
+
55
+ @modified
56
+ end
57
+
58
+ def files
59
+ @files ||= inclusion_rules.map(&:files).reduce(SortedSet.new, :merge) - exclusion_rules.map(&:files).reduce(SortedSet.new, :merge)
60
+ end
61
+
62
+ def digest
63
+ @digest ||= begin
64
+ digests = files.map { |file| Digest::MD5.file(Buildkite::Builder.root.join(file)).hexdigest }
65
+ Digest::MD5.hexdigest(digests.join)
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def rules
72
+ @rules ||= @patterns.each_with_object([]) do |pattern, rules|
73
+ pattern = pattern.strip
74
+ unless pattern.match?(/\A(#|\z)/)
75
+ rules << Rule.new(root, pattern)
76
+ end
77
+ end
78
+ end
79
+
80
+ def inclusion_rules
81
+ @inclusion_rules ||= rules.reject(&:exclude)
82
+ end
83
+
84
+ def exclusion_rules
85
+ @exclusion_rules ||= rules.select(&:exclude)
86
+ end
87
+ end
88
+ end
89
+ end