|
| 1 | +# lite-bootstrap 1.1.0 — Lifecycle hardening, config validation, CI gate |
| 2 | + |
| 3 | +**1.1.0 is a minor release. No intentional public-API breakage.** The two behavior changes that could affect existing code are fixes to genuine bugs and are called out in *Behavior changes* below. |
| 4 | + |
| 5 | +This release closes a 26-finding bug-audit cycle (audits + retro live under `planning/specs/2026-06-05-bug-audit-v2*.md`). The changes split across four shipped PRs: |
| 6 | + |
| 7 | +- **#108** — Lifecycle & teardown correctness (10 findings) |
| 8 | +- **#109** — Config UX & security validation (6 findings) |
| 9 | +- **#110** — Hygiene + CI gate (4 findings) |
| 10 | +- **#111** — Generalized `TeardownError` aggregation + cascade tests + README lifecycle docs (3 deferred follow-ups) |
| 11 | + |
| 12 | +Test suite grew from 153 → 194 (+27%) at 100% line coverage throughout. `pip-audit` now runs on every PR and weekly via cron; a new `filterwarnings` config catches accidental `InstrumentSkippedWarning` emissions. |
| 13 | + |
| 14 | +## New features |
| 15 | + |
| 16 | +- **Injectable `prometheus_collector_registry` on `FastStreamConfig`.** Pass an existing `prometheus_client.CollectorRegistry` to expose counters registered elsewhere through FastStream's `/metrics` endpoint. Defaults to a fresh per-instance registry — fully backward compatible. |
| 17 | +- **`opentelemetry_excluded_urls` field on `FastStreamConfig`.** Was a `getattr` fallback before; now a discoverable, IDE-completable config field matching `FastAPIConfig` and `LitestarConfig`. |
| 18 | +- **`SentryInstrument.teardown()`.** Calls `sentry_sdk.flush(timeout=2)` then `sentry_sdk.init()` (no args) to reset the SDK to a no-op state. Previously the SDK stayed globally configured after bootstrapper teardown, leaking state across process-local tests. |
| 19 | +- **`FastStreamLoggingInstrument.teardown()`.** Restores `broker.config.logger.params_storage` to its pre-bootstrap value. The bootstrap mutated broker state; teardown didn't reverse it. |
| 20 | + |
| 21 | +## Bug fixes |
| 22 | + |
| 23 | +### Lifecycle & teardown (PR #108) |
| 24 | + |
| 25 | +- **OpenTelemetry teardown now flushes spans and shuts down the tracer provider** (LOG-1, LOG-2). `bootstrap()` stored the `TracerProvider` only as a local; `teardown()` couldn't reach it to call `shutdown()`. Buffered spans in `BatchSpanProcessor` were dropped on graceful shutdown. Teardown also restores the two OTel-namespace stdlib loggers (`opentelemetry.instrumentation.instrumentor`, `opentelemetry.trace`) to their pre-bootstrap `disabled` state. |
| 26 | +- **`LoggingInstrument.teardown()` runs all cleanup steps even on partial failure** (LOG-3). A raise from any `handler.close()` previously left remaining handlers attached, skipped the root-level reset, and never called `close_handlers()` on the memory factory. Now wrapped in `try/finally` with per-handler error capture; all collected errors raise together via `TeardownError(errors)`. |
| 27 | +- **`LitestarOpenTelemetryInstrumentationMiddleware` cache evicts dead refs** (LOG-6). The old `dict[int, ASGIApp]` keyed by `id()` never evicted, holding wrapper `OpenTelemetryMiddleware` instances alive after Litestar dropped `next_app`. Replaced with `weakref.WeakKeyDictionary`; non-weakrefable apps fall through to the un-cached path. |
| 28 | +- **Double-bootstrap on the same application is now detected and warned** (LOG-7, LOG-8). Constructing two `FastMcpBootstrapper`s around the same `FastMCP` (or two `FastAPIBootstrapper`s around the same `FastAPI`) previously stacked teardown hooks. The second construction now emits a `UserWarning`, skips the re-attachment, and tells you the second bootstrapper's `teardown()` won't fire on ASGI shutdown. |
| 29 | +- **Generalized `TeardownError` aggregation across all instruments** (PR #111). `OpenTelemetryInstrument`, `FastStreamLoggingInstrument`, and `SentryInstrument` now run their full cleanup sequence even if an early step raises — aggregating errors into a single `TeardownError` or letting `super().teardown()` run via `try/finally`. Previously a misbehaving instrumentor / broker / Sentry flush silently skipped subsequent cleanup. |
| 30 | +- **Pyroscope's precondition check survives `python -O`** (LOG-5/SEC-4). Replaced two `assert` statements (in `_narrow_app` and `PyroscopeInstrument.bootstrap`) with explicit `raise TypeError(...)` and `raise RuntimeError(...)`. `python -O` strips asserts; the invariants now hold under all optimization levels. Closes the two `bandit` B101 findings. |
| 31 | + |
| 32 | +### Config & security (PR #109) |
| 33 | + |
| 34 | +- **`FastAPIConfig` no longer stomps user-supplied app's `title`/`debug`/`version`** (UX-1). Previously the three assignments ran unconditionally; a user passing `FastAPI(title="My API", version="3.0.0")` would silently have those clobbered by lite-bootstrap defaults. Now the assignments only run in the `UnsetType` branch (when lite-bootstrap constructed the app). **See Behavior changes below.** |
| 35 | +- **`enable_offline_docs` validates `request.scope["root_path"]` against the existing path allowlist** (SEC-1). Invalid root paths (e.g., HTML-injection payloads via a malicious upstream proxy's `X-Forwarded-Prefix`) now fall back to empty and emit a warning instead of being reflected into Swagger/Redoc HTML script tags. Threat model: not an issue in default ASGI deployments; only matters if `ProxyHeadersMiddleware` trusts upstream prefix headers. |
| 36 | +- **OpenTelemetry endpoint with `insecure=True` emits a warning for non-local hosts** (SEC-2). New `OpenTelemetryConfig.__post_init__` parses the endpoint (handles both `host:port` and `scheme://host:port` forms, including IPv6 brackets and `unix://`) and warns when traces would ship unencrypted to a non-`localhost`/`127.0.0.1`/`::1`/`unix://` target. |
| 37 | +- **`CorsConfig` rejects unsafe wildcard + credentials combos at construction** (SEC-3). `cors_allowed_credentials=True` combined with `cors_allowed_origins=["*"]` (or a permissive regex like `".*"`/`".+"`) is the canonical CORS misconfiguration — browsers reject the response. Now raises `ConfigurationError` immediately instead of silently building a non-functional CORS layer. **See Behavior changes below.** |
| 38 | + |
| 39 | +### Hygiene & process (PR #110) |
| 40 | + |
| 41 | +- **Missing-dependency events now log via stdlib `logging` in addition to `warnings.warn`** (UX-4). Users running under `python -W ignore` or `PYTHONWARNINGS=ignore` previously saw nothing when a configured instrument's optional dep was missing. The new `logger.warning` line on `lite_bootstrap.bootstrappers.base` is unaffected by warning filters. |
| 42 | + |
| 43 | +## Behavior changes |
| 44 | + |
| 45 | +Two changes could affect existing code in ways the previous release wouldn't have. Both fix real bugs; surfaced here so you can audit. |
| 46 | + |
| 47 | +1. **User-supplied `FastAPI` instance retains its own `title`, `debug`, `version`.** If you were relying on lite-bootstrap to overwrite these from `service_name`/`service_debug`/`service_version` after handing it a pre-built `FastAPI()`, you'll now see your original values. Migration: set these on your `FastAPI()` directly, or use `FastAPIConfig(application_kwargs={...})` to have lite-bootstrap construct the app. |
| 48 | + |
| 49 | +2. **`CorsConfig(cors_allowed_origins=["*"], cors_allowed_credentials=True)` now raises `ConfigurationError`.** This combo was never functional — browsers reject responses with `Access-Control-Allow-Credentials: true` and `Access-Control-Allow-Origin: *`. If your code constructed this combo and worked anyway (because the credentials header was silently dropped by FastAPI's CORSMiddleware), construction now fails with a clear message. Migration: enumerate allowed origins explicitly, or set `cors_allowed_credentials=False`. |
| 50 | + |
| 51 | +## New constraints documented |
| 52 | + |
| 53 | +Three lifecycle constraints surfaced by the audit are now documented in `README.md` and `CLAUDE.md`: |
| 54 | + |
| 55 | +- **One bootstrapper per application instance.** Second construction emits a warning and skips re-attachment (see LOG-7/LOG-8 above). |
| 56 | +- **One `OpenTelemetryInstrument` per process.** The OTel SDK enforces `set_tracer_provider` as set-once via `_TRACER_PROVIDER_SET_ONCE.do_once(...)` (verified against `opentelemetry/trace/__init__.py:548-556`); `teardown()` cannot reset the global pointer. |
| 57 | +- **`__post_init__` cascade invariant.** Every config-class `__post_init__` must call `super().__post_init__()`. `BaseConfig` ships a no-op as the chain terminator. `FastAPIConfig` uses the explicit `super(FastAPIConfig, self).__post_init__()` form because `@dataclass(slots=True)` breaks bare `super()`. |
| 58 | + |
| 59 | +## CI changes |
| 60 | + |
| 61 | +- **`security-audit.yml` workflow added.** `pip-audit` runs on every PR and weekly via cron against the lockfile (`uv export --all-extras --no-hashes`). The default-branch run is purely informational; PRs that introduce CVEs will block until resolved. |
| 62 | +- **`InstrumentSkippedWarning` escalated to error in tests.** Any unexpected emission outside a `pytest.warns(...)` block now fails the test (registered via `pytest_configure()` in `tests/conftest.py`; can't live in `pyproject.toml` because that import order breaks pytest-cov tracing). |
| 63 | + |
| 64 | +## Backwards compatibility |
| 65 | + |
| 66 | +Aside from the two *Behavior changes* called out above, every public API behaves identically. New fields default to their old behavior (`prometheus_collector_registry=None` → fresh registry as before; `opentelemetry_excluded_urls=[]` → empty set as before). New warnings/validators trigger only on configurations that were already broken or risky. |
| 67 | + |
| 68 | +The 26-fix list with full file:line references and rationale is in: |
| 69 | + |
| 70 | +- `planning/specs/2026-06-05-bug-audit-v2.md` — the audit |
| 71 | +- `planning/specs/2026-06-05-bug-audit-v2-sequencing.md` — the 3-PR breakdown |
| 72 | +- `planning/specs/2026-06-05-bug-audit-v2-retro.md` — what the cycle taught us |
| 73 | + |
| 74 | +## References |
| 75 | + |
| 76 | +- Audit: [`planning/specs/2026-06-05-bug-audit-v2.md`](../specs/2026-06-05-bug-audit-v2.md) |
| 77 | +- Sequencing: [`planning/specs/2026-06-05-bug-audit-v2-sequencing.md`](../specs/2026-06-05-bug-audit-v2-sequencing.md) |
| 78 | +- Retro: [`planning/specs/2026-06-05-bug-audit-v2-retro.md`](../specs/2026-06-05-bug-audit-v2-retro.md) |
| 79 | +- PRs: [#108](https://github.com/modern-python/lite-bootstrap/pull/108), [#109](https://github.com/modern-python/lite-bootstrap/pull/109), [#110](https://github.com/modern-python/lite-bootstrap/pull/110), [#111](https://github.com/modern-python/lite-bootstrap/pull/111) |
0 commit comments