envshield 4.6.2__tar.gz → 4.7.1__tar.gz
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.
- {envshield-4.6.2 → envshield-4.7.1}/PKG-INFO +80 -5
- {envshield-4.6.2 → envshield-4.7.1}/README.md +79 -4
- envshield-4.7.1/envshield/__init__.py +1 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/cli.py +324 -56
- {envshield-4.6.2 → envshield-4.7.1}/envshield/config/manager.py +100 -24
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/dependency_snapshot.py +33 -6
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/discovery.py +245 -31
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/doctor.py +169 -18
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/explain.py +106 -26
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/hooks_manager.py +13 -2
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/importer.py +16 -1
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/scanner.py +95 -30
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/schema_manager.py +371 -5
- envshield-4.7.1/envshield/parsers/_deployment.py +126 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/parsers/_docker_compose.py +23 -14
- envshield-4.7.1/envshield/parsers/_kubernetes.py +222 -0
- envshield-4.7.1/envshield/utils/paths.py +49 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield.egg-info/PKG-INFO +80 -5
- envshield-4.7.1/envshield.egg-info/SOURCES.txt +46 -0
- {envshield-4.6.2 → envshield-4.7.1}/pyproject.toml +2 -1
- envshield-4.6.2/envshield/__init__.py +0 -1
- envshield-4.6.2/envshield/config/__init.py +0 -0
- envshield-4.6.2/envshield/parsers/_deployment.py +0 -63
- envshield-4.6.2/envshield/parsers/_kubernetes.py +0 -148
- envshield-4.6.2/envshield/tests/conftest.py +0 -33
- envshield-4.6.2/envshield/tests/core/test_contract_diff.py +0 -433
- envshield-4.6.2/envshield/tests/core/test_dependency_diff.py +0 -167
- envshield-4.6.2/envshield/tests/core/test_dependency_snapshot.py +0 -528
- envshield-4.6.2/envshield/tests/core/test_discovery.py +0 -574
- envshield-4.6.2/envshield/tests/core/test_doctor.py +0 -736
- envshield-4.6.2/envshield/tests/core/test_explain.py +0 -397
- envshield-4.6.2/envshield/tests/core/test_file_updater.py +0 -223
- envshield-4.6.2/envshield/tests/core/test_generator.py +0 -815
- envshield-4.6.2/envshield/tests/core/test_hook_injection.py +0 -282
- envshield-4.6.2/envshield/tests/core/test_hooks_manager.py +0 -37
- envshield-4.6.2/envshield/tests/core/test_importer.py +0 -808
- envshield-4.6.2/envshield/tests/core/test_post_merge_hook.py +0 -139
- envshield-4.6.2/envshield/tests/core/test_pre_commit_hook.py +0 -332
- envshield-4.6.2/envshield/tests/core/test_scanner_compliance.py +0 -1419
- envshield-4.6.2/envshield/tests/core/test_schema_manager.py +0 -1308
- envshield-4.6.2/envshield/tests/core/test_schema_snapshot.py +0 -424
- envshield-4.6.2/envshield/tests/core/test_schema_types.py +0 -347
- envshield-4.6.2/envshield/tests/core/test_service_discovery.py +0 -406
- envshield-4.6.2/envshield/tests/core/test_service_manager.py +0 -326
- envshield-4.6.2/envshield/tests/core/test_setup_manager.py +0 -1084
- envshield-4.6.2/envshield/tests/parsers/__init__.py +0 -0
- envshield-4.6.2/envshield/tests/parsers/test_docker_compose_parser.py +0 -436
- envshield-4.6.2/envshield/tests/parsers/test_dotenv_parser.py +0 -220
- envshield-4.6.2/envshield/tests/parsers/test_kubernetes_parser.py +0 -354
- envshield-4.6.2/envshield/tests/parsers/test_python_parser.py +0 -113
- envshield-4.6.2/envshield/tests/test_cli.py +0 -1569
- envshield-4.6.2/envshield/tests/test_config_manager.py +0 -951
- envshield-4.6.2/envshield/tests/test_diff_aware_scanning.py +0 -201
- envshield-4.6.2/envshield/tests/test_explain_cli.py +0 -306
- envshield-4.6.2/envshield/tests/test_git_utils.py +0 -463
- envshield-4.6.2/envshield/tests/test_hook_cli.py +0 -187
- envshield-4.6.2/envshield/tests/test_schema_diff_cli.py +0 -658
- envshield-4.6.2/envshield/tests/test_service_cli.py +0 -549
- envshield-4.6.2/envshield/tests/test_undeclared_cli.py +0 -482
- envshield-4.6.2/envshield/utils/__init__.py +0 -0
- envshield-4.6.2/envshield.egg-info/SOURCES.txt +0 -82
- {envshield-4.6.2 → envshield-4.7.1}/LICENSE.md +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/__main__.py +0 -0
- {envshield-4.6.2/envshield/core → envshield-4.7.1/envshield/config}/__init__.py +0 -0
- {envshield-4.6.2/envshield/parsers → envshield-4.7.1/envshield/core}/__init__.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/contract_diff.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/dependency_diff.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/exceptions.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/file_updater.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/generator.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/inspector.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/schema_snapshot.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/schema_types.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/service_discovery.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/service_manager.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/core/setup_manager.py +0 -0
- {envshield-4.6.2/envshield/tests → envshield-4.7.1/envshield/parsers}/__init__.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/parsers/_base.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/parsers/_dotenv.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/parsers/_python.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/parsers/factory.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/state.py +0 -0
- {envshield-4.6.2/envshield/tests/core → envshield-4.7.1/envshield/utils}/__init__.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield/utils/git_utils.py +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield.egg-info/dependency_links.txt +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield.egg-info/entry_points.txt +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield.egg-info/requires.txt +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/envshield.egg-info/top_level.txt +0 -0
- {envshield-4.6.2 → envshield-4.7.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: envshield
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.7.1
|
|
4
4
|
Summary: EnvShield: environment variables as a version-controlled configuration contract.
|
|
5
5
|
Author-email: Rabbil Yasar Sajal <rabbilyasar@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -100,9 +100,11 @@ env.schema.toml
|
|
|
100
100
|
## Quick Start
|
|
101
101
|
|
|
102
102
|
```bash
|
|
103
|
-
|
|
103
|
+
pipx install envshield
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
Prefer an existing Python/pip workflow instead? `pip install envshield` works the same way, as an alternative.
|
|
107
|
+
|
|
106
108
|
In an existing project:
|
|
107
109
|
|
|
108
110
|
```bash
|
|
@@ -299,7 +301,7 @@ jobs:
|
|
|
299
301
|
steps:
|
|
300
302
|
- uses: actions/checkout@v4
|
|
301
303
|
with: { fetch-depth: 0 }
|
|
302
|
-
- run:
|
|
304
|
+
- run: pipx install envshield
|
|
303
305
|
- run: |
|
|
304
306
|
BASE=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
|
|
305
307
|
envshield schema diff "$BASE" HEAD
|
|
@@ -353,12 +355,67 @@ envshield check --service api
|
|
|
353
355
|
|
|
354
356
|
Every command is service-aware. `--service` is optional with exactly one service configured, inferred automatically when you're standing inside a registered service's own directory, and only prompted for interactively when there's more than one and neither applies.
|
|
355
357
|
|
|
358
|
+
### Discovering reads in a shared library (`additional_source_roots`)
|
|
359
|
+
|
|
360
|
+
`undeclared` and `explain` only walk a service's own directory by default. If part of a service's real configuration is read from a shared internal library that lives outside that directory — a common-utilities package imported by several services, say — those reads are invisible to both commands, for every service that imports the library.
|
|
361
|
+
|
|
362
|
+
`additional_source_roots` widens a service's discovery scope to cover extra directories:
|
|
363
|
+
|
|
364
|
+
```yaml
|
|
365
|
+
services:
|
|
366
|
+
api:
|
|
367
|
+
schema: services/api/env.schema.toml
|
|
368
|
+
additional_source_roots:
|
|
369
|
+
- libs/shared
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`undeclared --service api` and `explain --service api` now also look inside `libs/shared` — a variable read there counts toward `api`'s own discovery results exactly as if it lived in `services/api/` itself. The same directory can be listed by more than one service; each one independently sees the reads it contains. A path that doesn't exist yet contributes nothing (no error); a path resolving outside the project is refused, the same protection every other path in `envshield.yml` already has. `scan` is unaffected — it's already repository-wide.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Multiple sources per service (`completeness: union`)
|
|
377
|
+
|
|
378
|
+
By default, every registered source for a service — its `local_file`, each registered deployment manifest — is validated independently against the *whole* schema: `check`/`doctor` expect any one of them to be self-sufficient on its own. Some services genuinely don't work that way: part of the configuration lives in a local config file, the rest is supplied by a deployment manifest. Under the default model, a variable that only ever lives in one of them shows up as "missing" everywhere else, even though nothing is actually wrong.
|
|
379
|
+
|
|
380
|
+
Set `completeness: union` on a service to validate against the *combined* presence of all its registered sources instead — a variable satisfied by any one of them satisfies the service as a whole:
|
|
381
|
+
|
|
382
|
+
```yaml
|
|
383
|
+
services:
|
|
384
|
+
api:
|
|
385
|
+
schema: services/api/env.schema.toml
|
|
386
|
+
local_file: services/api/config/settings.py
|
|
387
|
+
completeness: union
|
|
388
|
+
manifests:
|
|
389
|
+
- file: docker-compose.yml
|
|
390
|
+
containers:
|
|
391
|
+
api: api
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
```toml
|
|
395
|
+
[DB_HOST]
|
|
396
|
+
description = "set in config/settings.py"
|
|
397
|
+
|
|
398
|
+
[FEATURE_MODE]
|
|
399
|
+
description = "set via docker-compose.yml"
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
With `completeness: union`, `check`/`doctor` stop expecting `config/settings.py` to declare `FEATURE_MODE`, or `docker-compose.yml` to set `DB_HOST` — the schema is satisfied as long as every variable is present *somewhere* among the registered sources. `envshield schema sync` respects this too: it only ever appends a variable to a Python `local_file` that's genuinely missing from every registered source, never one that another source already covers.
|
|
403
|
+
|
|
404
|
+
A few things worth knowing:
|
|
405
|
+
|
|
406
|
+
- **Opt-in, per service.** Unset (the default) means every source must be independently self-sufficient, exactly as before this existed — nothing changes unless you set the key.
|
|
407
|
+
- **Union means presence, not values.** Whether an ordinary variable is satisfied depends only on whether it's present (and valid) somewhere — never on merging, comparing, or picking between its values across sources. A blank or invalid value is still reported, wherever it occurs. **The one deliberate exception** is a `requiredIf` trigger's own value (see next) — deciding whether a condition currently holds has always meant reading one specific value, even before `completeness: union` existed; union only extends *where* that one value may come from, it doesn't turn this into general value merging.
|
|
408
|
+
- **A `requiredIf` condition (see [Conditional requirements](#conditional-requirements-requiredif) above) is resolved against the union too** — if the variable a condition depends on lives in a different source than the one being checked, `completeness: union` still finds it. Sources that agree on the trigger's value resolve normally; if two sources genuinely disagree, EnvShield reports it as an explicit conflict rather than guessing which one wins — registration order is never used as a tiebreaker.
|
|
409
|
+
- **A source that fails to load is always a failure**, even if the sources that did load happen to satisfy the schema between them. `doctor` reports this as two separate signals for a union-mode service — source health (does this file exist and parse) and registered-source completeness (does the union satisfy the schema) — so one can never quietly hide behind the other. `check --json` reflects the same split: each source's own result is still reported individually, plus one `combined` entry per union-mode service carrying the aggregate verdict.
|
|
410
|
+
|
|
356
411
|
---
|
|
357
412
|
|
|
358
413
|
## Supported languages / discovery
|
|
359
414
|
|
|
360
415
|
Source-code discovery (what powers `undeclared` and `explain`'s "used in source" section) is AST-based for **Python** (`os.environ.get`, `os.getenv`, `os.environ[...]`) and pattern-based for **JavaScript/TypeScript** (`process.env.X`, `process.env["X"]`, `import.meta.env.X`, including single-level destructuring). No other language is discovered yet — the schema and validation commands work with any stack, but `undeclared`/`explain` can only see what's read from these two.
|
|
361
416
|
|
|
417
|
+
`explain` additionally recognizes Flask's `current_app.config[...]`/`.get(...)` in Python — including the common `from flask import current_app as <alias>` import form — and reports it at **medium confidence**, shown inline as `(medium confidence)`: it's one level of indirection through a config object whose contents could come from anywhere, not a direct environment read. This recognition is deliberately `explain`-only — it never affects `undeclared`'s or `scan`'s pass/fail signal, so a Flask-config read can never quietly satisfy (or break) a completeness check the way a direct `os.environ`/`os.getenv` read does.
|
|
418
|
+
|
|
362
419
|
---
|
|
363
420
|
|
|
364
421
|
## Secret safety
|
|
@@ -370,12 +427,30 @@ A supporting check alongside the contract, not the product itself. `secret = tru
|
|
|
370
427
|
|
|
371
428
|
---
|
|
372
429
|
|
|
430
|
+
## Removing EnvShield
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
envshield uninstall
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Removes EnvShield's Git hook integration while preserving your project configuration — it does not delete `envshield.yml`, any `env.schema.toml`, `.env.example`, any registered local configuration file (`.env`, a Python config module, etc.), `.gitignore`, or any code `envshield generate` has produced, regardless of whether EnvShield originally created any of it. Those are your project's own configuration contract and application config, not disposable installation artifacts.
|
|
437
|
+
|
|
438
|
+
The only thing `uninstall` ever deletes is a Git hook whose content exactly matches what EnvShield would generate right now — the same ownership rule `hook remove` already uses. A hand-modified hook, a foreign hook (even one that happens to contain EnvShield's marker comment), or an EnvShield hook that's gone stale relative to your current `envshield.yml` (e.g. after registering a new service) is left in place and reported, never deleted or silently regenerated.
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
envshield uninstall --yes # skip the confirmation prompt
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
`--yes` only skips the prompt — it never overrides the ownership check; a modified, foreign, or stale hook is preserved with `--yes` exactly as it is interactively. There is no `--force` and no `--purge`: `uninstall` is intentionally not a "delete everything EnvShield ever created" command. To remove EnvShield's configuration from a project entirely, delete `envshield.yml` and the schema files yourself once you're sure you no longer need them.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
373
448
|
## Known limitations
|
|
374
449
|
|
|
375
450
|
EnvShield is built around the patterns real projects actually use. A few narrower cases aren't handled yet — none of them let a secret leak or let a genuinely missing required variable pass as clean:
|
|
376
451
|
|
|
377
|
-
- **Docker Compose:** `${VAR}
|
|
378
|
-
- **Kubernetes:** `secretKeyRef`/`configMapKeyRef` are matched by the container's environment-variable name (`env[].name`), not the referenced Secret/ConfigMap
|
|
452
|
+
- **Docker Compose:** `${VAR}`, `${VAR:-default}`, and the no-colon `${VAR-default}` form are all supported. The `${VAR:?}`/`${VAR:+}` forms and embedded/multiple interpolation references in one value aren't evaluated yet.
|
|
453
|
+
- **Kubernetes:** `env[].valueFrom.secretKeyRef`/`configMapKeyRef` are matched by the container's environment-variable name (`env[].name`) — name your schema variable after the env var, not the key — and, when the referenced Secret/ConfigMap is defined in the same manifest, the referenced key is validated to actually exist there. `envFrom.prefix` is applied. `initContainers` are selectable via `--container` but never auto-selected by default.
|
|
379
454
|
- **Source discovery:** Python is AST-based; JavaScript/TypeScript is pattern-based. No other language is discovered yet.
|
|
380
455
|
- **.env:** multiline quoted values aren't supported.
|
|
381
456
|
|
|
@@ -41,9 +41,11 @@ env.schema.toml
|
|
|
41
41
|
## Quick Start
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
|
|
44
|
+
pipx install envshield
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Prefer an existing Python/pip workflow instead? `pip install envshield` works the same way, as an alternative.
|
|
48
|
+
|
|
47
49
|
In an existing project:
|
|
48
50
|
|
|
49
51
|
```bash
|
|
@@ -240,7 +242,7 @@ jobs:
|
|
|
240
242
|
steps:
|
|
241
243
|
- uses: actions/checkout@v4
|
|
242
244
|
with: { fetch-depth: 0 }
|
|
243
|
-
- run:
|
|
245
|
+
- run: pipx install envshield
|
|
244
246
|
- run: |
|
|
245
247
|
BASE=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
|
|
246
248
|
envshield schema diff "$BASE" HEAD
|
|
@@ -294,12 +296,67 @@ envshield check --service api
|
|
|
294
296
|
|
|
295
297
|
Every command is service-aware. `--service` is optional with exactly one service configured, inferred automatically when you're standing inside a registered service's own directory, and only prompted for interactively when there's more than one and neither applies.
|
|
296
298
|
|
|
299
|
+
### Discovering reads in a shared library (`additional_source_roots`)
|
|
300
|
+
|
|
301
|
+
`undeclared` and `explain` only walk a service's own directory by default. If part of a service's real configuration is read from a shared internal library that lives outside that directory — a common-utilities package imported by several services, say — those reads are invisible to both commands, for every service that imports the library.
|
|
302
|
+
|
|
303
|
+
`additional_source_roots` widens a service's discovery scope to cover extra directories:
|
|
304
|
+
|
|
305
|
+
```yaml
|
|
306
|
+
services:
|
|
307
|
+
api:
|
|
308
|
+
schema: services/api/env.schema.toml
|
|
309
|
+
additional_source_roots:
|
|
310
|
+
- libs/shared
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
`undeclared --service api` and `explain --service api` now also look inside `libs/shared` — a variable read there counts toward `api`'s own discovery results exactly as if it lived in `services/api/` itself. The same directory can be listed by more than one service; each one independently sees the reads it contains. A path that doesn't exist yet contributes nothing (no error); a path resolving outside the project is refused, the same protection every other path in `envshield.yml` already has. `scan` is unaffected — it's already repository-wide.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Multiple sources per service (`completeness: union`)
|
|
318
|
+
|
|
319
|
+
By default, every registered source for a service — its `local_file`, each registered deployment manifest — is validated independently against the *whole* schema: `check`/`doctor` expect any one of them to be self-sufficient on its own. Some services genuinely don't work that way: part of the configuration lives in a local config file, the rest is supplied by a deployment manifest. Under the default model, a variable that only ever lives in one of them shows up as "missing" everywhere else, even though nothing is actually wrong.
|
|
320
|
+
|
|
321
|
+
Set `completeness: union` on a service to validate against the *combined* presence of all its registered sources instead — a variable satisfied by any one of them satisfies the service as a whole:
|
|
322
|
+
|
|
323
|
+
```yaml
|
|
324
|
+
services:
|
|
325
|
+
api:
|
|
326
|
+
schema: services/api/env.schema.toml
|
|
327
|
+
local_file: services/api/config/settings.py
|
|
328
|
+
completeness: union
|
|
329
|
+
manifests:
|
|
330
|
+
- file: docker-compose.yml
|
|
331
|
+
containers:
|
|
332
|
+
api: api
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
```toml
|
|
336
|
+
[DB_HOST]
|
|
337
|
+
description = "set in config/settings.py"
|
|
338
|
+
|
|
339
|
+
[FEATURE_MODE]
|
|
340
|
+
description = "set via docker-compose.yml"
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
With `completeness: union`, `check`/`doctor` stop expecting `config/settings.py` to declare `FEATURE_MODE`, or `docker-compose.yml` to set `DB_HOST` — the schema is satisfied as long as every variable is present *somewhere* among the registered sources. `envshield schema sync` respects this too: it only ever appends a variable to a Python `local_file` that's genuinely missing from every registered source, never one that another source already covers.
|
|
344
|
+
|
|
345
|
+
A few things worth knowing:
|
|
346
|
+
|
|
347
|
+
- **Opt-in, per service.** Unset (the default) means every source must be independently self-sufficient, exactly as before this existed — nothing changes unless you set the key.
|
|
348
|
+
- **Union means presence, not values.** Whether an ordinary variable is satisfied depends only on whether it's present (and valid) somewhere — never on merging, comparing, or picking between its values across sources. A blank or invalid value is still reported, wherever it occurs. **The one deliberate exception** is a `requiredIf` trigger's own value (see next) — deciding whether a condition currently holds has always meant reading one specific value, even before `completeness: union` existed; union only extends *where* that one value may come from, it doesn't turn this into general value merging.
|
|
349
|
+
- **A `requiredIf` condition (see [Conditional requirements](#conditional-requirements-requiredif) above) is resolved against the union too** — if the variable a condition depends on lives in a different source than the one being checked, `completeness: union` still finds it. Sources that agree on the trigger's value resolve normally; if two sources genuinely disagree, EnvShield reports it as an explicit conflict rather than guessing which one wins — registration order is never used as a tiebreaker.
|
|
350
|
+
- **A source that fails to load is always a failure**, even if the sources that did load happen to satisfy the schema between them. `doctor` reports this as two separate signals for a union-mode service — source health (does this file exist and parse) and registered-source completeness (does the union satisfy the schema) — so one can never quietly hide behind the other. `check --json` reflects the same split: each source's own result is still reported individually, plus one `combined` entry per union-mode service carrying the aggregate verdict.
|
|
351
|
+
|
|
297
352
|
---
|
|
298
353
|
|
|
299
354
|
## Supported languages / discovery
|
|
300
355
|
|
|
301
356
|
Source-code discovery (what powers `undeclared` and `explain`'s "used in source" section) is AST-based for **Python** (`os.environ.get`, `os.getenv`, `os.environ[...]`) and pattern-based for **JavaScript/TypeScript** (`process.env.X`, `process.env["X"]`, `import.meta.env.X`, including single-level destructuring). No other language is discovered yet — the schema and validation commands work with any stack, but `undeclared`/`explain` can only see what's read from these two.
|
|
302
357
|
|
|
358
|
+
`explain` additionally recognizes Flask's `current_app.config[...]`/`.get(...)` in Python — including the common `from flask import current_app as <alias>` import form — and reports it at **medium confidence**, shown inline as `(medium confidence)`: it's one level of indirection through a config object whose contents could come from anywhere, not a direct environment read. This recognition is deliberately `explain`-only — it never affects `undeclared`'s or `scan`'s pass/fail signal, so a Flask-config read can never quietly satisfy (or break) a completeness check the way a direct `os.environ`/`os.getenv` read does.
|
|
359
|
+
|
|
303
360
|
---
|
|
304
361
|
|
|
305
362
|
## Secret safety
|
|
@@ -311,12 +368,30 @@ A supporting check alongside the contract, not the product itself. `secret = tru
|
|
|
311
368
|
|
|
312
369
|
---
|
|
313
370
|
|
|
371
|
+
## Removing EnvShield
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
envshield uninstall
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Removes EnvShield's Git hook integration while preserving your project configuration — it does not delete `envshield.yml`, any `env.schema.toml`, `.env.example`, any registered local configuration file (`.env`, a Python config module, etc.), `.gitignore`, or any code `envshield generate` has produced, regardless of whether EnvShield originally created any of it. Those are your project's own configuration contract and application config, not disposable installation artifacts.
|
|
378
|
+
|
|
379
|
+
The only thing `uninstall` ever deletes is a Git hook whose content exactly matches what EnvShield would generate right now — the same ownership rule `hook remove` already uses. A hand-modified hook, a foreign hook (even one that happens to contain EnvShield's marker comment), or an EnvShield hook that's gone stale relative to your current `envshield.yml` (e.g. after registering a new service) is left in place and reported, never deleted or silently regenerated.
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
envshield uninstall --yes # skip the confirmation prompt
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
`--yes` only skips the prompt — it never overrides the ownership check; a modified, foreign, or stale hook is preserved with `--yes` exactly as it is interactively. There is no `--force` and no `--purge`: `uninstall` is intentionally not a "delete everything EnvShield ever created" command. To remove EnvShield's configuration from a project entirely, delete `envshield.yml` and the schema files yourself once you're sure you no longer need them.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
314
389
|
## Known limitations
|
|
315
390
|
|
|
316
391
|
EnvShield is built around the patterns real projects actually use. A few narrower cases aren't handled yet — none of them let a secret leak or let a genuinely missing required variable pass as clean:
|
|
317
392
|
|
|
318
|
-
- **Docker Compose:** `${VAR}
|
|
319
|
-
- **Kubernetes:** `secretKeyRef`/`configMapKeyRef` are matched by the container's environment-variable name (`env[].name`), not the referenced Secret/ConfigMap
|
|
393
|
+
- **Docker Compose:** `${VAR}`, `${VAR:-default}`, and the no-colon `${VAR-default}` form are all supported. The `${VAR:?}`/`${VAR:+}` forms and embedded/multiple interpolation references in one value aren't evaluated yet.
|
|
394
|
+
- **Kubernetes:** `env[].valueFrom.secretKeyRef`/`configMapKeyRef` are matched by the container's environment-variable name (`env[].name`) — name your schema variable after the env var, not the key — and, when the referenced Secret/ConfigMap is defined in the same manifest, the referenced key is validated to actually exist there. `envFrom.prefix` is applied. `initContainers` are selectable via `--container` but never auto-selected by default.
|
|
320
395
|
- **Source discovery:** Python is AST-based; JavaScript/TypeScript is pattern-based. No other language is discovered yet.
|
|
321
396
|
- **.env:** multiline quoted values aren't supported.
|
|
322
397
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.7.1"
|