semaph 0.6.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31aeabe99b27b7a10bc26f91c6a60e87e3374b309df5e41b8354694d99bd1a90
4
- data.tar.gz: 34499279ff5fedb14739093655ad29ff703c9c3d40222208783106d73d746841
3
+ metadata.gz: 7db37cbca829e1b13372c7a8687b86080b1e4a579bbb7707e6cc207ebdcb4ba1
4
+ data.tar.gz: 4b5015379f62ad0382991f26b525583d2a6e077889071e5bde58dc1399d69ada
5
5
  SHA512:
6
- metadata.gz: 47939331aee8435b5584f29ab39cf9647730dc0ead3b340c20175df3da849120aec5e9b97d3cd7dc116aa6044ac4fd9e1c4648b6ece862afe01a40b8e5007da9
7
- data.tar.gz: 4f7b74225fd180ea85fad14c73a8a808733fc1d4836b69b99069a25314996bdd70066c1c1bb35b5e247cbeeefe5191760af50102d338e65992fdbd44ebfe529c
6
+ metadata.gz: 243de9cc3425299a5d7a5a11d3a6e06a89e56ad22e0bab59fe4568544fc8e05f0a6e902363497d493811a00c30f84a95456a0679417fb7d4d39b10ad35ece692
7
+ data.tar.gz: f1fd8d22e22885357f0b5c1ba962ac1edb91b61b6ab3b9e03cfec9aeda88197a40bfcfe2b047417d3d2629e5eb8f9fa3e32b9dde40e8feefeb09bae8e7e5527d
@@ -1,3 +1,42 @@
1
+ Layout/EmptyLinesAroundAttributeAccessor:
2
+ Enabled: true
3
+
4
+ Layout/SpaceAroundMethodCallOperator:
5
+ Enabled: true
6
+
7
+ Lint/DeprecatedOpenSSLConstant:
8
+ Enabled: true
9
+
10
+ Lint/MixedRegexpCaptureTypes:
11
+ Enabled: true
12
+
13
+ Lint/RaiseException:
14
+ Enabled: true
15
+
16
+ Lint/StructNewOverride:
17
+ Enabled: true
18
+
19
+ Style/ExponentialNotation:
20
+ Enabled: true
21
+
22
+ Style/HashEachMethods:
23
+ Enabled: true
24
+
25
+ Style/HashTransformKeys:
26
+ Enabled: true
27
+
28
+ Style/HashTransformValues:
29
+ Enabled: true
30
+
31
+ Style/RedundantRegexpCharacterClass:
32
+ Enabled: true
33
+
34
+ Style/RedundantRegexpEscape:
35
+ Enabled: true
36
+
37
+ Style/SlicingWithRange:
38
+ Enabled: true
39
+
1
40
  Style/Documentation:
2
41
  Enabled: false
3
42
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- semaph (0.6.0)
4
+ semaph (0.7.0)
5
5
  faraday
6
6
  rainbow
7
7
  shell_shock
@@ -3,7 +3,17 @@ require "json"
3
3
 
4
4
  module Semaph
5
5
  # Refer to https://docs.semaphoreci.com/reference/api-v1alpha/
6
- class Api
6
+ class Client
7
+ class RequestException < RuntimeError
8
+ attr_reader :url, :response
9
+
10
+ def initialize(url, response)
11
+ @url = url
12
+ @response = response
13
+ super("http response #{response.status} received for #{url}:\n#{response.body}")
14
+ end
15
+ end
16
+
7
17
  attr_reader :host, :name
8
18
 
9
19
  def initialize(token, host)
@@ -42,10 +52,22 @@ module Semaph
42
52
  get "pipelines/#{id}", { detailed: true }
43
53
  end
44
54
 
55
+ def promotions(pipeline_id)
56
+ get "promotions", { pipeline_id: pipeline_id }
57
+ end
58
+
59
+ def promote(pipeline_id, name)
60
+ post "promotions", { pipeline_id: pipeline_id, name: name }
61
+ end
62
+
45
63
  def job(id)
46
64
  get "jobs/#{id}"
47
65
  end
48
66
 
67
+ def stop_job(id)
68
+ post "jobs/#{id}/stop"
69
+ end
70
+
49
71
  def job_log(id)
50
72
  get_raw "jobs/#{id}/plain_logs.json"
51
73
  end
@@ -87,8 +109,7 @@ module Semaph
87
109
  def check_response(response, url)
88
110
  return response.body if response.status == 200
89
111
 
90
- puts "http response #{response.status} received for #{url}:\n#{response.body}"
91
- exit 1
112
+ raise RequestException.new(url, response)
92
113
  end
93
114
  end
94
115
  end
@@ -1,3 +1,5 @@
1
+ require "rainbow"
2
+
1
3
  module Semaph
2
4
  module Formatting
3
5
  TIME_FORMAT = "%m-%d %H:%M".freeze
@@ -5,5 +7,17 @@ module Semaph
5
7
  def self.time(time)
6
8
  time.strftime(TIME_FORMAT)
7
9
  end
10
+
11
+ def self.hours_minutes_seconds(total_seconds)
12
+ seconds = total_seconds % 60
13
+ minutes = (total_seconds / 60) % 60
14
+ hours = total_seconds / (60 * 60)
15
+
16
+ format("%02<hours>d:%02<minutes>d:%02<seconds>d", hours: hours, minutes: minutes, seconds: seconds)
17
+ end
18
+
19
+ def self.index(number)
20
+ Rainbow(number.to_s.rjust(2)).yellow
21
+ end
8
22
  end
9
23
  end
@@ -22,6 +22,14 @@ module Semaph
22
22
  assign_from_block(raw_block)
23
23
  end
24
24
 
25
+ def stop
26
+ pipeline.workflow.project.client.stop_job(id)
27
+ end
28
+
29
+ def show
30
+ pp pipeline.workflow.project.client.job(id)
31
+ end
32
+
25
33
  def write_log(base)
26
34
  FileUtils.mkdir_p(base)
27
35
  filename = "#{base}/#{id}.log"
@@ -1,15 +1,18 @@
1
+ require "semaph/formatting"
1
2
  require "semaph/model/job_collection"
3
+ require "semaph/model/promotion_collection"
2
4
 
3
5
  module Semaph
4
6
  module Model
5
7
  class Pipeline
6
- attr_reader :workflow, :raw, :id, :yaml, :state, :result
8
+ attr_reader :workflow, :raw, :id, :name, :yaml, :state, :result
7
9
 
8
10
  def initialize(workflow, raw)
9
11
  @workflow = workflow
10
12
  @raw = raw
11
13
  @id = raw["ppl_id"]
12
14
  @yaml = raw["yaml_file_name"]
15
+ @name = raw["name"]
13
16
  @state = raw["state"]
14
17
  @result = raw["result"]
15
18
  %w[created done pending queuing running stopping].each do |name|
@@ -17,16 +20,33 @@ module Semaph
17
20
  end
18
21
  end
19
22
 
23
+ def promote(name)
24
+ workflow.project.client.promote(id, name)
25
+ end
26
+
20
27
  def job_collection
21
28
  @job_collection ||= JobCollection.new(self)
22
29
  end
23
30
 
31
+ def promotion_collection
32
+ @promotion_collection ||= PromotionCollection.new(self)
33
+ end
34
+
24
35
  def description
25
- "#{icon} #{yaml}"
36
+ [
37
+ icon,
38
+ time,
39
+ name,
40
+ "(#{yaml})",
41
+ ].compact.join(" ")
42
+ end
43
+
44
+ def done?
45
+ @state == "DONE"
26
46
  end
27
47
 
28
48
  def icon
29
- return "🔵" unless @state == "DONE"
49
+ return "🔵" unless done?
30
50
 
31
51
  return "⛔" if @result == "STOPPED"
32
52
 
@@ -37,6 +57,12 @@ module Semaph
37
57
 
38
58
  private
39
59
 
60
+ def time
61
+ return ::Semaph::Formatting.hours_minutes_seconds(@done_at.to_i - @created_at.to_i) if done?
62
+
63
+ ::Semaph::Formatting.hours_minutes_seconds(Time.now.to_i - @created_at.to_i)
64
+ end
65
+
40
66
  def extract_time(name)
41
67
  key = "#{name}_at"
42
68
  return if raw[key]["seconds"].zero?
@@ -0,0 +1,31 @@
1
+ require "semaph/formatting"
2
+
3
+ module Semaph
4
+ module Model
5
+ class Promotion
6
+ attr_reader :pipeline, :raw
7
+
8
+ def initialize(pipeline, raw)
9
+ @pipeline = pipeline
10
+ @raw = raw
11
+ @name = raw["name"]
12
+ @status = raw["status"]
13
+ @triggered_at = Time.at(raw["triggered_at"]["seconds"])
14
+ end
15
+
16
+ def description
17
+ [
18
+ status_icon,
19
+ Semaph::Formatting.time(@triggered_at),
20
+ @name,
21
+ ].join(" ")
22
+ end
23
+
24
+ def status_icon
25
+ return "🟢" if @status == "passed"
26
+
27
+ "🔴"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ require "semaph/model/promotion"
2
+
3
+ module Semaph
4
+ module Model
5
+ class PromotionCollection
6
+ attr_reader :all, :pipeline
7
+
8
+ def initialize(pipeline)
9
+ @pipeline = pipeline
10
+ end
11
+
12
+ def reload
13
+ workflow = @pipeline.workflow
14
+ project = workflow.project
15
+ @all = project.client.promotions(@pipeline.id).map do |promotion_response|
16
+ Promotion.new(@pipeline, promotion_response)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,4 +1,4 @@
1
- require "semaph/api"
1
+ require "semaph/client"
2
2
  require "semaph/commands/reload_command"
3
3
  require "semaph/model/project_collection"
4
4
  require "semaph/shells/organisation/projects_list_command"
@@ -12,7 +12,7 @@ module Semaph
12
12
  include ShellShock::Context
13
13
 
14
14
  def initialize(organisation)
15
- @client = ::Semaph::Api.new(organisation["auth"]["token"], organisation["host"])
15
+ @client = ::Semaph::Client.new(organisation["auth"]["token"], organisation["host"])
16
16
  @prompt = "🏗 #{@client.name} > "
17
17
  add_commands
18
18
  @project_list_command.execute("")
@@ -18,6 +18,12 @@ module Semaph
18
18
 
19
19
  def execute(name)
20
20
  selected_project = @project_collection.all.find { |project| project.name == name }
21
+
22
+ unless selected_project
23
+ puts "There is no project called #{name}"
24
+ return
25
+ end
26
+
21
27
  ::Semaph::Shells::Project::ProjectShell.new(selected_project).push
22
28
  end
23
29
  end
@@ -17,7 +17,14 @@ module Semaph
17
17
  end
18
18
 
19
19
  def execute(name)
20
- ::Semaph::Shells::Organisation::OrganisationShell.new(@organisations[name]).push
20
+ organisation = @organisations[name]
21
+
22
+ unless organisation
23
+ puts "There is no organisation called #{name}"
24
+ return
25
+ end
26
+
27
+ ::Semaph::Shells::Organisation::OrganisationShell.new(organisation).push
21
28
  end
22
29
  end
23
30
  end
@@ -0,0 +1,29 @@
1
+ module Semaph
2
+ module Shells
3
+ module Pipeline
4
+ class JobDebugCommand
5
+ attr_reader :usage, :help
6
+
7
+ def initialize(job_collection)
8
+ @job_collection = job_collection
9
+ @usage = "<job index>"
10
+ @help = "debug job"
11
+ end
12
+
13
+ def execute(index_string)
14
+ index = index_string.to_i - 1
15
+
16
+ job = @job_collection.all[index]
17
+
18
+ unless job
19
+ puts "There is no job at position #{index}"
20
+ return
21
+ end
22
+
23
+ puts "Debugging #{job.description}"
24
+ system("sem debug job #{job.id}")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ module Semaph
2
+ module Shells
3
+ module Pipeline
4
+ class JobShowCommand
5
+ attr_reader :usage, :help
6
+
7
+ def initialize(job_collection)
8
+ @job_collection = job_collection
9
+ @usage = "<job index>"
10
+ @help = "show job"
11
+ end
12
+
13
+ def execute(index_string)
14
+ index = index_string.to_i - 1
15
+
16
+ job = @job_collection.all[index]
17
+
18
+ unless job
19
+ puts "There is no job at position #{index}"
20
+ return
21
+ end
22
+
23
+ puts job.show
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ module Semaph
2
+ module Shells
3
+ module Pipeline
4
+ class JobStopCommand
5
+ attr_reader :usage, :help
6
+
7
+ def initialize(job_collection)
8
+ @job_collection = job_collection
9
+ @usage = "<job index>"
10
+ @help = "stop job"
11
+ end
12
+
13
+ def execute(index_string)
14
+ index = index_string.to_i - 1
15
+
16
+ job = @job_collection.all[index]
17
+
18
+ unless job
19
+ puts "There is no job at position #{index}"
20
+ return
21
+ end
22
+
23
+ job.stop
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,3 +1,5 @@
1
+ require "semaph/formatting"
2
+
1
3
  module Semaph
2
4
  module Shells
3
5
  module Pipeline
@@ -9,10 +11,10 @@ module Semaph
9
11
  @help = "list jobs"
10
12
  end
11
13
 
12
- def execute(_whatever)
14
+ def execute(_whatever = nil)
13
15
  @job_collection.reload
14
16
  @job_collection.all.each_with_index do |job, index|
15
- puts "#{index + 1} #{job.description}"
17
+ puts [::Semaph::Formatting.index(index + 1), job.description].join(" ")
16
18
  end
17
19
  end
18
20
  end
@@ -1,34 +1,33 @@
1
+ require "semaph/formatting"
2
+
1
3
  module Semaph
2
4
  module Shells
3
5
  module Pipeline
4
6
  class JobsPollCommand
5
7
  attr_reader :usage, :help, :job_collection
6
8
 
7
- def initialize(job_collection)
9
+ def initialize(job_collection, list_command)
8
10
  @job_collection = job_collection
11
+ @list_command = list_command
9
12
  @help = "poll jobs"
10
13
  @can_notify = !`which terminal-notifier`.chomp.empty?
11
14
  end
12
15
 
13
- def execute(_whatever)
14
- while job_collection.incomplete.count.positive?
15
- report_incomplete(job_collection.incomplete)
16
- if job_collection.failed.count.positive?
17
- report_failures(job_collection.failed)
18
- return
19
- end
20
- sleep 20
21
- job_collection.reload
22
- end
16
+ def execute(_whatever = nil)
17
+ report_and_reload(15) while job_collection.incomplete.count.positive? && job_collection.failed.count.zero?
23
18
  report_final
24
19
  end
25
20
 
26
21
  private
27
22
 
23
+ def report_and_reload(period)
24
+ report_incomplete(job_collection.incomplete)
25
+ sleep period
26
+ job_collection.reload
27
+ end
28
+
28
29
  def report_final
29
- job_collection.all.each_with_index do |job, index|
30
- puts "#{index + 1} #{job.description}"
31
- end
30
+ @list_command.execute
32
31
  failed_job_count = job_collection.failed.count
33
32
  notify(
34
33
  "Workflow completed",
@@ -43,12 +42,6 @@ module Semaph
43
42
  incomplete_jobs.each { |job| puts job.description }
44
43
  end
45
44
 
46
- def report_failures(failed_jobs)
47
- puts "Some jobs have failed:"
48
- failed_jobs.each { |job| puts job.description }
49
- notify("Job Failures", "#{failed_jobs.count} jobs have failed", true)
50
- end
51
-
52
45
  def notify(title, message, failed)
53
46
  return unless @can_notify
54
47
 
@@ -1,8 +1,13 @@
1
1
  require "semaph/commands"
2
- require "semaph/shells/pipeline/jobs_list_command"
2
+ require "semaph/shells/pipeline/job_debug_command"
3
3
  require "semaph/shells/pipeline/job_log_command"
4
4
  require "semaph/shells/pipeline/job_log_grep_command"
5
+ require "semaph/shells/pipeline/job_show_command"
6
+ require "semaph/shells/pipeline/job_stop_command"
7
+ require "semaph/shells/pipeline/jobs_list_command"
5
8
  require "semaph/shells/pipeline/jobs_poll_command"
9
+ require "semaph/shells/pipeline/promote_command"
10
+ require "semaph/shells/pipeline/promotions_list_command"
6
11
  require "shell_shock/context"
7
12
 
8
13
  module Semaph
@@ -18,27 +23,37 @@ module Semaph
18
23
  workflow = pipeline.workflow
19
24
  project = workflow.project
20
25
  @prompt = "🏗 #{project.client.name} #{project.name} #{workflow.id} #{pipeline.yaml} > "
26
+ add_command ::Semaph::Commands::ReloadCommand.new, "reload" if ENV["SEMAPH_RELOAD"]
21
27
  add_commands
22
- @jobs_list_command.execute("")
28
+ jobs_list_command.execute
23
29
  end
24
30
 
25
31
  private
26
32
 
27
33
  def add_commands
28
- add_command ::Semaph::Commands::ReloadCommand.new, "reload" if ENV["SEMAPH_RELOAD"]
29
- workflow = pipeline.workflow
30
- ::Semaph::Commands.workflow_commands(self, workflow)
34
+ ::Semaph::Commands.workflow_commands(self, pipeline.workflow)
31
35
  add_job_collection_commands(pipeline.job_collection)
36
+ add_command PromoteCommand.new(pipeline), "promote"
37
+ add_command PromotionsListCommand.new(pipeline.promotion_collection), "list-promotions"
32
38
  end
33
39
 
34
40
  def add_job_collection_commands(job_collection)
35
- @jobs_list_command = JobsListCommand.new(job_collection)
36
- add_command @jobs_list_command, "list-jobs", "ls"
37
- add_command JobsPollCommand.new(job_collection), "poll-jobs"
38
- add_command JobLogCommand.new(job_collection), "job-log"
41
+ add_command JobsPollCommand.new(job_collection, jobs_list_command), "poll"
42
+ add_command JobLogCommand.new(job_collection), "log"
43
+ add_command JobDebugCommand.new(job_collection), "debug"
44
+ add_command JobShowCommand.new(job_collection), "show"
45
+ add_command JobStopCommand.new(job_collection), "stop"
39
46
  add_command JobLogGrepCommand.new(job_collection, :all), "grep-all-logs"
40
47
  add_command JobLogGrepCommand.new(job_collection, :failed), "grep-failed-logs"
41
48
  end
49
+
50
+ def jobs_list_command
51
+ return @jobs_list_command if @jobs_list_command
52
+
53
+ @jobs_list_command = JobsListCommand.new(pipeline.job_collection)
54
+ add_command @jobs_list_command, "list-jobs", "ls"
55
+ @jobs_list_command
56
+ end
42
57
  end
43
58
  end
44
59
  end
@@ -0,0 +1,21 @@
1
+ module Semaph
2
+ module Shells
3
+ module Pipeline
4
+ class PromoteCommand
5
+ attr_reader :usage, :help
6
+
7
+ def initialize(pipeline)
8
+ @pipeline = pipeline
9
+ @usage = "<promotion name>"
10
+ @help = "promote pipeline"
11
+ end
12
+
13
+ def execute(name)
14
+ @pipeline.promote(name.chomp.strip)
15
+ rescue ::Semaph::Client::RequestException => e
16
+ puts e.message
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Semaph
2
+ module Shells
3
+ module Pipeline
4
+ class PromotionsListCommand
5
+ attr_reader :usage, :help
6
+
7
+ def initialize(promotion_collection)
8
+ @promotion_collection = promotion_collection
9
+ @help = "list promotions"
10
+ end
11
+
12
+ def execute(_whatever)
13
+ @promotion_collection.reload
14
+ @promotion_collection.all.each_with_index do |promotion, index|
15
+ puts "#{index + 1} #{promotion.description}"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,5 @@
1
+ require "semaph/formatting"
2
+
1
3
  module Semaph
2
4
  module Shells
3
5
  module Project
@@ -15,7 +17,7 @@ module Semaph
15
17
  @workflow_collection.all.each_with_index do |workflow, index|
16
18
  next unless workflow.branch.include?(branch)
17
19
 
18
- puts "#{index + 1} #{workflow.description}"
20
+ puts [::Semaph::Formatting.index(index + 1), workflow.description].join(" ")
19
21
  end
20
22
  end
21
23
  end
@@ -1,3 +1,5 @@
1
+ require "semaph/formatting"
2
+
1
3
  module Semaph
2
4
  module Shells
3
5
  module Workflow
@@ -12,7 +14,7 @@ module Semaph
12
14
  def execute(_whatever)
13
15
  @pipeline_collection.reload
14
16
  @pipeline_collection.all.each_with_index do |pipeline, index|
15
- puts "#{index + 1} #{pipeline.description}"
17
+ puts [::Semaph::Formatting.index(index + 1), pipeline.description].join(" ")
16
18
  end
17
19
  end
18
20
  end
@@ -1,3 +1,3 @@
1
1
  module Semaph
2
- VERSION = "0.6.0".freeze
2
+ VERSION = "0.7.0".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: semaph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Ryall
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-05 00:00:00.000000000 Z
11
+ date: 2020-06-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -103,7 +103,7 @@ files:
103
103
  - bin/setup
104
104
  - exe/semaph
105
105
  - lib/semaph.rb
106
- - lib/semaph/api.rb
106
+ - lib/semaph/client.rb
107
107
  - lib/semaph/commands.rb
108
108
  - lib/semaph/commands/reload_command.rb
109
109
  - lib/semaph/commands/rerun_workflow_command.rb
@@ -116,6 +116,8 @@ files:
116
116
  - lib/semaph/model/pipeline_collection.rb
117
117
  - lib/semaph/model/project.rb
118
118
  - lib/semaph/model/project_collection.rb
119
+ - lib/semaph/model/promotion.rb
120
+ - lib/semaph/model/promotion_collection.rb
119
121
  - lib/semaph/model/workflow.rb
120
122
  - lib/semaph/model/workflow_collection.rb
121
123
  - lib/semaph/shells/organisation/organisation_shell.rb
@@ -124,11 +126,16 @@ files:
124
126
  - lib/semaph/shells/organisations/organisations_list_command.rb
125
127
  - lib/semaph/shells/organisations/organisations_select_command.rb
126
128
  - lib/semaph/shells/organisations/organisations_shell.rb
129
+ - lib/semaph/shells/pipeline/job_debug_command.rb
127
130
  - lib/semaph/shells/pipeline/job_log_command.rb
128
131
  - lib/semaph/shells/pipeline/job_log_grep_command.rb
132
+ - lib/semaph/shells/pipeline/job_show_command.rb
133
+ - lib/semaph/shells/pipeline/job_stop_command.rb
129
134
  - lib/semaph/shells/pipeline/jobs_list_command.rb
130
135
  - lib/semaph/shells/pipeline/jobs_poll_command.rb
131
136
  - lib/semaph/shells/pipeline/pipeline_shell.rb
137
+ - lib/semaph/shells/pipeline/promote_command.rb
138
+ - lib/semaph/shells/pipeline/promotions_list_command.rb
132
139
  - lib/semaph/shells/project/project_shell.rb
133
140
  - lib/semaph/shells/project/workflows_list_command.rb
134
141
  - lib/semaph/shells/project/workflows_select_command.rb