tinyci 0.4.2 → 0.5
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 +2 -2
- data/.rspec +2 -0
- data/.rubocop.yml +17 -2
- data/.ruby-version +1 -1
- data/.tinyci.yml +3 -1
- data/Dockerfile +3 -2
- data/Gemfile +2 -1
- data/Gemfile.lock +56 -29
- data/Guardfile +5 -3
- data/Rakefile +3 -1
- data/bin/tinyci +3 -2
- data/lib/pidfile.rb +28 -39
- data/lib/tinyci/builders/script_builder.rb +2 -0
- data/lib/tinyci/builders/test_builder.rb +3 -1
- data/lib/tinyci/cli.rb +159 -79
- data/lib/tinyci/cli_ssh_delegator.rb +111 -0
- data/lib/tinyci/compactor.rb +26 -26
- data/lib/tinyci/config.rb +22 -23
- data/lib/tinyci/config_transformer.rb +14 -16
- data/lib/tinyci/executor.rb +14 -12
- data/lib/tinyci/git_utils.rb +87 -18
- data/lib/tinyci/hookers/script_hooker.rb +26 -27
- data/lib/tinyci/installer.rb +23 -21
- data/lib/tinyci/log_viewer.rb +87 -0
- data/lib/tinyci/logging.rb +5 -5
- data/lib/tinyci/multi_logger.rb +30 -21
- data/lib/tinyci/path_utils.rb +44 -0
- data/lib/tinyci/runner.rb +95 -78
- data/lib/tinyci/scheduler.rb +44 -42
- data/lib/tinyci/subprocesses.rb +41 -32
- data/lib/tinyci/symbolize.rb +3 -1
- data/lib/tinyci/testers/script_tester.rb +2 -0
- data/lib/tinyci/testers/test_tester.rb +2 -0
- data/lib/tinyci/version.rb +3 -1
- data/lib/yard_plugin.rb +9 -1
- data/tinyci.gemspec +29 -22
- metadata +87 -14
data/lib/tinyci/config.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'tinyci/config_transformer'
|
2
4
|
require 'tinyci/symbolize'
|
3
5
|
require 'yaml'
|
@@ -6,60 +8,57 @@ module TinyCI
|
|
6
8
|
# Represents the Configuration for a repo, parsed from the `.tinyci.yml` file in the repo root.
|
7
9
|
# Mainly a wrapper around a the hash object parsed from the yaml in the config file.
|
8
10
|
# The keys of the hash are recursively symbolized.
|
9
|
-
#
|
11
|
+
#
|
10
12
|
# As it is loaded, the configuration file data is passed through the {TinyCI::ConfigTransformer}
|
11
13
|
# class, which translates any definitions in the concise format into the more verbose format
|
12
14
|
class Config
|
13
15
|
include Symbolize
|
14
|
-
|
16
|
+
|
17
|
+
attr_accessor :config_path
|
18
|
+
|
15
19
|
# Constructor
|
16
|
-
#
|
20
|
+
#
|
17
21
|
# @param [String] working_dir The working directory in which to find the config file
|
18
22
|
# @param [String] config_path Override the path to the config file
|
19
23
|
# @param [String] config Override the config content
|
20
|
-
#
|
24
|
+
#
|
21
25
|
# @raise [ConfigMissingError] if the config file is not found
|
22
|
-
def initialize(
|
23
|
-
@
|
24
|
-
@config_pathname = config_path
|
26
|
+
def initialize(config_path, config: nil)
|
27
|
+
@config_path = config_path
|
25
28
|
@config_content = config
|
26
|
-
|
27
|
-
raise ConfigMissingError, "config file #{
|
29
|
+
|
30
|
+
raise ConfigMissingError, "config file #{config_path} not found" unless config_file_exists?
|
28
31
|
end
|
29
|
-
|
32
|
+
|
30
33
|
# Address into the config object
|
31
|
-
#
|
34
|
+
#
|
32
35
|
# @param [Symbol] key The key to address
|
33
36
|
def [](key)
|
34
37
|
config_content[key]
|
35
38
|
end
|
36
|
-
|
39
|
+
|
37
40
|
# Return the raw hash representation
|
38
|
-
#
|
41
|
+
#
|
39
42
|
# @return [Hash] The configuration as a hash
|
40
43
|
def to_hash
|
41
44
|
config_content
|
42
45
|
end
|
43
|
-
|
46
|
+
|
44
47
|
private
|
45
|
-
|
48
|
+
|
46
49
|
def config_file_exists?
|
47
|
-
File.exist?
|
48
|
-
end
|
49
|
-
|
50
|
-
def config_pathname
|
51
|
-
@config_pathname || File.expand_path('.tinyci.yml', @working_dir)
|
50
|
+
File.exist? config_path
|
52
51
|
end
|
53
|
-
|
52
|
+
|
54
53
|
def config_content
|
55
54
|
@config_content ||= begin
|
56
|
-
config = YAML.safe_load(File.read(
|
55
|
+
config = YAML.safe_load(File.read(config_path))
|
57
56
|
transformed_config = ConfigTransformer.new(config).transform!
|
58
57
|
symbolize(transformed_config).freeze
|
59
58
|
end
|
60
59
|
end
|
61
60
|
end
|
62
|
-
|
61
|
+
|
63
62
|
# Error raised when the config file cannot be found
|
64
63
|
class ConfigMissingError < StandardError; end
|
65
64
|
end
|
@@ -1,61 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TinyCI
|
2
4
|
# Transforms the configuration format from the condensed format to the
|
3
5
|
# more verbose format accepted by the rest of the system
|
4
6
|
class ConfigTransformer
|
5
|
-
|
6
7
|
# Constructor
|
7
|
-
#
|
8
|
+
#
|
8
9
|
# @param [Hash] input The configuration object, in the condensed format
|
9
10
|
def initialize(input)
|
10
11
|
@input = input
|
11
12
|
end
|
12
|
-
|
13
|
+
|
13
14
|
# Transforms the config object
|
14
|
-
#
|
15
|
+
#
|
15
16
|
# @return [Hash] The config object in the verbose form
|
16
17
|
def transform!
|
17
|
-
@input.
|
18
|
+
@input.each_with_object({}) do |(key, value), acc|
|
18
19
|
method_name = "transform_#{key}"
|
19
|
-
|
20
|
+
|
20
21
|
if respond_to? method_name, true
|
21
|
-
|
22
22
|
acc.merge! send(method_name, value)
|
23
23
|
else
|
24
24
|
acc[key] = value
|
25
25
|
end
|
26
|
-
|
27
|
-
acc
|
28
26
|
end
|
29
27
|
end
|
30
|
-
|
28
|
+
|
31
29
|
private
|
32
|
-
|
30
|
+
|
33
31
|
def transform_build(value)
|
34
32
|
{
|
35
33
|
builder: {
|
36
|
-
:
|
34
|
+
class: 'ScriptBuilder',
|
37
35
|
config: {
|
38
36
|
command: value
|
39
37
|
}
|
40
38
|
}
|
41
39
|
}
|
42
40
|
end
|
43
|
-
|
41
|
+
|
44
42
|
def transform_test(value)
|
45
43
|
{
|
46
44
|
tester: {
|
47
|
-
:
|
45
|
+
class: 'ScriptTester',
|
48
46
|
config: {
|
49
47
|
command: value
|
50
48
|
}
|
51
49
|
}
|
52
50
|
}
|
53
51
|
end
|
54
|
-
|
52
|
+
|
55
53
|
def transform_hooks(value)
|
56
54
|
{
|
57
55
|
hooker: {
|
58
|
-
:
|
56
|
+
class: 'ScriptHooker',
|
59
57
|
config: value
|
60
58
|
}
|
61
59
|
}
|
data/lib/tinyci/executor.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'tinyci/subprocesses'
|
2
4
|
require 'tinyci/logging'
|
3
5
|
require 'ostruct'
|
@@ -5,33 +7,33 @@ require 'erb'
|
|
5
7
|
|
6
8
|
module TinyCI
|
7
9
|
# Parent class for Builder and Tester classes
|
8
|
-
#
|
10
|
+
#
|
9
11
|
# @abstract
|
10
12
|
class Executor
|
11
13
|
include Subprocesses
|
12
14
|
include Logging
|
13
|
-
|
15
|
+
|
14
16
|
# Returns a new instance of the executor.
|
15
|
-
#
|
16
|
-
# @param config [Hash] Configuration hash, typically taken
|
17
|
-
#
|
18
|
-
def initialize(config
|
17
|
+
#
|
18
|
+
# @param config [Hash] Configuration hash, typically taken
|
19
|
+
# from relevant key in the {Config} object.
|
20
|
+
def initialize(config)
|
19
21
|
@config = config
|
20
|
-
@logger = logger
|
22
|
+
@logger = config[:logger]
|
21
23
|
end
|
22
|
-
|
24
|
+
|
23
25
|
def command
|
24
26
|
['/bin/sh', '-c', "'#{interpolate(@config[:command])}'"]
|
25
27
|
end
|
26
|
-
|
28
|
+
|
27
29
|
private
|
28
|
-
|
30
|
+
|
29
31
|
def interpolate(command)
|
30
32
|
erb = ERB.new command
|
31
|
-
|
33
|
+
|
32
34
|
erb.result(erb_scope)
|
33
35
|
end
|
34
|
-
|
36
|
+
|
35
37
|
def template_vars
|
36
38
|
OpenStruct.new(
|
37
39
|
commit: @config[:commit],
|
data/lib/tinyci/git_utils.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'tinyci/subprocesses'
|
4
|
+
require 'git_clone_url'
|
2
5
|
|
3
6
|
module TinyCI
|
4
|
-
|
5
7
|
# Methods for dealing with git repos.
|
6
8
|
module GitUtils
|
7
|
-
|
8
9
|
# Returns the absolute path to the root of the current git directory
|
9
|
-
#
|
10
|
+
#
|
10
11
|
# @return [String] the path
|
11
12
|
def repo_root
|
12
13
|
return git_directory_path if inside_bare_repo?
|
13
|
-
|
14
|
+
|
14
15
|
if inside_git_directory?
|
15
16
|
File.expand_path('..', git_directory_path)
|
16
17
|
elsif inside_work_tree?
|
@@ -19,48 +20,116 @@ module TinyCI
|
|
19
20
|
raise 'not in git directory or work tree!?'
|
20
21
|
end
|
21
22
|
end
|
22
|
-
|
23
|
+
|
23
24
|
# Are we currently under a git repo?
|
24
25
|
def inside_git_directory?
|
25
26
|
execute(git_cmd('rev-parse', '--is-inside-git-dir')) == 'true'
|
26
27
|
end
|
27
|
-
|
28
|
+
|
28
29
|
# Are we under a bare repo?
|
29
30
|
def inside_bare_repo?
|
30
31
|
execute(git_cmd('rev-parse', '--is-bare-repository')) == 'true'
|
31
32
|
end
|
32
|
-
|
33
|
+
|
33
34
|
# Are we currently under a git work tree?
|
34
35
|
def inside_work_tree?
|
35
36
|
execute(git_cmd('rev-parse', '--is-inside-work-tree')) == 'true'
|
36
37
|
end
|
37
|
-
|
38
|
+
|
38
39
|
# Are we currently under a repo in any sense?
|
39
40
|
def inside_repository?
|
40
|
-
|
41
|
+
cmd = git_cmd('rev-parse', '--is-inside-work-tree', '--is-inside-git-dir')
|
42
|
+
execute(cmd).split.any? { |s| s == 'true' }
|
41
43
|
end
|
42
|
-
|
44
|
+
|
43
45
|
# Returns the absolute path to the .git directory
|
44
46
|
def git_directory_path
|
45
|
-
|
47
|
+
base = defined?(@working_dir) ? @working_dir.to_s : nil
|
48
|
+
|
49
|
+
File.expand_path(execute(git_cmd('rev-parse', '--git-dir')), base)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Does the specified file exist in the repo at the specified revision
|
53
|
+
def file_exists_in_git?(path, ref = @commit)
|
54
|
+
cmd = git_cmd('cat-file', '-e', "#{ref}:#{path}")
|
55
|
+
|
56
|
+
execute_and_return_status(cmd).success?
|
57
|
+
end
|
58
|
+
|
59
|
+
# Does the given sha exist in this repo's history?
|
60
|
+
def commit_exists?(commit = @commit)
|
61
|
+
cmd = git_cmd('cat-file', '-e', commit)
|
62
|
+
|
63
|
+
execute_and_return_status(cmd).success?
|
64
|
+
end
|
65
|
+
|
66
|
+
# Does the repo have a commit matching the given name?
|
67
|
+
def remote_exists?(remote = @remote)
|
68
|
+
execute(git_cmd('remote')).split("\n").include? remote
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get the url for a given remote
|
72
|
+
# Alias for #push_url
|
73
|
+
def remote_url(remote = @remote)
|
74
|
+
push_url remote
|
46
75
|
end
|
47
|
-
|
76
|
+
|
77
|
+
# Does the given remote point to github?
|
78
|
+
def github_remote?(remote = @remote)
|
79
|
+
remote_url(remote).host == 'github.com'
|
80
|
+
end
|
81
|
+
|
82
|
+
# Does the given remote point to an ssh url?
|
83
|
+
def ssh_remote?(remote = @remote)
|
84
|
+
remote_url(remote).is_a? URI::SshGit::Generic
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return the upstream remote for the current branch
|
88
|
+
def current_tracking_remote
|
89
|
+
full = execute git_cmd 'rev-parse', '--symbolic-full-name', '--abbrev-ref', '@{push}'
|
90
|
+
full.split('/')[0]
|
91
|
+
rescue TinyCI::Subprocesses::SubprocessError => e
|
92
|
+
log_error 'Current branch does not have an upstream remote' if e.status.exitstatus == 128
|
93
|
+
end
|
94
|
+
|
95
|
+
# The current HEAD branch
|
96
|
+
def current_branch
|
97
|
+
execute git_cmd 'rev-parse', '--abbrev-ref', 'HEAD'
|
98
|
+
end
|
99
|
+
|
100
|
+
# The push url for the specified remote, parsed into a `URI` object
|
101
|
+
def push_url(remote = @remote)
|
102
|
+
url = raw_push_url(remote)
|
103
|
+
GitCloneUrl.parse url
|
104
|
+
rescue URI::InvalidComponentError
|
105
|
+
URI.parse url
|
106
|
+
end
|
107
|
+
|
108
|
+
# Parse the commit time from git
|
109
|
+
def time
|
110
|
+
@time ||= Time.at execute(git_cmd('show', '-s', '--format=%at', @commit)).to_i
|
111
|
+
end
|
112
|
+
|
48
113
|
# Execute a git command, passing the -C parameter if the current object has
|
49
114
|
# the working_directory instance var set
|
50
115
|
def git_cmd(*args)
|
51
116
|
cmd = ['git']
|
52
|
-
cmd += ['-C', @working_dir] if defined?(@working_dir) && !@working_dir.nil?
|
117
|
+
cmd += ['-C', @working_dir.to_s] if defined?(@working_dir) && !@working_dir.nil?
|
53
118
|
cmd += args
|
54
|
-
|
119
|
+
|
55
120
|
cmd
|
56
121
|
end
|
57
|
-
|
58
|
-
|
59
|
-
|
122
|
+
|
123
|
+
# Get push url as a string. Not intended to be called directly, instead call {#push_url}
|
124
|
+
def raw_push_url(remote = @remote)
|
125
|
+
@push_urls ||= {}
|
126
|
+
@push_urls[remote] ||= execute git_cmd 'remote', 'get-url', '--push', remote
|
127
|
+
end
|
128
|
+
|
60
129
|
def self.included(base)
|
61
130
|
base.include Subprocesses
|
62
131
|
end
|
63
|
-
|
132
|
+
|
64
133
|
def self.extended(base)
|
65
134
|
base.extend Subprocesses
|
66
135
|
end
|
@@ -1,60 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'tinyci/executor'
|
2
4
|
|
3
5
|
module TinyCI
|
4
6
|
module Hookers
|
5
7
|
class ScriptHooker < TinyCI::Executor
|
6
8
|
# All the hooks
|
7
|
-
HOOKS = %w
|
9
|
+
HOOKS = %w[
|
8
10
|
before_build
|
9
|
-
|
11
|
+
|
10
12
|
after_build_success
|
11
13
|
after_build_failure
|
12
|
-
|
14
|
+
|
13
15
|
after_build
|
14
|
-
|
16
|
+
|
15
17
|
before_test
|
16
|
-
|
18
|
+
|
17
19
|
after_test_success
|
18
20
|
after_test_failure
|
19
|
-
|
21
|
+
|
20
22
|
after_test
|
21
|
-
|
23
|
+
|
22
24
|
after_all
|
23
|
-
|
24
|
-
|
25
|
+
].freeze
|
26
|
+
|
25
27
|
# Those hooks that will halt exectution if they fail
|
26
|
-
BLOCKING_HOOKS = %w
|
28
|
+
BLOCKING_HOOKS = %w[
|
27
29
|
before_build
|
28
30
|
before_test
|
29
|
-
|
30
|
-
|
31
|
+
].freeze
|
32
|
+
|
31
33
|
HOOKS.each do |hook|
|
32
|
-
define_method hook+
|
34
|
+
define_method hook + '_present?' do
|
33
35
|
@config.key? hook.to_sym
|
34
36
|
end
|
35
|
-
|
36
|
-
define_method hook+
|
37
|
+
|
38
|
+
define_method hook + '!' do
|
37
39
|
return unless send("#{hook}_present?")
|
38
|
-
|
40
|
+
|
39
41
|
log_info "executing #{hook} hook..."
|
40
42
|
begin
|
41
43
|
execute_stream(script_location(hook), label: hook, pwd: @config[:export])
|
42
|
-
|
44
|
+
|
43
45
|
return true
|
44
46
|
rescue SubprocessError => e
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
else
|
52
|
-
return true
|
53
|
-
end
|
47
|
+
return true unless BLOCKING_HOOKS.include? hook
|
48
|
+
|
49
|
+
raise e if ENV['TINYCI_ENV'] == 'test'
|
50
|
+
|
51
|
+
log_error e
|
52
|
+
return false
|
54
53
|
end
|
55
54
|
end
|
56
55
|
end
|
57
|
-
|
56
|
+
|
58
57
|
def script_location(hook)
|
59
58
|
['/bin/sh', '-c', "'#{interpolate(@config[hook.to_sym])}'"]
|
60
59
|
end
|