Skip to content

fix(build): align pyproject.toml version with __version__.py + skip-existing on TestPyPI#50

Merged
maltsev-dev merged 1 commit into
masterfrom
fix/publish-version-mismatch
Jul 3, 2026
Merged

fix(build): align pyproject.toml version with __version__.py + skip-existing on TestPyPI#50
maltsev-dev merged 1 commit into
masterfrom
fix/publish-version-mismatch

Conversation

@maltsev-dev

Copy link
Copy Markdown
Member

What

The publish-test workflow at commit 79a6e7e (PR #49) failed with
HTTPError: 400 Bad Request — File already exists. Two issues, one
fix each:

  1. Version drift: feat(sdk) bumped src/nullrun/__version__.py
    to 0.12.0 but left version in pyproject.toml at 0.11.0.
    Hatchling uses pyproject.toml's version, so the wheel was built
    as nullrun-0.11.0-py3-none-any.whl — the same name as the 0.11.0
    artifact already on TestPyPI from a previous successful publish
    (at 18a91e2).
  2. No skip-existing guard: pypa/gh-action-pypi-publish rejects
    same-hash re-uploads with HTTP 400. Adding skip-existing: true
    makes a re-run of the same SHA a no-op (matching twine's
    --skip-existing). Production PyPI cannot overwrite anyway.

Why

  • The 0.12.0 wheel is a new artifact (different hash), so it should
    upload cleanly once the version is corrected.
  • skip-existing is defensive: even if the build ever produces a
    duplicate of an already-published artifact (e.g. a re-run of an
    unchanged SHA), the workflow will not fail.

How

pyproject.toml version field: 0.11.00.12.0 with a comment
explaining the prior drift so the next version bump doesn't repeat it.

.github/workflows/publish-test.yml: add skip-existing: true to the
pypa/gh-action-pypi-publish@release/v1 step.

Test plan

Local verification (.venv-ci, Python 3.14):

  • python -m build --wheel: wheel is now
    nullrun-0.12.0-py3-none-any.whl (was nullrun-0.11.0-…).
  • Wheel METADATA sanity:
Name: nullrun
Version: 0.12.0
Author: Anatolii Maltsev
Author-email: support@nullrun.io
Maintainer-email: "nullrun.io" <support@nullrun.io>

The Author field is still populated correctly by the metadata hook
from PR #49 — version bump didn't regress it.

Risk

  • Version bump to 0.12.0 on PyPI: this aligns the build artifact
    with the runtime __version__ that feat(sdk) already shipped.
    If a 0.12.0 was somehow intended to be a different release than
    the one in PR fix(sdk): make Author field non-empty + v3 capability probe #49, this would conflict — but per the commit
    message ("Bumped 0.11.0 → 0.12.0"), the intent was always 0.12.0.
  • skip-existing on TestPyPI: only suppresses the 400 on
    duplicates. A genuinely new artifact still uploads. Production
    PyPI is unaffected (immutable versions there).

Checklist

  • My change does not introduce new lint warnings
  • I have considered backwards compatibility

…→ 0.12.0)

The `feat(sdk)` commit (79a6e7e, PR #49) bumped
`src/nullrun/__version__.py` to 0.12.0 but left `version` in
`pyproject.toml` at 0.11.0. Hatchling uses `pyproject.toml`'s
version, so `python -m build` was producing a wheel named
`nullrun-0.11.0-py3-none-any.whl` — same name as the 0.11.0
artifact that `publish-test` had already uploaded to TestPyPI
on its previous successful run (commit 18a91e2).

TestPyPI rejects re-uploads of the same wheel hash with HTTP 400
"File already exists" (no overwrite semantics), so the
`publish-test` workflow failed at the very last step. Verify
locally:

  $ python -m build --wheel
  Successfully built nullrun-0.12.0-py3-none-any.whl

Also adds `skip-existing: true` to the
`pypa/gh-action-pypi-publish` step so a re-run of the same
SHA becomes a no-op (matching twine's --skip-existing). Production
PyPI cannot overwrite anyway, so this flag is harmless there too.
@maltsev-dev maltsev-dev merged commit 1cc2d1e into master Jul 3, 2026
4 checks passed
@maltsev-dev maltsev-dev deleted the fix/publish-version-mismatch branch July 3, 2026 15:07
@codecov

codecov Bot commented Jul 3, 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 added a commit that referenced this pull request Jul 4, 2026
…-> 0.12.1)

Same drift pattern as #50: the runtime commit bumped
src/nullrun/__version__.py to 0.12.1 but missed this field. Without
the sync-up `python -m build` would publish a `nullrun-0.12.0-*`
wheel that PyPI Trusted Publishing rejects with HTTP 400 "File
already exists", blocking the publish workflow that fires on
`v*` tags.

Refreshes the inline comment to point at the current bump and
explains why the fix had to be applied again.
maltsev-dev added a commit that referenced this pull request Jul 4, 2026
#51)

* release(0.12.1): wire server-minted execution_id through /check -> /track

Bug-fix release. The 0.12.0 changelog claimed the SDK propagates the
server-minted execution_id from /check to /track but the wiring was
never shipped -- the SDK still sent client-supplied ids on
/track/batch and ignored reservation_id on /check responses.

Closes the four gaps documented in docs/sdk-v3-migration-gaps.md:

* check_workflow_budget() now reads response["reservation_id"] into
  a contextvar (nullrun.context._server_minted_execution_id_var).
* New helpers set_/get_/reset_/clear_server_minted_execution_id plus
  a paired _server_minted_reservation_at timestamp for the 295s TTL
  guard.
* _enrich_event stamps execution_id on the /track payload while the
  captured reservation is fresh; past the 295s safety window it
  drops and clears the capture so a doomed id never ships to /track
  (which would 503 RESERVATION_NOT_FOUND -- CLAUDE.md section 33).
* _route_track dispatches llm_call events to the v3 /api/v1/track
  single-event endpoint via Transport.track_single() so backend
  gate_consume_v3 validates the consume-vs-reserve + epsilon
  invariant (CLAUDE.md section 25). Span / tool events keep using
  the legacy /api/v1/track/batch. NULLRUN_V3_TRACK_DISABLE=1 forces
  everything through the legacy batch path (backends still on
  v1/v2).

Adds 27 contract tests in tests/test_v3_server_minted.py covering
contextvar hygiene, capture defence-in-depth, _enrich_event age
threshold, _route_track dispatch, and end-to-end /gate -> /track
round trip.

* fix(build): align pyproject.toml version with __version__.py (0.12.0 -> 0.12.1)

Same drift pattern as #50: the runtime commit bumped
src/nullrun/__version__.py to 0.12.1 but missed this field. Without
the sync-up `python -m build` would publish a `nullrun-0.12.0-*`
wheel that PyPI Trusted Publishing rejects with HTTP 400 "File
already exists", blocking the publish workflow that fires on
`v*` tags.

Refreshes the inline comment to point at the current bump and
explains why the fix had to be applied again.
maltsev-dev added a commit that referenced this pull request Jul 5, 2026
…e on decisions (#53)

* fix(sdk): drift.md 2026-07-04 - wire idempotency_key + preserve status code + fail-CLOSED honesty

Three SDK-side fixes for drift.md (2026-07-04) P1 items + open Q4.
The remaining drift items (P0-1, P0-2, P0-3, P0-4, P1-3, P1-4) are
docs-only - SDK code is correct, SDK_README.md is wrong. Those need a
README rewrite, not a code fix.

F1 (drift.md P1-5 + open Q4): wire idempotency_key on /track v3 single-event
- new contextvar get_server_minted_idempotency_key + symmetric set/reset/clear
- _capture_server_minted_execution_id now also reads response["operation_id"]
  (which equals the /check idempotency_key, runtime.py:1260)
- _enrich_event stamps it onto the wire_event for llm_call
- _build_v3_track_payload propagates onto the v3 /track payload (with
  contextvar fallback for tests / direct callers)
- why: without this, transport-level retry on the SAME event either
  re-runs CONSUME_SCRIPT (-> 503 RESERVATION_NOT_FOUND since reservation
  key was DEL-ed after first consume per CLAUDE.md sec 25) or double-bills

F2 (drift.md P1-1): HTTP status_code on every decision exception
- NullRunBlockedException / NullRunBudgetError / NullRunChainError /
  NullRunWorkflowInactiveError / NullRunConsumeOverbudgetError accept
  status_code parameter
- _parse_v3_error_envelope populates status_code from response.status_code
  for every branch (402 budget, 403 workflow/chain cross-org, 422
  CONSUME_OVERBUDGET, 503 RATE_LIMIT_REDIS_UNAVAILABLE, ...)
- why: FastAPI exception handlers reading exc.status_code previously got
  None / 500 for budget blocks (the backend's 402 was lost in the
  NullRunBudgetError -> NullRunBlockedException constructor chain)

F3 (drift.md P1-2): fail-CLOSED/OPEN honesty in module-top docstring
- runtime.py docstring table now distinguishes SDK-side transport failure
  (network/5xx/breaker open -> fail-OPEN on /check path) from wire 4xx/5xx
  that names an enforcement failure (BUDGET_REDIS_UNAVAILABLE -> 402
  fail-CLOSED; RATE_LIMIT_REDIS_UNAVAILABLE -> 503 fail-CLOSED)
- why: SDK_README claim "Fail-OPEN na infrastructure failures" was
  half-wrong - conflated two different failure modes. The README fix
  belongs in the docs rewrite (out of scope here).

Tests: tests/test_drift_fixes_2026_07_04.py - 15 tests, all pass.
- F1: 5 tests pinning contextvar lifecycle + payload shape
- F2: 8 tests pinning status_code on every decision exception class
- F3: 2 tests pinning fail-CLOSED on Redis-unavailable wire responses

Regression: 140+70 passed in targeted critical-path suites
(test_v3_server_minted, test_error_envelope, test_handle, test_protect,
test_capabilities, test_drift_fixes_2026_07_04, test_runtime_branches,
test_transport_branches, test_integration_contract,
test_high_reliability_fixes). No regression.

Per scripts-commit-no-push rule: commit locally, NOT push.

* release(0.13.0): drift-fixes — idempotency_key on /track + status_code on decisions + patch coverage

Three SDK-side fixes per docs/drift.md (2026-07-04) P1 items + open
Q4. The remaining drift items (P0-1 / P0-2 / P0-3 / P0-4 / P1-3 /
P1-4) are README-only — SDK code is correct, SDK_README.md is
wrong. Those go in a separate README rewrite PR; do not block this
release.

Plus a 4th fix that was missing on 0.12.2: this commit also closes
the codecov/patch-coverage gap that dragged PR #52 below the 70%
floor.

1. Idempotency-key propagation to /track v3 single-event (P1-5 + Q4)

* `_capture_server_minted_execution_id` now also reads
  `response["operation_id"]` (which equals the /check
  `idempotency_key`, runtime.py:1260).
* `_enrich_event` stamps the value onto `wire_event` for `llm_call`.
* `_build_v3_track_payload` propagates it onto the v3 /track body
  with a contextvar fallback for tests + direct callers.

Why: without this, transport-level retry on the same event either
(a) re-runs CONSUME_SCRIPT -> 503 RESERVATION_NOT_FOUND since the
reservation key was DEL-ed after the first consume per CLAUDE.md
§25, or (b) double-bills the underlying budget.

2. status_code preserved on every decision exception (P1-1)

* NullRunBlockedException / NullRunBudgetError / NullRunChainError
  / NullRunWorkflowInactiveError / NullRunConsumeOverbudgetError
  now accept status_code: int | None = None.
* _parse_v3_error_envelope populates it from response.status_code
  for every branch: 402 budget, 403 workflow/chain cross-org, 422
  CONSUME_OVERBUDGET, 503 RATE_LIMIT_REDIS_UNAVAILABLE, etc.

Why: FastAPI exception handlers reading `exc.status_code` previously
got None / 500 for budget blocks — the backend's 402 was lost in
the NullRunBudgetError -> NullRunBlockedException constructor chain.

3. fail-CLOSED / fail-OPEN honesty in the runtime.py module docstring
   (P1-2)

Distinguishes SDK-side transport failure (network / 5xx / breaker
open -> fail-OPEN on the /check path) from wire 4xx/5xx that names
an enforcement failure (BUDGET_REDIS_UNAVAILABLE -> 402 fail-CLOSED;
RATE_LIMIT_REDIS_UNAVAILABLE -> 503 fail-CLOSED). The README had
conflated the two with a single "Fail-OPEN on infra failures"
claim; README rewrite is tracked separately under drift.md P0-1.

4. Patch-coverage gap (regression fix from 0.12.2)

* tests/test_v3_wire_contract.py::TestGateCacheRuntimeFlow — 3
  runtime-level chain-mode cache tests that drive
  NullRunRuntime.check_workflow_budget inside
  `with workflow(...) + with chain(...)`. Covers
  runtime.py:1287-1310 (cache_enabled predicate, cache key, cache
  hit/miss branches, NULLRUN_GATE_CACHE_DISABLE=1 bypass).
* These cover the exact range that dragged PR #52 codecov/patch
  below 70%.

Files in this commit

* src/nullrun/__version__.py — bumped 0.12.2 -> 0.13.0 + 0.13.0
  release block in the docstring.
* pyproject.toml — version = "0.13.0" + drift-release comment
  (drift-prevention, same pattern as #50).
* CHANGELOG.md — new [0.13.0] - 2026-07-04 section preceding
  [0.12.2].
* docs/drift.md — NEW audit document (the file referenced by the
  fix(sdk) commit message).
* tests/test_v3_wire_contract.py — 190 lines of TestGateCacheRuntimeFlow
  (3 tests) appended to the existing TestGateCache class block.

Tests: 140+70 critical-path tests pass (test_v3_server_minted,
test_error_envelope, test_handle, test_protect, test_capabilities,
test_drift_fixes_2026_07_04, test_runtime_branches,
test_transport_branches, test_integration_contract,
test_high_reliability_fixes). The 3 new TestGateCacheRuntimeFlow
tests are confirmed green on local pytest pre-commit. No regression
on test_drift_fixes_2026_07_04 (15 fix(sdk) tests).

Backends on 1.0.0 keep working unchanged. Pinning unchanged:
SDK_MIN_VERSION_FOR_V3 = "0.12.0". Recommended upgrade path:
0.12.2 -> 0.13.0 (no on-wire breaking change).

* fix(version): close orphan docstring so 0.13.0 section parses

CI on PR #53 (`test (3.10/3.11/3.12)` all FAIL, coverage 0.05%)
surfaced a SyntaxError collected in `src/nullrun/__version__.py`
line 86:

  v3.13 / 0.13.0 (2026-07-04) — drift-fixes release: closes the SDK-side
                         ^
  SyntaxError: leading zeros in decimal integer literals are not
  permitted; use an 0o prefix for octal integers

Root cause: in the 0.13.0 release commit I closed the module
docstring with `"""` after the 0.12.2 section and then started
writing the 0.13.0 section as if it were still inside a docstring —
Python parsed `v3.13 / 0.13.0 (2026-07-04) — drift-fixes ...` as
module-level expressions, barfed on `(2026-07-04)` (the `07` is a
leading-zero integer literal — illegal in Python 3), and the rest
of the test collection cascade-failed with 64 collection errors.
Coverage hit 0.05% because pytest couldn't even collect, not
because tests regressed.

Fix: drop the orphan `"""` between the 0.12.2 and 0.13.0 sections
and replace it with a `---` separator (the docstring stays open
all the way to the module-final `"""` above `__version__ = "0.13.0"`).

Verified locally:
  $ python -c "import src.nullrun.__version__; print(__version__.__file__)"
  (no SyntaxError, imports cleanly)

`grep -c '"""' src/nullrun/__version__.py` now reports 2
(open + close), as expected for a single module docstring.

* fix(lint): sort server_minted_idempotency_key import per ruff I001

CI on PR #53 (`test (3.11)` failure, others matrix-cancelled at
fail-fast) surfaced `ruff check src/ I001` on the import block
inside `NullRunRuntime._capture_server_minted_execution_id`:

  I001 Import block is un-sorted or un-formatted
      --> src/nullrun/runtime.py:2590:5

The block (added in the `fix(sdk)` commit) imported context
helpers in this order:

  set_server_minted_execution_id
  set_server_minted_reservation_at
  set_server_minted_idempotency_key

Ruff's alphabetic sort puts `idempotency` before `reservation`,
so the fourth line is sorted up to the third slot. Pure
cosmetic — no behaviour change, same set of imports.

`ruff check src/` after fix: All checks passed.

Verified on Python 3.10 / 3.11 / 3.12 locally before commit.
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