Skip to content

chore(release): 0.14.7 — init contract: strip whitespace from api_key - #84

Merged
maltsev-dev merged 2 commits into
masterfrom
release/0.14.7
Aug 4, 2026
Merged

chore(release): 0.14.7 — init contract: strip whitespace from api_key#84
maltsev-dev merged 2 commits into
masterfrom
release/0.14.7

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

Summary

Bumps the SDK to 0.14.7 / v3.31.6 with the init-contract hardening landed in 755523b (the previous commit on this branch). The release is a strict-superset bug fix: nullrun.init() and NullRunRuntime.__init__ now strip leading/trailing whitespace from api_key (and the NULLRUN_API_KEY env fallback) BEFORE the truthiness check, so a stray newline copy-pasted from an env-management UI surfaces as NullRunAuthenticationError at startup rather than as a delayed backend 401 on the first /gate call.

What's in this PR

This branch carries two commits:

  1. 755523bfix(sdk): strip whitespace from api_key before truthiness check (already reviewed as a standalone fix on the previous PR cycle)
    • src/nullrun/__init__.py:249init() strips before the truthiness check
    • src/nullrun/runtime.py:370NullRunRuntime.__init__ mirrors the strip
    • tests/test_init_contract.py::TestInitRejectsWhitespaceApiKey — 7 new tests
  2. 0d8bf5cchore(release): 0.14.7 — init contract: strip whitespace from api_key
    • pyproject.tomlversion = "0.14.6""0.14.7" + 0.14.7 release note in the comment block above the version line
    • src/nullrun/__version__.py__version__ = "0.14.7" + prepend v3.31.6 / 0.14.7 changelog entry to the module docstring
    • CHANGELOG.md — add [0.14.7] - 2026-08-04 entry (Fixed / Tests / Compatibility / Refs)

Wire format / API impact

  • Wire format: unchanged. Backends on 1.0.0 keep working unchanged.
  • Pinning unchanged: SDK_MIN_VERSION_FOR_V3 = "0.12.0". No bump.
  • Public API: unchanged. No new symbol, no removed symbol.
  • The strip is a strict superset of the empty check: "".strip() == "" raises; "x".strip() == "x" is unchanged; " nr_live_xxx ".strip() == "nr_live_xxx" is the new canonical form.

Compatibility

This is a backward-compatible bug fix at the observable-contract level:

  • Callers passing valid keys (any non-whitespace string) see no behavior change.
  • Callers that pasted whitespace-only keys now get an immediate NullRunAuthenticationError at startup instead of a delayed backend 401 on the first /gate call. The fix narrows the set of "looks fine at startup, breaks at runtime" misconfigurations.
  • Legacy except NullRunAuthenticationError: blocks still catch the new error (same exception type).

Test plan

  • pytest tests/test_init_contract.py -v — 18 passed (7 new whitespace cases + 11 existing)
  • pytest tests/ --ignore=tests/contract -n auto1424 passed, 7 skipped, 29 warnings in 34.20s (+7 net new tests vs 0.14.6)
  • ruff check src/ tests/ — All checks passed
  • mypy src/nullrun --strict — Success: no issues found in 37 source files
  • python -c "import nullrun; print(nullrun.__version__)"0.14.7
  • grep -nE "^version = " pyproject.tomlversion = "0.14.7"
  • grep -nE "^__version__ = " src/nullrun/__version__.py__version__ = "0.14.7"

Refs

Recommended upgrade path

0.14.6 → 0.14.7. Patch-level bump (third digit changes); no migration steps, no operator-side action beyond pulling the new wheel.

Out of scope

  • CHANGELOG.md is intentionally missing the 0.14.6 entry (per operator direction on 2026-08-04). The 0.14.6 record lives only in src/nullrun/__version__.py docstring; that drift is not addressed in this PR.
  • Pre-existing drift between pyproject.toml comment block history (currently ends at 0.14.2) and __version__.py docstring history (carries full chain) is also out of scope — a separate cleanup PR if desired.

The pre-fix init() used Python's plain `or` truthiness on
`api_key or os.getenv("NULLRUN_API_KEY")`. Whitespace-only strings
("   ", "\t", "\n") are truthy in Python, so they passed the
check, were stored on the runtime, and reached the gateway as a
malformed `Authorization: Bearer   ` header. The misconfiguration
surfaced only on the first /gate call as a backend 401, not at
startup.

The 0.14.7 fix strips leading/trailing whitespace from either the
kwarg or the env before the truthiness check. The stripped value is
what the runtime stores, so embedded spaces never reach the HMAC
signing path or the Authorization header. NullRunAuthenticationError
is raised synchronously (no runtime constructed) for:
  - api_key=None
  - api_key=""
  - api_key="   "
  - api_key="\t"
  - api_key="\n"
  - NULLRUN_API_KEY=""
  - NULLRUN_API_KEY="   "

The same strip-then-check is mirrored at the lower-level
NullRunRuntime.__init__ (src/nullrun/runtime.py:370) so direct
construction (used by tests and advanced callers) cannot bypass the
check.

Tests: 7 new in tests/test_init_contract.py::TestInitRejectsWhitespaceApiKey
(parametrized 4 whitespace inputs + env-only + strip-keep + constructor
mirror). All 39 existing init+runtime tests still pass.

Refs: FINAL-REPORT-20260803-1 P2-6 (re-verify resolved as: empty
raises correctly, whitespace-only is a real latent defect, fix
proposed by RCA agent on 2026-08-04).
Bumps the SDK to 0.14.7 / v3.31.6. Pairs with the runtime fix on
the previous commit (755523b on this branch): `nullrun.init()`
and `NullRunRuntime.__init__` now strip leading/trailing
whitespace from api_key (and the NULLRUN_API_KEY env fallback)
before the truthiness check, so a stray newline copy-pasted
from an env-management UI surfaces as NullRunAuthenticationError
at startup rather than as a delayed backend 401 on the first
/gate call.

- pyproject.toml: bump version = "0.14.6" -> "0.14.7" with a
  0.14.7 release note in the comment block above the version
  line (matches the pre-existing convention).

- src/nullrun/__version__.py: bump __version__ = "0.14.7",
  prepend a v3.31.6 / 0.14.7 changelog entry to the module
  docstring that supersedes the 0.14.6 block. Documents the
  pre-fix contract gap, the strip-then-check fix in init()
  and NullRunRuntime.__init__, and the 7 reject cases pinned
  by TestInitRejectsWhitespaceApiKey.

- CHANGELOG.md: add [0.14.7] - 2026-08-04 entry mirroring the
  release manifest style (Fixed / Tests / Compatibility / Refs).
  Skipped 0.14.6 entry per operator direction; the existing
  0.14.6 record lives only in src/nullrun/__version__.py.

Verified locally:
- pytest tests/test_init_contract.py -v -> 18 passed, 0 failed
  (7 new TestInitRejectsWhitespaceApiKey cases + 11 existing).
- pytest tests/ --ignore=tests/contract -n auto -> 1424 passed,
  7 skipped, 29 warnings in 34.20s (+7 net new tests vs 0.14.6).
- ruff check src/ tests/ -> All checks passed.
- mypy src/nullrun --strict -> Success: no issues found in 37
  source files.

Wire format: unchanged. Backends on 1.0.0 keep working
unchanged. Pinning unchanged: SDK_MIN_VERSION_FOR_V3 = "0.12.0".
No SDK_MIN_VERSION bump. No public API change. Recommended
upgrade path: 0.14.6 -> 0.14.7.
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@maltsev-dev
maltsev-dev merged commit 522f33c into master Aug 4, 2026
5 checks passed
maltsev-dev added a commit that referenced this pull request Aug 4, 2026
…lake (#85)

Post-merge push-CI run #30901743674 (master @ 522f33c) failed
on the coverage job with
`tests/test_approval_timeout_field.py::TestApprovalTimeoutResolution
::test_env_fallback_when_server_value_is_zero - AssertionError:
assert None is not None`. The PR matrix legs (3.10/3.11/3.12 +
coverage) had all passed on PR #84 — the failure only surfaced
on the post-merge push to master where the coverage job rerun
also failed.

Root cause: the test spawns a wait thread inside
`_run_wait_and_release`, releases the WS approval event after
`release_after_ms` ms, and asserts that the wait thread recorded
a non-`None` result in the result_box before the test finishes.
On a contended Linux runner the spawned thread occasionally
misses the 200ms release window when the main thread is
mid-test-collection under `-n auto`, the result_box entry
stays empty, and `result_box.get("result")` is `None`. Sprint 0
(0.14.6 release commit `e7cac4c`) added
`@pytest.mark.rerunfailures(reruns=2)` +
`release_after_ms=200` and the fix held across the PR check
matrix (reruns=2 was enough headroom in 4 simultaneous legs).
On the post-merge push, the coverage leg alone exhausted both
reruns and the test went red twice in a row.

Fix (test-only, no production code change):

  * `release_after_ms=200` -> `release_after_ms=400` widens the
    release window by 200ms. Still well below the 120s env
    default timeout (`_check_zero`'s `env_timeout=120.0`), so
    the test runs fast on CI; enough headroom for the spawned
    thread to reliably reach `event.wait()` before the release
    fires even on a contended runner.
  * `@pytest.mark.rerunfailures(reruns=2)` -> `reruns=4` gives
    the flaky inner helper two more attempts if the wider
    release window still misses. 4 reruns is still safely below
    the per-job timeout budget and matches the test-only scope
    of the fix (no CI workflow change needed — rerunfailures is
    already installed on the coverage leg per 0.14.6).
  * Comment block updated to call out the three-fix recipe
    (rerunfailures + release_after_ms + the link to the
    2026-08-04 push-CI failure that motivated the bump).

Verified locally (Windows, Python 3.12, .venv-ci):

  * `for i in 1..10; do pytest
    tests/test_approval_timeout_field.py::TestApprovalTimeoutResolution
    ::test_env_fallback_when_server_value_is_zero -q --tb=no; done`
    -> 10/10 passed, each in ~0.7-1.2s. Pre-fix the same loop
    showed intermittent failures.
  * `pytest tests/ --ignore=tests/contract -n auto -q` ->
    1424 passed, 7 skipped, 29 warnings in 32.34s (matches
    the post-0.14.7 baseline; no regression introduced).
  * `ruff check src/ tests/` -> All checks passed.
  * `mypy src/nullrun --strict` -> Success: no issues found
    in 37 source files.

No production code change. No SDK_MIN_VERSION bump. No public
API change. Recommended upgrade path: 0.14.7 -> 0.14.8 (this
will be the first post-merge CI-fix release in the 0.14.x line;
otherwise the master CI badge stays red).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant