semaph 0.1.0 → 0.6.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 +4 -4
- data/.gitignore +3 -0
- data/.rubocop.yml +18 -0
- data/.semaphore/semaphore.yml +23 -0
- data/.tool-versions +1 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +55 -0
- data/README.md +85 -11
- data/Rakefile +4 -1
- data/exe/semaph +5 -0
- data/lib/semaph.rb +17 -1
- data/lib/semaph/api.rb +94 -0
- data/lib/semaph/commands.rb +63 -0
- data/lib/semaph/commands/reload_command.rb +17 -0
- data/lib/semaph/commands/rerun_workflow_command.rb +16 -0
- data/lib/semaph/commands/stop_workflow_command.rb +16 -0
- data/lib/semaph/commands/visit_url_command.rb +16 -0
- data/lib/semaph/formatting.rb +9 -0
- data/lib/semaph/model/job.rb +101 -0
- data/lib/semaph/model/job_collection.rb +47 -0
- data/lib/semaph/model/pipeline.rb +48 -0
- data/lib/semaph/model/pipeline_collection.rb +20 -0
- data/lib/semaph/model/project.rb +34 -0
- data/lib/semaph/model/project_collection.rb +19 -0
- data/lib/semaph/model/workflow.rb +48 -0
- data/lib/semaph/model/workflow_collection.rb +19 -0
- data/lib/semaph/shells/organisation/organisation_shell.rb +33 -0
- data/lib/semaph/shells/organisation/projects_list_command.rb +21 -0
- data/lib/semaph/shells/organisation/projects_select_command.rb +26 -0
- data/lib/semaph/shells/organisations/organisations_list_command.rb +20 -0
- data/lib/semaph/shells/organisations/organisations_select_command.rb +25 -0
- data/lib/semaph/shells/organisations/organisations_shell.rb +24 -0
- data/lib/semaph/shells/pipeline/job_log_command.rb +44 -0
- data/lib/semaph/shells/pipeline/job_log_grep_command.rb +28 -0
- data/lib/semaph/shells/pipeline/jobs_list_command.rb +21 -0
- data/lib/semaph/shells/pipeline/jobs_poll_command.rb +62 -0
- data/lib/semaph/shells/pipeline/pipeline_shell.rb +45 -0
- data/lib/semaph/shells/project/project_shell.rb +61 -0
- data/lib/semaph/shells/project/workflows_list_command.rb +24 -0
- data/lib/semaph/shells/project/workflows_select_command.rb +31 -0
- data/lib/semaph/shells/workflow/pipelines_list_command.rb +21 -0
- data/lib/semaph/shells/workflow/pipelines_select_command.rb +30 -0
- data/lib/semaph/shells/workflow/workflow_shell.rb +35 -0
- data/lib/semaph/version.rb +1 -1
- data/semaph.gemspec +12 -7
- metadata +115 -7
@@ -0,0 +1,21 @@
|
|
1
|
+
module Semaph
|
2
|
+
module Shells
|
3
|
+
module Organisation
|
4
|
+
class ProjectsListCommand
|
5
|
+
attr_reader :usage, :help
|
6
|
+
|
7
|
+
def initialize(project_collection)
|
8
|
+
@project_collection = project_collection
|
9
|
+
@help = "list available projects"
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(_whatever)
|
13
|
+
@project_collection.reload
|
14
|
+
@project_collection.all.each do |project|
|
15
|
+
puts project.name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "semaph/shells/project/project_shell"
|
2
|
+
|
3
|
+
module Semaph
|
4
|
+
module Shells
|
5
|
+
module Organisation
|
6
|
+
class ProjectsSelectCommand
|
7
|
+
attr_reader :usage, :help
|
8
|
+
|
9
|
+
def initialize(project_collection)
|
10
|
+
@project_collection = project_collection
|
11
|
+
@usage = "<project>"
|
12
|
+
@help = "choose project"
|
13
|
+
end
|
14
|
+
|
15
|
+
def completion(text)
|
16
|
+
@project_collection.all.map(&:name).grep(/^#{text}/).sort
|
17
|
+
end
|
18
|
+
|
19
|
+
def execute(name)
|
20
|
+
selected_project = @project_collection.all.find { |project| project.name == name }
|
21
|
+
::Semaph::Shells::Project::ProjectShell.new(selected_project).push
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Semaph
|
2
|
+
module Shells
|
3
|
+
module Organisations
|
4
|
+
class OrganisationsListCommand
|
5
|
+
attr_reader :usage, :help
|
6
|
+
|
7
|
+
def initialize(organisations)
|
8
|
+
@organisations = organisations
|
9
|
+
@help = "list available organisations"
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(_whatever)
|
13
|
+
@organisations.each_key do |name|
|
14
|
+
puts "#{name} (#{@organisations[name]['host']})"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "semaph/shells/organisation/organisation_shell"
|
2
|
+
|
3
|
+
module Semaph
|
4
|
+
module Shells
|
5
|
+
module Organisations
|
6
|
+
class OrganisationsSelectCommand
|
7
|
+
attr_reader :usage, :help
|
8
|
+
|
9
|
+
def initialize(organisations)
|
10
|
+
@organisations = organisations
|
11
|
+
@usage = "<organisation>"
|
12
|
+
@help = "choose organisation"
|
13
|
+
end
|
14
|
+
|
15
|
+
def completion(text)
|
16
|
+
@organisations.keys.grep(/^#{text}/).sort
|
17
|
+
end
|
18
|
+
|
19
|
+
def execute(name)
|
20
|
+
::Semaph::Shells::Organisation::OrganisationShell.new(@organisations[name]).push
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "semaph/commands/reload_command"
|
2
|
+
require "semaph/shells/organisations/organisations_list_command"
|
3
|
+
require "semaph/shells/organisations/organisations_select_command"
|
4
|
+
require "shell_shock/context"
|
5
|
+
|
6
|
+
module Semaph
|
7
|
+
module Shells
|
8
|
+
module Organisations
|
9
|
+
class OrganisationsShell
|
10
|
+
include ShellShock::Context
|
11
|
+
|
12
|
+
def initialize(organisations)
|
13
|
+
@organisations = organisations
|
14
|
+
@prompt = "🏗 > "
|
15
|
+
organisations_list_command = OrganisationsListCommand.new(organisations)
|
16
|
+
add_command organisations_list_command, "list-organisations", "ls"
|
17
|
+
add_command OrganisationsSelectCommand.new(organisations), "select-organisation", "cd"
|
18
|
+
add_command ::Semaph::Commands::ReloadCommand.new, "reload" if ENV["SEMAPH_RELOAD"]
|
19
|
+
organisations_list_command.execute("")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module Semaph
|
4
|
+
module Shells
|
5
|
+
module Pipeline
|
6
|
+
class JobLogCommand
|
7
|
+
attr_reader :usage, :help
|
8
|
+
|
9
|
+
def initialize(job_collection)
|
10
|
+
@job_collection = job_collection
|
11
|
+
@usage = "<job index>"
|
12
|
+
@help = "retrieve log for job"
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(index_string)
|
16
|
+
base = "tmp/logs/pipeline/#{@job_collection.pipeline.id}"
|
17
|
+
with_job(index_string) do |job|
|
18
|
+
unless job.finished?
|
19
|
+
puts "This job has not finished yet"
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
system("less #{job.write_log(base)}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def with_job(index_string)
|
30
|
+
index = index_string.to_i - 1
|
31
|
+
|
32
|
+
job = @job_collection.all[index]
|
33
|
+
|
34
|
+
unless job
|
35
|
+
puts "There is no job at position #{index}"
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
yield job
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Semaph
|
2
|
+
module Shells
|
3
|
+
module Pipeline
|
4
|
+
class JobLogGrepCommand
|
5
|
+
attr_reader :usage, :help, :job_collection
|
6
|
+
|
7
|
+
def initialize(job_collection, scope)
|
8
|
+
@job_collection = job_collection
|
9
|
+
@scope = scope
|
10
|
+
@usage = "<expression>"
|
11
|
+
@help = "retrieve logs for #{scope} jobs and grep for text"
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(expression)
|
15
|
+
base = "tmp/logs/pipeline/#{job_collection.pipeline.id}"
|
16
|
+
@job_collection.send(@scope).each do |job|
|
17
|
+
unless job.finished?
|
18
|
+
puts "skipping incomplete job #{job.id}"
|
19
|
+
next
|
20
|
+
end
|
21
|
+
job.write_log(base)
|
22
|
+
end
|
23
|
+
system("ag #{expression} #{base} | less")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Semaph
|
2
|
+
module Shells
|
3
|
+
module Pipeline
|
4
|
+
class JobsListCommand
|
5
|
+
attr_reader :usage, :help
|
6
|
+
|
7
|
+
def initialize(job_collection)
|
8
|
+
@job_collection = job_collection
|
9
|
+
@help = "list jobs"
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(_whatever)
|
13
|
+
@job_collection.reload
|
14
|
+
@job_collection.all.each_with_index do |job, index|
|
15
|
+
puts "#{index + 1} #{job.description}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Semaph
|
2
|
+
module Shells
|
3
|
+
module Pipeline
|
4
|
+
class JobsPollCommand
|
5
|
+
attr_reader :usage, :help, :job_collection
|
6
|
+
|
7
|
+
def initialize(job_collection)
|
8
|
+
@job_collection = job_collection
|
9
|
+
@help = "poll jobs"
|
10
|
+
@can_notify = !`which terminal-notifier`.chomp.empty?
|
11
|
+
end
|
12
|
+
|
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
|
23
|
+
report_final
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def report_final
|
29
|
+
job_collection.all.each_with_index do |job, index|
|
30
|
+
puts "#{index + 1} #{job.description}"
|
31
|
+
end
|
32
|
+
failed_job_count = job_collection.failed.count
|
33
|
+
notify(
|
34
|
+
"Workflow completed",
|
35
|
+
"#{job_collection.pipeline.workflow.description} completed with #{failed_job_count} failed jobs",
|
36
|
+
failed_job_count.positive?,
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def report_incomplete(incomplete_jobs)
|
41
|
+
puts "polling #{job_collection.pipeline.workflow.description}"
|
42
|
+
puts "#{incomplete_jobs.count} incomplete jobs remaining:"
|
43
|
+
incomplete_jobs.each { |job| puts job.description }
|
44
|
+
end
|
45
|
+
|
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
|
+
def notify(title, message, failed)
|
53
|
+
return unless @can_notify
|
54
|
+
|
55
|
+
sound = failed ? "basso" : "blow"
|
56
|
+
|
57
|
+
`terminal-notifier -group semaph -message "#{message}" -title "#{title}" -sound #{sound}`
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "semaph/commands"
|
2
|
+
require "semaph/shells/pipeline/jobs_list_command"
|
3
|
+
require "semaph/shells/pipeline/job_log_command"
|
4
|
+
require "semaph/shells/pipeline/job_log_grep_command"
|
5
|
+
require "semaph/shells/pipeline/jobs_poll_command"
|
6
|
+
require "shell_shock/context"
|
7
|
+
|
8
|
+
module Semaph
|
9
|
+
module Shells
|
10
|
+
module Pipeline
|
11
|
+
class PipelineShell
|
12
|
+
attr_reader :pipeline
|
13
|
+
|
14
|
+
include ShellShock::Context
|
15
|
+
|
16
|
+
def initialize(pipeline)
|
17
|
+
@pipeline = pipeline
|
18
|
+
workflow = pipeline.workflow
|
19
|
+
project = workflow.project
|
20
|
+
@prompt = "🏗 #{project.client.name} #{project.name} #{workflow.id} #{pipeline.yaml} > "
|
21
|
+
add_commands
|
22
|
+
@jobs_list_command.execute("")
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
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)
|
31
|
+
add_job_collection_commands(pipeline.job_collection)
|
32
|
+
end
|
33
|
+
|
34
|
+
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"
|
39
|
+
add_command JobLogGrepCommand.new(job_collection, :all), "grep-all-logs"
|
40
|
+
add_command JobLogGrepCommand.new(job_collection, :failed), "grep-failed-logs"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "semaph/commands/reload_command"
|
2
|
+
require "semaph/commands/visit_url_command"
|
3
|
+
require "semaph/shells/project/workflows_list_command"
|
4
|
+
require "semaph/shells/project/workflows_select_command"
|
5
|
+
require "shell_shock/context"
|
6
|
+
|
7
|
+
module Semaph
|
8
|
+
module Shells
|
9
|
+
module Project
|
10
|
+
class ProjectShell
|
11
|
+
attr_reader :project
|
12
|
+
|
13
|
+
include ShellShock::Context
|
14
|
+
|
15
|
+
def initialize(project)
|
16
|
+
@project = project
|
17
|
+
@prompt = "🏗 #{project.client.name} #{project.name} > "
|
18
|
+
add_commands
|
19
|
+
@workflows_list_command.execute("")
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def workflow_collection
|
25
|
+
project.workflow_collection
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_commands
|
29
|
+
add_github_command
|
30
|
+
add_open_project_command
|
31
|
+
@workflows_list_command = WorkflowsListCommand.new(workflow_collection)
|
32
|
+
add_command @workflows_list_command, "list-workflows", "ls"
|
33
|
+
add_command WorkflowsSelectCommand.new(workflow_collection), "select-workflow", "cd"
|
34
|
+
add_command ::Semaph::Commands::ReloadCommand.new, "reload" if ENV["SEMAPH_RELOAD"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_github_command
|
38
|
+
return unless project.github_url
|
39
|
+
|
40
|
+
add_command(
|
41
|
+
::Semaph::Commands::VisitUrlCommand.new(
|
42
|
+
project.github_url,
|
43
|
+
"browse to github project",
|
44
|
+
),
|
45
|
+
"open-github",
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_open_project_command
|
50
|
+
add_command(
|
51
|
+
::Semaph::Commands::VisitUrlCommand.new(
|
52
|
+
"https://#{project.client.host}/projects/#{project.name}",
|
53
|
+
"browse to project",
|
54
|
+
),
|
55
|
+
"open-project",
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Semaph
|
2
|
+
module Shells
|
3
|
+
module Project
|
4
|
+
class WorkflowsListCommand
|
5
|
+
attr_reader :usage, :help
|
6
|
+
|
7
|
+
def initialize(workflow_collection)
|
8
|
+
@workflow_collection = workflow_collection
|
9
|
+
@usage = "<branch>"
|
10
|
+
@help = "list available workflows"
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(branch)
|
14
|
+
@workflow_collection.reload
|
15
|
+
@workflow_collection.all.each_with_index do |workflow, index|
|
16
|
+
next unless workflow.branch.include?(branch)
|
17
|
+
|
18
|
+
puts "#{index + 1} #{workflow.description}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "semaph/shells/workflow/workflow_shell"
|
2
|
+
require "semaph/shells/pipeline/pipeline_shell"
|
3
|
+
|
4
|
+
module Semaph
|
5
|
+
module Shells
|
6
|
+
module Project
|
7
|
+
class WorkflowsSelectCommand
|
8
|
+
attr_reader :usage, :help
|
9
|
+
|
10
|
+
def initialize(workflow_collection)
|
11
|
+
@workflow_collection = workflow_collection
|
12
|
+
@usage = "<workflow index>"
|
13
|
+
@help = "choose workflow by index"
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(index_string)
|
17
|
+
index = index_string.to_i - 1
|
18
|
+
|
19
|
+
workflow = @workflow_collection.all[index]
|
20
|
+
|
21
|
+
unless workflow
|
22
|
+
puts "There is no workflow at position #{index}"
|
23
|
+
return
|
24
|
+
end
|
25
|
+
|
26
|
+
::Semaph::Shells::Workflow::WorkflowShell.new(workflow).push
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|