buildkite-cli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 54ac29683a5b656c0b77fa4006c3d479ddc6b636b184ebbe5528886af788d5d5
4
+ data.tar.gz: e413c8e3c6935c79605046bed1d438752b088f2fb05d908a7f6d0792617d7ed4
5
+ SHA512:
6
+ metadata.gz: 647c6117019a268e476a8e34333259344d874585f06c83bb0b114771d600fb6fcfad8d7992740d4cfe148585c7627ec8bf922cfc948d58164c4f3c432f8d765a
7
+ data.tar.gz: 941e18fbf12e853be9735d52ce1a9348a0b226227802ccaa65e2c1ff09cffd0a779c79d54347cb27c7c02f61be31e29dbb396f6d6058d90ee3d1a328261cf389
data/.pryrc ADDED
@@ -0,0 +1,4 @@
1
+ require "pry-rescue"
2
+ require "pry-stack_explorer"
3
+
4
+ require_relative "./lib/bk"
data/.rubocop.yml ADDED
@@ -0,0 +1,9 @@
1
+ # Only use standardrb, but keep .rubocop.yml for tool compatibility: https://github.com/standardrb/standard#running-standards-rules-via-rubocop
2
+ require:
3
+ - standard
4
+ - rubocop-performance
5
+
6
+ inherit_gem:
7
+ standard: config/base.yml
8
+ standard-performance: config/base.yml
9
+ standard-custom: config/base.yml
data/.standard.yml ADDED
@@ -0,0 +1,2 @@
1
+ fix: true # default: false
2
+ parallel: true # default: false
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in bk.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,142 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ buildkite-cli (0.1.0)
5
+ dry-cli
6
+ graphql-client
7
+ httparty
8
+ json
9
+ tty-box
10
+ tty-markdown
11
+ tty-pager
12
+ tty-spinner
13
+ zeitwerk
14
+
15
+ GEM
16
+ remote: https://rubygems.org/
17
+ specs:
18
+ activesupport (7.0.4.3)
19
+ concurrent-ruby (~> 1.0, >= 1.0.2)
20
+ i18n (>= 1.6, < 2)
21
+ minitest (>= 5.1)
22
+ tzinfo (~> 2.0)
23
+ ast (2.4.2)
24
+ binding_of_caller (1.0.0)
25
+ debug_inspector (>= 0.0.1)
26
+ coderay (1.1.3)
27
+ concurrent-ruby (1.2.2)
28
+ debug_inspector (1.1.0)
29
+ dry-cli (0.7.0)
30
+ graphql (2.0.21)
31
+ graphql-client (0.18.0)
32
+ activesupport (>= 3.0)
33
+ graphql
34
+ httparty (0.21.0)
35
+ mini_mime (>= 1.0.0)
36
+ multi_xml (>= 0.5.2)
37
+ i18n (1.13.0)
38
+ concurrent-ruby (~> 1.0)
39
+ json (2.6.3)
40
+ kramdown (2.4.0)
41
+ rexml
42
+ language_server-protocol (3.17.0.3)
43
+ lint_roller (1.0.0)
44
+ method_source (1.0.0)
45
+ mini_mime (1.1.2)
46
+ minitest (5.18.0)
47
+ multi_xml (0.6.0)
48
+ parallel (1.23.0)
49
+ parser (3.2.2.1)
50
+ ast (~> 2.4.1)
51
+ pastel (0.8.0)
52
+ tty-color (~> 0.5)
53
+ prettier_print (1.2.1)
54
+ pry (0.14.2)
55
+ coderay (~> 1.1)
56
+ method_source (~> 1.0)
57
+ pry-stack_explorer (0.6.1)
58
+ binding_of_caller (~> 1.0)
59
+ pry (~> 0.13)
60
+ rainbow (3.1.1)
61
+ rake (13.0.6)
62
+ regexp_parser (2.8.0)
63
+ rexml (3.2.5)
64
+ rouge (4.1.0)
65
+ rubocop (1.50.2)
66
+ json (~> 2.3)
67
+ parallel (~> 1.10)
68
+ parser (>= 3.2.0.0)
69
+ rainbow (>= 2.2.2, < 4.0)
70
+ regexp_parser (>= 1.8, < 3.0)
71
+ rexml (>= 3.2.5, < 4.0)
72
+ rubocop-ast (>= 1.28.0, < 2.0)
73
+ ruby-progressbar (~> 1.7)
74
+ unicode-display_width (>= 2.4.0, < 3.0)
75
+ rubocop-ast (1.28.1)
76
+ parser (>= 3.2.1.0)
77
+ rubocop-performance (1.16.0)
78
+ rubocop (>= 1.7.0, < 2.0)
79
+ rubocop-ast (>= 0.4.0)
80
+ ruby-lsp (0.5.1)
81
+ language_server-protocol (~> 3.17.0)
82
+ sorbet-runtime
83
+ syntax_tree (>= 6.1.1, < 7)
84
+ ruby-progressbar (1.13.0)
85
+ sorbet-runtime (0.5.10821)
86
+ standard (1.28.2)
87
+ language_server-protocol (~> 3.17.0.2)
88
+ lint_roller (~> 1.0)
89
+ rubocop (~> 1.50.2)
90
+ standard-custom (~> 1.0.0)
91
+ standard-performance (~> 1.0.1)
92
+ standard-custom (1.0.0)
93
+ lint_roller (~> 1.0)
94
+ standard-performance (1.0.1)
95
+ lint_roller (~> 1.0)
96
+ rubocop-performance (~> 1.16.0)
97
+ strings (0.2.1)
98
+ strings-ansi (~> 0.2)
99
+ unicode-display_width (>= 1.5, < 3.0)
100
+ unicode_utils (~> 1.4)
101
+ strings-ansi (0.2.0)
102
+ syntax_tree (6.1.1)
103
+ prettier_print (>= 1.2.0)
104
+ tty-box (0.7.0)
105
+ pastel (~> 0.8)
106
+ strings (~> 0.2.0)
107
+ tty-cursor (~> 0.7)
108
+ tty-color (0.6.0)
109
+ tty-cursor (0.7.1)
110
+ tty-markdown (0.7.2)
111
+ kramdown (>= 1.16.2, < 3.0)
112
+ pastel (~> 0.8)
113
+ rouge (>= 3.14, < 5.0)
114
+ strings (~> 0.2.0)
115
+ tty-color (~> 0.5)
116
+ tty-screen (~> 0.8)
117
+ tty-pager (0.14.0)
118
+ strings (~> 0.2.0)
119
+ tty-screen (~> 0.8)
120
+ tty-screen (0.8.1)
121
+ tty-spinner (0.9.3)
122
+ tty-cursor (~> 0.7)
123
+ tzinfo (2.0.6)
124
+ concurrent-ruby (~> 1.0)
125
+ unicode-display_width (2.4.2)
126
+ unicode_utils (1.4.0)
127
+ zeitwerk (2.6.8)
128
+
129
+ PLATFORMS
130
+ arm64-darwin-21
131
+ arm64-darwin-22
132
+
133
+ DEPENDENCIES
134
+ buildkite-cli!
135
+ pry
136
+ pry-stack_explorer
137
+ rake (~> 13.0)
138
+ ruby-lsp
139
+ standard
140
+
141
+ BUNDLED WITH
142
+ 2.4.10
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Joshua Nichols
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # Bk
2
+
3
+ CLI for poking around Buildkite, like `gh` for GitHub
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/bk`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ See Development section while in release
10
+
11
+ ## Usage
12
+
13
+ See `bk --help` and `bk <subcommand> --help` for most accurate usage. Below are some examples!
14
+
15
+ ### Annotations
16
+
17
+ Usage: `bk annotations [slug_or_url]`
18
+
19
+ Display annotations of a specific build:
20
+
21
+ $ bk annotations https://buildkite.com/your-org/your-pipeline/builds/1234
22
+
23
+ Display annotations of the most recent build (requires `gh`):
24
+
25
+ $ bk annotations
26
+
27
+ ### Artifacts
28
+
29
+ Usage: `bk artifacts [slug_or_url] [--glob <pattern>] [--download]`
30
+
31
+ Display artifacts of a specific build:
32
+
33
+ $ bk annotations https://buildkite.com/your-org/your-pipeline/builds/1234
34
+
35
+ Display artifacts of a specific build matching a glob (tip: quote the glob pattern to avoid your shell expanding):
36
+
37
+ $ bk annotations https://buildkite.com/your-org/your-pipeline/builds/1234 --glob "*.log"
38
+
39
+ Download artifacts of a specific build matching a glob (tip: quote the glob pattern to avoid your shell expanding):
40
+
41
+ $ bk annotations https://buildkite.com/your-org/your-pipeline/builds/1234 --glob "*.log" --download
42
+
43
+ ### To be continue?
44
+
45
+ More to come? Whatchu want? Feature requests and PRs welcome!
46
+
47
+ ## Development
48
+
49
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment. You can run the command from this checkout with:
50
+
51
+ bk $ bundle exec exe/bk [args...]
52
+
53
+ To install this gem onto your local machine, run `bundle exec rake install`. If you want to use `bk` in different ruby versions, you'll need to use your version manager to switch and install it. You might find this snippet useful:
54
+
55
+ bk $ rake build
56
+ bk 0.1.0 built to pkg/bk-0.1.0.gem.
57
+
58
+ bk $ cd ~/workspace/some-project
59
+ some-project $ gem install ~/workspace/bk/bk-0.1.0.gem
60
+
61
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
62
+
63
+ ## Contributing
64
+
65
+ Bug reports and pull requests are welcome on GitHub at https://github.com/technicalpickles/bk.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
5
+
6
+ namespace :buildkite do
7
+ task :dump_schema do
8
+ require "./lib/bk"
9
+ require "graphql/client"
10
+ GraphQL::Client.dump_schema(Bk::HTTP, "schema.json")
11
+ end
12
+ end
data/exe/bk ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require "bk"
3
+
4
+ require "pry"
5
+ Dry::CLI.new(Bk::Commands).call
data/lefthook.yml ADDED
@@ -0,0 +1,7 @@
1
+ pre-commit:
2
+ parallel: true
3
+ commands:
4
+ standardrb:
5
+ glob: "{*.rb,exe/*}"
6
+ run: standardrb --fix {staged_files}
7
+ auto_stage: true
@@ -0,0 +1,58 @@
1
+ module TTYMarkdownConverterExtension
2
+ def convert_html_element(el, opts)
3
+ if el.value == "span"
4
+ color = color_from_span_class(el.attr["class"])
5
+ return super unless color
6
+
7
+ pastel.send(color, inner(el, opts))
8
+ else
9
+ super
10
+ end
11
+ end
12
+
13
+ def color_from_span_class(css_class)
14
+ match = css_class.match(/^term-(?:[fb]g)(\d+)$/)
15
+ return unless match
16
+
17
+ color_code = Integer(match[1])
18
+
19
+ Pastel::ANSI::ATTRIBUTES.key(color_code)
20
+ end
21
+
22
+ def pastel
23
+ @pastel ||= Pastel.new
24
+ end
25
+ end
26
+
27
+ class TTY::Markdown::Converter
28
+ prepend TTYMarkdownConverterExtension
29
+ end
30
+
31
+ module Bk
32
+ module AnnotationFormatter
33
+ class Markdown
34
+ include Color
35
+ include Format
36
+
37
+ def call(annotation)
38
+ io = StringIO.new
39
+
40
+ style = annotation.style
41
+ color = annotation_colors[style]
42
+
43
+ context = annotation.context
44
+ io.puts " #{color.call("#{vertical_pipe}#{context}")}"
45
+ io.puts " #{color.call(vertical_pipe)}"
46
+
47
+ output = TTY::Markdown.parse(annotation.body.text)
48
+ output = CGI.unescape_html(output)
49
+
50
+ output.each_line do |line|
51
+ io.puts " #{color.call(vertical_pipe)} #{line}"
52
+ end
53
+
54
+ io.string
55
+ end
56
+ end
57
+ end
58
+ end
data/lib/bk/color.rb ADDED
@@ -0,0 +1,32 @@
1
+ module Bk
2
+ module Color
3
+ def success_color
4
+ @success_color ||= pastel.green.detach
5
+ end
6
+
7
+ def error_color
8
+ @error_color ||= pastel.red.detach
9
+ end
10
+
11
+ def warning_color
12
+ @warning_color ||= pastel.yellow.detach
13
+ end
14
+
15
+ def info_color
16
+ @info_color ||= pastel.blue.detach
17
+ end
18
+
19
+ def default_color
20
+ @default_color ||= pastel.white.detach
21
+ end
22
+
23
+ def colorize(text, color)
24
+ is_tty? ? color.call(text) : text
25
+ end
26
+
27
+ def create_color_hash(mappings = {})
28
+ hash = Hash.new(default_color)
29
+ hash.merge!(mappings)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,73 @@
1
+ module Bk
2
+ module Commands
3
+ class Annotations < Base
4
+ desc "Show Annotations for a Build"
5
+ argument :url_or_slug, type: :string, required: false, desc: "Build URL or Build slug"
6
+
7
+ BuildAnnotationsQuery = Client.parse <<-GRAPHQL
8
+ query($slug: ID!) {
9
+ build(slug: $slug) {
10
+ number
11
+
12
+ pipeline {
13
+ slug
14
+ }
15
+
16
+ branch
17
+ message
18
+
19
+ url
20
+ pullRequest {
21
+ id
22
+ }
23
+ state
24
+ startedAt
25
+ finishedAt
26
+ canceledAt
27
+
28
+ annotations(first: 200) {
29
+ edges {
30
+ node {
31
+ context
32
+ style
33
+ body {
34
+ text
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ GRAPHQL
42
+
43
+ def call(args: {}, url_or_slug: nil)
44
+ slug = determine_slug(url_or_slug)
45
+ unless slug
46
+ raise ArgumentError, "Unable to figure out slug to use"
47
+ end
48
+
49
+ result = query(BuildAnnotationsQuery, variables: {slug: slug})
50
+
51
+ build = result.data.build
52
+
53
+ $stdout.puts build_header(build)
54
+ $stdout.puts ""
55
+
56
+ annotation_edges = build.annotations.edges
57
+ annotations = annotation_edges.map { |edge| edge.node }
58
+
59
+ format = AnnotationFormatter::Markdown.new
60
+ # indent each annotation to separate it from the build status
61
+ annotations.each_with_index do |annotation, index|
62
+ $stdout.puts format.call(annotation)
63
+ # horizontal separator between each
64
+ unless index == annotations.length - 1
65
+ $stdout.puts ""
66
+ $stdout.puts " #{HORIZONTAL_PIPE * (TTY::Screen.width - 4)} "
67
+ $stdout.puts ""
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,148 @@
1
+ module Bk
2
+ module Commands
3
+ class Artifacts < Base
4
+ desc "Show Artifacts for a Build"
5
+ argument :url_or_slug, type: :string, required: false, desc: "Build URL or Build slug"
6
+
7
+ option :glob, required: false, desc: "Glob of artifacts to list"
8
+ option :download, type: :boolean, required: false, desc: "Should or should not download"
9
+
10
+ BuildArtifactsQuery = Client.parse <<-GRAPHQL
11
+ query($slug: ID!, $jobs_after: String) {
12
+ build(slug: $slug) {
13
+ number
14
+ message
15
+ uuid
16
+
17
+ branch
18
+ pipeline {
19
+ slug
20
+ }
21
+
22
+ url
23
+ pullRequest {
24
+ id
25
+ }
26
+
27
+ state
28
+ scheduledAt
29
+ startedAt
30
+ finishedAt
31
+ canceledAt
32
+
33
+ jobs(first: 500, after: $jobs_after) {
34
+ pageInfo {
35
+ endCursor
36
+ hasNextPage
37
+ }
38
+ edges {
39
+ node {
40
+ __typename
41
+ ... on JobTypeWait {
42
+ uuid
43
+ label
44
+ }
45
+ ... on JobTypeTrigger {
46
+ uuid
47
+ label
48
+ }
49
+
50
+ ... on JobTypeCommand {
51
+ uuid
52
+ label
53
+
54
+ url
55
+ exitStatus
56
+
57
+ parallelGroupIndex
58
+ parallelGroupTotal
59
+
60
+ artifacts(first: 500) {
61
+ edges {
62
+ node {
63
+ state
64
+ path
65
+ downloadURL
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ GRAPHQL
77
+
78
+ def call(args: {}, url_or_slug: nil, **options)
79
+ slug = determine_slug(url_or_slug)
80
+ unless slug
81
+ raise ArgumentError, "Unable to figure out slug to use"
82
+ end
83
+
84
+ glob = options[:glob]
85
+ download = options[:download]
86
+
87
+ jobs_after = nil
88
+ has_next_page = true
89
+
90
+ while has_next_page
91
+ result = query(BuildArtifactsQuery, variables: {slug: slug, jobs_after: jobs_after})
92
+
93
+ build = result.data.build
94
+ # only show the first time
95
+ if jobs_after.nil?
96
+ puts build_header(build)
97
+ puts ""
98
+ end
99
+
100
+ jobs_after = build.jobs.page_info.end_cursor
101
+ has_next_page = build.jobs.page_info.has_next_page
102
+
103
+ jobs = build.jobs.edges.map(&:node)
104
+ jobs.each do |job|
105
+ next unless job.respond_to?(:exit_status)
106
+
107
+ artifacts = job.artifacts.edges.map(&:node).select { |artifact| glob_matches?(glob, artifact.path) }
108
+ next unless artifacts.any?
109
+
110
+ color = job_colors[job.exit_status]
111
+ header = color.call(job.label)
112
+ if job.parallel_group_index && job.parallel_group_total
113
+ header = "#{header} (#{job.parallel_group_index + 1}/#{job.parallel_group_total})"
114
+ end
115
+
116
+ puts header
117
+
118
+ artifacts.each do |artifact|
119
+ if download
120
+ puts " - #{artifact.path} (downloading to tmp/bk/[filename])"
121
+ download_artifact(artifact)
122
+ else
123
+ puts " - #{artifact.path}"
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def glob_matches?(glob, path)
131
+ if glob
132
+ File.fnmatch?(glob, path, File::FNM_PATHNAME)
133
+ else
134
+ true
135
+ end
136
+ end
137
+
138
+ def download_artifact(artifact)
139
+ download_url = artifact.to_h["downloadURL"]
140
+ redirected_response_from_aws = Net::HTTP.get_response(URI(download_url))
141
+ artifact_response = Net::HTTP.get_response(URI(redirected_response_from_aws["location"]))
142
+ path = Pathname.new("tmp/bk/#{artifact.path}")
143
+ FileUtils.mkdir_p(path.dirname)
144
+ path.write(artifact_response.body)
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,54 @@
1
+ module Bk
2
+ module Commands
3
+ extend Dry::CLI::Registry
4
+
5
+ class Base < Dry::CLI::Command
6
+ include Color
7
+ include Format
8
+
9
+ attr_reader :spinner
10
+
11
+ def initialize(buildkite_api_token: nil)
12
+ @spinner = TTY::Spinner.new("Talking to Buildkite API... :spinner", clear: true, format: :dots)
13
+ end
14
+
15
+ private
16
+
17
+ def parse_slug_from_url(url)
18
+ # https://buildkite.com/my-org/my-pipeline/builds/1234 => my-org/my-pipeline/1234
19
+ url.delete_prefix("https://buildkite.com/").gsub("/builds", "")
20
+ end
21
+
22
+ def determine_slug(url_or_slug = nil)
23
+ if url_or_slug
24
+ if url_or_slug.start_with?("https:")
25
+ parse_slug_from_url(url_or_slug)
26
+ else
27
+ url_or_slug
28
+ end
29
+ else
30
+ output = `gh pr checks`
31
+ output.lines.each do |line|
32
+ if line =~ %r{https://buildkite.com/([^/]+)/([^/]+)/builds/(\d+)}
33
+ return "#{$1}/#{$2}/#{$3}"
34
+ end
35
+ end
36
+
37
+ nil
38
+ end
39
+ end
40
+
41
+ def query(graphql_query, **kwargs)
42
+ result = nil
43
+ spinner.run("Done") do |spinner|
44
+ result = Client.query(graphql_query, **kwargs)
45
+ end
46
+
47
+ result
48
+ end
49
+ end
50
+
51
+ register "annotations", Annotations
52
+ register "artifacts", Artifacts
53
+ end
54
+ end