Module: EnvSpec::Init

Defined in:
lib/envspec/init.rb

Overview

Generates an initial env.spec by scanning a repo for env var usages.

Constant Summary collapse

HEADER =
<<~HEADER
  # env.spec — generated by `envspec init` %s
  #
  # This is a starting point. Review and adjust:
  #   1. Move secrets (passwords, tokens, keys) under [secrets]
  #   2. Add types: str (default) | int | bool | dsn | enum(a,b,c)
  #   3. Mark optional vars with ?
  #   4. Add defaults for configmap vars: KEY: type = value
  #   5. Group per-env vars under [env: production], [env: staging], etc
  #
  # Discovered usages are listed as comments above each key.
  # Confirm or remove before committing.
  #
  # Validate: envspec lint env.spec
  # See docs:  https://github.com/repleadfy/envspec
HEADER

Class Method Summary collapse

Class Method Details

.render(results, root) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/envspec/init.rb', line 46

def self.render(results, root)
  out = []
  out << format(HEADER, Time.now.strftime("%Y-%m-%d"))
  out << ""

  configmap_keys = []
  secret_keys    = []

  results.keys.sort.each do |name|
    usages    = results[name]
    inference = Heuristics.infer(name, usages)
    block = render_key(name, usages, inference, root)
    if inference[:secret]
      secret_keys << block
    else
      configmap_keys << block
    end
  end

  out.concat(configmap_keys) unless configmap_keys.empty?

  unless secret_keys.empty?
    out << ""
    out << "[secrets]"
    out.concat(secret_keys)
  end

  out.join("\n").gsub(/\n{3,}/, "\n\n").strip + "\n"
end

.render_key(name, usages, inference, root) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/envspec/init.rb', line 76

def self.render_key(name, usages, inference, root)
  lines = []

  usages.first(3).each do |u|
    rel = u[:file].sub(/\A#{Regexp.escape(root)}\/?/, "")
    lines << "# Found in: #{rel}:#{u[:line]}"
  end
  lines << "# (and #{usages.size - 3} more usages)" if usages.size > 3

  hints = []
  hints << "name suggests #{inference[:type]}" if inference[:type] != :str
  hints << "name suggests secret" if inference[:secret] && Heuristics::SECRET_NAME_RE.match?(name) && inference[:type] != :dsn
  hints << "uses fetch with default" if inference[:optional] && inference[:default]
  lines << "# Heuristic: #{hints.join(', ')}" unless hints.empty?

  decl = name.dup
  decl << "?" if inference[:optional]

  type = inference[:type]
  decl << ": #{type}" if type != :str || (inference[:default] && type == :str)
  decl << " = #{inference[:default]}" if inference[:default] && !inference[:secret]

  lines << decl
  lines << ""
  lines.join("\n")
end

.run(root: Dir.pwd, force: false, output: "env.spec") ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/envspec/init.rb', line 24

def self.run(root: Dir.pwd, force: false, output: "env.spec")
  out_path = File.expand_path(output, root)
  if File.exist?(out_path) && !force
    return { ok: false, reason: :exists, path: out_path }
  end

  t0 = Time.now
  results = Scanner.scan(root)
  elapsed = Time.now - t0

  content = render(results, root)
  File.write(out_path, content)

  {
    ok:      true,
    path:    out_path,
    keys:    results.size,
    files:   results.values.flatten.map { |u| u[:file] }.uniq.size,
    elapsed: elapsed,
  }
end