jiminy 0.1.0.pre1

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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +20 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +0 -0
  6. data/Gemfile +12 -0
  7. data/Gemfile.lock +245 -0
  8. data/LICENSE +21 -0
  9. data/README.md +173 -0
  10. data/Rakefile +12 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/example.png +0 -0
  14. data/exe/jiminy +9 -0
  15. data/jiminy.gemspec +42 -0
  16. data/lib/jiminy/cli.rb +137 -0
  17. data/lib/jiminy/configuration.rb +97 -0
  18. data/lib/jiminy/github_apiable.rb +17 -0
  19. data/lib/jiminy/recording/n_plus_one.rb +50 -0
  20. data/lib/jiminy/recording/prosopite_ext/send_notifications_with_tmp_file.rb +44 -0
  21. data/lib/jiminy/recording/prosopite_ext/tmp_file_recorder.rb +40 -0
  22. data/lib/jiminy/recording/rspec.rb +22 -0
  23. data/lib/jiminy/recording/test_controller_concern.rb +26 -0
  24. data/lib/jiminy/recording.rb +14 -0
  25. data/lib/jiminy/reporting/ci_providers/circle_ci/api_request.rb +45 -0
  26. data/lib/jiminy/reporting/ci_providers/circle_ci/artifact.rb +17 -0
  27. data/lib/jiminy/reporting/ci_providers/circle_ci/base.rb +46 -0
  28. data/lib/jiminy/reporting/ci_providers/circle_ci/job.rb +17 -0
  29. data/lib/jiminy/reporting/ci_providers/circle_ci/pipeline.rb +49 -0
  30. data/lib/jiminy/reporting/ci_providers/circle_ci/vcs.rb +13 -0
  31. data/lib/jiminy/reporting/ci_providers/circle_ci/workflow.rb +41 -0
  32. data/lib/jiminy/reporting/ci_providers/circle_ci.rb +54 -0
  33. data/lib/jiminy/reporting/ci_providers/github.rb +29 -0
  34. data/lib/jiminy/reporting/ci_providers/local/artifact.rb +25 -0
  35. data/lib/jiminy/reporting/ci_providers/local.rb +30 -0
  36. data/lib/jiminy/reporting/ci_providers/provider_configuration.rb +28 -0
  37. data/lib/jiminy/reporting/ci_providers.rb +12 -0
  38. data/lib/jiminy/reporting/n_plus_one.rb +39 -0
  39. data/lib/jiminy/reporting/reporters/base_reporter.rb +26 -0
  40. data/lib/jiminy/reporting/reporters/dry_run_reporter.rb +13 -0
  41. data/lib/jiminy/reporting/reporters/github_reporter.rb +28 -0
  42. data/lib/jiminy/reporting/reporters.rb +11 -0
  43. data/lib/jiminy/reporting/yaml_file_comment_presenter.rb +71 -0
  44. data/lib/jiminy/reporting.rb +32 -0
  45. data/lib/jiminy/rspec.rb +3 -0
  46. data/lib/jiminy/setup.rb +10 -0
  47. data/lib/jiminy/templates/reporting/comment_header.md.erb +5 -0
  48. data/lib/jiminy/templates/reporting/n_plus_one.md.erb +12 -0
  49. data/lib/jiminy/version.rb +5 -0
  50. data/lib/jiminy.rb +13 -0
  51. metadata +254 -0
data/lib/jiminy/cli.rb ADDED
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ require "thor"
5
+ require "byebug"
6
+ class CLI < Thor
7
+ require "jiminy/reporting/ci_providers/circle_ci"
8
+ include Jiminy::Reporting::CIProviders
9
+
10
+ class WorkflowStillRunningError < StandardError; end
11
+ private_constant :WorkflowStillRunningError
12
+
13
+ MAX_TIMEOUT = 1800 # 1 hour
14
+ POLL_INTERVAL = 60 # 1 min
15
+
16
+ def self.exit_on_failure?
17
+ false
18
+ end
19
+
20
+ desc "Report results", "Reports the results of tests"
21
+ method_option :commit, type: :string, aliases: "c", required: true,
22
+ banner: "3e078f8770743549b722382ec5d412a30b9fdcc5",
23
+ desc: "The full SHA for the current HEAD commit"
24
+ method_option :pr_number, type: :numeric, aliases: %w[pr p], required: true,
25
+ banner: "1",
26
+ desc: "The GitHub PR number"
27
+ method_option :dry_run, type: :boolean, default: false, lazy_default: true,
28
+ desc: "Print to STDOUT instead of leaving a comment on GitHub"
29
+ method_option :timeout, type: :numeric, aliases: %w[max-timeout], default: MAX_TIMEOUT,
30
+ desc: "How long to poll CircleCI before timing out (in seconds)"
31
+ method_option :poll_interval, type: :numeric, aliases: %w[poll-interval], default: POLL_INTERVAL,
32
+ desc: "How frequently to poll CircleCI (in seconds)"
33
+ method_option :source, type: :string, default: "circleci",
34
+ desc: "Where are the results.yml files we should report?"
35
+ def report
36
+ self.start_time = Time.now
37
+ artifact_urls = artifacts.map(&:url)
38
+
39
+ Jiminy::Reporting.report!(*artifact_urls,
40
+ pr_number: options[:pr_number],
41
+ dry_run: options[:dry_run])
42
+
43
+ $stdout.puts "Reported N+1s successfully"
44
+ exit(0)
45
+ end
46
+
47
+ # rubocop:disable Metrics/BlockLength
48
+ no_tasks do
49
+ attr_accessor :start_time
50
+
51
+ def poll_interval
52
+ options[:poll_interval] || POLL_INTERVAL
53
+ end
54
+
55
+ def source
56
+ options[:source] || :circleci
57
+ end
58
+
59
+ def max_timeout
60
+ options[:timeout] || MAX_TIMEOUT
61
+ end
62
+
63
+ def timed_out?
64
+ (Time.now - start_time) > max_timeout
65
+ end
66
+
67
+ def git_revision
68
+ options[:commit]
69
+ end
70
+
71
+ def pr_number
72
+ options[:pr_number]
73
+ end
74
+
75
+ def pipeline
76
+ @_pipeline ||= CircleCI::Pipeline.find_by_revision(git_revision: git_revision, pr_number: pr_number) or
77
+ abort("No such Pipeline with commit SHA #{git_revision}")
78
+ end
79
+
80
+ def missing_options
81
+ [].tap do |array|
82
+ array.concat("--pr-number") unless pr_number
83
+ array.concat("--commit") unless git_revision
84
+ end
85
+ end
86
+
87
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
88
+ def workflow
89
+ @_workflow ||= begin
90
+ result = CircleCI::Workflow.find(pipeline_id: pipeline.id, workflow_name: Jiminy.config.ci_workflow_name)
91
+ if result.nil?
92
+ abort("Unable to find workflow called '#{Jiminy.config.ci_workflow_name}' in Pipeline #{pipeline.id}")
93
+ end
94
+
95
+ if result.not_run? || result.running?
96
+ $stdout.puts "Workflow still running..."
97
+ raise(WorkflowStillRunningError)
98
+ end
99
+ abort("Workflow #{result.status}—aborting...") unless result.success?
100
+
101
+ result
102
+ rescue WorkflowStillRunningError
103
+ sleep(poll_interval)
104
+ $stdout.puts "Retrying..."
105
+ retry unless timed_out?
106
+
107
+ abort("Process timed out after #{Time.now - start_time} seconds")
108
+ end
109
+ end
110
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
111
+
112
+ def jobs
113
+ @_jobs ||= CircleCI::Job.all(workflow_id: workflow.id)
114
+ end
115
+
116
+ def artifacts
117
+ @_artifacts ||= begin
118
+ unless respond_to?(:"artifacts_from_#{source}")
119
+ raise ArgumentError, "Uknown value for source option #{source}"
120
+ end
121
+
122
+ public_send(:"artifacts_from_#{source}")
123
+ end
124
+ end
125
+
126
+ def artifacts_from_local
127
+ Local::Artifact.all
128
+ end
129
+
130
+ def artifacts_from_circle_ci
131
+ CircleCI::Artifact.all(job_number: jobs.first.job_number)
132
+ end
133
+ alias_method :artifacts_from_circleci, :artifacts_from_circle_ci
134
+ end
135
+ # rubocop:enable Metrics/BlockLength
136
+ end
137
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ class Configuration
5
+ DEFAULT_CONFIG_READER = -> { instance_variable_get(:"@_#{__callee__}" || raise_missing_required(__callee__)) }
6
+ DEFAULT_CONFIG_WRITER = ->(value) { instance_variable_set(:"@_#{__callee__}"[0..-2], value) }
7
+
8
+ class MissingConfigError < StandardError
9
+ attr_reader :missing_config_name
10
+
11
+ def initialize(missing_config_name)
12
+ super()
13
+ @missing_config_name = missing_config_name
14
+ end
15
+
16
+ def message
17
+ <<~MESSAGE
18
+ You must provide a configuration value for ##{missing_config_name}
19
+
20
+ This probably means you haven't set Jiminy.config.#{missing_config_name}
21
+ MESSAGE
22
+ end
23
+ end
24
+
25
+ class << self
26
+ def define_config(name, options = {})
27
+ define_method(:"#{name}=", DEFAULT_CONFIG_WRITER)
28
+ define_method(name.to_sym, DEFAULT_CONFIG_READER)
29
+ defined_configs[name].merge!(options)
30
+ end
31
+
32
+ def defined_configs
33
+ @_defined_configs ||= Hash.new { |hash, key| hash[key] = { default: nil, required: true } }
34
+ end
35
+ end
36
+
37
+ define_config :ci_workflow_name, default: "build"
38
+
39
+ define_config :circle_ci_api_token
40
+
41
+ define_config :github_token
42
+
43
+ define_config :ignore_file_path, default: File.join("./jiminy_ignores.yml")
44
+
45
+ define_config :project_reponame
46
+
47
+ define_config :project_username
48
+
49
+ define_config :temp_file_location, default: File.join("./tmp/jiminy/results.yml")
50
+
51
+ def initialize
52
+ apply_defaults!
53
+ end
54
+
55
+ def repo_path
56
+ [project_username, project_reponame].join("/")
57
+ end
58
+
59
+ private
60
+
61
+ def defined_configs_with_defaults
62
+ self.class.defined_configs.select { |_config_name, options| options[:default] }
63
+ end
64
+
65
+ def required_configs
66
+ self.class.defined_configs.select { |_config_name, options| options[:required] }
67
+ end
68
+
69
+ def apply_defaults!
70
+ defined_configs_with_defaults.each do |config_name, options|
71
+ public_send(:"#{config_name}=", options[:default])
72
+ end
73
+ end
74
+
75
+ def raise_missing_required(config_name)
76
+ return unless required_configs.key?(config_name)
77
+
78
+ raise MissingConfigError, config_name
79
+ end
80
+ end
81
+
82
+ module ConfigurationMethods
83
+ def configure(&block)
84
+ block.call(configuration)
85
+ end
86
+
87
+ def configuration
88
+ @_configuration ||= Configuration.new
89
+ end
90
+
91
+ alias config configuration
92
+
93
+ def configured?
94
+ !!configuration
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module GithubAPIable
5
+ require "octokit"
6
+
7
+ private
8
+
9
+ def env_config
10
+ @_env_config ||= Reporting::CIProviders::Github::Configuration.new
11
+ end
12
+
13
+ def client
14
+ @_client ||= Octokit::Client.new(access_token: env_config.github_token)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ class NPlusOne
6
+ attr_reader :file, :location
7
+
8
+ LOCATION_MATCHER = /(?<file>.+\.rb):
9
+ (?<line>\d+):in\s`
10
+ (?:block\sin\s)?
11
+ (?<method_name>.+)'
12
+ /x.freeze
13
+
14
+ EXAMPLES_COUNT = 3
15
+
16
+ def initialize(location:, queries: [])
17
+ @location = location.to_s.strip
18
+ @queries = queries
19
+ match = location.match(LOCATION_MATCHER)
20
+ @line = match[:line]
21
+ @method_name = match[:method_name]
22
+ @file = match[:file]
23
+ freeze
24
+ end
25
+
26
+ def ==(other)
27
+ location == other.location
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ location => attributes
33
+ }
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :queries, :line, :method_name
39
+
40
+ def attributes
41
+ {
42
+ "file" => file,
43
+ "line" => line,
44
+ "method" => method_name,
45
+ "examples" => queries.take(EXAMPLES_COUNT)
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ module ProsopiteExt
6
+ require_relative "tmp_file_recorder"
7
+
8
+ module SendNotificationsWithTmpFile
9
+ def prepare_results_file!
10
+ TmpFileRecorder.prepare_results_file!
11
+ end
12
+
13
+ def tmp_file=(value)
14
+ @tmp_file = value
15
+ end
16
+
17
+ def tmp_file
18
+ !!@tmp_file
19
+ end
20
+
21
+ def send_notifications
22
+ super
23
+
24
+ return unless Prosopite.tmp_file
25
+
26
+ # https://github.com/charkost/prosopite/blob/main/lib/prosopite.rb#L157
27
+ tc[:prosopite_notifications].each do |queries, backtrace|
28
+ absolute_location = backtrace.detect { |path| path.exclude?(Bundler.bundle_path.to_s) }
29
+ next unless absolute_location
30
+
31
+ relative_location = absolute_location.gsub("#{Rails.root.realpath}/", "")
32
+ tmp_file_recorder.record(location: relative_location, queries: queries)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def tmp_file_recorder
39
+ @_tmp_file_recorder ||= TmpFileRecorder.new
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ module ProsopiteExt
6
+ class TmpFileRecorder
7
+ require_relative "../n_plus_one"
8
+
9
+ def record(location:, queries:)
10
+ yaml_content = File.read(Jiminy.config.temp_file_location)
11
+ array = YAML.safe_load(yaml_content)
12
+ n_plus_one = NPlusOne.new(location: location, queries: queries)
13
+
14
+ array << n_plus_one.to_h unless location_in_array?(location, array) || filepath_ignored?(n_plus_one.file)
15
+ File.write(Jiminy.config.temp_file_location, array.to_yaml)
16
+ end
17
+
18
+ private
19
+
20
+ def location_in_array?(location, array)
21
+ array.detect { |hash| hash.key?(location) }
22
+ end
23
+
24
+ def filepath_ignored?(filepath)
25
+ ignored_files.include?(filepath)
26
+ end
27
+
28
+ def ignored_files
29
+ @_ignored_files ||= load_ignored_files
30
+ end
31
+
32
+ def load_ignored_files
33
+ return [] unless File.exist?(Jiminy.config.ignore_file_path)
34
+
35
+ YAML.load_file(Jiminy.config.ignore_file_path) || []
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ require "jiminy/recording/prosopite_ext/send_notifications_with_tmp_file"
6
+
7
+ module RSpec
8
+ require "prosopite"
9
+ require "jiminy/recording/test_controller_concern"
10
+
11
+ ::Prosopite.singleton_class.prepend ProsopiteExt::SendNotificationsWithTmpFile
12
+
13
+ def wrap_rspec_example(example)
14
+ Prosopite.tmp_file = true
15
+ ActionController::Base.include(Jiminy::Recording::TestControllerConcern)
16
+ example.run
17
+ Prosopite.tmp_file = false
18
+ end
19
+ Jiminy.extend(self)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ module TestControllerConcern
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ around_action :_test_n_plus_one_detection
10
+ end
11
+
12
+ private
13
+
14
+ def _test_n_plus_one_detection
15
+ yield and return if Prosopite.scan?
16
+
17
+ begin
18
+ Prosopite.scan
19
+ yield
20
+ ensure
21
+ Prosopite.finish
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ abort("Jiminy::Recording must be run from a Rails app") unless defined?(Rails)
6
+
7
+ module_function
8
+
9
+ def reset_results_file!
10
+ Rails.root.join(File.dirname(Jiminy.config.temp_file_location)).mkpath
11
+ File.write(Jiminy.config.temp_file_location, [].to_yaml)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ class APIRequest
8
+ API_BASE = "https://circleci.com/api/v2/"
9
+
10
+ CIRCLE_TOKEN_HEADER = "Circle-Token"
11
+
12
+ def initialize(path)
13
+ @url = URI.join(API_BASE, path)
14
+ end
15
+
16
+ def perform!
17
+ puts url
18
+ response
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :url
24
+
25
+ def response
26
+ @_response ||= http.request(request)
27
+ end
28
+
29
+ def request
30
+ @_request ||= Net::HTTP::Get.new(url).tap do |req|
31
+ req[CIRCLE_TOKEN_HEADER] = Jiminy.config.circle_ci_api_token
32
+ end
33
+ end
34
+
35
+ def http
36
+ @_http ||= Net::HTTP.new(url.host, url.port).tap do |http_instance|
37
+ http_instance.use_ssl = true
38
+ http_instance.verify_mode = OpenSSL::SSL::VERIFY_NONE
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ class Artifact < Base
8
+ define_attribute_readers :url
9
+
10
+ def self.all(job_number:)
11
+ fetch_api_resource("project/gh/#{Jiminy.config.repo_path}/#{job_number}/artifacts")
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ require_relative "api_request"
8
+
9
+ class Base
10
+ require "uri"
11
+ require "net/http"
12
+ require "json"
13
+
14
+ attr_reader :attributes
15
+
16
+ def self.define_attribute_readers(*attr_names)
17
+ attr_names.each do |attr_name|
18
+ define_method(attr_name, -> { attributes[__callee__.to_s] })
19
+ end
20
+ end
21
+
22
+ class << self
23
+ attr_reader :next_token
24
+ end
25
+
26
+ class << self
27
+ attr_writer :next_token
28
+ end
29
+
30
+ def self.fetch_api_resource(path)
31
+ response = APIRequest.new(path).perform!
32
+ raise "Error response: #{response.body}" unless response.is_a?(Net::HTTPOK)
33
+
34
+ json_body = JSON.parse(response.read_body)
35
+ self.next_token = json_body["next_page_token"]
36
+ json_body["items"].map { |attributes| new(attributes) }
37
+ end
38
+
39
+ def initialize(attributes = {})
40
+ @attributes = attributes.transform_keys!(&:to_s)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ class Job < Base
8
+ define_attribute_readers :job_number
9
+
10
+ def self.all(workflow_id:)
11
+ fetch_api_resource("workflow/#{workflow_id}/job")
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ require_relative "base"
8
+ require_relative "vcs"
9
+
10
+ class Pipeline < Base
11
+ MAX_PAGE_LOOKUP = 20
12
+
13
+ define_attribute_readers :id, :project_slug, :vcs
14
+
15
+ def self.find_by_revision(git_revision:, pr_number:)
16
+ attempt_count = 0
17
+ matching_pipeline = nil
18
+ until matching_pipeline || attempt_count >= MAX_PAGE_LOOKUP
19
+ page_pipelines = fetch_page_from_api(next_token)
20
+ matching_pipeline = page_pipelines.detect { |p| pipeline_match?(p, git_revision, pr_number) }
21
+ attempt_count += 1
22
+ end
23
+ matching_pipeline
24
+ end
25
+
26
+ def self.fetch_page_from_api(page_token)
27
+ query = "org-slug=gh/#{Jiminy.config.project_username}&mine=false"
28
+ query += "&page-token=#{page_token}" if page_token
29
+ url = "pipeline?#{query}"
30
+ fetch_api_resource(url)
31
+ end
32
+
33
+ def self.pipeline_match?(pipeline, git_revision, _pr_number)
34
+ return false unless pipeline.project_slug.to_s.downcase.end_with?(Jiminy.config.repo_path.downcase)
35
+
36
+ pipeline.vcs.revision == git_revision
37
+
38
+ # TODO: Get PR comparison working too
39
+ # pipeline.vcs.review_url.end_with?("pull/#{pr_number}")
40
+ end
41
+
42
+ def vcs
43
+ VCS.new(attributes["vcs"])
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ class VCS < Base
8
+ define_attribute_readers :revision, :review_url
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ class Workflow < Base
8
+ # Link to states: https://circleci.com/docs/2.0/workflows/#states
9
+ SUCCESS = "success"
10
+ FAILED = "failed"
11
+ NOT_RUN = "not run"
12
+ RUNNING = "running"
13
+
14
+ define_attribute_readers :id, :name, :status
15
+
16
+ def self.find(pipeline_id:, workflow_name:)
17
+ url = "pipeline/#{pipeline_id}/workflow"
18
+ collection = fetch_api_resource(url)
19
+ collection.detect { |w| w.name.to_s == workflow_name.to_s }
20
+ end
21
+
22
+ def success?
23
+ status == SUCCESS
24
+ end
25
+
26
+ def running?
27
+ status == RUNNING
28
+ end
29
+
30
+ def not_run?
31
+ status == NOT_RUN
32
+ end
33
+
34
+ def failed?
35
+ status == FAILED
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end