envshield 4.2.0__tar.gz → 4.3.0__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.2.0 → envshield-4.3.0}/PKG-INFO +40 -15
- {envshield-4.2.0 → envshield-4.3.0}/README.md +39 -14
- envshield-4.3.0/envshield/__init__.py +1 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/cli.py +49 -14
- {envshield-4.2.0 → envshield-4.3.0}/envshield/config/manager.py +5 -3
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/exceptions.py +8 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/scanner.py +1 -1
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/service_discovery.py +65 -6
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/core/test_service_discovery.py +52 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/test_cli.py +83 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/test_config_manager.py +21 -1
- envshield-4.2.0/envshield/tests/test_c6_diff_aware_scanning.py → envshield-4.3.0/envshield/tests/test_diff_aware_scanning.py +1 -1
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/test_service_cli.py +19 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield.egg-info/PKG-INFO +40 -15
- {envshield-4.2.0 → envshield-4.3.0}/envshield.egg-info/SOURCES.txt +1 -1
- {envshield-4.2.0 → envshield-4.3.0}/pyproject.toml +1 -1
- envshield-4.2.0/envshield/__init__.py +0 -1
- {envshield-4.2.0 → envshield-4.3.0}/LICENSE.md +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/__main__.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/config/__init.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/doctor.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/file_updater.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/generator.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/hooks_manager.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/importer.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/inspector.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/schema_manager.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/schema_types.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/service_manager.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/core/setup_manager.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/parsers/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/parsers/_base.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/parsers/_deployment.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/parsers/_docker_compose.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/parsers/_dotenv.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/parsers/_kubernetes.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/parsers/_python.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/parsers/factory.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/state.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/conftest.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/core/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/core/test_doctor.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/core/test_file_updater.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/core/test_generator.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/core/test_importer.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/core/test_scanner_compliance.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/core/test_schema_manager.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/core/test_schema_types.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/core/test_service_manager.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/core/test_setup_manager.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/parsers/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/parsers/test_docker_compose_parser.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/parsers/test_dotenv_parser.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/parsers/test_kubernetes_parser.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/parsers/test_python_parser.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/tests/test_git_utils.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/utils/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield/utils/git_utils.py +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield.egg-info/dependency_links.txt +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield.egg-info/entry_points.txt +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield.egg-info/requires.txt +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/envshield.egg-info/top_level.txt +0 -0
- {envshield-4.2.0 → envshield-4.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: envshield
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.0
|
|
4
4
|
Summary: EnvShield: A CLI for secure local environment management and secret prevention.
|
|
5
5
|
Author-email: Rabbil Yasar Sajal <rabbilyasar@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -84,12 +84,14 @@ It works the same way whether you have one repo with one `.env` file, or a monor
|
|
|
84
84
|
- [Installation](#installation)
|
|
85
85
|
- [Quick start](#quick-start)
|
|
86
86
|
- [A single service](#a-single-service)
|
|
87
|
-
- [A monorepo with multiple services](#a-monorepo-with-multiple-services)
|
|
87
|
+
- [A monorepo with multiple services (monorepo)](#a-monorepo-with-multiple-services)
|
|
88
88
|
- [Core concept: the schema is the contract](#core-concept-the-schema-is-the-contract)
|
|
89
89
|
- [Every field a variable can have](#every-field-a-variable-can-have)
|
|
90
90
|
- [Conditional requirements (`requiredIf`)](#conditional-requirements-requiredif)
|
|
91
|
-
- [Sharing variables across services (`extends`)](#sharing-variables-across-services-extends)
|
|
91
|
+
- [Sharing variables across services (monorepo, `extends`)](#sharing-variables-across-services-extends)
|
|
92
92
|
- [Command reference](#command-reference)
|
|
93
|
+
- [Core commands](#core-commands)
|
|
94
|
+
- [Monorepo: managing multiple services (monorepo)](#monorepo-managing-multiple-services)
|
|
93
95
|
- [Typed config code generation](#typed-config-code-generation)
|
|
94
96
|
- [Validating deployment manifests](#validating-deployment-manifests)
|
|
95
97
|
- [Secret scanning and git hooks](#secret-scanning-and-git-hooks)
|
|
@@ -138,22 +140,28 @@ envshield --version
|
|
|
138
140
|
|
|
139
141
|
### A single service
|
|
140
142
|
|
|
141
|
-
The common case: one repo, one `.env` file.
|
|
143
|
+
The common case: one repo, one `.env` file. This is the whole workflow — nothing else in this README is required to get full value out of EnvShield.
|
|
142
144
|
|
|
143
145
|
```bash
|
|
144
146
|
cd my-project
|
|
145
|
-
envshield init # Detects your framework
|
|
146
|
-
envshield import .env # Or: seed the schema from an existing .env file
|
|
147
|
+
envshield init # Detects your framework and builds env.schema.toml from your real config
|
|
147
148
|
envshield setup # Interactive wizard: fills in .env from the schema
|
|
148
149
|
envshield generate --lang python # Generates a typed config.py (or config.ts for TypeScript)
|
|
149
150
|
```
|
|
150
151
|
|
|
152
|
+
`init` looks for a real config source first — an existing `.env`, `.env.example`, or a recognizable Python config module (`config/settings.py` and similar) — and builds the schema from its actual variables, classifying each as secret or not, with a suggested default and, where the value's shape is unambiguous, an inferred type. Only a genuinely fresh project with nothing to read yet falls back to a generic framework template:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
Found config/settings.py -- building your schema from its real variables.
|
|
156
|
+
✓ Created/updated schema: env.schema.toml
|
|
157
|
+
```
|
|
158
|
+
|
|
151
159
|
`init` also offers to install a git pre-commit hook (secret scanning) and a post-merge hook (drift check after every `git pull`) — say yes unless you already have your own hook-management tooling (Husky, `pre-commit`, etc.), in which case EnvShield will detect it and won't clobber it (see [Secret scanning and git hooks](#secret-scanning-and-git-hooks)).
|
|
152
160
|
|
|
153
|
-
|
|
161
|
+
`envshield import <file>` does the same real-variable analysis `init` runs automatically, as its own command — reach for it later, when you want to re-import after adding new variables to your code, point at a file `init` wouldn't have found on its own, or add `--interactive` to confirm each classification by hand instead of accepting the automatic guess:
|
|
154
162
|
|
|
155
163
|
```bash
|
|
156
|
-
envshield import .env
|
|
164
|
+
envshield import .env --interactive
|
|
157
165
|
```
|
|
158
166
|
|
|
159
167
|
```
|
|
@@ -166,8 +174,14 @@ Analyzing variables...
|
|
|
166
174
|
- Inferred a type (int/port/bool/url/email) for 3 variable(s).
|
|
167
175
|
```
|
|
168
176
|
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
**Everything above is the complete single-service story.** Everything below this point — multiple services, schema composition, deployment manifests with more than one container — is opt-in, and only relevant once your repo actually has more than one service. Skip straight to [Core concept: the schema is the contract](#core-concept-the-schema-is-the-contract) if that's not you yet.
|
|
180
|
+
|
|
169
181
|
### A monorepo with multiple services
|
|
170
182
|
|
|
183
|
+
*(Optional — skip this if you only have one service.)*
|
|
184
|
+
|
|
171
185
|
If your repo has more than one service (an API, a web frontend, a worker), run `service discover` at the repo root instead of `init`:
|
|
172
186
|
|
|
173
187
|
```bash
|
|
@@ -300,6 +314,8 @@ With `FEATURE_X_ENABLED=false`, `FEATURE_X_API_KEY` is optional — `setup` won'
|
|
|
300
314
|
|
|
301
315
|
### Sharing variables across services (`extends`)
|
|
302
316
|
|
|
317
|
+
*(Monorepo-only — skip this if you have one service.)*
|
|
318
|
+
|
|
303
319
|
A monorepo with ten services usually has five or six variables every single one of them needs — `LOG_LEVEL`, `SENTRY_DSN`, `DATADOG_API_KEY` — and copy-pasting the same `[LOG_LEVEL]` block into ten schema files is exactly the kind of drift EnvShield exists to prevent.
|
|
304
320
|
|
|
305
321
|
Factor them into a shared base schema, and have each service extend it:
|
|
@@ -336,22 +352,21 @@ Loading `services/api/env.schema.toml` now transparently gives you `LOG_LEVEL`,
|
|
|
336
352
|
|
|
337
353
|
## Command reference
|
|
338
354
|
|
|
339
|
-
|
|
355
|
+
### Core commands
|
|
356
|
+
|
|
357
|
+
Everything a single-service project ever needs. `--service` shows up on most of these for the multi-service case (see below), but it's entirely optional until you actually have more than one service.
|
|
340
358
|
|
|
341
359
|
| Command | What it does |
|
|
342
360
|
|---|---|
|
|
343
|
-
| `envshield init [--force/-f]` | Detects your framework
|
|
344
|
-
| `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` |
|
|
361
|
+
| `envshield init [--force/-f]` | Detects your framework and builds `env.schema.toml` from a real config source if it finds one, otherwise a framework-aware template. Also scaffolds `envshield.yml`, updates `.gitignore`, and offers to install git hooks. Auto-registers a root-level `docker-compose.yml` as the project's deployment manifest if it finds one. `--force` re-runs on a project that already has a config (with a confirmation before overwriting). |
|
|
362
|
+
| `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` | Runs the same real-variable analysis `init` does automatically, as its own command — for re-importing after your code gains new variables, pointing at a file `init` wouldn't have found, or adding `--interactive` to confirm each secret/type classification by hand instead of accepting the automatic guess. `--output` changes where the schema is written (defaults to `env.schema.toml`, or the target service's schema path with `--service`). |
|
|
345
363
|
| `envshield check [file] [--service NAME] [--container NAME]` | Validates a local file (or, if omitted, the project's/service's default local file *and* its registered deployment manifest, if any) against the schema. `file` can be a plain `.env`, a Python config module, a docker-compose file, or a Kubernetes manifest. `--container` picks which service/container to check in a manifest that declares more than one (tried against `--service`'s name automatically first). Exits non-zero on any drift — safe to use as a CI gate. |
|
|
346
364
|
| `envshield doctor [--fix] [--service NAME]` | Runs every health check at once (see below) and reports a summary. `--fix` interactively offers to fix whatever it can — re-running `init`, regenerating the template, installing the git hook, or running `setup` to fill in missing/invalid local values. Exits non-zero if anything's still broken afterward. |
|
|
347
365
|
| `envshield setup [output_file] [--service NAME]` | Interactive onboarding wizard: walks through every variable that's missing, blank, or has an existing value the schema no longer allows, prompting with the variable's description, masking secret input, and offering a picker for `enum` fields. Leaves everything already correct untouched. |
|
|
348
|
-
| `envshield schema sync [--service NAME]` | Regenerates `.env.example` from the schema (a dotenv project), or patches a Python-module local file in place to declare any schema variable it's missing (never rewrites it wholesale — only appends/patches the specific lines it owns). |
|
|
366
|
+
| `envshield schema sync [--service NAME]` | Regenerates `.env.example` from the schema (a dotenv project), or patches a Python-module local file in place to declare any schema variable it's missing (never rewrites it wholesale — only appends/patches the specific lines it owns). `import` already calls this automatically for you when it changes a project's/service's real schema, so you'll rarely need to run it by hand except after a manual schema edit. |
|
|
349
367
|
| `envshield generate [output_file] [--lang/-l python\|typescript] [--force/-f] [--service NAME]` | Compiles the schema into a typed, validated config module. `--lang` is auto-detected from your project (Next.js/Vite/Node.js → TypeScript, everything else → Python) if omitted. Defaults to writing `config.py`/`config.ts`; `--force` overwrites an existing output file. See [Typed config code generation](#typed-config-code-generation). |
|
|
350
368
|
| `envshield scan [paths...] [--staged] [--config/-c PATH] [--exclude/-e PATTERN]` [--service NAME] | Scans code for hardcoded secrets and for env vars used in code (`os.getenv`, `os.environ.get`, `process.env.X`) but never declared in the schema. `--staged` scans only what's staged for the next commit (what the pre-commit hook runs); `--exclude` (repeatable) adds glob patterns to skip, on top of whatever `secret_scanning.exclude_files` is set in `envshield.yml`. See [Secret scanning and git hooks](#secret-scanning-and-git-hooks). |
|
|
351
369
|
| `envshield install-hook` | Installs both git hooks by hand, without going through `init`/`setup`/`service discover`'s interactive prompt. |
|
|
352
|
-
| `envshield service list` | Lists every service currently configured in `envshield.yml`, with its schema and local file paths. |
|
|
353
|
-
| `envshield service add <name> <directory> [--local-file PATH] [--example-file PATH] [--description/-d TEXT] [--schema PATH] [--import FILE] [--deployment-manifest PATH] [--container NAME]` | Registers one service by hand. `--local-file` is required when the service's real config isn't a dotenv file (e.g. a Python module) — see below. `--import` seeds the new service's schema from an existing config file in one step. `--deployment-manifest` is auto-detected (a compose file in the given directory, or the project root) if not given explicitly. |
|
|
354
|
-
| `envshield service discover [root] [--yes/-y]` | Scans for service-like directories not already registered, and offers to add them — see [Quick start](#a-monorepo-with-multiple-services). `--yes` skips the interactive confirmation, for CI/scripting. |
|
|
355
370
|
| `envshield --version` / `-v` | Prints the installed version and exits. |
|
|
356
371
|
|
|
357
372
|
**Not using a `.env` file at all?** Some projects (a Flask app whose local config is a checked-in Python module, for example) don't use dotenv at all. Point `local_file` at it instead, and EnvShield reads and writes it as source code, not as a dotenv file — appending or patching only the specific assignments it owns, never touching anything else in the file:
|
|
@@ -363,6 +378,16 @@ services:
|
|
|
363
378
|
local_file: athena/config/env_config.local.py
|
|
364
379
|
```
|
|
365
380
|
|
|
381
|
+
### Monorepo: managing multiple services
|
|
382
|
+
|
|
383
|
+
*(Only relevant once your repo has more than one service — see [A monorepo with multiple services](#a-monorepo-with-multiple-services).)* Every core command above already accepts `--service`: automatically, if there's only one service configured; interactively (or against every service at once, via "All services"), if there's more than one.
|
|
384
|
+
|
|
385
|
+
| Command | What it does |
|
|
386
|
+
|---|---|
|
|
387
|
+
| `envshield service list` | Lists every service currently configured in `envshield.yml`, with its schema and local file paths. |
|
|
388
|
+
| `envshield service add <name> <directory> [--local-file PATH] [--example-file PATH] [--description/-d TEXT] [--schema PATH] [--import FILE] [--deployment-manifest PATH] [--container NAME]` | Registers one service by hand. `--local-file` is required when the service's real config isn't a dotenv file (e.g. a Python module) — see above. `--import` seeds the new service's schema from an existing config file in one step. `--deployment-manifest` is auto-detected (a compose file in the given directory or the project root that actually declares this service) if not given explicitly. |
|
|
389
|
+
| `envshield service discover [root] [--yes/-y]` | Scans for service-like directories not already registered, and offers to add them — see [Quick start](#a-monorepo-with-multiple-services). `--yes` skips the interactive confirmation, for CI/scripting. |
|
|
390
|
+
|
|
366
391
|
---
|
|
367
392
|
|
|
368
393
|
## Typed config code generation
|
|
@@ -29,12 +29,14 @@ It works the same way whether you have one repo with one `.env` file, or a monor
|
|
|
29
29
|
- [Installation](#installation)
|
|
30
30
|
- [Quick start](#quick-start)
|
|
31
31
|
- [A single service](#a-single-service)
|
|
32
|
-
- [A monorepo with multiple services](#a-monorepo-with-multiple-services)
|
|
32
|
+
- [A monorepo with multiple services (monorepo)](#a-monorepo-with-multiple-services)
|
|
33
33
|
- [Core concept: the schema is the contract](#core-concept-the-schema-is-the-contract)
|
|
34
34
|
- [Every field a variable can have](#every-field-a-variable-can-have)
|
|
35
35
|
- [Conditional requirements (`requiredIf`)](#conditional-requirements-requiredif)
|
|
36
|
-
- [Sharing variables across services (`extends`)](#sharing-variables-across-services-extends)
|
|
36
|
+
- [Sharing variables across services (monorepo, `extends`)](#sharing-variables-across-services-extends)
|
|
37
37
|
- [Command reference](#command-reference)
|
|
38
|
+
- [Core commands](#core-commands)
|
|
39
|
+
- [Monorepo: managing multiple services (monorepo)](#monorepo-managing-multiple-services)
|
|
38
40
|
- [Typed config code generation](#typed-config-code-generation)
|
|
39
41
|
- [Validating deployment manifests](#validating-deployment-manifests)
|
|
40
42
|
- [Secret scanning and git hooks](#secret-scanning-and-git-hooks)
|
|
@@ -83,22 +85,28 @@ envshield --version
|
|
|
83
85
|
|
|
84
86
|
### A single service
|
|
85
87
|
|
|
86
|
-
The common case: one repo, one `.env` file.
|
|
88
|
+
The common case: one repo, one `.env` file. This is the whole workflow — nothing else in this README is required to get full value out of EnvShield.
|
|
87
89
|
|
|
88
90
|
```bash
|
|
89
91
|
cd my-project
|
|
90
|
-
envshield init # Detects your framework
|
|
91
|
-
envshield import .env # Or: seed the schema from an existing .env file
|
|
92
|
+
envshield init # Detects your framework and builds env.schema.toml from your real config
|
|
92
93
|
envshield setup # Interactive wizard: fills in .env from the schema
|
|
93
94
|
envshield generate --lang python # Generates a typed config.py (or config.ts for TypeScript)
|
|
94
95
|
```
|
|
95
96
|
|
|
97
|
+
`init` looks for a real config source first — an existing `.env`, `.env.example`, or a recognizable Python config module (`config/settings.py` and similar) — and builds the schema from its actual variables, classifying each as secret or not, with a suggested default and, where the value's shape is unambiguous, an inferred type. Only a genuinely fresh project with nothing to read yet falls back to a generic framework template:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Found config/settings.py -- building your schema from its real variables.
|
|
101
|
+
✓ Created/updated schema: env.schema.toml
|
|
102
|
+
```
|
|
103
|
+
|
|
96
104
|
`init` also offers to install a git pre-commit hook (secret scanning) and a post-merge hook (drift check after every `git pull`) — say yes unless you already have your own hook-management tooling (Husky, `pre-commit`, etc.), in which case EnvShield will detect it and won't clobber it (see [Secret scanning and git hooks](#secret-scanning-and-git-hooks)).
|
|
97
105
|
|
|
98
|
-
|
|
106
|
+
`envshield import <file>` does the same real-variable analysis `init` runs automatically, as its own command — reach for it later, when you want to re-import after adding new variables to your code, point at a file `init` wouldn't have found on its own, or add `--interactive` to confirm each classification by hand instead of accepting the automatic guess:
|
|
99
107
|
|
|
100
108
|
```bash
|
|
101
|
-
envshield import .env
|
|
109
|
+
envshield import .env --interactive
|
|
102
110
|
```
|
|
103
111
|
|
|
104
112
|
```
|
|
@@ -111,8 +119,14 @@ Analyzing variables...
|
|
|
111
119
|
- Inferred a type (int/port/bool/url/email) for 3 variable(s).
|
|
112
120
|
```
|
|
113
121
|
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
**Everything above is the complete single-service story.** Everything below this point — multiple services, schema composition, deployment manifests with more than one container — is opt-in, and only relevant once your repo actually has more than one service. Skip straight to [Core concept: the schema is the contract](#core-concept-the-schema-is-the-contract) if that's not you yet.
|
|
125
|
+
|
|
114
126
|
### A monorepo with multiple services
|
|
115
127
|
|
|
128
|
+
*(Optional — skip this if you only have one service.)*
|
|
129
|
+
|
|
116
130
|
If your repo has more than one service (an API, a web frontend, a worker), run `service discover` at the repo root instead of `init`:
|
|
117
131
|
|
|
118
132
|
```bash
|
|
@@ -245,6 +259,8 @@ With `FEATURE_X_ENABLED=false`, `FEATURE_X_API_KEY` is optional — `setup` won'
|
|
|
245
259
|
|
|
246
260
|
### Sharing variables across services (`extends`)
|
|
247
261
|
|
|
262
|
+
*(Monorepo-only — skip this if you have one service.)*
|
|
263
|
+
|
|
248
264
|
A monorepo with ten services usually has five or six variables every single one of them needs — `LOG_LEVEL`, `SENTRY_DSN`, `DATADOG_API_KEY` — and copy-pasting the same `[LOG_LEVEL]` block into ten schema files is exactly the kind of drift EnvShield exists to prevent.
|
|
249
265
|
|
|
250
266
|
Factor them into a shared base schema, and have each service extend it:
|
|
@@ -281,22 +297,21 @@ Loading `services/api/env.schema.toml` now transparently gives you `LOG_LEVEL`,
|
|
|
281
297
|
|
|
282
298
|
## Command reference
|
|
283
299
|
|
|
284
|
-
|
|
300
|
+
### Core commands
|
|
301
|
+
|
|
302
|
+
Everything a single-service project ever needs. `--service` shows up on most of these for the multi-service case (see below), but it's entirely optional until you actually have more than one service.
|
|
285
303
|
|
|
286
304
|
| Command | What it does |
|
|
287
305
|
|---|---|
|
|
288
|
-
| `envshield init [--force/-f]` | Detects your framework
|
|
289
|
-
| `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` |
|
|
306
|
+
| `envshield init [--force/-f]` | Detects your framework and builds `env.schema.toml` from a real config source if it finds one, otherwise a framework-aware template. Also scaffolds `envshield.yml`, updates `.gitignore`, and offers to install git hooks. Auto-registers a root-level `docker-compose.yml` as the project's deployment manifest if it finds one. `--force` re-runs on a project that already has a config (with a confirmation before overwriting). |
|
|
307
|
+
| `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` | Runs the same real-variable analysis `init` does automatically, as its own command — for re-importing after your code gains new variables, pointing at a file `init` wouldn't have found, or adding `--interactive` to confirm each secret/type classification by hand instead of accepting the automatic guess. `--output` changes where the schema is written (defaults to `env.schema.toml`, or the target service's schema path with `--service`). |
|
|
290
308
|
| `envshield check [file] [--service NAME] [--container NAME]` | Validates a local file (or, if omitted, the project's/service's default local file *and* its registered deployment manifest, if any) against the schema. `file` can be a plain `.env`, a Python config module, a docker-compose file, or a Kubernetes manifest. `--container` picks which service/container to check in a manifest that declares more than one (tried against `--service`'s name automatically first). Exits non-zero on any drift — safe to use as a CI gate. |
|
|
291
309
|
| `envshield doctor [--fix] [--service NAME]` | Runs every health check at once (see below) and reports a summary. `--fix` interactively offers to fix whatever it can — re-running `init`, regenerating the template, installing the git hook, or running `setup` to fill in missing/invalid local values. Exits non-zero if anything's still broken afterward. |
|
|
292
310
|
| `envshield setup [output_file] [--service NAME]` | Interactive onboarding wizard: walks through every variable that's missing, blank, or has an existing value the schema no longer allows, prompting with the variable's description, masking secret input, and offering a picker for `enum` fields. Leaves everything already correct untouched. |
|
|
293
|
-
| `envshield schema sync [--service NAME]` | Regenerates `.env.example` from the schema (a dotenv project), or patches a Python-module local file in place to declare any schema variable it's missing (never rewrites it wholesale — only appends/patches the specific lines it owns). |
|
|
311
|
+
| `envshield schema sync [--service NAME]` | Regenerates `.env.example` from the schema (a dotenv project), or patches a Python-module local file in place to declare any schema variable it's missing (never rewrites it wholesale — only appends/patches the specific lines it owns). `import` already calls this automatically for you when it changes a project's/service's real schema, so you'll rarely need to run it by hand except after a manual schema edit. |
|
|
294
312
|
| `envshield generate [output_file] [--lang/-l python\|typescript] [--force/-f] [--service NAME]` | Compiles the schema into a typed, validated config module. `--lang` is auto-detected from your project (Next.js/Vite/Node.js → TypeScript, everything else → Python) if omitted. Defaults to writing `config.py`/`config.ts`; `--force` overwrites an existing output file. See [Typed config code generation](#typed-config-code-generation). |
|
|
295
313
|
| `envshield scan [paths...] [--staged] [--config/-c PATH] [--exclude/-e PATTERN]` [--service NAME] | Scans code for hardcoded secrets and for env vars used in code (`os.getenv`, `os.environ.get`, `process.env.X`) but never declared in the schema. `--staged` scans only what's staged for the next commit (what the pre-commit hook runs); `--exclude` (repeatable) adds glob patterns to skip, on top of whatever `secret_scanning.exclude_files` is set in `envshield.yml`. See [Secret scanning and git hooks](#secret-scanning-and-git-hooks). |
|
|
296
314
|
| `envshield install-hook` | Installs both git hooks by hand, without going through `init`/`setup`/`service discover`'s interactive prompt. |
|
|
297
|
-
| `envshield service list` | Lists every service currently configured in `envshield.yml`, with its schema and local file paths. |
|
|
298
|
-
| `envshield service add <name> <directory> [--local-file PATH] [--example-file PATH] [--description/-d TEXT] [--schema PATH] [--import FILE] [--deployment-manifest PATH] [--container NAME]` | Registers one service by hand. `--local-file` is required when the service's real config isn't a dotenv file (e.g. a Python module) — see below. `--import` seeds the new service's schema from an existing config file in one step. `--deployment-manifest` is auto-detected (a compose file in the given directory, or the project root) if not given explicitly. |
|
|
299
|
-
| `envshield service discover [root] [--yes/-y]` | Scans for service-like directories not already registered, and offers to add them — see [Quick start](#a-monorepo-with-multiple-services). `--yes` skips the interactive confirmation, for CI/scripting. |
|
|
300
315
|
| `envshield --version` / `-v` | Prints the installed version and exits. |
|
|
301
316
|
|
|
302
317
|
**Not using a `.env` file at all?** Some projects (a Flask app whose local config is a checked-in Python module, for example) don't use dotenv at all. Point `local_file` at it instead, and EnvShield reads and writes it as source code, not as a dotenv file — appending or patching only the specific assignments it owns, never touching anything else in the file:
|
|
@@ -308,6 +323,16 @@ services:
|
|
|
308
323
|
local_file: athena/config/env_config.local.py
|
|
309
324
|
```
|
|
310
325
|
|
|
326
|
+
### Monorepo: managing multiple services
|
|
327
|
+
|
|
328
|
+
*(Only relevant once your repo has more than one service — see [A monorepo with multiple services](#a-monorepo-with-multiple-services).)* Every core command above already accepts `--service`: automatically, if there's only one service configured; interactively (or against every service at once, via "All services"), if there's more than one.
|
|
329
|
+
|
|
330
|
+
| Command | What it does |
|
|
331
|
+
|---|---|
|
|
332
|
+
| `envshield service list` | Lists every service currently configured in `envshield.yml`, with its schema and local file paths. |
|
|
333
|
+
| `envshield service add <name> <directory> [--local-file PATH] [--example-file PATH] [--description/-d TEXT] [--schema PATH] [--import FILE] [--deployment-manifest PATH] [--container NAME]` | Registers one service by hand. `--local-file` is required when the service's real config isn't a dotenv file (e.g. a Python module) — see above. `--import` seeds the new service's schema from an existing config file in one step. `--deployment-manifest` is auto-detected (a compose file in the given directory or the project root that actually declares this service) if not given explicitly. |
|
|
334
|
+
| `envshield service discover [root] [--yes/-y]` | Scans for service-like directories not already registered, and offers to add them — see [Quick start](#a-monorepo-with-multiple-services). `--yes` skips the interactive confirmation, for CI/scripting. |
|
|
335
|
+
|
|
311
336
|
---
|
|
312
337
|
|
|
313
338
|
## Typed config code generation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.3.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# envshield/cli.py
|
|
2
2
|
import os
|
|
3
|
-
from typing import List, Optional
|
|
3
|
+
from typing import List, Optional, cast
|
|
4
4
|
|
|
5
5
|
import questionary
|
|
6
6
|
import typer
|
|
@@ -92,7 +92,7 @@ def init(
|
|
|
92
92
|
help="Overwrite existing EnvShield configuration files.",
|
|
93
93
|
),
|
|
94
94
|
):
|
|
95
|
-
"""Initializes EnvShield
|
|
95
|
+
"""Initializes EnvShield -- builds env.schema.toml from your real config if one is found, otherwise a framework-aware template."""
|
|
96
96
|
console.print(
|
|
97
97
|
Panel(
|
|
98
98
|
"[bold cyan]Welcome to EnvShield! Setting up your secure foundation...[/bold cyan]",
|
|
@@ -130,7 +130,20 @@ def init(
|
|
|
130
130
|
)
|
|
131
131
|
|
|
132
132
|
project_name = os.path.basename(os.getcwd())
|
|
133
|
-
|
|
133
|
+
|
|
134
|
+
# Prefer building the schema from a real, already-existing config
|
|
135
|
+
# source over a generic framework template -- a fixed template can
|
|
136
|
+
# only ever guess at your actual variables.
|
|
137
|
+
config_source = service_discovery.find_config_source(".")
|
|
138
|
+
used_real_source = bool(config_source)
|
|
139
|
+
if config_source:
|
|
140
|
+
console.print(
|
|
141
|
+
f"Found [bold yellow]{config_source}[/bold yellow] -- building your schema from its real variables."
|
|
142
|
+
)
|
|
143
|
+
schema_content = importer.generate_schema_from_file(config_source, interactive=False)
|
|
144
|
+
else:
|
|
145
|
+
schema_content = config_manager.generate_default_schema_content(project_type)
|
|
146
|
+
|
|
134
147
|
config_manager.write_file(
|
|
135
148
|
config_manager.SCHEMA_FILE_NAME,
|
|
136
149
|
schema_content,
|
|
@@ -167,9 +180,15 @@ def init(
|
|
|
167
180
|
raise typer.Exit()
|
|
168
181
|
|
|
169
182
|
console.print("\n[bold green]✨ Setup Complete! ✨[/bold green]")
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
183
|
+
if used_real_source:
|
|
184
|
+
console.print(
|
|
185
|
+
"Your project is now protected. Review 'env.schema.toml' -- "
|
|
186
|
+
"it was built from your real config, but double-check the secret/type guesses."
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
console.print(
|
|
190
|
+
"Your project is now protected. Define your variables in 'env.schema.toml'."
|
|
191
|
+
)
|
|
173
192
|
console.print("\n[bold cyan]Next step:[/bold cyan]")
|
|
174
193
|
console.print(" envshield setup # Configure your local environment")
|
|
175
194
|
|
|
@@ -434,7 +453,8 @@ def generate(
|
|
|
434
453
|
)
|
|
435
454
|
raise typer.Exit()
|
|
436
455
|
|
|
437
|
-
|
|
456
|
+
resolved_service = cast(Optional[str], service_manager.resolve_service(service))
|
|
457
|
+
schema = config_manager.load_schema(service_name=resolved_service)
|
|
438
458
|
content = generator.generate_config(schema, lang=resolved_lang)
|
|
439
459
|
|
|
440
460
|
with open(resolved_output, "w") as f:
|
|
@@ -481,6 +501,11 @@ def scan(
|
|
|
481
501
|
):
|
|
482
502
|
"""Scans files for hardcoded secrets and undeclared variables."""
|
|
483
503
|
try:
|
|
504
|
+
if service:
|
|
505
|
+
# Validate eagerly for a consistent "Available: ..." error --
|
|
506
|
+
# run_scan's own service_name=None path means "check every
|
|
507
|
+
# configured service", so this only fires for an explicit name.
|
|
508
|
+
service_manager.resolve_service(service)
|
|
484
509
|
scanner.run_scan(
|
|
485
510
|
paths=paths,
|
|
486
511
|
staged_only=staged,
|
|
@@ -537,11 +562,8 @@ def import_command(
|
|
|
537
562
|
try:
|
|
538
563
|
# If service is specified, use that service's schema path
|
|
539
564
|
if service:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
console.print(f"[bold red]Error:[/bold red] Service '{service}' not found.")
|
|
543
|
-
raise typer.Exit(code=1)
|
|
544
|
-
output = resolved_output
|
|
565
|
+
service_manager.resolve_service(service)
|
|
566
|
+
output = cast(str, config_manager.get_service_schema_path(service))
|
|
545
567
|
|
|
546
568
|
if os.path.exists(output) and not force and not interactive:
|
|
547
569
|
console.print(
|
|
@@ -565,6 +587,13 @@ def import_command(
|
|
|
565
587
|
console.print(
|
|
566
588
|
f"\nSuccessfully generated schema at [bold cyan]{output}[/bold cyan]"
|
|
567
589
|
)
|
|
590
|
+
|
|
591
|
+
# Keep the tracked template (.env.example) in sync with the schema
|
|
592
|
+
# we just (re)wrote -- only when `output` is the project's/service's
|
|
593
|
+
# real configured schema path, not some arbitrary --output destination
|
|
594
|
+
# there's no template mapping for.
|
|
595
|
+
if service or output == config_manager.SCHEMA_FILE_NAME:
|
|
596
|
+
schema_manager.sync_schema(service_name=service)
|
|
568
597
|
except EnvShieldException as e:
|
|
569
598
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
570
599
|
raise typer.Exit(code=1)
|
|
@@ -643,8 +672,14 @@ def service_add(
|
|
|
643
672
|
try:
|
|
644
673
|
schema_path = schema or os.path.join(directory, config_manager.SCHEMA_FILE_NAME)
|
|
645
674
|
if not deployment_manifest:
|
|
646
|
-
|
|
647
|
-
|
|
675
|
+
found_manifest = service_discovery.find_compose_file(directory, ".")
|
|
676
|
+
# Auto-discovery only wires it up when the manifest actually
|
|
677
|
+
# names this service -- an explicit --deployment-manifest below
|
|
678
|
+
# always overrides this, since that's the user saying so directly.
|
|
679
|
+
if found_manifest and service_discovery.compose_declares_service(
|
|
680
|
+
found_manifest, manifest_container or name
|
|
681
|
+
):
|
|
682
|
+
deployment_manifest = found_manifest
|
|
648
683
|
console.print(
|
|
649
684
|
f"[dim]Found deployment manifest {deployment_manifest} -- registering it too.[/dim]"
|
|
650
685
|
)
|
|
@@ -7,6 +7,7 @@ from rich.console import Console
|
|
|
7
7
|
|
|
8
8
|
from envshield.core.exceptions import (
|
|
9
9
|
ConfigNotFoundError,
|
|
10
|
+
ConfigParseError,
|
|
10
11
|
SchemaNotFoundError,
|
|
11
12
|
SchemaParseError,
|
|
12
13
|
UnsafePathError,
|
|
@@ -57,9 +58,10 @@ def load_config(path: Optional[str] = None) -> Dict[str, Any]:
|
|
|
57
58
|
with open(config_path, "r") as f:
|
|
58
59
|
config_data = yaml.safe_load(f)
|
|
59
60
|
return config_data if config_data else {}
|
|
60
|
-
except
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
except yaml.YAMLError as e:
|
|
62
|
+
raise ConfigParseError(config_path, str(e))
|
|
63
|
+
except IOError as e:
|
|
64
|
+
raise ConfigParseError(config_path, str(e))
|
|
63
65
|
|
|
64
66
|
|
|
65
67
|
def load_schema(service_name: Optional[str] = None) -> Dict[str, Any]:
|
|
@@ -51,6 +51,14 @@ class SchemaParseError(EnvShieldException):
|
|
|
51
51
|
super().__init__(self.message)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
class ConfigParseError(EnvShieldException):
|
|
55
|
+
"""Raised when the envshield.yml file cannot be parsed."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, config_path: str, details: str):
|
|
58
|
+
self.message = f"Config parse error in {config_path}: {details}"
|
|
59
|
+
super().__init__(self.message)
|
|
60
|
+
|
|
61
|
+
|
|
54
62
|
class UnsafePathError(EnvShieldException):
|
|
55
63
|
"""
|
|
56
64
|
Raised when a path taken from 'envshield.yml' (a service's schema,
|
|
@@ -444,7 +444,7 @@ def run_scan(
|
|
|
444
444
|
skipped_large_files.append(file_path)
|
|
445
445
|
continue
|
|
446
446
|
|
|
447
|
-
#
|
|
447
|
+
# Diff-aware scanning for excluded files
|
|
448
448
|
new_lines_only = None
|
|
449
449
|
if file_path in excluded_files:
|
|
450
450
|
new_lines = _get_diff_lines(file_path)
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import os
|
|
6
6
|
from typing import Dict, List, Optional
|
|
7
7
|
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
8
10
|
from . import inspector
|
|
9
11
|
from .scanner import DEFAULT_EXCLUDED_DIRS
|
|
10
12
|
from ..parsers.factory import get_parser
|
|
@@ -66,6 +68,30 @@ def find_compose_file(service_dir: str, project_root: str = ".") -> Optional[str
|
|
|
66
68
|
return None
|
|
67
69
|
|
|
68
70
|
|
|
71
|
+
def compose_declares_service(compose_path: str, name: str) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Whether `name` appears as a top-level service key in the compose file at
|
|
74
|
+
`compose_path`.
|
|
75
|
+
|
|
76
|
+
A shared root compose file is found (by find_compose_file) for every
|
|
77
|
+
directory being registered, regardless of whether that directory is
|
|
78
|
+
actually one of its containers. Auto-attaching it unconditionally would
|
|
79
|
+
silently validate an unrelated service against the wrong container's
|
|
80
|
+
variables whenever the compose file happens to declare exactly one
|
|
81
|
+
service -- that single container gets picked with no name check at all
|
|
82
|
+
(see the parsers' `prefer` logic). Requiring a name match before
|
|
83
|
+
auto-attaching turns that into "nothing attached, register it by hand
|
|
84
|
+
with --deployment-manifest" instead of a silent wrong answer.
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
with open(compose_path, "r") as f:
|
|
88
|
+
doc = yaml.safe_load(f) or {}
|
|
89
|
+
except (OSError, yaml.YAMLError):
|
|
90
|
+
return False
|
|
91
|
+
services = doc.get("services") if isinstance(doc, dict) else None
|
|
92
|
+
return isinstance(services, dict) and name in services
|
|
93
|
+
|
|
94
|
+
|
|
69
95
|
def _looks_like_python_config_module(path: str) -> bool:
|
|
70
96
|
parser = get_parser(path)
|
|
71
97
|
if not parser:
|
|
@@ -123,6 +149,34 @@ def _find_dotenv_template(service_dir: str) -> Optional[str]:
|
|
|
123
149
|
return None
|
|
124
150
|
|
|
125
151
|
|
|
152
|
+
def find_config_source(service_dir: str) -> Optional[str]:
|
|
153
|
+
"""
|
|
154
|
+
Finds the best real config source at `service_dir` to seed a schema
|
|
155
|
+
from -- a real dotenv file, a checked-in dotenv template, or a
|
|
156
|
+
recognizable Python config module -- and returns its actual path.
|
|
157
|
+
|
|
158
|
+
Unlike detect_env_style, this always returns the real path when one is
|
|
159
|
+
found, even when it's the conventional '.env'/'.env.example' name --
|
|
160
|
+
detect_env_style omits that case because its caller only needs to know
|
|
161
|
+
when a path override is required, while this one is for callers (like
|
|
162
|
+
'init') that need a real file to read.
|
|
163
|
+
"""
|
|
164
|
+
real_file = _find_real_dotenv_file(service_dir)
|
|
165
|
+
if real_file:
|
|
166
|
+
return real_file
|
|
167
|
+
|
|
168
|
+
template_file = _find_dotenv_template(service_dir)
|
|
169
|
+
if template_file:
|
|
170
|
+
return template_file
|
|
171
|
+
|
|
172
|
+
for rel_path in PYTHON_CONFIG_CANDIDATES:
|
|
173
|
+
candidate = os.path.join(service_dir, rel_path)
|
|
174
|
+
if os.path.isfile(candidate) and _looks_like_python_config_module(candidate):
|
|
175
|
+
return candidate
|
|
176
|
+
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
126
180
|
def detect_env_style(service_dir: str) -> Dict[str, Optional[str]]:
|
|
127
181
|
"""
|
|
128
182
|
Looks inside `service_dir` for how it manages environment variables.
|
|
@@ -217,11 +271,12 @@ def discover_candidates(
|
|
|
217
271
|
example_file, deployment_manifest}. `deployment_manifest`, when found,
|
|
218
272
|
is registered automatically -- no '--deployment-manifest' flag needed
|
|
219
273
|
for the common case of a docker-compose file at the project root or
|
|
220
|
-
inside the service's own directory (see find_compose_file)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
274
|
+
inside the service's own directory (see find_compose_file) that
|
|
275
|
+
declares a service matching this directory's name (see
|
|
276
|
+
compose_declares_service). A compose file that doesn't name this
|
|
277
|
+
service isn't attached at all -- register it by hand with
|
|
278
|
+
'--deployment-manifest'/'--container' instead of risking a silent
|
|
279
|
+
validation against the wrong container.
|
|
225
280
|
"""
|
|
226
281
|
known_dirs_norm = {os.path.normpath(d) for d in (known_dirs or [])}
|
|
227
282
|
candidates = []
|
|
@@ -245,6 +300,10 @@ def discover_candidates(
|
|
|
245
300
|
name = f"{parent}-{base_name}" if parent else f"{base_name}-{seen_names[base_name]}"
|
|
246
301
|
seen_names[base_name] = seen_names.get(base_name, 0) + 1
|
|
247
302
|
|
|
303
|
+
compose_file = find_compose_file(normalized, root)
|
|
304
|
+
if compose_file and not compose_declares_service(compose_file, base_name):
|
|
305
|
+
compose_file = None
|
|
306
|
+
|
|
248
307
|
candidates.append(
|
|
249
308
|
{
|
|
250
309
|
"name": name,
|
|
@@ -253,7 +312,7 @@ def discover_candidates(
|
|
|
253
312
|
"format": env_style["format"],
|
|
254
313
|
"local_file": env_style["local_file"],
|
|
255
314
|
"example_file": env_style["example_file"],
|
|
256
|
-
"deployment_manifest":
|
|
315
|
+
"deployment_manifest": compose_file,
|
|
257
316
|
}
|
|
258
317
|
)
|
|
259
318
|
|
|
@@ -234,3 +234,55 @@ def test_discover_candidates_includes_deployment_manifest_when_found(tmp_path):
|
|
|
234
234
|
|
|
235
235
|
api = next(c for c in candidates if c["name"] == "api")
|
|
236
236
|
assert api["deployment_manifest"] == os.path.normpath(str(tmp_path / "docker-compose.yml"))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_discover_candidates_does_not_attach_manifest_that_does_not_name_the_service(tmp_path):
|
|
240
|
+
"""
|
|
241
|
+
Regression: find_compose_file only checks that *a* compose file exists
|
|
242
|
+
nearby, not that this service is actually declared in it. A shared root
|
|
243
|
+
compose file with exactly one container would otherwise get silently
|
|
244
|
+
attached to every unrelated directory discover finds -- and since a
|
|
245
|
+
single-container compose file is used regardless of --container/prefer,
|
|
246
|
+
that means validating an unrelated service against the wrong container's
|
|
247
|
+
variables with no error at all.
|
|
248
|
+
"""
|
|
249
|
+
(tmp_path / "docs").mkdir()
|
|
250
|
+
(tmp_path / "docs" / ".env").write_text("KEY=1\n")
|
|
251
|
+
(tmp_path / "docker-compose.yml").write_text("services:\n api:\n image: x\n")
|
|
252
|
+
|
|
253
|
+
candidates = service_discovery.discover_candidates(str(tmp_path))
|
|
254
|
+
|
|
255
|
+
docs = next(c for c in candidates if c["name"] == "docs")
|
|
256
|
+
assert docs["deployment_manifest"] is None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_compose_declares_service(tmp_path):
|
|
260
|
+
compose = tmp_path / "docker-compose.yml"
|
|
261
|
+
compose.write_text("services:\n api:\n image: x\n worker:\n image: y\n")
|
|
262
|
+
|
|
263
|
+
assert service_discovery.compose_declares_service(str(compose), "api") is True
|
|
264
|
+
assert service_discovery.compose_declares_service(str(compose), "docs") is False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_find_config_source_returns_conventional_dotenv_path(tmp_path):
|
|
268
|
+
"""
|
|
269
|
+
Unlike detect_env_style (which omits the path for the conventional
|
|
270
|
+
'.env' name since its caller only needs an override signal),
|
|
271
|
+
find_config_source always returns the real path -- callers like 'init'
|
|
272
|
+
need an actual file to read from.
|
|
273
|
+
"""
|
|
274
|
+
(tmp_path / ".env").write_text("KEY=1\n")
|
|
275
|
+
|
|
276
|
+
assert service_discovery.find_config_source(str(tmp_path)) == str(tmp_path / ".env")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_find_config_source_finds_python_config_module(tmp_path):
|
|
280
|
+
(tmp_path / "config").mkdir()
|
|
281
|
+
settings = tmp_path / "config" / "settings.py"
|
|
282
|
+
settings.write_text("SECRET_KEY = 'x'\nDEBUG = True\nAPI_PORT = 5000\n")
|
|
283
|
+
|
|
284
|
+
assert service_discovery.find_config_source(str(tmp_path)) == str(settings)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_find_config_source_returns_none_when_nothing_found(tmp_path):
|
|
288
|
+
assert service_discovery.find_config_source(str(tmp_path)) is None
|
|
@@ -362,3 +362,86 @@ def test_check_without_service_runs_for_all_services(mocker, tmp_path):
|
|
|
362
362
|
assert "── athena ──" in result.stdout
|
|
363
363
|
assert "── hermes ──" in result.stdout
|
|
364
364
|
assert "perfectly in sync" in result.stdout
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_generate_scan_and_import_report_available_services_for_an_unknown_one(tmp_path):
|
|
368
|
+
"""
|
|
369
|
+
Regression: 'check'/'doctor'/'setup' listed available services on an
|
|
370
|
+
unknown --service, but 'generate'/'scan'/'import' each had their own
|
|
371
|
+
inline check with no listing. All should behave the same now.
|
|
372
|
+
"""
|
|
373
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
374
|
+
_write_two_service_project()
|
|
375
|
+
|
|
376
|
+
for args in (
|
|
377
|
+
["generate", "--service", "bogus"],
|
|
378
|
+
["scan", ".", "--service", "bogus"],
|
|
379
|
+
["import", "athena/env.schema.toml", "--service", "bogus"],
|
|
380
|
+
):
|
|
381
|
+
result = runner.invoke(app, args)
|
|
382
|
+
assert result.exit_code == 1, f"{args}: {result.stdout}"
|
|
383
|
+
assert "not found" in result.stdout
|
|
384
|
+
assert "athena, hermes" in result.stdout, f"{args}: {result.stdout}"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def test_import_command_syncs_the_env_example_template(tmp_path):
|
|
388
|
+
"""Regression: import used to leave .env.example stale after changing the schema it feeds."""
|
|
389
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
390
|
+
with open("settings.py", "w") as f:
|
|
391
|
+
f.write("SECRET_KEY = 'x'\nAPI_PORT = 5000\n")
|
|
392
|
+
|
|
393
|
+
result = runner.invoke(app, ["import", "settings.py"])
|
|
394
|
+
|
|
395
|
+
assert result.exit_code == 0
|
|
396
|
+
assert os.path.exists(".env.example")
|
|
397
|
+
with open(".env.example", "r") as f:
|
|
398
|
+
content = f.read()
|
|
399
|
+
assert "SECRET_KEY" in content
|
|
400
|
+
assert "API_PORT" in content
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_init_builds_schema_from_a_real_config_source_instead_of_the_template(tmp_path, mocker):
|
|
404
|
+
"""
|
|
405
|
+
Regression: init used to always write the generic framework template even
|
|
406
|
+
when the project already had real config (e.g. config/settings.py) sitting
|
|
407
|
+
right there, forcing a separate 'envshield import ... --force' just to get
|
|
408
|
+
an accurate schema.
|
|
409
|
+
"""
|
|
410
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
411
|
+
os.system("git init -q")
|
|
412
|
+
mocker.patch("questionary.confirm").return_value.ask.return_value = False
|
|
413
|
+
with open("requirements.txt", "w") as f:
|
|
414
|
+
f.write("Flask\n")
|
|
415
|
+
os.makedirs("config")
|
|
416
|
+
with open("config/settings.py", "w") as f:
|
|
417
|
+
f.write(
|
|
418
|
+
"SECRET_KEY = 'x'\nDATABASE_URL = 'postgres://x'\n"
|
|
419
|
+
"DEBUG = True\nAPI_PORT = 5000\nLOG_LEVEL = 'info'\n"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
result = runner.invoke(app, ["init"])
|
|
423
|
+
|
|
424
|
+
assert result.exit_code == 0, result.stdout
|
|
425
|
+
with open(SCHEMA_FILE_NAME, "r") as f:
|
|
426
|
+
content = f.read()
|
|
427
|
+
# The fixed python-flask template only ever has these three --
|
|
428
|
+
# DEBUG/API_PORT/LOG_LEVEL prove the real file was used instead.
|
|
429
|
+
assert "DEBUG" in content
|
|
430
|
+
assert "API_PORT" in content
|
|
431
|
+
assert "LOG_LEVEL" in content
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_init_falls_back_to_the_framework_template_when_no_real_config_exists(tmp_path, mocker):
|
|
435
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
436
|
+
os.system("git init -q")
|
|
437
|
+
mocker.patch("questionary.confirm").return_value.ask.return_value = False
|
|
438
|
+
with open("requirements.txt", "w") as f:
|
|
439
|
+
f.write("Flask\n")
|
|
440
|
+
|
|
441
|
+
result = runner.invoke(app, ["init"])
|
|
442
|
+
|
|
443
|
+
assert result.exit_code == 0, result.stdout
|
|
444
|
+
with open(SCHEMA_FILE_NAME, "r") as f:
|
|
445
|
+
content = f.read()
|
|
446
|
+
assert "SECRET_KEY" in content
|
|
447
|
+
assert "FLASK_ENV" in content
|
|
@@ -4,7 +4,12 @@ import os
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
6
|
from envshield.config import manager as config_manager
|
|
7
|
-
from envshield.core.exceptions import
|
|
7
|
+
from envshield.core.exceptions import (
|
|
8
|
+
ConfigParseError,
|
|
9
|
+
SchemaNotFoundError,
|
|
10
|
+
SchemaParseError,
|
|
11
|
+
UnsafePathError,
|
|
12
|
+
)
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
def test_update_gitignore_creates_file_with_env_pattern(tmp_path, monkeypatch):
|
|
@@ -268,6 +273,21 @@ def test_load_schema_supports_multiple_extends_with_later_entries_winning(tmp_pa
|
|
|
268
273
|
assert schema["SHARED"]["description"] == "from b"
|
|
269
274
|
|
|
270
275
|
|
|
276
|
+
def test_load_config_raises_clean_error_on_malformed_yaml(tmp_path, monkeypatch):
|
|
277
|
+
"""
|
|
278
|
+
Regression: a malformed envshield.yml used to print a message and then
|
|
279
|
+
re-raise the raw yaml.YAMLError, which no cli.py handler catches (they
|
|
280
|
+
only catch EnvShieldException) -- producing an unhandled traceback
|
|
281
|
+
instead of the clean error every other parse failure gets.
|
|
282
|
+
"""
|
|
283
|
+
monkeypatch.chdir(tmp_path)
|
|
284
|
+
with open("envshield.yml", "w") as f:
|
|
285
|
+
f.write("services: [unclosed\n")
|
|
286
|
+
|
|
287
|
+
with pytest.raises(ConfigParseError, match="envshield.yml"):
|
|
288
|
+
config_manager.load_config()
|
|
289
|
+
|
|
290
|
+
|
|
271
291
|
def test_load_schema_detects_circular_extends(tmp_path, monkeypatch):
|
|
272
292
|
monkeypatch.chdir(tmp_path)
|
|
273
293
|
with open("a.schema.toml", "w") as f:
|
|
@@ -187,6 +187,25 @@ def test_service_add_auto_detects_compose_file_in_service_directory(tmp_path):
|
|
|
187
187
|
)
|
|
188
188
|
|
|
189
189
|
|
|
190
|
+
def test_service_add_does_not_auto_attach_a_manifest_that_does_not_name_it(tmp_path):
|
|
191
|
+
"""
|
|
192
|
+
Regression: a shared root compose file used to get auto-attached to
|
|
193
|
+
any service directory regardless of whether it's actually declared in
|
|
194
|
+
it -- silently validating against the wrong container. An explicit
|
|
195
|
+
--deployment-manifest still always works (see the test right below).
|
|
196
|
+
"""
|
|
197
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
198
|
+
os.makedirs("docs")
|
|
199
|
+
with open("docker-compose.yml", "w") as f:
|
|
200
|
+
f.write("services:\n api:\n image: x\n")
|
|
201
|
+
|
|
202
|
+
result = runner.invoke(app, ["service", "add", "docs", "docs"])
|
|
203
|
+
|
|
204
|
+
assert result.exit_code == 0
|
|
205
|
+
assert config_manager.get_services()["docs"].get("deployment_manifest") is None
|
|
206
|
+
assert "docker-compose.yml" not in result.stdout
|
|
207
|
+
|
|
208
|
+
|
|
190
209
|
def test_service_add_explicit_deployment_manifest_and_container(tmp_path):
|
|
191
210
|
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
192
211
|
os.makedirs("api")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: envshield
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.0
|
|
4
4
|
Summary: EnvShield: A CLI for secure local environment management and secret prevention.
|
|
5
5
|
Author-email: Rabbil Yasar Sajal <rabbilyasar@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -84,12 +84,14 @@ It works the same way whether you have one repo with one `.env` file, or a monor
|
|
|
84
84
|
- [Installation](#installation)
|
|
85
85
|
- [Quick start](#quick-start)
|
|
86
86
|
- [A single service](#a-single-service)
|
|
87
|
-
- [A monorepo with multiple services](#a-monorepo-with-multiple-services)
|
|
87
|
+
- [A monorepo with multiple services (monorepo)](#a-monorepo-with-multiple-services)
|
|
88
88
|
- [Core concept: the schema is the contract](#core-concept-the-schema-is-the-contract)
|
|
89
89
|
- [Every field a variable can have](#every-field-a-variable-can-have)
|
|
90
90
|
- [Conditional requirements (`requiredIf`)](#conditional-requirements-requiredif)
|
|
91
|
-
- [Sharing variables across services (`extends`)](#sharing-variables-across-services-extends)
|
|
91
|
+
- [Sharing variables across services (monorepo, `extends`)](#sharing-variables-across-services-extends)
|
|
92
92
|
- [Command reference](#command-reference)
|
|
93
|
+
- [Core commands](#core-commands)
|
|
94
|
+
- [Monorepo: managing multiple services (monorepo)](#monorepo-managing-multiple-services)
|
|
93
95
|
- [Typed config code generation](#typed-config-code-generation)
|
|
94
96
|
- [Validating deployment manifests](#validating-deployment-manifests)
|
|
95
97
|
- [Secret scanning and git hooks](#secret-scanning-and-git-hooks)
|
|
@@ -138,22 +140,28 @@ envshield --version
|
|
|
138
140
|
|
|
139
141
|
### A single service
|
|
140
142
|
|
|
141
|
-
The common case: one repo, one `.env` file.
|
|
143
|
+
The common case: one repo, one `.env` file. This is the whole workflow — nothing else in this README is required to get full value out of EnvShield.
|
|
142
144
|
|
|
143
145
|
```bash
|
|
144
146
|
cd my-project
|
|
145
|
-
envshield init # Detects your framework
|
|
146
|
-
envshield import .env # Or: seed the schema from an existing .env file
|
|
147
|
+
envshield init # Detects your framework and builds env.schema.toml from your real config
|
|
147
148
|
envshield setup # Interactive wizard: fills in .env from the schema
|
|
148
149
|
envshield generate --lang python # Generates a typed config.py (or config.ts for TypeScript)
|
|
149
150
|
```
|
|
150
151
|
|
|
152
|
+
`init` looks for a real config source first — an existing `.env`, `.env.example`, or a recognizable Python config module (`config/settings.py` and similar) — and builds the schema from its actual variables, classifying each as secret or not, with a suggested default and, where the value's shape is unambiguous, an inferred type. Only a genuinely fresh project with nothing to read yet falls back to a generic framework template:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
Found config/settings.py -- building your schema from its real variables.
|
|
156
|
+
✓ Created/updated schema: env.schema.toml
|
|
157
|
+
```
|
|
158
|
+
|
|
151
159
|
`init` also offers to install a git pre-commit hook (secret scanning) and a post-merge hook (drift check after every `git pull`) — say yes unless you already have your own hook-management tooling (Husky, `pre-commit`, etc.), in which case EnvShield will detect it and won't clobber it (see [Secret scanning and git hooks](#secret-scanning-and-git-hooks)).
|
|
152
160
|
|
|
153
|
-
|
|
161
|
+
`envshield import <file>` does the same real-variable analysis `init` runs automatically, as its own command — reach for it later, when you want to re-import after adding new variables to your code, point at a file `init` wouldn't have found on its own, or add `--interactive` to confirm each classification by hand instead of accepting the automatic guess:
|
|
154
162
|
|
|
155
163
|
```bash
|
|
156
|
-
envshield import .env
|
|
164
|
+
envshield import .env --interactive
|
|
157
165
|
```
|
|
158
166
|
|
|
159
167
|
```
|
|
@@ -166,8 +174,14 @@ Analyzing variables...
|
|
|
166
174
|
- Inferred a type (int/port/bool/url/email) for 3 variable(s).
|
|
167
175
|
```
|
|
168
176
|
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
**Everything above is the complete single-service story.** Everything below this point — multiple services, schema composition, deployment manifests with more than one container — is opt-in, and only relevant once your repo actually has more than one service. Skip straight to [Core concept: the schema is the contract](#core-concept-the-schema-is-the-contract) if that's not you yet.
|
|
180
|
+
|
|
169
181
|
### A monorepo with multiple services
|
|
170
182
|
|
|
183
|
+
*(Optional — skip this if you only have one service.)*
|
|
184
|
+
|
|
171
185
|
If your repo has more than one service (an API, a web frontend, a worker), run `service discover` at the repo root instead of `init`:
|
|
172
186
|
|
|
173
187
|
```bash
|
|
@@ -300,6 +314,8 @@ With `FEATURE_X_ENABLED=false`, `FEATURE_X_API_KEY` is optional — `setup` won'
|
|
|
300
314
|
|
|
301
315
|
### Sharing variables across services (`extends`)
|
|
302
316
|
|
|
317
|
+
*(Monorepo-only — skip this if you have one service.)*
|
|
318
|
+
|
|
303
319
|
A monorepo with ten services usually has five or six variables every single one of them needs — `LOG_LEVEL`, `SENTRY_DSN`, `DATADOG_API_KEY` — and copy-pasting the same `[LOG_LEVEL]` block into ten schema files is exactly the kind of drift EnvShield exists to prevent.
|
|
304
320
|
|
|
305
321
|
Factor them into a shared base schema, and have each service extend it:
|
|
@@ -336,22 +352,21 @@ Loading `services/api/env.schema.toml` now transparently gives you `LOG_LEVEL`,
|
|
|
336
352
|
|
|
337
353
|
## Command reference
|
|
338
354
|
|
|
339
|
-
|
|
355
|
+
### Core commands
|
|
356
|
+
|
|
357
|
+
Everything a single-service project ever needs. `--service` shows up on most of these for the multi-service case (see below), but it's entirely optional until you actually have more than one service.
|
|
340
358
|
|
|
341
359
|
| Command | What it does |
|
|
342
360
|
|---|---|
|
|
343
|
-
| `envshield init [--force/-f]` | Detects your framework
|
|
344
|
-
| `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` |
|
|
361
|
+
| `envshield init [--force/-f]` | Detects your framework and builds `env.schema.toml` from a real config source if it finds one, otherwise a framework-aware template. Also scaffolds `envshield.yml`, updates `.gitignore`, and offers to install git hooks. Auto-registers a root-level `docker-compose.yml` as the project's deployment manifest if it finds one. `--force` re-runs on a project that already has a config (with a confirmation before overwriting). |
|
|
362
|
+
| `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` | Runs the same real-variable analysis `init` does automatically, as its own command — for re-importing after your code gains new variables, pointing at a file `init` wouldn't have found, or adding `--interactive` to confirm each secret/type classification by hand instead of accepting the automatic guess. `--output` changes where the schema is written (defaults to `env.schema.toml`, or the target service's schema path with `--service`). |
|
|
345
363
|
| `envshield check [file] [--service NAME] [--container NAME]` | Validates a local file (or, if omitted, the project's/service's default local file *and* its registered deployment manifest, if any) against the schema. `file` can be a plain `.env`, a Python config module, a docker-compose file, or a Kubernetes manifest. `--container` picks which service/container to check in a manifest that declares more than one (tried against `--service`'s name automatically first). Exits non-zero on any drift — safe to use as a CI gate. |
|
|
346
364
|
| `envshield doctor [--fix] [--service NAME]` | Runs every health check at once (see below) and reports a summary. `--fix` interactively offers to fix whatever it can — re-running `init`, regenerating the template, installing the git hook, or running `setup` to fill in missing/invalid local values. Exits non-zero if anything's still broken afterward. |
|
|
347
365
|
| `envshield setup [output_file] [--service NAME]` | Interactive onboarding wizard: walks through every variable that's missing, blank, or has an existing value the schema no longer allows, prompting with the variable's description, masking secret input, and offering a picker for `enum` fields. Leaves everything already correct untouched. |
|
|
348
|
-
| `envshield schema sync [--service NAME]` | Regenerates `.env.example` from the schema (a dotenv project), or patches a Python-module local file in place to declare any schema variable it's missing (never rewrites it wholesale — only appends/patches the specific lines it owns). |
|
|
366
|
+
| `envshield schema sync [--service NAME]` | Regenerates `.env.example` from the schema (a dotenv project), or patches a Python-module local file in place to declare any schema variable it's missing (never rewrites it wholesale — only appends/patches the specific lines it owns). `import` already calls this automatically for you when it changes a project's/service's real schema, so you'll rarely need to run it by hand except after a manual schema edit. |
|
|
349
367
|
| `envshield generate [output_file] [--lang/-l python\|typescript] [--force/-f] [--service NAME]` | Compiles the schema into a typed, validated config module. `--lang` is auto-detected from your project (Next.js/Vite/Node.js → TypeScript, everything else → Python) if omitted. Defaults to writing `config.py`/`config.ts`; `--force` overwrites an existing output file. See [Typed config code generation](#typed-config-code-generation). |
|
|
350
368
|
| `envshield scan [paths...] [--staged] [--config/-c PATH] [--exclude/-e PATTERN]` [--service NAME] | Scans code for hardcoded secrets and for env vars used in code (`os.getenv`, `os.environ.get`, `process.env.X`) but never declared in the schema. `--staged` scans only what's staged for the next commit (what the pre-commit hook runs); `--exclude` (repeatable) adds glob patterns to skip, on top of whatever `secret_scanning.exclude_files` is set in `envshield.yml`. See [Secret scanning and git hooks](#secret-scanning-and-git-hooks). |
|
|
351
369
|
| `envshield install-hook` | Installs both git hooks by hand, without going through `init`/`setup`/`service discover`'s interactive prompt. |
|
|
352
|
-
| `envshield service list` | Lists every service currently configured in `envshield.yml`, with its schema and local file paths. |
|
|
353
|
-
| `envshield service add <name> <directory> [--local-file PATH] [--example-file PATH] [--description/-d TEXT] [--schema PATH] [--import FILE] [--deployment-manifest PATH] [--container NAME]` | Registers one service by hand. `--local-file` is required when the service's real config isn't a dotenv file (e.g. a Python module) — see below. `--import` seeds the new service's schema from an existing config file in one step. `--deployment-manifest` is auto-detected (a compose file in the given directory, or the project root) if not given explicitly. |
|
|
354
|
-
| `envshield service discover [root] [--yes/-y]` | Scans for service-like directories not already registered, and offers to add them — see [Quick start](#a-monorepo-with-multiple-services). `--yes` skips the interactive confirmation, for CI/scripting. |
|
|
355
370
|
| `envshield --version` / `-v` | Prints the installed version and exits. |
|
|
356
371
|
|
|
357
372
|
**Not using a `.env` file at all?** Some projects (a Flask app whose local config is a checked-in Python module, for example) don't use dotenv at all. Point `local_file` at it instead, and EnvShield reads and writes it as source code, not as a dotenv file — appending or patching only the specific assignments it owns, never touching anything else in the file:
|
|
@@ -363,6 +378,16 @@ services:
|
|
|
363
378
|
local_file: athena/config/env_config.local.py
|
|
364
379
|
```
|
|
365
380
|
|
|
381
|
+
### Monorepo: managing multiple services
|
|
382
|
+
|
|
383
|
+
*(Only relevant once your repo has more than one service — see [A monorepo with multiple services](#a-monorepo-with-multiple-services).)* Every core command above already accepts `--service`: automatically, if there's only one service configured; interactively (or against every service at once, via "All services"), if there's more than one.
|
|
384
|
+
|
|
385
|
+
| Command | What it does |
|
|
386
|
+
|---|---|
|
|
387
|
+
| `envshield service list` | Lists every service currently configured in `envshield.yml`, with its schema and local file paths. |
|
|
388
|
+
| `envshield service add <name> <directory> [--local-file PATH] [--example-file PATH] [--description/-d TEXT] [--schema PATH] [--import FILE] [--deployment-manifest PATH] [--container NAME]` | Registers one service by hand. `--local-file` is required when the service's real config isn't a dotenv file (e.g. a Python module) — see above. `--import` seeds the new service's schema from an existing config file in one step. `--deployment-manifest` is auto-detected (a compose file in the given directory or the project root that actually declares this service) if not given explicitly. |
|
|
389
|
+
| `envshield service discover [root] [--yes/-y]` | Scans for service-like directories not already registered, and offers to add them — see [Quick start](#a-monorepo-with-multiple-services). `--yes` skips the interactive confirmation, for CI/scripting. |
|
|
390
|
+
|
|
366
391
|
---
|
|
367
392
|
|
|
368
393
|
## Typed config code generation
|
|
@@ -37,9 +37,9 @@ envshield/parsers/_python.py
|
|
|
37
37
|
envshield/parsers/factory.py
|
|
38
38
|
envshield/tests/__init__.py
|
|
39
39
|
envshield/tests/conftest.py
|
|
40
|
-
envshield/tests/test_c6_diff_aware_scanning.py
|
|
41
40
|
envshield/tests/test_cli.py
|
|
42
41
|
envshield/tests/test_config_manager.py
|
|
42
|
+
envshield/tests/test_diff_aware_scanning.py
|
|
43
43
|
envshield/tests/test_git_utils.py
|
|
44
44
|
envshield/tests/test_service_cli.py
|
|
45
45
|
envshield/tests/core/__init__.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "envshield"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.3.0" # must be a string for PEP 621 compliance
|
|
8
8
|
description = "EnvShield: A CLI for secure local environment management and secret prevention."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "4.2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|