termcity 0.4.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dc6bd575a30f6f6645b4b570814226738ac30c94
4
+ data.tar.gz: 8a17bd4fcbe547978d0fe25cf950ac157f251c8f
5
+ SHA512:
6
+ metadata.gz: feb982a06dfe3285d17df91e688758bf17928f2186f325d59ac3e8e394f0336c6e34ce7159c99c485f6f6888fef93f7aba0d5391dcf625552641310a007ff4bb
7
+ data.tar.gz: 58a7a64dfb71260546b71d4b6dae2d3b81c5732cd456f963eedf09a6755a32ebef7345a8ad6a31c23a9cf98bb599bd1d133fde1fd1f2c72f8bd93137d1fb5367
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
10
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in termcity.gemspec
6
+ gemspec
7
+
8
+ gem "pry"
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "termcity"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,126 @@
1
+ #! /usr/bin/env ruby
2
+ require 'base64'
3
+ require 'yaml'
4
+ require "termcity/cli"
5
+ require "optparse"
6
+ require "uri"
7
+
8
+ HELP = <<~TXT
9
+ Run termcity to see your test results on TeamCity:
10
+
11
+ termcity --branch myBranchName --project MyProjectId
12
+
13
+ [Options]
14
+
15
+ -b, --branch: defaults to current repo branch
16
+ -p, --project: defaults to camelcasing repo directory
17
+ -r, --revision: defaults to latest revision known to CI
18
+
19
+ [CREDENTIALS]:
20
+
21
+ The backend uses your Github organization memberships to verify your
22
+ access to the data. You must configure the CLI with a token granting
23
+ it access to read your org memberships:
24
+
25
+ 1. Visit https://github.com/settings/tokens/new
26
+ 2. Generate a new token
27
+ 3. Only grant the `read:org` permission (leave all others unchecked)
28
+ 4. Paste the token in the configuration file as specified below.
29
+
30
+ [CONFIGURATION]
31
+ Put a json formatted file at ~/.termcity with the following data:
32
+
33
+ {
34
+ "host": "https://my.termcity.api.com",
35
+ "token": "your github auth token"
36
+ }
37
+
38
+ [OUTPUT]
39
+
40
+ Builds are listed in alphabetical order. Builds that have a result for
41
+ the latest revision but are also re-enqueued have a ",q" next to them.
42
+ For example, "failed,q" means the latest build on the branch has
43
+ failed, but it is enqueued to run again.
44
+ TXT
45
+
46
+ options = {}
47
+ OptionParser.new do |opts|
48
+ opts.banner = "Usage: termcity [options]"
49
+
50
+ opts.on("-b BRANCH", "--branch BRANCH", "git branch (defaults to current repo branch)") do |b|
51
+ options[:branch] = b
52
+ end
53
+
54
+ opts.on("-p PROJECT_ID", "--project PROJECT_ID", "The project id (defaults to camelCasing on repo directory)") do |p|
55
+ options[:project_id] = p
56
+ end
57
+
58
+ opts.on("-r REVISION", "--revision REVISION", "The git revision to look for (defaults to revision of latest test run)") do |r|
59
+ options[:revision] = r
60
+
61
+ if !r.match(/\h{40}/)
62
+ warn("#{r} is not a full/valid git revision")
63
+ exit(1)
64
+ end
65
+ end
66
+
67
+ opts.on("-h", "--help", "See help") do
68
+ puts HELP
69
+ exit(0)
70
+ end
71
+
72
+ end.parse!
73
+
74
+ if ARGV.any?
75
+ puts HELP
76
+ exit(1)
77
+ end
78
+
79
+ if !options[:branch]
80
+ options[:branch] = `git rev-parse --abbrev-ref @`.chomp
81
+ if $? != 0
82
+ warn "Could not identify branch. Please specify one"
83
+ exit(1)
84
+ end
85
+ end
86
+
87
+ if !options[:project_id]
88
+ root_dir =
89
+ `git rev-parse --show-toplevel`
90
+ .chomp
91
+ .tap { raise "detecting root dir failed" unless $?==0}
92
+
93
+ camelcase_root_dir =
94
+ File
95
+ .basename(root_dir)
96
+ .sub(/^[a-z\d]*/) { $&.capitalize }
97
+ .gsub(/(?:_|(\/))([a-z\d]*)/) { $2.capitalize }
98
+
99
+ options[:project_id] = camelcase_root_dir
100
+ end
101
+
102
+ creds_file = File.expand_path("~/.termcity")
103
+ if !File.exist?(creds_file)
104
+ warn "Could not find a credentials file. See --help for info."
105
+ exit(1)
106
+ end
107
+
108
+ yaml = YAML.load_file(creds_file)
109
+
110
+ if yaml["host"].nil?
111
+ warn "Could not find `host` in credentials file. See --help for more info"
112
+ exit(1)
113
+ end
114
+
115
+ if yaml["token"].nil?
116
+ warn "Could not find `token` in credentials file. See --help for more info"
117
+ exit(1)
118
+ end
119
+
120
+ Termcity::CLI.simple_format(
121
+ token: yaml["token"],
122
+ host: yaml["host"],
123
+ branch: options[:branch],
124
+ revision: options[:revision],
125
+ project_id: options[:project_id]
126
+ )
@@ -0,0 +1,5 @@
1
+ require "termcity/version"
2
+
3
+ module Termcity
4
+
5
+ end
@@ -0,0 +1,42 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ module Termcity
6
+ class Api
7
+ ALLOWED_BUILD_FILTERS = [
8
+ :branch,
9
+ :count,
10
+ :defaultFilter,
11
+ :failedtoStart,
12
+ :project,
13
+ :revision,
14
+ :state,
15
+ ]
16
+
17
+ def initialize(token:, host:)
18
+ @token = token
19
+ @host = host
20
+ end
21
+
22
+ def summary(branch:, project_id:, revision:)
23
+ path = "/builds?branch=#{branch}&project_id=#{project_id}"
24
+ path = "#{path}&revision=#{revision}" if revision
25
+ url = URI.join(@host, path).to_s
26
+ get(url)
27
+ end
28
+
29
+ private
30
+
31
+ def get(url)
32
+ uri = URI.parse(url)
33
+ http = Net::HTTP.new(uri.host, uri.port)
34
+ request = Net::HTTP::Get.new(uri.request_uri)
35
+ request["authorization"] = @token
36
+ http.use_ssl = true
37
+ response = http.request(request)
38
+ raise "request failure:\n\n#{response.body}" unless response.code == "200"
39
+ JSON.parse(response.body)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,49 @@
1
+ require "termcity/api"
2
+ require "termcity/version"
3
+ require "termcity/formatters/simple"
4
+ require "termcity/formatters/iterm2"
5
+ require "termcity/summary"
6
+
7
+ module Termcity
8
+ class CLI
9
+ def self.simple_format(**args)
10
+ new(**args).simple_format
11
+ end
12
+
13
+ def initialize(branch:, token:, host:, project_id:, revision:)
14
+ @branch = branch
15
+ @token = token
16
+ @host = host
17
+ @project_id = project_id
18
+ @revision = revision
19
+ end
20
+
21
+ def simple_format
22
+ api = Termcity::Api.new(
23
+ token: @token,
24
+ host: @host,
25
+ )
26
+
27
+ summary = Termcity::Summary.new(
28
+ api: api,
29
+ branch: @branch,
30
+ revision: @revision,
31
+ project_id: @project_id,
32
+ )
33
+
34
+ formatter =
35
+ if using_iterm?
36
+ Termcity::Formatters::Iterm2
37
+ else
38
+ Termcity::Formatters::Simple
39
+ end
40
+
41
+ formatter.new($stdout).format(summary)
42
+ end
43
+
44
+ def using_iterm?
45
+ ENV["TERM_PROGRAM"] == "iTerm.app" &&
46
+ ENV.fetch("TERM_PROGRAM_VERSION", "").match(/3.[23456789].[123456789]/)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ require "termcity/formatters/simple"
2
+
3
+ module Termcity
4
+ module Formatters
5
+ class Iterm2
6
+
7
+ attr_reader :io, :simple_formatter
8
+ def initialize(io)
9
+ @io = io
10
+ @simple_formatter = Simple.new(io)
11
+ end
12
+
13
+ def format(summary)
14
+ rows = summary.builds.map do |raw:, status:|
15
+ cols = []
16
+ cols << simple_formatter.status_string(status)
17
+ cols << linkify(raw.fetch("web_url"))
18
+ cols << raw.fetch("build_type")
19
+ cols.join(" ")
20
+ end
21
+
22
+ @io.puts(simple_formatter.summarize(summary, rows))
23
+ end
24
+
25
+ def linkify(url)
26
+ "\e]8;;#{url}\aLink\e]8;;\a"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,83 @@
1
+ require "termcity/formatters/util/color"
2
+
3
+ module Termcity
4
+ module Formatters
5
+ class Simple
6
+ include Utils::Color
7
+
8
+ STATUSES = {
9
+ queued: "queued",
10
+ failing: "failing",
11
+ running: "running",
12
+ failed_to_start: "failstrt",
13
+ failed: "failed",
14
+ success: "success"
15
+ }
16
+
17
+ COLORS = {
18
+ failing: :red,
19
+ queued: :yellow,
20
+ running: :blue,
21
+ failed_to_start: :default,
22
+ failed: :red,
23
+ success: :green
24
+ }
25
+
26
+ attr_reader :io
27
+ def initialize(io)
28
+ @io = io
29
+ end
30
+
31
+ def format(summary)
32
+ rows = summary.builds.map do |raw:, status:|
33
+ cols = []
34
+ cols << status_string(status)
35
+ cols << raw.fetch("build_type")
36
+ cols << raw.fetch("web_url")
37
+
38
+ cols.join(" : ")
39
+ end
40
+
41
+ @io.puts(summarize(summary, rows))
42
+ end
43
+
44
+ def status_string(type:, re_enqueued:)
45
+ name = STATUSES.fetch(type)
46
+ name = "#{name},q" if re_enqueued
47
+ color = COLORS[type]
48
+
49
+ colorize(name.ljust(10), color)
50
+ end
51
+
52
+ def summarize(summary, rows)
53
+ if summary.builds.empty?
54
+ "No builds found"
55
+ elsif summary.counts[:queued] == summary.counts[:total]
56
+ "This revision may still be in the queue (or may be unkown/old)"
57
+ else
58
+ [
59
+ rows,
60
+ "",
61
+ "Revision: #{summary.builds.first.dig(:raw, "sha")}",
62
+ "Overview: #{summary.overview_link}",
63
+ counts(summary),
64
+ ].join("\n")
65
+ end
66
+ end
67
+
68
+ def counts(summary)
69
+ [
70
+ ["Total", summary.counts[:total]],
71
+ ["Success", summary.counts[:success]],
72
+ ["Failure", summary.counts[:failure]],
73
+ ["Running", summary.counts[:running]],
74
+ ["FailedToStart", summary.counts[:failed_to_start]],
75
+ ["Queued", summary.counts[:queued]],
76
+ ["Re-Queued", summary.counts[:re_enqueued]]
77
+ ]
78
+ .map {|name, count| "#{name}: #{count}"}
79
+ .join(", ")
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,33 @@
1
+ module Termcity
2
+ module Formatters
3
+ module Utils
4
+ module Color
5
+ CODES = {
6
+ red: 31,
7
+ green: 32,
8
+ yellow: 33,
9
+ blue: 34,
10
+ pink: 35,
11
+ }.freeze
12
+
13
+ CODES.each do |color, code|
14
+ define_method(color) do |string|
15
+ colorize_code(string, code)
16
+ end
17
+ end
18
+
19
+ def colorize(string, color_name)
20
+ return string unless CODES.key?(color_name)
21
+ colorize_code(string, CODES[color_name])
22
+ end
23
+
24
+ private
25
+
26
+ # colorization
27
+ def colorize_code(string, color_code)
28
+ "\e[#{color_code}m#{string}\e[0m"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,91 @@
1
+ module Termcity
2
+ class Summary
3
+ def initialize(revision:, api:, branch:, project_id:)
4
+ @branch = branch
5
+ @project_id = project_id
6
+ @api = api
7
+ @revision = revision
8
+ end
9
+
10
+ def builds
11
+ data[:builds]
12
+ end
13
+
14
+ def counts
15
+ data[:counts]
16
+ end
17
+
18
+ def overview_link
19
+ data.dig(:links, :overview)
20
+ end
21
+
22
+ def data
23
+ # this is messy but allows for a single pass
24
+ @data ||= begin
25
+ counts = {
26
+ total: 0,
27
+ success: 0,
28
+ failure: 0,
29
+ running: 0,
30
+ queued: 0,
31
+ failed_to_start: 0,
32
+ re_enqueued: 0,
33
+ }
34
+
35
+ summary = @api.summary(branch: @branch, project_id: @project_id, revision: @revision)
36
+
37
+ builds = summary.fetch("builds")
38
+ .sort_by {|b| b.fetch("build_type")}
39
+ .map {|b| summarize(b, counts) }
40
+
41
+ {
42
+ links: {overview: summary.dig("links", "overview")},
43
+ builds: builds,
44
+ counts: counts
45
+ }
46
+ end
47
+ end
48
+
49
+ def summarize(build, counts)
50
+ status = status(build)
51
+
52
+ count_type =
53
+ if [:failing, :failed].include?(status[:type])
54
+ :failure
55
+ else
56
+ status[:type]
57
+ end
58
+ counts[count_type] += 1
59
+ counts[:total] += 1
60
+ counts[:re_enqueued] +=1 if status[:re_enqueued]
61
+
62
+ {
63
+ raw: build,
64
+ status: status
65
+ }
66
+ end
67
+
68
+ def status(build)
69
+ re_enqueued = build.fetch("re_enqueued")
70
+
71
+ if build.fetch("state") == "queued"
72
+ {type: :queued, re_enqueued: false}
73
+ elsif build.fetch("state") == "running"
74
+ if build.fetch("status") == "FAILURE"
75
+ {type: :failing, re_enqueued: re_enqueued}
76
+ else
77
+ {type: :running, re_enqueued: re_enqueued}
78
+ end
79
+ else
80
+ if build.fetch("failed_to_start", false)
81
+ {type: :failed_to_start, re_enqueued: re_enqueued}
82
+ elsif build.fetch("status") == "FAILURE"
83
+ {type: :failed, re_enqueued: re_enqueued}
84
+ else
85
+ {type: :success, re_enqueued: re_enqueued}
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
@@ -0,0 +1,3 @@
1
+ module Termcity
2
+ VERSION = "0.4.0"
3
+ end
@@ -0,0 +1,28 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "termcity/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "termcity"
8
+ spec.version = Termcity::VERSION
9
+ spec.authors = ["Pete Kinnecom"]
10
+ spec.email = ["git@k7u7.com"]
11
+
12
+ spec.summary = %q{Terminal view of TeamCity}
13
+ spec.description = %q{See TeamCity build status for a branch in your terminal. Pipe it to grep or whatever. Get nicely formatted links if you use iTerm2.}
14
+ # spec.homepage = "none"
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
19
+ spec.files = Dir.chdir(__dir__) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.16"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: termcity
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Pete Kinnecom
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-09-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description: See TeamCity build status for a branch in your terminal. Pipe it to grep
42
+ or whatever. Get nicely formatted links if you use iTerm2.
43
+ email:
44
+ - git@k7u7.com
45
+ executables:
46
+ - termcity
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".gitignore"
51
+ - Gemfile
52
+ - Rakefile
53
+ - bin/console
54
+ - bin/setup
55
+ - exe/termcity
56
+ - lib/termcity.rb
57
+ - lib/termcity/api.rb
58
+ - lib/termcity/cli.rb
59
+ - lib/termcity/formatters/iterm2.rb
60
+ - lib/termcity/formatters/simple.rb
61
+ - lib/termcity/formatters/util/color.rb
62
+ - lib/termcity/summary.rb
63
+ - lib/termcity/version.rb
64
+ - termcity.gemspec
65
+ homepage:
66
+ licenses: []
67
+ metadata:
68
+ allowed_push_host: https://rubygems.org
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.5.2
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Terminal view of TeamCity
89
+ test_files: []