consist 0.1.0 → 0.1.2
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/LICENSE.txt +256 -21
- data/README.md +624 -9
- data/lib/consist/cli.rb +38 -1
- data/lib/consist/commands/check.rb +36 -0
- data/lib/consist/commands/exec.rb +20 -0
- data/lib/consist/commands/includes/erbable.rb +24 -0
- data/lib/consist/commands/includes/stream_logger.rb +21 -0
- data/lib/consist/commands/mutate.rb +27 -0
- data/lib/consist/commands/upload.rb +37 -0
- data/lib/consist/consistfile.rb +71 -0
- data/lib/consist/recipe.rb +42 -0
- data/lib/consist/recipes.rb +28 -0
- data/lib/consist/step.rb +74 -0
- data/lib/consist/thor_ext.rb +2 -2
- data/lib/consist/version.rb +1 -1
- data/lib/recipes/kamal_single_server.rb +8 -0
- data/lib/steps/install_apt_packages/step.rb +19 -0
- data/lib/steps/update_apt_packages/apt_auto_upgrades +3 -0
- data/lib/steps/update_apt_packages/step.rb +11 -0
- metadata +51 -9
data/lib/consist/cli.rb
CHANGED
@@ -1,4 +1,17 @@
|
|
1
1
|
require "thor"
|
2
|
+
require "sshkit"
|
3
|
+
require "sshkit/dsl"
|
4
|
+
|
5
|
+
require "consist/recipe"
|
6
|
+
require "consist/recipes"
|
7
|
+
require "consist/step"
|
8
|
+
require "consist/consistfile"
|
9
|
+
require "consist/commands/includes/stream_logger"
|
10
|
+
require "consist/commands/includes/erbable"
|
11
|
+
require "consist/commands/exec"
|
12
|
+
require "consist/commands/upload"
|
13
|
+
require "consist/commands/mutate"
|
14
|
+
require "consist/commands/check"
|
2
15
|
|
3
16
|
module Consist
|
4
17
|
class CLI < Thor
|
@@ -6,9 +19,33 @@ module Consist
|
|
6
19
|
|
7
20
|
map %w[-v --version] => "version"
|
8
21
|
|
9
|
-
desc "version", "Display consist version"
|
22
|
+
desc "version", "Display consist version"
|
10
23
|
def version
|
11
24
|
say "consist/#{VERSION} #{RUBY_DESCRIPTION}"
|
12
25
|
end
|
26
|
+
|
27
|
+
desc "lightup", "Attempt to connect to a server and execute an idempotent statement."
|
28
|
+
def lightup(user, server)
|
29
|
+
puts "---> Attempting to connect to #{server} as #{user}"
|
30
|
+
on("#{user}@#{server}") do
|
31
|
+
as user do
|
32
|
+
execute "true"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "scaffold", "Apply a given recipe to (a) server(s)"
|
38
|
+
def scaffold(_recipe, server_ip)
|
39
|
+
Consist::Recipes.new(server_ip)
|
40
|
+
end
|
41
|
+
|
42
|
+
option :step, type: :string
|
43
|
+
option :consistfile, type: :string
|
44
|
+
desc "up", "Run a Consistfile against a server"
|
45
|
+
def up(server_ip)
|
46
|
+
specified_step = options[:step]
|
47
|
+
consistfile = options[:consistfile]
|
48
|
+
Consist::Consistfile.new(server_ip, consistfile:, specified_step:)
|
49
|
+
end
|
13
50
|
end
|
14
51
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Consist
|
4
|
+
module Commands
|
5
|
+
class Check
|
6
|
+
def initialize(command)
|
7
|
+
@command = command
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform!(executor)
|
11
|
+
status = @command[:status]
|
12
|
+
|
13
|
+
flag, val = if @command.has_key?(:path) && !@command[:path].nil?
|
14
|
+
["d", @command[:path]]
|
15
|
+
else
|
16
|
+
["f", @command[:file]]
|
17
|
+
end
|
18
|
+
|
19
|
+
exists = executor.test("[ -#{flag} #{val} ]")
|
20
|
+
|
21
|
+
if status == :exist && !exists
|
22
|
+
@command[:block].call
|
23
|
+
elsif status == :nonexistant && !exists
|
24
|
+
@command[:block].call
|
25
|
+
else
|
26
|
+
tense = if status == :exist
|
27
|
+
"should"
|
28
|
+
elsif status == :nonexistant
|
29
|
+
"shoudlnt"
|
30
|
+
end
|
31
|
+
puts "Checking path `#{status}` - `#{val}` - #{exists ? "exists" : "doesn't exist"} and #{tense} - skipping"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Consist
|
4
|
+
module Commands
|
5
|
+
class Exec
|
6
|
+
include Erbable
|
7
|
+
|
8
|
+
def initialize(command)
|
9
|
+
@command = command
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform!(executor)
|
13
|
+
@command[:commands].each do
|
14
|
+
executor.execute(erb_template(_1), interaction_handler: Consist::Commands::StreamLogger.new,
|
15
|
+
**@command[:params])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
|
5
|
+
module Consist
|
6
|
+
module Commands
|
7
|
+
module Erbable
|
8
|
+
def self.included(klass)
|
9
|
+
klass.extend ClassMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
def erb_template(contents)
|
13
|
+
b = binding
|
14
|
+
Consist.config.keys.each do |key|
|
15
|
+
b.local_variable_set(key, Consist.config[key])
|
16
|
+
end
|
17
|
+
ERB.new(contents).result(b)
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Consist
|
4
|
+
module Commands
|
5
|
+
class StreamLogger
|
6
|
+
def initialize(log_level = :info)
|
7
|
+
@log_level = log_level
|
8
|
+
end
|
9
|
+
|
10
|
+
def on_data(_command, _stream_name, data, _channel)
|
11
|
+
log(data)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def log(message)
|
17
|
+
SSHKit.config.output.send(@log_level, message) unless @log_level.nil?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Consist
|
4
|
+
module Commands
|
5
|
+
class Mutate
|
6
|
+
include Erbable
|
7
|
+
|
8
|
+
def initialize(command)
|
9
|
+
@command = command
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform!(executor)
|
13
|
+
delim = @command[:delim]
|
14
|
+
target_file = @command[:target_file]
|
15
|
+
target_string = erb_template(@command[:target_string])
|
16
|
+
match = erb_template(@command[:match])
|
17
|
+
|
18
|
+
case @command[:mode]
|
19
|
+
when :replace
|
20
|
+
cmd = "sed -i -E 's#{delim}#{match}#{delim}#{target_string}#{delim}' #{target_file} "
|
21
|
+
end
|
22
|
+
|
23
|
+
executor.execute(cmd, interaction_handler: Consist::Commands::StreamLogger.new)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Consist
|
4
|
+
module Commands
|
5
|
+
class Upload
|
6
|
+
include Erbable
|
7
|
+
|
8
|
+
def initialize(command)
|
9
|
+
@command = command
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform!(executor)
|
13
|
+
# rubocop:disable Style/ClassEqualityComparison
|
14
|
+
if @command[:local_file].class == Symbol
|
15
|
+
puts "---> Uploading defined file #{@command[:local_file]}"
|
16
|
+
target_file = Consist.files.detect { |f| f[:id] == @command[:local_file] }
|
17
|
+
raise "\n\nNo declared file of ID `#{@command[:local_file]}`" unless target_file
|
18
|
+
|
19
|
+
contents = StringIO.new(erb_template(target_file[:contents]))
|
20
|
+
upload_defined_file(executor, contents, @command[:remote_path])
|
21
|
+
else
|
22
|
+
local_path = File.expand_path("../steps/#{@id}/#{@command[:local_file]}", __dir__)
|
23
|
+
upload(executor, local_path, @command[:remote_path])
|
24
|
+
end
|
25
|
+
# rubocop:enable Style/ClassEqualityComparison
|
26
|
+
end
|
27
|
+
|
28
|
+
def upload(executor, local_path, remote_path)
|
29
|
+
executor.send(:upload!, local_path, remote_path, interaction_handler: Consist::Commands::StreamLogger.new)
|
30
|
+
end
|
31
|
+
|
32
|
+
def upload_defined_file(executor, contents, remote_path)
|
33
|
+
executor.upload! contents, remote_path, interaction_handler: Consist::Commands::StreamLogger.new
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true37236
|
2
|
+
|
3
|
+
module Consist
|
4
|
+
class << self
|
5
|
+
attr_accessor :files, :config
|
6
|
+
end
|
7
|
+
|
8
|
+
@files = []
|
9
|
+
@config = {}
|
10
|
+
|
11
|
+
class Consistfile
|
12
|
+
include SSHKit::DSL
|
13
|
+
|
14
|
+
def initialize(server_ip, specified_step:, consistfile:)
|
15
|
+
@server_ip = server_ip
|
16
|
+
@specified_step = specified_step
|
17
|
+
consistfile_path = if consistfile
|
18
|
+
File.expand_path(consistfile, Dir.pwd)
|
19
|
+
else
|
20
|
+
File.expand_path("Consistfile", Dir.pwd)
|
21
|
+
end
|
22
|
+
|
23
|
+
consistfile_contents = File.read(consistfile_path)
|
24
|
+
instance_eval(consistfile_contents)
|
25
|
+
end
|
26
|
+
|
27
|
+
def consist(&definition)
|
28
|
+
instance_eval(&definition)
|
29
|
+
end
|
30
|
+
|
31
|
+
def recipe(id, &definition)
|
32
|
+
recipe = Consist::Recipe.new(id, &definition)
|
33
|
+
|
34
|
+
puts "Executing Recipe: #{recipe.name}"
|
35
|
+
|
36
|
+
if @specified_step.nil?
|
37
|
+
recipe.steps.each { exec_step(_1) }
|
38
|
+
else
|
39
|
+
puts "Specific step targeted: #{@specified_step.to_sym}"
|
40
|
+
specified_step, *_rest = recipe.steps.select { _1.id === @specified_step.to_sym }
|
41
|
+
raise "Step with id #{@specified_step.to_sym} not found." unless specified_step
|
42
|
+
|
43
|
+
exec_step(specified_step)
|
44
|
+
end
|
45
|
+
|
46
|
+
puts "Execution of #{recipe.name} has completed."
|
47
|
+
end
|
48
|
+
|
49
|
+
def file(id, &definition)
|
50
|
+
return unless definition
|
51
|
+
|
52
|
+
contents = yield
|
53
|
+
|
54
|
+
Consist.files << {id:, contents:}
|
55
|
+
end
|
56
|
+
|
57
|
+
def config(id, val)
|
58
|
+
Consist.config[id] = val
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def exec_step(specified_step)
|
64
|
+
puts "Executing Step: #{specified_step.name}"
|
65
|
+
|
66
|
+
on("#{specified_step.required_user}@#{@server_ip}") do
|
67
|
+
specified_step.perform(self)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Consist
|
4
|
+
class Recipe
|
5
|
+
def initialize(_id = nil, &definition)
|
6
|
+
@steps = []
|
7
|
+
instance_eval(&definition)
|
8
|
+
end
|
9
|
+
|
10
|
+
def name(name = nil)
|
11
|
+
@name = name if name
|
12
|
+
@name
|
13
|
+
end
|
14
|
+
|
15
|
+
def description(description = nil)
|
16
|
+
@description = description if description
|
17
|
+
@description
|
18
|
+
end
|
19
|
+
|
20
|
+
def user(user = nil)
|
21
|
+
@user = user if @user
|
22
|
+
@user
|
23
|
+
end
|
24
|
+
|
25
|
+
def steps(&block)
|
26
|
+
instance_eval(&block) if block
|
27
|
+
@steps
|
28
|
+
end
|
29
|
+
|
30
|
+
def step(step_name, &block)
|
31
|
+
if block
|
32
|
+
@steps << Step.new(id: step_name, &block)
|
33
|
+
return
|
34
|
+
end
|
35
|
+
|
36
|
+
target_path = File.join("../../", "steps", step_name.to_s, "step.rb")
|
37
|
+
step_path = File.expand_path(target_path, __FILE__)
|
38
|
+
step_content = File.read(step_path)
|
39
|
+
@steps << Step.new(id: step_name) { instance_eval(step_content) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Consist
|
4
|
+
class Recipes
|
5
|
+
include SSHKit::DSL
|
6
|
+
|
7
|
+
def initialize(server_ip)
|
8
|
+
recipe_directory = File.expand_path("../recipes", __dir__)
|
9
|
+
recipes = Dir[File.join(recipe_directory, "*.rb")]
|
10
|
+
|
11
|
+
recipes.each do |recipe_file|
|
12
|
+
recipe_content = File.read(recipe_file)
|
13
|
+
recipe = Recipe.new { instance_eval(recipe_content) }
|
14
|
+
|
15
|
+
puts "Executing Recipe: #{recipe.name}"
|
16
|
+
recipe.steps.each do |step|
|
17
|
+
puts "Executing Step: #{step.name}"
|
18
|
+
|
19
|
+
on("#{step.required_user}@#{server_ip}") do
|
20
|
+
step.perform(self)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
puts "Execution of #{recipe.name} has completed."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/consist/step.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
|
5
|
+
module Consist
|
6
|
+
class Step
|
7
|
+
include SSHKit::DSL
|
8
|
+
|
9
|
+
def initialize(id:, &block)
|
10
|
+
@commands = []
|
11
|
+
@id = id
|
12
|
+
@required_user = :root
|
13
|
+
instance_eval(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def id(id = nil)
|
17
|
+
@id = id if id
|
18
|
+
@id
|
19
|
+
end
|
20
|
+
|
21
|
+
def name(name = nil)
|
22
|
+
@name = name if name
|
23
|
+
@name
|
24
|
+
end
|
25
|
+
|
26
|
+
def required_user(user = nil)
|
27
|
+
@required_user = user if user
|
28
|
+
@required_user
|
29
|
+
end
|
30
|
+
|
31
|
+
def shell(message = "", params: {})
|
32
|
+
return unless block_given?
|
33
|
+
|
34
|
+
command = yield
|
35
|
+
commands = command.split(/(?<!\\)\n/).select { !_1.start_with?("#") }.compact
|
36
|
+
|
37
|
+
@commands << {message:, type: :exec, commands:, params:}
|
38
|
+
end
|
39
|
+
|
40
|
+
def mutate_file(mode:, target_file:, match:, target_string:, delim: "/", message: "")
|
41
|
+
@commands << {type: :mutate, mode:, message:, match:, target_file:, delim:, target_string:}
|
42
|
+
end
|
43
|
+
|
44
|
+
def upload_file(local_file:, remote_path:, message: "")
|
45
|
+
@commands << {message:, type: :upload, local_file:, remote_path:}
|
46
|
+
end
|
47
|
+
|
48
|
+
def check(status:, path: nil, file: nil, message: "", &block)
|
49
|
+
@commands << {type: :check, message:, status:, file:, path:, block: -> { instance_eval(&block) }}
|
50
|
+
end
|
51
|
+
|
52
|
+
def perform(executor)
|
53
|
+
@commands.each do |command|
|
54
|
+
banner(command[:message]) unless command[:message].empty?
|
55
|
+
|
56
|
+
execable = Object.const_get("Consist::Commands::#{command[:type].capitalize}").new(command)
|
57
|
+
executor.as @required_user do
|
58
|
+
execable.perform!(executor)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def banner(message)
|
66
|
+
return if message.empty?
|
67
|
+
|
68
|
+
msg = "********* #{message} ********"
|
69
|
+
puts "*" * msg.length
|
70
|
+
puts msg
|
71
|
+
puts "*" * msg.length
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/consist/thor_ext.rb
CHANGED
@@ -28,12 +28,12 @@ module Consist
|
|
28
28
|
base.check_unknown_options!
|
29
29
|
end
|
30
30
|
|
31
|
-
def start(given_args=ARGV, config={})
|
31
|
+
def start(given_args = ARGV, config = {})
|
32
32
|
config[:shell] ||= Thor::Base.shell.new
|
33
33
|
handle_help_switches(given_args) do |args|
|
34
34
|
dispatch(nil, args, nil, config)
|
35
35
|
end
|
36
|
-
rescue
|
36
|
+
rescue => e
|
37
37
|
handle_exception_on_start(e, config)
|
38
38
|
end
|
39
39
|
|
data/lib/consist/version.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
name "Install APT packages"
|
2
|
+
required_user :root
|
3
|
+
|
4
|
+
shell "Installing essential packages" do
|
5
|
+
<<~EOS
|
6
|
+
apt-get -y remove systemd-timesyncd
|
7
|
+
timedatectl set-ntp no
|
8
|
+
apt-get -y install build-essential curl fail2ban git ntp vim
|
9
|
+
apt-get autoremove
|
10
|
+
apt-get autoclean
|
11
|
+
EOS
|
12
|
+
end
|
13
|
+
|
14
|
+
shell "Start NTP and Fail2Ban" do
|
15
|
+
<<~EOS
|
16
|
+
service ntp restart
|
17
|
+
service fail2ban restart
|
18
|
+
EOS
|
19
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
name "Update the APT packages"
|
2
|
+
required_user :root
|
3
|
+
|
4
|
+
upload_file message: "Uploading APT config...", local_file: "apt_auto_upgrades",
|
5
|
+
remote_path: "/etc/apt/apt.conf.d/20auto-upgrades"
|
6
|
+
|
7
|
+
shell do
|
8
|
+
<<~EOS
|
9
|
+
apt-get update && apt-get upgrade -y
|
10
|
+
EOS
|
11
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: consist
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John McDowall
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-11-
|
11
|
+
date: 2023-11-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: net-ssh
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sshkit
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.21'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.21'
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: thor
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -26,7 +54,7 @@ dependencies:
|
|
26
54
|
version: '1.2'
|
27
55
|
description:
|
28
56
|
email:
|
29
|
-
-
|
57
|
+
- john@kantan.io
|
30
58
|
executables:
|
31
59
|
- consist
|
32
60
|
extensions: []
|
@@ -37,16 +65,30 @@ files:
|
|
37
65
|
- exe/consist
|
38
66
|
- lib/consist.rb
|
39
67
|
- lib/consist/cli.rb
|
68
|
+
- lib/consist/commands/check.rb
|
69
|
+
- lib/consist/commands/exec.rb
|
70
|
+
- lib/consist/commands/includes/erbable.rb
|
71
|
+
- lib/consist/commands/includes/stream_logger.rb
|
72
|
+
- lib/consist/commands/mutate.rb
|
73
|
+
- lib/consist/commands/upload.rb
|
74
|
+
- lib/consist/consistfile.rb
|
75
|
+
- lib/consist/recipe.rb
|
76
|
+
- lib/consist/recipes.rb
|
77
|
+
- lib/consist/step.rb
|
40
78
|
- lib/consist/thor_ext.rb
|
41
79
|
- lib/consist/version.rb
|
42
|
-
|
80
|
+
- lib/recipes/kamal_single_server.rb
|
81
|
+
- lib/steps/install_apt_packages/step.rb
|
82
|
+
- lib/steps/update_apt_packages/apt_auto_upgrades
|
83
|
+
- lib/steps/update_apt_packages/step.rb
|
84
|
+
homepage: https://github.com/consist-sh/consist
|
43
85
|
licenses:
|
44
|
-
-
|
86
|
+
- LGPL-3.0
|
45
87
|
metadata:
|
46
|
-
bug_tracker_uri: https://github.com/
|
47
|
-
changelog_uri: https://github.com/
|
48
|
-
source_code_uri: https://github.com/
|
49
|
-
homepage_uri: https://github.com/
|
88
|
+
bug_tracker_uri: https://github.com/consist-sh/consist/issues
|
89
|
+
changelog_uri: https://github.com/consist-sh/consist/releases
|
90
|
+
source_code_uri: https://github.com/consist-sh/consist
|
91
|
+
homepage_uri: https://github.com/consist-sh/consist
|
50
92
|
rubygems_mfa_required: 'true'
|
51
93
|
post_install_message:
|
52
94
|
rdoc_options: []
|