buildkite-cli 0.1.0

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