cic-tools 0.0.1.alpha1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ require_relative 'track/command'
2
+ require_relative 'track/exercise'
3
+ require_relative 'track/learning_track'
@@ -0,0 +1,68 @@
1
+ require 'octokit'
2
+ require 'netrc'
3
+ require 'yaml'
4
+ require 'uri'
5
+ require_relative 'errors'
6
+ require_relative 'helpers'
7
+
8
+ module Commands
9
+ module Track
10
+ class Command < Thor
11
+ attr_reader :tracks, :tracks_yaml, :api_endpoint
12
+
13
+ def initialize(args = [],
14
+ options = {},
15
+ config = {},
16
+ api_endpoint = Octokit::Default::API_ENDPOINT,
17
+ tracks_yaml = "#{__dir__}/../../../../../../tracks/tracks.yml")
18
+ super(args, options, config)
19
+
20
+ @tracks_yaml = tracks_yaml
21
+
22
+ @api_endpoint = api_endpoint
23
+ raise TracksFileNotFoundError.new(path: tracks_yaml) unless File.exist?(tracks_yaml)
24
+
25
+ @tracks = load_tracks(YAML.safe_load(File.read(tracks_yaml))['tracks'])
26
+ end
27
+
28
+ desc 'list', 'get a list of available learning tracks'
29
+
30
+ def list
31
+ Dir.chdir(tracks_dir) do
32
+ track_names = tracks.keys
33
+ say "Available Tracks:\n #{track_names.join("\n")}"
34
+ end
35
+ end
36
+
37
+ desc 'start TRACK_NAME', 'start a track'
38
+ option :fork, desc: 'the account/repo of your fork'
39
+ def start(track_name)
40
+ Dir.chdir(tracks_dir) do
41
+ setup!(track_name)
42
+
43
+ project = create_project("Learn #{track_name}")
44
+
45
+ columns = create_columns(project.id, %w[TODO in-progress done])
46
+
47
+ create_exercises(columns['TODO'], track_name)
48
+
49
+ say ok "Project '#{project.name}' created: #{project.html_url}"
50
+ end
51
+ rescue StandardError => e
52
+ raise error_for(e)
53
+ end
54
+
55
+ no_commands do
56
+ include Helpers
57
+ include Commandline::Output
58
+
59
+ def error_for(error)
60
+ mapped_errors = { Octokit::Unauthorized => CredentialsError,
61
+ Octokit::InvalidRepository => InvalidForkFormatError }
62
+
63
+ mapped_errors[error.class] || error
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,57 @@
1
+ module Commands
2
+ module Track
3
+ class NetrcMissingError < Thor::Error
4
+ def initialize
5
+ super '.netrc file missing. Add a .netrc file to your home folder with your github credentials in it'
6
+ end
7
+ end
8
+
9
+ class InvalidForkFormatError < Thor::Error
10
+ def initialize
11
+ super 'Fork option container invalid characters. Check you have typed it correctly.'
12
+ end
13
+ end
14
+
15
+ class MissingCredentialsError < Thor::Error
16
+ def initialize(host)
17
+ super ".netrc does not container credentials for #{host}"
18
+ end
19
+
20
+ def ==(other)
21
+ to_s == other.to_s
22
+ end
23
+ end
24
+
25
+ class CredentialsError < Thor::Error
26
+ def initialize
27
+ super "Authorisation failed. Check the fork you're targeting and the credentials in your .netrc"
28
+ end
29
+ end
30
+
31
+ class RepoIsNotForkError < Thor::Error
32
+ def initialize(fork)
33
+ super "#{fork} is not a fork. Please for the CIC repo and try again"
34
+ end
35
+
36
+ def ==(other)
37
+ to_s == other.to_s
38
+ end
39
+ end
40
+
41
+ class TrackNotFoundError < Thor::Error
42
+ def initialize(track_name:, track_names:)
43
+ super "Track #{track_name} not found.\nPlease choose from:#{track_names.join("\n")}"
44
+ end
45
+
46
+ def ==(other)
47
+ to_s == other.to_s
48
+ end
49
+ end
50
+
51
+ class TracksFileNotFoundError < Thor::Error
52
+ def initialize(path:)
53
+ super "Tracks file not found at: #{path}"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,52 @@
1
+ module Commands
2
+ module Track
3
+ class Exercise
4
+ attr_reader :track, :name, :path
5
+
6
+ def initialize(track:, name:, path:)
7
+ @track = track
8
+ @name = name
9
+ @path = path
10
+ end
11
+
12
+ def detail
13
+ "#{preamble}[Click here](../tree/master/#{path}) to read the #{name} exercise"
14
+ end
15
+
16
+ def validate
17
+ raise ArgumentError, name_error_msg unless name
18
+ raise ArgumentError, path_error_msg unless File.exist?(path)
19
+
20
+ true
21
+ end
22
+
23
+ def ==(other)
24
+ name == other.name && path == other.path
25
+ end
26
+
27
+ def path_error_msg
28
+ "Exercise: #{name} - not found at path: #{path}"
29
+ end
30
+
31
+ def name_error_msg
32
+ 'Name not given for exercise'
33
+ end
34
+
35
+ private
36
+
37
+ def preamble
38
+ return '' unless File.exist?(preamble_path)
39
+
40
+ "#{File.read(preamble_path)}\n"
41
+ end
42
+
43
+ def preamble_path
44
+ "#{track}/#{to_snake_case(name)}.md"
45
+ end
46
+
47
+ def to_snake_case(string)
48
+ string.downcase.gsub(/\s+/, '_')
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,101 @@
1
+ module Commands
2
+ module Track
3
+ module Helpers
4
+ private
5
+
6
+ def build_track(track)
7
+ LearningTrack.new(track['name']).tap do |learning_track|
8
+ track['exercises'].each do |exercise|
9
+ name, path = exercise.flatten.to_a
10
+ learning_track.exercise(name: name, path: path)
11
+ end
12
+
13
+ learning_track.validate
14
+ end
15
+ end
16
+
17
+ def create_columns(project_id, names)
18
+ {}.tap do |columns|
19
+ names.each do |name|
20
+ columns[name] = create_column(project_id, name)
21
+ end
22
+ end
23
+ end
24
+
25
+ def create_column(project_id, name)
26
+ github_client.create_project_column(project_id, name, default_projects_options)
27
+ end
28
+
29
+ def create_exercises(todo_column, track_name)
30
+ exercises(track_name).each do |exercise|
31
+ issue = github_client.create_issue(fork, exercise.name, exercise.detail, labels: '')
32
+
33
+ options = default_projects_options.merge(content_id: issue.id, content_type: 'Issue')
34
+ github_client.create_project_card(todo_column.id, options)
35
+ end
36
+ end
37
+
38
+ def create_project(board_name)
39
+ github_client.create_project(fork, board_name, default_projects_options)
40
+ end
41
+
42
+ def default_projects_options
43
+ { accept: 'application/vnd.github.inertia-preview+json' }
44
+ end
45
+
46
+ def enable_issues
47
+ github_client.edit_repository(fork, has_issues: true)
48
+ end
49
+
50
+ def exercises(track_name)
51
+ tracks[track_name].exercises.reverse
52
+ end
53
+
54
+ def fork
55
+ options[:fork]
56
+ end
57
+
58
+ def github_client
59
+ @github_client ||= begin
60
+ options = { netrc: true, api_endpoint: api_endpoint }
61
+ Octokit::Client.new(options).tap do |client|
62
+ raise NetrcMissingError unless File.exist?(client.netrc_file)
63
+
64
+ host = URI(client.api_endpoint).host
65
+ raise MissingCredentialsError, host unless Netrc.read(client.netrc_file)[host]
66
+ end
67
+ end
68
+ end
69
+
70
+ def load_tracks(hash)
71
+ {}.tap do |tracks|
72
+ hash.each do |track|
73
+ tracks[build_track(track).name] = build_track(track)
74
+ end
75
+ end
76
+ end
77
+
78
+ def track_exists?(track_name)
79
+ tracks.key?(track_name)
80
+ end
81
+
82
+ def track_names
83
+ tracks.keys
84
+ end
85
+
86
+ def tracks_dir
87
+ File.dirname(File.expand_path(tracks_yaml))
88
+ end
89
+
90
+ def setup!(track_name)
91
+ raise TrackNotFoundError.new(track_name: track_name, track_names: track_names) unless track_exists?(track_name)
92
+
93
+ repo = github_client.repo(fork)
94
+
95
+ raise RepoIsNotForkError, fork unless repo['fork'] == true
96
+
97
+ enable_issues
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,23 @@
1
+ module Commands
2
+ module Track
3
+ class LearningTrack
4
+ attr_reader :name, :exercises
5
+ def initialize(name)
6
+ @name = name
7
+ @exercises = []
8
+ end
9
+
10
+ def exercise(name:, path:)
11
+ exercises << Exercise.new(track: self.name, name: name, path: path)
12
+ end
13
+
14
+ def validate
15
+ exercises.each(&:validate)
16
+ end
17
+
18
+ def ==(other)
19
+ name == other.name && exercises == other.exercises
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ require 'colorize'
2
+ require 'open3'
3
+ require 'io/wait'
4
+ require_relative 'commandline/return'
5
+ require_relative 'commandline/output'
6
+
7
+ module Commandline
8
+ def capture_output(io, silent: true)
9
+ StringIO.new.tap do |store|
10
+ Thread.new do
11
+ while (line = io.getc)
12
+ store.write(line.dup)
13
+ print line unless silent || ENV['SILENT']
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def run(command, dir: nil, silent: true)
20
+ options = {}
21
+ options[:chdir] = dir if dir
22
+ stdin, stdout, stderr, thread = Open3.popen3("bash -lc '#{command}'", options)
23
+ stderr_output = capture_output(stderr, silent: silent)
24
+ stdout_output = capture_output(stdout, silent: silent)
25
+
26
+ wait_for_thread(thread)
27
+
28
+ [stdin, stdout, stderr].each(&:close)
29
+
30
+ Return.new(stdout: stdout_output.string, stderr: stderr_output.string, exit_code: thread.value.exitstatus)
31
+ end
32
+
33
+ # TODO: - think about how to bring the run and execute methods together or give more differentiating names
34
+ def execute(*commands, fail_message:, pass_message:)
35
+ fail = false
36
+ commands.each do |command|
37
+ result = run command
38
+ say result.stdout
39
+ next unless result.error?
40
+
41
+ fail = true
42
+ say result.stderr
43
+ say error fail_message
44
+ end
45
+ say ok pass_message unless fail
46
+ end
47
+
48
+ private
49
+
50
+ def wait_for_thread(thread)
51
+ sleep 1 while thread.alive?
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ module Commandline
2
+ module Output
3
+ def output
4
+ @output ||= STDOUT
5
+ end
6
+
7
+ def say(msg)
8
+ output.puts msg unless ENV['SILENT']
9
+ end
10
+
11
+ def ok(text)
12
+ prefix(text, '[OK] ').green
13
+ end
14
+
15
+ def error(text)
16
+ prefix(text, '[ERROR] ').red
17
+ end
18
+
19
+ def prefix(text, prefix)
20
+ lines = text.lines
21
+ message = "#{prefix}#{lines[0].strip}\n"
22
+ lines[1..-1].each_with_index do |line, index|
23
+ line = line.strip.chomp
24
+ line = line.rjust(line.length + prefix.size)
25
+
26
+ message << (index.zero? ? line : line.prepend("\n"))
27
+ end
28
+ message
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ module Commandline
2
+ class Return
3
+ attr_reader :stdout, :stderr, :exit_code
4
+
5
+ def initialize(stdout:, stderr:, exit_code:)
6
+ @stdout = normalise(stdout)
7
+ @stderr = normalise(stderr)
8
+ @exit_code = exit_code
9
+ end
10
+
11
+ def error?
12
+ exit_code != 0
13
+ end
14
+
15
+ def to_s
16
+ <<~OUTPUT
17
+ EXIT CODE: #{exit_code}
18
+
19
+ STDOUT:
20
+ #{stdout}
21
+
22
+ STDERR:
23
+ #{stderr}
24
+ OUTPUT
25
+ end
26
+
27
+ def ==(other)
28
+ other.to_s == to_s
29
+ end
30
+
31
+ private
32
+
33
+ def normalise(string)
34
+ string.chomp.strip
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'commandline'
4
+ require 'json'
5
+ module Docker
6
+ class Error < StandardError
7
+ include Commandline::Output
8
+ def initialize(msg)
9
+ super error(msg)
10
+ end
11
+ end
12
+
13
+ include Commandline
14
+
15
+ def container_id(container_name)
16
+ id = docker(%(container ps -q --filter "name=#{container_name}")).stdout
17
+ id.empty? ? raise(Error, "container with name #{container_name} does not exist") : id
18
+ end
19
+
20
+ def docker_exec(command)
21
+ system "docker exec #{command}"
22
+ end
23
+
24
+ def docker(command)
25
+ command = "docker #{command}"
26
+ run(command).tap do |output|
27
+ raise(Error, "Failed to run: #{command}\n#{output}") if output.error?
28
+ end
29
+ end
30
+ end