standard_health 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/app/controllers/standard_health/diagnostics_controller.rb +2 -1
- data/lib/standard_health/env_spec.rb +70 -5
- data/lib/standard_health/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 73c1743f46e03601e44970d282ca02dd195b22e5f22fe502e20e8a45281e0d69
|
|
4
|
+
data.tar.gz: 507f8e5fda5420690b7146b0f6a9136561c0a1b61d6c567c5be3dcd5c250be61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38b527cf8973dc2557b1b8c13e40ace60f73efb78f1aab34100b4a51cf4f53069700c7ac0047f038a9bcf5fdcb9a1f3223fa92ddc653e1293f6c5d4b51f320ed
|
|
7
|
+
data.tar.gz: 3662477381f1149d26394ba63fc2380c2104d1cb78a65bf3d943681a907b591ad0e7e9a6f990c868cf48087306cf6cb845c399fd976a346858ba083647b6bc3e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-05-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Consumer-presence detection for `consumed_by:` paths. `audit()` accepts an optional `root:` keyword (host-app root). When given, each entry whose `consumed_by:` is set is checked against the host app's tree, producing a new `consumer:` field on the audit row: `:present` (file exists and references the var via `ENV[...]` or `ENV.fetch(...)`), `:file_missing` (path missing on disk), or `:not_referenced` (file exists but no `ENV` reference). Catches renamed/deleted consumer files, `consumed_by:` typos, and vars declared in env-spec but never actually `ENV.fetch`'d.
|
|
15
|
+
- `DiagnosticsController#env` now passes `Rails.root` automatically, so host apps get the new `consumer:` field with no host-side change.
|
|
16
|
+
- Deprecation metadata on `required` / `recommended`: `deprecated: true`, `sunset_on:` (target removal date), `replacement:` (what to use instead). Surfaced verbatim in audit rows. Lets vars be staged for removal with audit trail.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `Entry` struct extended with `deprecated`, `sunset_on`, `replacement`. Backward-compatible — every 0.3.0 spec produces identical audit output when the new opts aren't used and `root:` isn't passed.
|
|
21
|
+
|
|
10
22
|
## [0.3.0] - 2026-04-29
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -19,8 +19,9 @@ module StandardHealth
|
|
|
19
19
|
def env
|
|
20
20
|
spec = StandardHealth.config.env_spec
|
|
21
21
|
mode = ENV["APP_ENVIRONMENT"].to_s
|
|
22
|
+
root = defined?(Rails) ? Rails.root : nil
|
|
22
23
|
|
|
23
|
-
audit = spec ? spec.audit(ENV.to_h, mode: mode) : []
|
|
24
|
+
audit = spec ? spec.audit(ENV.to_h, mode: mode, root: root) : []
|
|
24
25
|
|
|
25
26
|
render json: {
|
|
26
27
|
mode: mode,
|
|
@@ -31,12 +31,22 @@ module StandardHealth
|
|
|
31
31
|
# - `description:` (String, optional) — human-readable hint surfaced
|
|
32
32
|
# verbatim by `/diagnostics/env`.
|
|
33
33
|
# - `consumed_by:` (String or Array<String>, optional) — pointer(s) to
|
|
34
|
-
# where the value is read in the host app. Surfaced verbatim.
|
|
34
|
+
# where the value is read in the host app. Surfaced verbatim. When
|
|
35
|
+
# `audit` is called with `root:`, each path is checked for an
|
|
36
|
+
# `ENV[...]` / `ENV.fetch(...)` reference to the var; the result is
|
|
37
|
+
# reported as `consumer:` in the audit row
|
|
38
|
+
# (`:present`, `:file_missing`, or `:not_referenced`).
|
|
35
39
|
# - `if:` / `unless:` (Proc, optional) — predicates evaluated at audit
|
|
36
40
|
# time. When `unless:` returns truthy or `if:` returns falsy the entry
|
|
37
41
|
# is reported with `status: :not_applicable`.
|
|
38
42
|
# - `group` (String, optional) — set implicitly by enclosing `group`
|
|
39
43
|
# block; surfaced verbatim.
|
|
44
|
+
# - `deprecated:` (Boolean, optional) — flag for staged removal. When
|
|
45
|
+
# true, the audit row includes `deprecated: true`.
|
|
46
|
+
# - `sunset_on:` (String/Date, optional) — target removal date.
|
|
47
|
+
# Surfaced verbatim in audit rows.
|
|
48
|
+
# - `replacement:` (String, optional) — what to use instead. Surfaced
|
|
49
|
+
# in audit rows.
|
|
40
50
|
class EnvSpec
|
|
41
51
|
# Raised when `in:` references a Symbol that hasn't been declared via
|
|
42
52
|
# `mode_alias`.
|
|
@@ -53,6 +63,9 @@ module StandardHealth
|
|
|
53
63
|
:if_predicate,
|
|
54
64
|
:unless_predicate,
|
|
55
65
|
:group,
|
|
66
|
+
:deprecated,
|
|
67
|
+
:sunset_on,
|
|
68
|
+
:replacement,
|
|
56
69
|
keyword_init: true
|
|
57
70
|
)
|
|
58
71
|
|
|
@@ -119,12 +132,20 @@ module StandardHealth
|
|
|
119
132
|
#
|
|
120
133
|
# @param env_hash [Hash{String, Symbol => String}] e.g. ENV.to_h
|
|
121
134
|
# @param mode [String, Symbol] current APP_ENVIRONMENT value
|
|
135
|
+
# @param root [String, Pathname, nil] host app root for resolving
|
|
136
|
+
# `consumed_by` paths. When given, each row gains a `consumer:` field:
|
|
137
|
+
# `:present` (file exists and references the var),
|
|
138
|
+
# `:file_missing` (path does not exist),
|
|
139
|
+
# `:not_referenced` (file exists but no `ENV[...]` / `ENV.fetch(...)`
|
|
140
|
+
# match for the var). When the entry has no `consumed_by` or `root`
|
|
141
|
+
# is nil, the field is omitted.
|
|
122
142
|
# @return [Array<Hash>] one row per applicable entry. Each row has at
|
|
123
143
|
# least `name`, `level`, `status`, `mode`. When an entry is suppressed
|
|
124
144
|
# by an `if:`/`unless:` predicate, `status` is `:not_applicable` and a
|
|
125
|
-
# `reason` field explains why. `description`, `consumed_by`,
|
|
126
|
-
# `
|
|
127
|
-
|
|
145
|
+
# `reason` field explains why. `description`, `consumed_by`, `group`,
|
|
146
|
+
# `deprecated`, `sunset_on`, `replacement`, and `consumer` are
|
|
147
|
+
# included when set on the entry / computed during audit.
|
|
148
|
+
def audit(env_hash, mode:, root: nil)
|
|
128
149
|
mode_str = mode.to_s
|
|
129
150
|
env = stringify(env_hash)
|
|
130
151
|
|
|
@@ -142,6 +163,9 @@ module StandardHealth
|
|
|
142
163
|
row[:status] = classify(entry, value)
|
|
143
164
|
end
|
|
144
165
|
|
|
166
|
+
consumer_status = consumer_state(entry, root)
|
|
167
|
+
row[:consumer] = consumer_status if consumer_status
|
|
168
|
+
|
|
145
169
|
out << row
|
|
146
170
|
end
|
|
147
171
|
end
|
|
@@ -158,7 +182,10 @@ module StandardHealth
|
|
|
158
182
|
consumed_by: normalize_consumed_by(opts[:consumed_by]),
|
|
159
183
|
if_predicate: opts[:if],
|
|
160
184
|
unless_predicate: opts[:unless],
|
|
161
|
-
group: @group_stack.last
|
|
185
|
+
group: @group_stack.last,
|
|
186
|
+
deprecated: opts[:deprecated] ? true : nil,
|
|
187
|
+
sunset_on: opts[:sunset_on],
|
|
188
|
+
replacement: opts[:replacement]
|
|
162
189
|
)
|
|
163
190
|
end
|
|
164
191
|
|
|
@@ -211,9 +238,47 @@ module StandardHealth
|
|
|
211
238
|
row[:description] = entry.description if entry.description
|
|
212
239
|
row[:consumed_by] = serialize_consumed_by(entry.consumed_by) if entry.consumed_by
|
|
213
240
|
row[:group] = entry.group if entry.group
|
|
241
|
+
row[:deprecated] = true if entry.deprecated
|
|
242
|
+
row[:sunset_on] = entry.sunset_on.to_s if entry.sunset_on
|
|
243
|
+
row[:replacement] = entry.replacement if entry.replacement
|
|
214
244
|
row
|
|
215
245
|
end
|
|
216
246
|
|
|
247
|
+
# Resolve the consumer-presence state for an entry. Returns nil when
|
|
248
|
+
# we have nothing to check (no root, no consumed_by). Otherwise:
|
|
249
|
+
#
|
|
250
|
+
# :present — at least one consumed_by path exists AND mentions
|
|
251
|
+
# ENV["VAR"] or ENV.fetch("VAR", ...).
|
|
252
|
+
# :file_missing — every consumed_by path is missing on disk.
|
|
253
|
+
# :not_referenced — at least one path exists, but none reference the
|
|
254
|
+
# env var.
|
|
255
|
+
def consumer_state(entry, root)
|
|
256
|
+
return nil if root.nil? || entry.consumed_by.nil?
|
|
257
|
+
|
|
258
|
+
pattern = env_reference_pattern(entry.name)
|
|
259
|
+
any_file_present = false
|
|
260
|
+
any_referenced = false
|
|
261
|
+
|
|
262
|
+
entry.consumed_by.each do |relative|
|
|
263
|
+
path = File.join(root.to_s, relative)
|
|
264
|
+
next unless File.file?(path)
|
|
265
|
+
|
|
266
|
+
any_file_present = true
|
|
267
|
+
any_referenced = true if File.read(path).match?(pattern)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
return :file_missing unless any_file_present
|
|
271
|
+
any_referenced ? :present : :not_referenced
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Match ENV["VAR"], ENV['VAR'], ENV[:VAR], ENV.fetch("VAR", ...),
|
|
275
|
+
# ENV.fetch('VAR', ...), ENV.fetch(:VAR, ...). Word-boundary on the
|
|
276
|
+
# right so VAR_PREFIX doesn't accidentally match VAR.
|
|
277
|
+
def env_reference_pattern(name)
|
|
278
|
+
escaped = Regexp.escape(name.to_s)
|
|
279
|
+
/ENV(?:\.fetch)?\s*[\[(]\s*[:'"]?#{escaped}\b/
|
|
280
|
+
end
|
|
281
|
+
|
|
217
282
|
def serialize_consumed_by(value)
|
|
218
283
|
value.length == 1 ? value.first : value
|
|
219
284
|
end
|