odysseus-core 0.3.2 → 0.9.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +485 -1
  3. data/LICENSE.txt +25 -7
  4. data/README.md +12 -3
  5. data/lib/odysseus/builder/client.rb +15 -15
  6. data/lib/odysseus/caddy/client.rb +119 -42
  7. data/lib/odysseus/command_redaction.rb +38 -0
  8. data/lib/odysseus/config/parser.rb +43 -18
  9. data/lib/odysseus/core/deploy_versioning.rb +42 -0
  10. data/lib/odysseus/core/environment.rb +63 -0
  11. data/lib/odysseus/core/version.rb +1 -1
  12. data/lib/odysseus/core/volume_namespacer.rb +0 -0
  13. data/lib/odysseus/core.rb +2 -2
  14. data/lib/odysseus/deploy_log.rb +120 -0
  15. data/lib/odysseus/deploy_version.rb +9 -0
  16. data/lib/odysseus/deployer/dependency_manager.rb +121 -0
  17. data/lib/odysseus/deployer/executor.rb +213 -161
  18. data/lib/odysseus/deployer/retention_sweeper.rb +94 -0
  19. data/lib/odysseus/deployer/ssh.rb +92 -18
  20. data/lib/odysseus/docker/client.rb +240 -48
  21. data/lib/odysseus/docker/labels.rb +43 -0
  22. data/lib/odysseus/errors.rb +10 -0
  23. data/lib/odysseus/git.rb +70 -0
  24. data/lib/odysseus/host_paths.rb +79 -0
  25. data/lib/odysseus/host_providers/base.rb +1 -1
  26. data/lib/odysseus/host_providers/static.rb +1 -1
  27. data/lib/odysseus/host_providers.rb +3 -4
  28. data/lib/odysseus/host_verifier.rb +156 -0
  29. data/lib/odysseus/host_versions.rb +59 -0
  30. data/lib/odysseus/orchestrator/{accessory_deploy.rb → dependency_deploy.rb} +84 -88
  31. data/lib/odysseus/orchestrator/job_deploy.rb +31 -43
  32. data/lib/odysseus/orchestrator/web_deploy.rb +61 -55
  33. data/lib/odysseus/plugins.rb +72 -0
  34. data/lib/odysseus/retention_plan.rb +14 -0
  35. data/lib/odysseus/retention_planner.rb +58 -0
  36. data/lib/odysseus/rollback_plan.rb +20 -0
  37. data/lib/odysseus/rollback_planner.rb +107 -0
  38. data/lib/odysseus/sails.rb +0 -0
  39. data/lib/odysseus/secrets/encrypted_file.rb +5 -7
  40. data/lib/odysseus/secrets/loader.rb +1 -3
  41. data/lib/odysseus/setup/docker_apt.rb +178 -0
  42. data/lib/odysseus/setup/escalation.rb +73 -0
  43. data/lib/odysseus/setup/preparer.rb +372 -0
  44. data/lib/odysseus/setup/public_key.rb +127 -0
  45. data/lib/odysseus/validators/config.rb +38 -25
  46. data/lib/odysseus/version_resolver.rb +80 -0
  47. data/lib/odysseus.rb +0 -0
  48. data/sig/odysseus/core.rbs +0 -0
  49. metadata +48 -27
  50. data/Rakefile +0 -12
  51. data/lib/odysseus/version.rb +0 -5
@@ -0,0 +1,127 @@
1
+ # lib/odysseus/setup/public_key.rb
2
+
3
+ require 'open3'
4
+
5
+ module Odysseus
6
+ module Setup
7
+ # The public keys `odysseus setup` installs for the user it creates,
8
+ # resolved entirely on THIS machine before the host is touched.
9
+ #
10
+ # Getting this wrong produces the worst outcome the command has: a created
11
+ # user nobody can log in as. So it runs first, and it raises rather than
12
+ # returning an empty list — there is no sensible way to continue.
13
+ module PublicKey
14
+ # A recognised SSH public-key line: key type, whitespace, base64 body,
15
+ # optional comment (which may itself contain spaces). Anything that
16
+ # doesn't match -- a private key's PEM body, a mangled file, plain
17
+ # garbage -- must never survive to become an authorized_keys entry on a
18
+ # real host.
19
+ LINE_PATTERN = %r{
20
+ \A
21
+ (?: ssh-rsa | ssh-dss | ssh-ed25519
22
+ | ecdsa-sha2-\S+
23
+ | sk-ecdsa-sha2-\S+@openssh\.com
24
+ | sk-ssh-ed25519@openssh\.com )
25
+ [ \t]+
26
+ [A-Za-z0-9+/]+=*
27
+ (?: [ \t]+ \S .* )?
28
+ \z
29
+ }x
30
+
31
+ # @param keys [Array<String>] ssh.keys entries (private key paths)
32
+ # @param explicit [Array<String>] paths given with --key, which win
33
+ # @return [Array<String>] authorized_keys lines, newline-free
34
+ # @raise [Odysseus::SetupError] when nothing can be resolved
35
+ def self.resolve(keys:, explicit: [])
36
+ looked_at = []
37
+ from_explicit = explicit.any?
38
+
39
+ lines = (from_explicit ? explicit : Array(keys)).flat_map do |path|
40
+ expanded = File.expand_path(path)
41
+ looked_at << expanded
42
+ found = from_path(expanded, explicit: from_explicit)
43
+
44
+ # A --key path the operator named is not optional. Falling back to
45
+ # another one, or to ssh.keys, would install a different key than
46
+ # the one they asked for -- worse than refusing, because they would
47
+ # believe the named key was authorised when it was not.
48
+ if from_explicit && found.empty?
49
+ raise Odysseus::SetupError,
50
+ "#{expanded} does not contain a valid SSH public key. --key must point " \
51
+ 'at a public key (usually the file ending in .pub), not a private key.'
52
+ end
53
+
54
+ found
55
+ end.uniq
56
+
57
+ return lines if lines.any?
58
+
59
+ raise Odysseus::SetupError,
60
+ 'Found no public key to install, so the user would be created with no way ' \
61
+ "to log in. Looked at: #{looked_at.join(', ')}. Pass --key with a path to a " \
62
+ 'public key, or check ssh.keys in deploy.yml.'
63
+ end
64
+
65
+ # An explicit --key path must be a public key itself; an ssh.keys entry
66
+ # is a private key whose public half is read from its .pub sibling.
67
+ #
68
+ # A sibling with nothing written in it -- missing, empty, or
69
+ # whitespace-only -- is silently derived from the private key: nothing
70
+ # was expressed there, so deriving is helpful, not presumptuous. A
71
+ # sibling that has content but none of it validates (for example a
72
+ # restricted `command="..."` line, which LINE_PATTERN deliberately does
73
+ # not accept -- see its comment) is refused rather than derived around:
74
+ # silently substituting a different, less restricted key is the same
75
+ # "operator believes X is installed, Y is installed instead" failure
76
+ # the explicit --key raise below exists to prevent, just reached by the
77
+ # implicit path instead.
78
+ def self.from_path(path, explicit:)
79
+ return valid_lines(path) if explicit
80
+
81
+ sibling = "#{path}.pub"
82
+ sibling_lines = valid_lines(sibling)
83
+ return sibling_lines if sibling_lines.any?
84
+ return derive(path) unless content?(sibling)
85
+
86
+ raise Odysseus::SetupError,
87
+ "#{sibling} exists but has no valid, unrestricted public key in it, so " \
88
+ 'odysseus will not silently derive a different key in its place. Fix the ' \
89
+ 'file, remove it so the key is derived from the private key instead, or ' \
90
+ 'pass --key with the key to install.'
91
+ end
92
+
93
+ # @return [Array<String>] the valid public-key lines in the file, in
94
+ # order; blank or invalid lines are dropped rather than passed through
95
+ def self.valid_lines(path)
96
+ return [] unless File.file?(path)
97
+
98
+ File.readlines(path).filter_map do |raw|
99
+ line = raw.strip
100
+ line if !line.empty? && LINE_PATTERN.match?(line)
101
+ end
102
+ end
103
+
104
+ # @return [Boolean] whether the file exists and has more than
105
+ # whitespace in it -- distinguishes "nothing was expressed here" from
106
+ # "something was expressed here, and it didn't validate"
107
+ def self.content?(path)
108
+ File.file?(path) && !File.read(path).strip.empty?
109
+ end
110
+
111
+ # `ssh-keygen -y` prints the public half of a private key. It fails
112
+ # loudly on an encrypted key, which is the right outcome: odysseus cannot
113
+ # answer a passphrase prompt any more than it can answer sudo's.
114
+ def self.derive(private_key_path)
115
+ return [] unless File.file?(private_key_path)
116
+
117
+ out, _err, status = Open3.capture3('ssh-keygen', '-y', '-f', private_key_path)
118
+ return [] unless status.success?
119
+
120
+ line = out.strip
121
+ LINE_PATTERN.match?(line) ? [line] : []
122
+ end
123
+
124
+ private_class_method :from_path, :valid_lines, :content?, :derive
125
+ end
126
+ end
127
+ end
@@ -3,7 +3,7 @@
3
3
  module Odysseus
4
4
  module Validators
5
5
  class Config
6
- REQUIRED_KEYS = ['service', 'image', 'servers'].freeze
6
+ REQUIRED_KEYS = %w[service image servers].freeze
7
7
 
8
8
  def initialize(config)
9
9
  @config = config
@@ -17,6 +17,7 @@ module Odysseus
17
17
  validate_proxy! if @config['proxy']
18
18
  validate_env! if @config['env']
19
19
  validate_ssh! if @config['ssh']
20
+ validate_retain_versions! if @config.key?('retain_versions')
20
21
  end
21
22
 
22
23
  private
@@ -31,13 +32,16 @@ module Odysseus
31
32
 
32
33
  def validate_servers!
33
34
  servers = @config['servers']
34
- raise Odysseus::ConfigValidationError,
35
- "servers must be a hash" unless servers.is_a?(Hash)
35
+ unless servers.is_a?(Hash)
36
+ raise Odysseus::ConfigValidationError,
37
+ 'servers must be a hash'
38
+ end
36
39
 
37
40
  servers.each do |role, config|
38
- raise Odysseus::ConfigValidationError,
39
- "server role '#{role}' must have 'hosts' array" \
40
- unless config.is_a?(Hash) && config['hosts'].is_a?(Array)
41
+ unless config.is_a?(Hash) && config['hosts'].is_a?(Array)
42
+ raise Odysseus::ConfigValidationError,
43
+ "server role '#{role}' must have 'hosts' array"
44
+ end
41
45
 
42
46
  validate_containers!(role, config['containers']) if config['containers']
43
47
  validate_deploy!(role, config['deploy']) if config['deploy']
@@ -48,37 +52,35 @@ module Odysseus
48
52
  proxy = @config['proxy']
49
53
  return if proxy.nil?
50
54
 
55
+ return if proxy['app_port']
56
+
51
57
  raise Odysseus::ConfigValidationError,
52
- "proxy must have 'app_port'" unless proxy['app_port']
58
+ "proxy must have 'app_port'"
53
59
  end
54
60
 
55
61
  def validate_env!
56
62
  env = @config['env']
57
63
  return if env.nil?
58
64
 
59
- unless env.is_a?(Hash)
60
- raise Odysseus::ConfigValidationError, "env must be a hash"
61
- end
65
+ raise Odysseus::ConfigValidationError, 'env must be a hash' unless env.is_a?(Hash)
62
66
 
63
67
  clear = env['clear']
64
- unless clear.nil? || clear.is_a?(Hash)
65
- raise Odysseus::ConfigValidationError, "env.clear must be a hash"
66
- end
68
+ raise Odysseus::ConfigValidationError, 'env.clear must be a hash' unless clear.nil? || clear.is_a?(Hash)
67
69
 
68
70
  secret = env['secret']
69
- unless secret.nil? || secret.is_a?(Array)
70
- raise Odysseus::ConfigValidationError, "env.secret must be an array"
71
- end
71
+ return if secret.nil? || secret.is_a?(Array)
72
+
73
+ raise Odysseus::ConfigValidationError, 'env.secret must be an array'
72
74
  end
73
75
 
74
76
  def validate_containers!(role, containers)
75
77
  return unless containers.is_a?(Hash)
76
78
 
77
79
  count = containers['count']
78
- if count && (!count.is_a?(Integer) || count < 1)
79
- raise Odysseus::ConfigValidationError,
80
- "servers.#{role}.containers.count must be an integer >= 1"
81
- end
80
+ return unless count && (!count.is_a?(Integer) || count < 1)
81
+
82
+ raise Odysseus::ConfigValidationError,
83
+ "servers.#{role}.containers.count must be an integer >= 1"
82
84
  end
83
85
 
84
86
  def validate_deploy!(role, deploy)
@@ -94,7 +96,7 @@ module Odysseus
94
96
  val = deploy[key]
95
97
  next unless val
96
98
 
97
- unless val.is_a?(Integer) && val > 0
99
+ unless val.is_a?(Integer) && val.positive?
98
100
  raise Odysseus::ConfigValidationError,
99
101
  "servers.#{role}.deploy.#{key} must be a positive integer"
100
102
  end
@@ -105,13 +107,24 @@ module Odysseus
105
107
  ssh = @config['ssh']
106
108
  return if ssh.nil?
107
109
 
108
- raise Odysseus::ConfigValidationError,
109
- "ssh must be a hash" unless ssh.is_a?(Hash)
110
+ unless ssh.is_a?(Hash)
111
+ raise Odysseus::ConfigValidationError,
112
+ 'ssh must be a hash'
113
+ end
110
114
 
111
115
  keys = ssh['keys']
116
+ return if keys.nil? || keys.is_a?(Array)
117
+
118
+ raise Odysseus::ConfigValidationError,
119
+ 'ssh.keys must be an array'
120
+ end
121
+
122
+ def validate_retain_versions!
123
+ value = @config['retain_versions']
124
+ return if value.is_a?(Integer) && value >= 1
125
+
112
126
  raise Odysseus::ConfigValidationError,
113
- "ssh.keys must be an array" \
114
- unless keys.nil? || keys.is_a?(Array)
127
+ "retain_versions must be an integer of 1 or more, got #{value.inspect}"
115
128
  end
116
129
  end
117
130
  end
@@ -0,0 +1,80 @@
1
+ # lib/odysseus/version_resolver.rb
2
+
3
+ module Odysseus
4
+ # Turns an optional --image tag plus the state of the work tree into the
5
+ # version a deploy will use. The rules live here rather than in Executor so
6
+ # they can be tested without a config file or an SSH connection.
7
+ class VersionResolver
8
+ SHA_LENGTH = 12
9
+
10
+ # @param config_dir [String] directory holding deploy.yml
11
+ # @param logger [Object, nil] responds to #info and #warn
12
+ def initialize(config_dir:, logger: nil)
13
+ @config_dir = config_dir
14
+ @logger = logger
15
+ end
16
+
17
+ # @param image_tag [String, nil] explicit tag; wins over git when given
18
+ # @return [DeployVersion]
19
+ # @raise [Odysseus::ConfigError] when no version can be established
20
+ def resolve(image_tag: nil)
21
+ # ref stays nil here: an arbitrary explicit tag has no commit it honestly
22
+ # identifies, and recording HEAD would assert a link that may not exist.
23
+ # (The no-tag path below refuses on uncommitted changes for exactly this
24
+ # reason — so that when it does record a ref, the tag actually identifies
25
+ # the code.) deployer is available regardless, so there is no such reason
26
+ # to withhold it.
27
+ return DeployVersion.new(version: image_tag, ref: nil, deployer: deployer) if image_tag
28
+
29
+ unless git.repository?
30
+ raise Odysseus::ConfigError,
31
+ "#{@config_dir} is not a git repository, so the version cannot be taken from a " \
32
+ 'commit. Pass --image to name the version explicitly.'
33
+ end
34
+
35
+ if git.uncommitted_changes?
36
+ raise Odysseus::ConfigError,
37
+ 'The working tree has uncommitted changes, so the image tag would not identify ' \
38
+ 'the code being deployed. Commit them, or pass --image to name the version.'
39
+ end
40
+
41
+ sha = git.head_sha(length: SHA_LENGTH)
42
+
43
+ unless sha
44
+ raise Odysseus::ConfigError,
45
+ "#{@config_dir} is a git repository with no commits yet, so there is no version to " \
46
+ 'deploy. Commit first, or pass --image to name the version.'
47
+ end
48
+
49
+ warn_about_untracked_files
50
+
51
+ DeployVersion.new(version: sha, ref: git.ref, deployer: deployer)
52
+ end
53
+
54
+ # Who is running this command: git's configured email, falling back to the
55
+ # shell user. Works outside a repository — `git config user.email` reads
56
+ # global config — so a rollback can name a deployer even when the version
57
+ # came from a host rather than a commit.
58
+ #
59
+ # @return [String]
60
+ def deployer
61
+ git.committer_email || ENV.fetch('USER', 'unknown')
62
+ end
63
+
64
+ private
65
+
66
+ def git
67
+ @git ||= Odysseus::Git.new(@config_dir)
68
+ end
69
+
70
+ # The build context includes untracked files unless .dockerignore excludes
71
+ # them, so they can change the image while the tag stays the same. Worth
72
+ # saying out loud; not worth refusing over.
73
+ def warn_about_untracked_files
74
+ return unless git.untracked_files?
75
+ return unless @logger.respond_to?(:warn)
76
+
77
+ @logger.warn('Working tree has untracked files; they may enter the image without changing its tag')
78
+ end
79
+ end
80
+ end
data/lib/odysseus.rb CHANGED
File without changes
File without changes
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: odysseus-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas
@@ -10,61 +10,61 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: net-ssh
13
+ name: base64
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '7.2'
18
+ version: '0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '7.2'
25
+ version: '0'
26
26
  - !ruby/object:Gem::Dependency
27
- name: net-scp
27
+ name: logger
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '4.0'
32
+ version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '4.0'
39
+ version: '0'
40
40
  - !ruby/object:Gem::Dependency
41
- name: base64
41
+ name: net-scp
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - ">="
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0'
46
+ version: '4.0'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - ">="
51
+ - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '0'
53
+ version: '4.0'
54
54
  - !ruby/object:Gem::Dependency
55
- name: logger
55
+ name: net-ssh
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
- - - ">="
58
+ - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '0'
60
+ version: '7.2'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
- - - ">="
65
+ - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '0'
67
+ version: '7.2'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: zeitwerk
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -90,38 +90,59 @@ files:
90
90
  - CHANGELOG.md
91
91
  - LICENSE.txt
92
92
  - README.md
93
- - Rakefile
94
93
  - lib/odysseus.rb
95
94
  - lib/odysseus/builder/client.rb
96
95
  - lib/odysseus/caddy/client.rb
96
+ - lib/odysseus/command_redaction.rb
97
97
  - lib/odysseus/config/parser.rb
98
98
  - lib/odysseus/core.rb
99
+ - lib/odysseus/core/deploy_versioning.rb
100
+ - lib/odysseus/core/environment.rb
99
101
  - lib/odysseus/core/version.rb
100
102
  - lib/odysseus/core/volume_namespacer.rb
103
+ - lib/odysseus/deploy_log.rb
104
+ - lib/odysseus/deploy_version.rb
105
+ - lib/odysseus/deployer/dependency_manager.rb
101
106
  - lib/odysseus/deployer/executor.rb
107
+ - lib/odysseus/deployer/retention_sweeper.rb
102
108
  - lib/odysseus/deployer/ssh.rb
103
109
  - lib/odysseus/docker/client.rb
110
+ - lib/odysseus/docker/labels.rb
104
111
  - lib/odysseus/errors.rb
112
+ - lib/odysseus/git.rb
113
+ - lib/odysseus/host_paths.rb
105
114
  - lib/odysseus/host_providers.rb
106
115
  - lib/odysseus/host_providers/base.rb
107
116
  - lib/odysseus/host_providers/static.rb
108
- - lib/odysseus/orchestrator/accessory_deploy.rb
117
+ - lib/odysseus/host_verifier.rb
118
+ - lib/odysseus/host_versions.rb
119
+ - lib/odysseus/orchestrator/dependency_deploy.rb
109
120
  - lib/odysseus/orchestrator/job_deploy.rb
110
121
  - lib/odysseus/orchestrator/web_deploy.rb
122
+ - lib/odysseus/plugins.rb
123
+ - lib/odysseus/retention_plan.rb
124
+ - lib/odysseus/retention_planner.rb
125
+ - lib/odysseus/rollback_plan.rb
126
+ - lib/odysseus/rollback_planner.rb
111
127
  - lib/odysseus/sails.rb
112
128
  - lib/odysseus/secrets/encrypted_file.rb
113
129
  - lib/odysseus/secrets/loader.rb
130
+ - lib/odysseus/setup/docker_apt.rb
131
+ - lib/odysseus/setup/escalation.rb
132
+ - lib/odysseus/setup/preparer.rb
133
+ - lib/odysseus/setup/public_key.rb
114
134
  - lib/odysseus/validators/config.rb
115
- - lib/odysseus/version.rb
135
+ - lib/odysseus/version_resolver.rb
116
136
  - sig/odysseus/core.rbs
117
137
  homepage: https://github.com/WA-Systems-EU/odysseus
118
138
  licenses:
119
- - LGPL-3.0-only
139
+ - MIT
120
140
  metadata:
121
141
  allowed_push_host: https://rubygems.org
122
142
  homepage_uri: https://github.com/WA-Systems-EU/odysseus
123
143
  source_code_uri: https://github.com/WA-Systems-EU/odysseus
124
- changelog_uri: https://github.com/WA-Systems-EU/odysseus/blob/trunk/CHANGELOG.md
144
+ changelog_uri: https://github.com/WA-Systems-EU/odysseus/blob/trunk/odysseus-core/CHANGELOG.md
145
+ rubygems_mfa_required: 'true'
125
146
  rdoc_options: []
126
147
  require_paths:
127
148
  - lib
@@ -136,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
157
  - !ruby/object:Gem::Version
137
158
  version: '0'
138
159
  requirements: []
139
- rubygems_version: 3.6.9
160
+ rubygems_version: 4.0.16
140
161
  specification_version: 4
141
162
  summary: Core library for Odysseus deployment tool
142
163
  test_files: []
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require "rubocop/rake_task"
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]
@@ -1,5 +0,0 @@
1
- # lib/odysseus/version.rb
2
-
3
- module Odysseus
4
- VERSION = "0.1.0"
5
- end