semaph 0.1.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|