kdep 0.3.4 → 0.3.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 615a9eb7f9d29743543c3af91b7a40bb1c3144246b5610dfa87c0d56263daed3
4
- data.tar.gz: 17c34cf62596baeba616c1648c6013e499c73b6ad05f602bafb8d116287487f5
3
+ metadata.gz: 8194d84a25eda1737a9bdab8891db08ec092cfc71c86764fac24ff044b977626
4
+ data.tar.gz: d247a4155dea0105240416bf2af475d5a7e22b71917467f230bc13123d4c79b5
5
5
  SHA512:
6
- metadata.gz: 5daa74dd82e2dcc8bb3001d62fe03bf89d32cb96d004bbd303b0ef568c6881fc6d32cbb76fae4191c0df469c70051737996e363762712a08f1d27ff1d95a92fd
7
- data.tar.gz: 3061858aa0b926fce8bf3aadbc7cf047e01376e7e1206756b4233764efaca656e7dbc10966cff22233124963048db59113cd04dbc7ce796bfa94830072d2e17e
6
+ metadata.gz: e3fb390e4470e4daf795717de1cac5c32142eb208dc58e529eabccba079bfeb2dd737ee74ad92ea92a675298911091dbd52ea007f9c38cbc8e624ea8d0bbf13d
7
+ data.tar.gz: 36e1bc46602c74aa2027e0c4377b951d0602ce24ca05470f2f0a989fe6266288da5017f3f45e91d3923efe1e884830709718a7b75aa5db8fa3c3408e20f8e2a4
data/lib/kdep/cli.rb CHANGED
@@ -20,6 +20,7 @@ module Kdep
20
20
  "migrate" => Commands::Migrate,
21
21
  "helm-install" => Commands::HelmInstall,
22
22
  "dashboard" => Commands::Dashboard,
23
+ "doctor" => Commands::Doctor,
23
24
  }.freeze
24
25
 
25
26
  def initialize(argv)
@@ -13,6 +13,7 @@ module Kdep
13
13
  opts.on("--dry-run", "Run full pipeline but skip kubectl apply")
14
14
  opts.on("--no-dashboard", "Skip TUI dashboard after deploy")
15
15
  opts.on("--platform=PLATFORM", "Target platform (e.g., linux/amd64)")
16
+ opts.on("--init-state", "Seed state.yml at 0.0 when missing (non-interactive)")
16
17
  end
17
18
  end
18
19
 
@@ -43,7 +44,24 @@ module Kdep
43
44
 
44
45
  config = Kdep::Config.new(deploy_dir, env).load
45
46
 
46
- # Step 2: Context guard (bump touches the cluster)
47
+ # Step 2: Local config sanity read state.yml BEFORE touching the
48
+ # cluster. A missing/corrupt state.yml is a local config error and
49
+ # should fail fast without waking kubectl.
50
+ begin
51
+ current_tag = Kdep::State.tag(deploy_dir) || handle_missing_state(deploy_dir)
52
+ rescue Kdep::State::Error => e
53
+ @ui.error(e.message)
54
+ exit 1
55
+ end
56
+ # to_fix.md #3 — warn if someone has gitignored state.yml
57
+ require "kdep/doctor/check_state_gitignored"
58
+ gitignore_result = Kdep::Doctor::CheckStateGitignored.new(deploy_dir).run
59
+ if gitignore_result.severity == :error
60
+ @ui.warn(gitignore_result.message)
61
+ @ui.warn(gitignore_result.hint) if gitignore_result.hint
62
+ end
63
+
64
+ # Step 3: Context guard (bump touches the cluster)
47
65
  begin
48
66
  Kdep::ContextGuard.new(config["context"]).validate!
49
67
  rescue Kdep::Kubectl::Error => e
@@ -63,14 +81,7 @@ module Kdep
63
81
  repo_root = File.dirname(kdep_dir)
64
82
 
65
83
  begin
66
- # Step 3: Read current tag from state.yml, increment minor
67
- state_path = File.join(deploy_dir, "state.yml")
68
- if File.exist?(state_path)
69
- state = YAML.safe_load(File.read(state_path)) || {}
70
- current_tag = state["tag"] || "0.0"
71
- else
72
- current_tag = "0.0"
73
- end
84
+ # Step 4: Increment minor version.
74
85
  parsed = Kdep::VersionTagger.parse(current_tag)
75
86
  next_version = parsed ? "#{parsed[0]}.#{parsed[1] + 1}" : "0.1"
76
87
  @ui.info("#{current_tag} -> #{next_version}")
@@ -159,7 +170,7 @@ module Kdep
159
170
  @ui.info("#{applied} files applied, 0 errors")
160
171
 
161
172
  # Save new tag to state.yml
162
- File.write(File.join(deploy_dir, "state.yml"), YAML.dump({"tag" => next_version}))
173
+ Kdep::State.write(deploy_dir, tag: next_version)
163
174
 
164
175
  # Launch TUI dashboard for live monitoring
165
176
  unless @command_options[:"no-dashboard"]
@@ -185,6 +196,41 @@ module Kdep
185
196
 
186
197
  private
187
198
 
199
+ def handle_missing_state(deploy_dir)
200
+ if @command_options[:"init-state"]
201
+ seed_initial_state(deploy_dir, reason: :flag)
202
+ elsif $stdin.tty?
203
+ prompt_for_init(deploy_dir)
204
+ else
205
+ @ui.error(
206
+ "state.yml not found in #{deploy_dir}.\n" \
207
+ "Re-run with --init-state to seed at \"#{Kdep::State::INITIAL_TAG}\" " \
208
+ "(first deploy will be tag \"0.1\"), or commit a state.yml manually."
209
+ )
210
+ exit 1
211
+ end
212
+ end
213
+
214
+ def prompt_for_init(deploy_dir)
215
+ @ui.warn("No state.yml found in #{deploy_dir}.")
216
+ @ui.warn("If prod is already running a higher tag, this will regress it.")
217
+ print %Q(Seed state.yml at "#{Kdep::State::INITIAL_TAG}" and proceed? [y/N] )
218
+ answer = $stdin.gets&.strip&.downcase
219
+ unless %w[y yes].include?(answer)
220
+ @ui.info("Aborted.")
221
+ exit 0
222
+ end
223
+ seed_initial_state(deploy_dir, reason: :prompt)
224
+ end
225
+
226
+ def seed_initial_state(deploy_dir, reason:)
227
+ tag = Kdep::State::INITIAL_TAG
228
+ Kdep::State.write(deploy_dir, tag: tag)
229
+ source = reason == :flag ? "--init-state flag" : "user confirmation"
230
+ @ui.info("Seeded state.yml at #{tag} (#{source}). First deploy will be tag 0.1.")
231
+ tag
232
+ end
233
+
188
234
  def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
189
235
  if deploy_name
190
236
  deploy_dir = File.join(kdep_dir, deploy_name)
@@ -48,37 +48,43 @@ module Kdep
48
48
  exit 1
49
49
  end
50
50
 
51
- # Load preset resources and render
52
- preset = Kdep::Preset.new(config["preset"], deploy_dir)
53
- resources = preset.resources
54
-
55
- repo_root = File.dirname(kdep_dir)
56
- output_dir = File.join(deploy_dir, ".rendered")
57
-
58
- writer = Kdep::Writer.new(output_dir)
59
- writer.clean
60
- renderer = Kdep::Renderer.new(config, deploy_dir)
51
+ # Inject tag from state.yml if present (to_fix.md #1)
52
+ if (state_tag = Kdep::State.tag(deploy_dir))
53
+ config["tag"] = state_tag
54
+ else
55
+ @ui.warn("No state.yml in #{deploy_dir} -- diff will compare preset default tag against live cluster.")
56
+ end
61
57
 
62
- resources.each_with_index do |resource_name, idx|
63
- index = idx + 1
64
- begin
65
- content = renderer.render_resource(resource_name)
66
- rescue => e
67
- @ui.error("#{resource_name}: #{e.message}")
68
- next
58
+ # Render into a temp dir so we never clobber .rendered/ (owned by bump/render)
59
+ require "tmpdir"
60
+ Dir.mktmpdir("kdep-diff-") do |tmpdir|
61
+ preset = Kdep::Preset.new(config["preset"], deploy_dir)
62
+ writer = Kdep::Writer.new(tmpdir)
63
+ renderer = Kdep::Renderer.new(config, deploy_dir)
64
+
65
+ preset.resources.each_with_index do |resource_name, idx|
66
+ begin
67
+ content = renderer.render_resource(resource_name)
68
+ rescue => e
69
+ @ui.error("#{resource_name}: #{e.message}")
70
+ next
71
+ end
72
+ writer.write(resource_name, content, idx + 1)
69
73
  end
70
- writer.write(resource_name, content, index)
74
+
75
+ rendered_files = Dir.glob(File.join(tmpdir, "*.yml")).sort
76
+ run_diff(rendered_files)
71
77
  end
78
+ end
72
79
 
73
- # Get sorted rendered files
74
- rendered_files = Dir.glob(File.join(output_dir, "*.yml")).sort
80
+ private
75
81
 
82
+ def run_diff(rendered_files)
76
83
  if rendered_files.empty?
77
84
  @ui.info("No rendered files to diff")
78
85
  return
79
86
  end
80
87
 
81
- # Diff each file against live cluster state
82
88
  has_diffs = false
83
89
  errors = []
84
90
 
@@ -101,8 +107,6 @@ module Kdep
101
107
  end
102
108
  end
103
109
 
104
- private
105
-
106
110
  def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
107
111
  if deploy_name
108
112
  deploy_dir = File.join(kdep_dir, deploy_name)
@@ -0,0 +1,99 @@
1
+ require "optparse"
2
+ require "json"
3
+ require "fileutils"
4
+
5
+ module Kdep
6
+ module Commands
7
+ class Doctor
8
+ def self.option_parser
9
+ OptionParser.new do |opts|
10
+ opts.banner = "Usage: kdep doctor [deploy]"
11
+ opts.separator ""
12
+ opts.separator "Runs health checks against the kdep/ directory."
13
+ opts.separator "With a deploy name, checks only that deploy; otherwise, checks all."
14
+ opts.separator ""
15
+ opts.on("--fix", "Auto-fix safe issues (missing .gitignore only)")
16
+ opts.on("--only=CHECKS", "Comma-separated check IDs to run")
17
+ opts.on("--format=FORMAT", "Output format: text (default) or json")
18
+ end
19
+ end
20
+
21
+ def initialize(global_options:, command_options:, args:)
22
+ @global_options = global_options
23
+ @command_options = command_options
24
+ @args = args
25
+ @ui = Kdep::UI.new(color: false)
26
+ end
27
+
28
+ def execute
29
+ deploy_name = @args[0]
30
+ discovery = Kdep::Discovery.new
31
+ kdep_dir = discovery.find_kdep_dir
32
+
33
+ unless kdep_dir
34
+ @ui.error("No kdep/ directory found")
35
+ exit 1
36
+ end
37
+
38
+ deploys = deploy_name ? [deploy_name] : discovery.find_deploys
39
+ only = (@command_options[:only] || "").split(",").map(&:strip).reject(&:empty?)
40
+ format = @command_options[:format] || "text"
41
+
42
+ all_results = []
43
+ deploys.each do |name|
44
+ deploy_dir = File.join(kdep_dir, name)
45
+ unless File.directory?(deploy_dir)
46
+ @ui.error("Deploy not found: #{name}")
47
+ exit 1
48
+ end
49
+ Kdep::Doctor::CHECKS.each do |check_class|
50
+ id = check_class.const_get(:ID)
51
+ next if !only.empty? && !only.include?(id)
52
+ result = check_class.new(deploy_dir).run
53
+ all_results << { deploy: name, result: result }
54
+ apply_fix(check_class, deploy_dir, result) if @command_options[:fix]
55
+ end
56
+ end
57
+
58
+ if format == "json"
59
+ puts JSON.pretty_generate(all_results.map { |r| r[:result].to_h.merge(deploy: r[:deploy]) })
60
+ else
61
+ render_text(all_results)
62
+ end
63
+
64
+ exit 1 if all_results.any? { |r| r[:result].severity == :error }
65
+ end
66
+
67
+ private
68
+
69
+ def render_text(entries)
70
+ entries.each do |entry|
71
+ r = entry[:result]
72
+ prefix = case r.severity
73
+ when :ok then "OK "
74
+ when :info then "INFO "
75
+ when :warn then "WARN "
76
+ when :error then "ERR "
77
+ else r.severity.to_s.upcase.ljust(5)
78
+ end
79
+ line = "#{prefix} [#{entry[:deploy]}] #{r.id}"
80
+ line += ": #{r.message}" if r.message
81
+ puts line
82
+ puts " -> #{r.hint}" if r.hint
83
+ end
84
+ end
85
+
86
+ def apply_fix(check_class, deploy_dir, result)
87
+ return unless result.severity != :ok
88
+ return unless check_class == Kdep::Doctor::CheckGitignoreCanonical
89
+ path = File.join(deploy_dir, ".gitignore")
90
+ return if File.exist?(path)
91
+ FileUtils.cp(
92
+ File.join(Kdep.templates_dir, "init", "gitignore"),
93
+ path
94
+ )
95
+ @ui.file_written(path)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -57,6 +57,28 @@ module Kdep
57
57
  File.write(File.join(target_dir, "app.yml"), YAML.dump(kdep_config))
58
58
  @ui.file_written("kdep/#{name}/app.yml")
59
59
 
60
+ # Copy canonical .gitignore so the migrated deploy stops
61
+ # at the same ignores as 'kdep init' uses (no state.yml!).
62
+ FileUtils.cp(
63
+ File.join(Kdep.templates_dir, "init", "gitignore"),
64
+ File.join(target_dir, ".gitignore")
65
+ )
66
+ @ui.file_written("kdep/#{name}/.gitignore")
67
+
68
+ # Seed state.yml from the live deploy's image tag so the
69
+ # first 'kdep bump' post-migration produces the NEXT tag,
70
+ # not 0.1 (which would regress prod).
71
+ if (tag = old.extracted_tag)
72
+ Kdep::State.write(target_dir, tag: tag)
73
+ @ui.file_written("kdep/#{name}/state.yml")
74
+ @ui.info("Seeded state.yml from live deploy tag: #{tag}")
75
+ unless Kdep::State.parseable_tag?(tag)
76
+ @ui.warn("Tag #{tag} is not kdep's major.minor format. Verify state.yml before bumping; next bump may produce 0.1.")
77
+ end
78
+ else
79
+ @ui.warn("Could not extract tag from old deploy (latest/missing). Next 'kdep bump' will prompt to seed state.")
80
+ end
81
+
60
82
  # Extract and write env vars from ConfigMap
61
83
  env = old.extract_env
62
84
  unless env.empty?
@@ -40,6 +40,12 @@ module Kdep
40
40
  # Load config
41
41
  config = Kdep::Config.new(deploy_dir, env).load
42
42
 
43
+ if (state_tag = Kdep::State.tag(deploy_dir))
44
+ config["tag"] = state_tag
45
+ else
46
+ @ui.warn("No state.yml in #{deploy_dir} -- rendering with preset default tag. Run 'kdep bump' to establish state.")
47
+ end
48
+
43
49
  # Load preset resources
44
50
  preset = Kdep::Preset.new(config["preset"], deploy_dir)
45
51
  resources = preset.resources
@@ -0,0 +1,15 @@
1
+ module Kdep
2
+ module Doctor
3
+ class Check
4
+ attr_reader :deploy_dir
5
+
6
+ def initialize(deploy_dir)
7
+ @deploy_dir = deploy_dir
8
+ end
9
+
10
+ def run
11
+ raise NotImplementedError, "#{self.class}#run must be overridden"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ module Kdep
2
+ module Doctor
3
+ class CheckGitignoreCanonical < Check
4
+ ID = "gitignore_canonical".freeze
5
+
6
+ def run
7
+ path = File.join(deploy_dir, ".gitignore")
8
+ template_path = File.join(Kdep.templates_dir, "init", "gitignore")
9
+
10
+ unless File.exist?(path)
11
+ return Result.new(
12
+ id: ID, severity: :info,
13
+ message: "#{deploy_dir}/.gitignore missing",
14
+ hint: "Run 'kdep doctor --fix' to create it from the canonical template."
15
+ )
16
+ end
17
+
18
+ if File.read(path) == File.read(template_path)
19
+ Result.ok(ID)
20
+ else
21
+ Result.new(
22
+ id: ID, severity: :info,
23
+ message: "#{path} differs from canonical template",
24
+ hint: "Review differences; add new entries under the kdep-generated comment."
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ require "open3"
2
+ require "kdep/state"
3
+
4
+ module Kdep
5
+ module Doctor
6
+ class CheckStateGitignored < Check
7
+ ID = "state_gitignored".freeze
8
+
9
+ def run
10
+ state_path = File.join(deploy_dir, Kdep::State::FILENAME)
11
+ # capture3 swallows stderr so "fatal: not a git repository" noise
12
+ # never reaches the user when the caller runs outside a repo.
13
+ _, _, status = Open3.capture3("git", "check-ignore", "-q", state_path)
14
+ if status.success?
15
+ Result.new(
16
+ id: ID, severity: :error,
17
+ message: "#{state_path} is gitignored",
18
+ hint: "Remove 'state.yml' from any .gitignore (local or ancestor) and 'git add' the file so fresh clones see the last-deployed tag."
19
+ )
20
+ else
21
+ Result.ok(ID)
22
+ end
23
+ rescue Errno::ENOENT
24
+ Result.ok(ID)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ require "kdep/state"
2
+
3
+ module Kdep
4
+ module Doctor
5
+ class CheckStateParseable < Check
6
+ ID = "state_parseable".freeze
7
+
8
+ def run
9
+ return Result.ok(ID) unless Kdep::State.exists?(deploy_dir)
10
+ begin
11
+ Kdep::State.load(deploy_dir)
12
+ Result.ok(ID)
13
+ rescue Kdep::State::Error => e
14
+ Result.new(id: ID, severity: :error, message: e.message)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ require "kdep/state"
2
+
3
+ module Kdep
4
+ module Doctor
5
+ class CheckStatePresent < Check
6
+ ID = "state_present".freeze
7
+
8
+ def run
9
+ if Kdep::State.exists?(deploy_dir)
10
+ Result.ok(ID)
11
+ else
12
+ Result.new(
13
+ id: ID, severity: :warn,
14
+ message: "state.yml missing in #{deploy_dir}",
15
+ hint: "Run 'kdep bump --init-state' to seed, or commit a state.yml manually."
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ require "kdep/state"
2
+
3
+ module Kdep
4
+ module Doctor
5
+ class CheckStateTagFormat < Check
6
+ ID = "state_tag_format".freeze
7
+
8
+ def run
9
+ return Result.ok(ID) unless Kdep::State.exists?(deploy_dir)
10
+ tag = Kdep::State.tag(deploy_dir)
11
+ return Result.ok(ID) if Kdep::State.parseable_tag?(tag)
12
+ Result.new(
13
+ id: ID, severity: :warn,
14
+ message: "state.yml tag #{tag.inspect} is not kdep's major.minor format",
15
+ hint: "Next 'kdep bump' will fall back to 0.1 for this deploy."
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ module Kdep
2
+ module Doctor
3
+ class Result
4
+ attr_reader :id, :severity, :message, :hint
5
+
6
+ def initialize(id:, severity:, message: nil, hint: nil)
7
+ @id = id
8
+ @severity = severity
9
+ @message = message
10
+ @hint = hint
11
+ end
12
+
13
+ def ok?
14
+ @severity == :ok
15
+ end
16
+
17
+ def self.ok(id)
18
+ new(id: id, severity: :ok)
19
+ end
20
+
21
+ def to_h
22
+ { id: id, severity: severity, message: message, hint: hint }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ require "kdep/doctor/result"
2
+ require "kdep/doctor/check"
3
+ require "kdep/doctor/check_state_present"
4
+ require "kdep/doctor/check_state_parseable"
5
+ require "kdep/doctor/check_state_tag_format"
6
+ require "kdep/doctor/check_state_gitignored"
7
+ require "kdep/doctor/check_gitignore_canonical"
8
+
9
+ module Kdep
10
+ module Doctor
11
+ CHECKS = [
12
+ CheckStatePresent,
13
+ CheckStateParseable,
14
+ CheckStateTagFormat,
15
+ CheckStateGitignored,
16
+ CheckGitignoreCanonical,
17
+ ].freeze
18
+ end
19
+ end
@@ -258,8 +258,41 @@ module Kdep
258
258
  env
259
259
  end
260
260
 
261
+ # Extract the image tag from the live deploy (e.g., "0.44" from
262
+ # "ghcr.io/org/app:0.44"). Returns nil if the tag is "latest",
263
+ # empty, or absent. On multi-container deployments, returns the
264
+ # first usable tag found. Uses rpartition so registry URLs with
265
+ # explicit ports (host:5000/org/app:0.44) aren't misparsed.
266
+ def extracted_tag
267
+ deployed_images.each do |img|
268
+ before, sep, after = img.to_s.rpartition(":")
269
+ # rpartition returns ["", "", img] when no ":" exists
270
+ next if sep.empty?
271
+ # A host:port with no tag (e.g. "host:5000/img") has sep but the
272
+ # "tag" part contains a "/" — not a real tag.
273
+ next if after.empty? || after.include?("/")
274
+ next if after == "latest"
275
+ # before will include any host:port prefix intact
276
+ _ = before
277
+ return after
278
+ end
279
+ nil
280
+ end
281
+
261
282
  private
262
283
 
284
+ def deployed_images
285
+ return @deployed_images if defined?(@deployed_images)
286
+ @deployed_images = []
287
+ each_manifest do |doc|
288
+ containers = doc.dig("spec", "template", "spec", "containers") || []
289
+ containers.each do |c|
290
+ @deployed_images << c["image"] if c["image"]
291
+ end
292
+ end
293
+ @deployed_images
294
+ end
295
+
263
296
  def extract_from_manifests(result)
264
297
  has_service = false
265
298
  has_ingress = false
data/lib/kdep/state.rb ADDED
@@ -0,0 +1,48 @@
1
+ require "yaml"
2
+
3
+ module Kdep
4
+ module State
5
+ class Error < StandardError; end
6
+
7
+ FILENAME = "state.yml".freeze
8
+ INITIAL_TAG = "0.0".freeze
9
+
10
+ HEADER = <<~HEADER
11
+ # Last tag deployed by kdep bump -- do not edit by hand.
12
+ # This file is safe to commit. See: https://github.com/repleadfy/kdep
13
+ HEADER
14
+
15
+ def self.load(deploy_dir)
16
+ path = File.join(deploy_dir, FILENAME)
17
+ return nil unless File.exist?(path)
18
+ data = YAML.safe_load(File.read(path)) || {}
19
+ unless data.is_a?(Hash) && data["tag"]
20
+ raise Error, "#{path}: missing required key 'tag'"
21
+ end
22
+ data
23
+ end
24
+
25
+ def self.tag(deploy_dir)
26
+ loaded = load(deploy_dir)
27
+ loaded && loaded["tag"]
28
+ end
29
+
30
+ def self.write(deploy_dir, tag:)
31
+ path = File.join(deploy_dir, FILENAME)
32
+ tmp = "#{path}.tmp"
33
+ content = "#{HEADER}tag: \"#{tag}\"\n"
34
+ File.write(tmp, content)
35
+ File.rename(tmp, path)
36
+ path
37
+ end
38
+
39
+ def self.exists?(deploy_dir)
40
+ File.exist?(File.join(deploy_dir, FILENAME))
41
+ end
42
+
43
+ def self.parseable_tag?(tag)
44
+ require "kdep/version_tagger"
45
+ !!Kdep::VersionTagger.parse(tag)
46
+ end
47
+ end
48
+ end
data/lib/kdep/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kdep
2
- VERSION = "0.3.4"
2
+ VERSION = "0.3.6"
3
3
  end
data/lib/kdep.rb CHANGED
@@ -17,6 +17,8 @@ require "kdep/docker"
17
17
  require "kdep/helm"
18
18
  require "kdep/registry"
19
19
  require "kdep/version_tagger"
20
+ require "kdep/state"
21
+ require "kdep/doctor"
20
22
  require "kdep/commands/render"
21
23
  require "kdep/commands/init"
22
24
  require "kdep/commands/eject"
@@ -34,6 +36,7 @@ require "kdep/commands/scale"
34
36
  require "kdep/commands/migrate"
35
37
  require "kdep/commands/helm_install"
36
38
  require "kdep/commands/dashboard"
39
+ require "kdep/commands/doctor"
37
40
  require "kdep/dashboard/screen"
38
41
  require "kdep/dashboard/layout"
39
42
  require "kdep/dashboard/panel"
@@ -1,3 +1,4 @@
1
- # kdep generated files
1
+ # kdep-generated files (do NOT add state.yml -- it is tracked so
2
+ # deploys are reproducible across clones and CI runners).
2
3
  secrets.yml
3
4
  .rendered/
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kdep
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leadfy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-13 00:00:00.000000000 Z
11
+ date: 2026-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -70,6 +70,7 @@ files:
70
70
  - lib/kdep/commands/bump.rb
71
71
  - lib/kdep/commands/dashboard.rb
72
72
  - lib/kdep/commands/diff.rb
73
+ - lib/kdep/commands/doctor.rb
73
74
  - lib/kdep/commands/eject.rb
74
75
  - lib/kdep/commands/helm_install.rb
75
76
  - lib/kdep/commands/init.rb
@@ -95,12 +96,21 @@ files:
95
96
  - lib/kdep/defaults.rb
96
97
  - lib/kdep/discovery.rb
97
98
  - lib/kdep/docker.rb
99
+ - lib/kdep/doctor.rb
100
+ - lib/kdep/doctor/check.rb
101
+ - lib/kdep/doctor/check_gitignore_canonical.rb
102
+ - lib/kdep/doctor/check_state_gitignored.rb
103
+ - lib/kdep/doctor/check_state_parseable.rb
104
+ - lib/kdep/doctor/check_state_present.rb
105
+ - lib/kdep/doctor/check_state_tag_format.rb
106
+ - lib/kdep/doctor/result.rb
98
107
  - lib/kdep/helm.rb
99
108
  - lib/kdep/kubectl.rb
100
109
  - lib/kdep/old_format.rb
101
110
  - lib/kdep/preset.rb
102
111
  - lib/kdep/registry.rb
103
112
  - lib/kdep/renderer.rb
113
+ - lib/kdep/state.rb
104
114
  - lib/kdep/template_context.rb
105
115
  - lib/kdep/ui.rb
106
116
  - lib/kdep/update_check.rb