From 6c3a54e154d76191a267652ba74fb566d4b85d35 Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Wed, 21 Oct 2026 19:40:48 +0200 Subject: [PATCH 1/6] Move the Hugging Face hub fix into the 0.3.0 changelog --- CHANGELOG.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae89cc9..9dd9397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,6 @@ under Unreleased and move into a version section at release time. ## Unreleased -### Fixed - -- Enabling the `huggingface` hub no longer imports unused transformers model - modules. To find references to `hf_hub_download` and `snapshot_download`, - the hub tracker looked up the name on every loaded module, and transformers' - lazy modules import code on lookup. With transformers loaded this imported - about 140 extra modules, took about 0.15 s and logged about 430 "failed to - patch" debug lines. The tracker now reads each module's namespace instead. - ## 0.3.0 - 2026-10-21 ### Added @@ -142,6 +133,12 @@ under Unreleased and move into a version section at release time. queue, so events were duplicated and, depending on timing, later events could be removed without being sent. This showed up when a process started with events saved by an earlier run and flushed soon after. +- Enabling the `huggingface` hub no longer imports unused transformers model + modules. To find references to `hf_hub_download` and `snapshot_download`, + the hub tracker looked up the name on every loaded module, and transformers' + lazy modules import code on lookup. With transformers loaded this imported + about 140 extra modules, took about 0.15 s and logged about 430 "failed to + patch" debug lines. The tracker now reads each module's namespace instead. ## 0.2.2 - 2026-09-30 From 1c14a3ca077b62689f843d9a871c842a4332854b Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Thu, 22 Oct 2026 15:41:04 +0200 Subject: [PATCH 2/6] Cap error_message and provider_error_code for manual track_error calls The integrations cut error messages to ERROR_MSG_MAX_LEN and provider codes to PROVIDER_ERROR_CODE_MAX_LEN before calling track_error(). Manual calls sent the full text. ErrorEvent now applies both limits when serializing, so every error event gets them. --- CHANGELOG.md | 4 ++++ tests/test_event_serialization.py | 12 ++++++++++++ wildedge/events/error.py | 15 +++++++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd9397..f1e727b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,10 @@ under Unreleased and move into a version section at release time. ### Fixed +- `track_error()` now caps `error_message` at 200 characters and + `provider_error_code` at 64, the limits the integrations already used. + Manual calls sent the full text, so a long exception message could make + large error events. - The llama.cpp integration now tracks `Llama.create_chat_completion()` and `Llama.create_completion()`, streaming or not. It used to patch only `Llama.__call__`, so chat apps on llama-cpp-python reported model loads but diff --git a/tests/test_event_serialization.py b/tests/test_event_serialization.py index 0b0d0aa..74e6a95 100644 --- a/tests/test_event_serialization.py +++ b/tests/test_event_serialization.py @@ -1,5 +1,6 @@ from __future__ import annotations +from wildedge import constants from wildedge.events import common from wildedge.events.error import ErrorCode, ErrorEvent from wildedge.events.feedback import FeedbackEvent, FeedbackType @@ -77,6 +78,17 @@ def test_feedback_event_enum_and_string_forms(): assert string_event.to_dict()["feedback"]["feedback_type"] == "reject" +def test_error_event_caps_message_and_provider_code(): + event = ErrorEvent( + model_id="m1", + error_code=ErrorCode.UNKNOWN, + error_message="x" * 5000, + provider_error_code="c" * 500, + ).to_dict()["error"] + assert len(event["error_message"]) == constants.ERROR_MSG_MAX_LEN + assert len(event["provider_error_code"]) == constants.PROVIDER_ERROR_CODE_MAX_LEN + + def test_span_event_to_dict_includes_required_fields(): event = SpanEvent( kind="tool", diff --git a/wildedge/events/error.py b/wildedge/events/error.py index c5272c8..4ba5dd3 100644 --- a/wildedge/events/error.py +++ b/wildedge/events/error.py @@ -6,6 +6,7 @@ from enum import Enum from typing import Any +from wildedge import constants from wildedge.events.common import add_optional_fields @@ -19,6 +20,10 @@ class ErrorCode(str, Enum): UNKNOWN = "UNKNOWN" +def truncate(value: str | None, limit: int) -> str | None: + return None if value is None else str(value)[:limit] + + @dataclass class ErrorEvent: model_id: str @@ -49,11 +54,17 @@ def to_dict(self) -> dict: error_data = add_optional_fields( {"error_code": code}, { - "error_message": self.error_message, + # Capped here so manual track_error() calls get the same + # limits as the integrations. + "error_message": truncate( + self.error_message, constants.ERROR_MSG_MAX_LEN + ), "stack_trace_hash": self.stack_trace_hash, "related_event_id": self.related_event_id, "http_status": self.http_status, - "provider_error_code": self.provider_error_code, + "provider_error_code": truncate( + self.provider_error_code, constants.PROVIDER_ERROR_CODE_MAX_LEN + ), "duration_ms": self.duration_ms, }, ) From 451ef866795443daa70ae5d33d3b9c9c8c708cf9 Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Thu, 22 Oct 2026 15:57:52 +0200 Subject: [PATCH 3/6] Keep the start and end of long error messages, up to 500 characters Measured across 37 typical failures, 9 messages were over 200 characters. Several put their cause at the end. mlx-whisper audio errors start with the ffmpeg banner, and torch state_dict errors end with the shapes. The integrations kept only the first 200 characters. ErrorEvent now keeps the start and the end of a message over 500 characters, with an ellipsis between. The integrations and decorators no longer cut the message themselves, so the limit is applied once. --- CHANGELOG.md | 10 ++++++---- tests/test_event_serialization.py | 16 ++++++++++++++++ wildedge/constants.py | 2 +- wildedge/decorators.py | 7 ++----- wildedge/events/error.py | 25 ++++++++++++++++++++++--- wildedge/integrations/common.py | 2 +- wildedge/integrations/gguf.py | 4 ++-- wildedge/integrations/keras.py | 4 ++-- wildedge/integrations/mlx.py | 6 +++--- wildedge/integrations/onnx.py | 4 ++-- wildedge/integrations/tensorflow.py | 6 +++--- wildedge/integrations/transformers.py | 4 ++-- wildedge/integrations/ultralytics.py | 4 ++-- 13 files changed, 64 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1e727b..fdbfe59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,10 +67,12 @@ under Unreleased and move into a version section at release time. ### Fixed -- `track_error()` now caps `error_message` at 200 characters and - `provider_error_code` at 64, the limits the integrations already used. - Manual calls sent the full text, so a long exception message could make - large error events. +- Every error event now caps `error_message` at 500 characters, keeping the + start and the end of a longer message. Integrations used to cut it to the + first 200, which lost the cause in messages that end with it, such as + mlx-whisper audio errors that start with the ffmpeg banner. Manual + `track_error()` calls had no limit at all. `provider_error_code` is capped + at 64 characters for every event. - The llama.cpp integration now tracks `Llama.create_chat_completion()` and `Llama.create_completion()`, streaming or not. It used to patch only `Llama.__call__`, so chat apps on llama-cpp-python reported model loads but diff --git a/tests/test_event_serialization.py b/tests/test_event_serialization.py index 74e6a95..d6850e3 100644 --- a/tests/test_event_serialization.py +++ b/tests/test_event_serialization.py @@ -89,6 +89,22 @@ def test_error_event_caps_message_and_provider_code(): assert len(event["provider_error_code"]) == constants.PROVIDER_ERROR_CODE_MAX_LEN +def test_long_error_message_keeps_start_and_end(): + message = "ffmpeg banner " * 100 + "Error opening input file audio.wav" + sent = ErrorEvent( + model_id="m1", error_code=ErrorCode.UNKNOWN, error_message=message + ).to_dict()["error"]["error_message"] + assert len(sent) == constants.ERROR_MSG_MAX_LEN + assert sent.startswith("ffmpeg banner") + assert sent.endswith("Error opening input file audio.wav") + assert " … " in sent + + +def test_short_error_message_is_unchanged(): + event = ErrorEvent(model_id="m1", error_code="UNKNOWN", error_message="boom") + assert event.to_dict()["error"]["error_message"] == "boom" + + def test_span_event_to_dict_includes_required_fields(): event = SpanEvent( kind="tool", diff --git a/wildedge/constants.py b/wildedge/constants.py index 22ef327..acd0fcc 100644 --- a/wildedge/constants.py +++ b/wildedge/constants.py @@ -53,7 +53,7 @@ IDLE_POLL_INTERVAL = 5.0 # Error message truncation -ERROR_MSG_MAX_LEN = 200 +ERROR_MSG_MAX_LEN = 500 PROVIDER_ERROR_CODE_MAX_LEN = 64 # Device ID persistence diff --git a/wildedge/decorators.py b/wildedge/decorators.py index 86cbd65..23e90b2 100644 --- a/wildedge/decorators.py +++ b/wildedge/decorators.py @@ -4,7 +4,6 @@ import time from typing import TYPE_CHECKING, Any -from wildedge import constants from wildedge.timing import elapsed_ms if TYPE_CHECKING: @@ -87,7 +86,7 @@ def wrapper(*args, **kwargs): if self.capture_errors: self.handle.track_error( error_code="UNKNOWN", - error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + error_message=str(exc), **self._correlation, ) raise @@ -106,9 +105,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): if self.capture_errors: self.handle.track_error( error_code="UNKNOWN", - error_message=str(exc_val)[: constants.ERROR_MSG_MAX_LEN] - if exc_val - else None, + error_message=str(exc_val) if exc_val else None, **self._correlation, ) else: diff --git a/wildedge/events/error.py b/wildedge/events/error.py index 4ba5dd3..a3d6bd2 100644 --- a/wildedge/events/error.py +++ b/wildedge/events/error.py @@ -20,10 +20,29 @@ class ErrorCode(str, Enum): UNKNOWN = "UNKNOWN" +ELLIPSIS = " … " + + def truncate(value: str | None, limit: int) -> str | None: return None if value is None else str(value)[:limit] +def truncate_message(value: str | None, limit: int) -> str | None: + """Keep the start and the end of a long message. + + Errors put their detail at either end. mlx-whisper prints the ffmpeg banner + first and the failure last, and Hugging Face Hub starts with a request id. + """ + if value is None: + return None + value = str(value) + if len(value) <= limit: + return value + keep = limit - len(ELLIPSIS) + head = keep - keep // 2 + return value[:head] + ELLIPSIS + value[len(value) - keep // 2 :] + + @dataclass class ErrorEvent: model_id: str @@ -54,9 +73,9 @@ def to_dict(self) -> dict: error_data = add_optional_fields( {"error_code": code}, { - # Capped here so manual track_error() calls get the same - # limits as the integrations. - "error_message": truncate( + # Capped here, so every error event gets the same limit, + # from an integration or a manual track_error() call. + "error_message": truncate_message( self.error_message, constants.ERROR_MSG_MAX_LEN ), "stack_trace_hash": self.stack_trace_hash, diff --git a/wildedge/integrations/common.py b/wildedge/integrations/common.py index f51d8ec..86ffb01 100644 --- a/wildedge/integrations/common.py +++ b/wildedge/integrations/common.py @@ -196,7 +196,7 @@ def track_api_error( error_code = no_response_error_code(exc) or fallback_code handle.track_error( error_code=error_code, - error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + error_message=str(exc), http_status=http_status, provider_error_code=provider_error_code, duration_ms=duration_ms, diff --git a/wildedge/integrations/gguf.py b/wildedge/integrations/gguf.py index 2f54d6d..7e4ead8 100644 --- a/wildedge/integrations/gguf.py +++ b/wildedge/integrations/gguf.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from wildedge import accelerators, constants, quantizations +from wildedge import accelerators, quantizations from wildedge.events.inference import ( GenerationConfig, GenerationOutputMeta, @@ -333,7 +333,7 @@ def patched_call(self_inner, *args, **kwargs): except Exception as exc: handle.track_error( error_code="UNKNOWN", - error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + error_message=str(exc), ) raise diff --git a/wildedge/integrations/keras.py b/wildedge/integrations/keras.py index f54c8a0..f1f101b 100644 --- a/wildedge/integrations/keras.py +++ b/wildedge/integrations/keras.py @@ -11,7 +11,7 @@ except ImportError: _np = None # type: ignore[assignment] -from wildedge import accelerators, constants +from wildedge import accelerators from wildedge.events.inference import ClassificationOutputMeta, TopKPrediction from wildedge.integrations.base import BaseExtractor, patch_instance_call_once from wildedge.integrations.common import ( @@ -99,7 +99,7 @@ def patched_call(self_inner, *args, **kwargs): except Exception as exc: handle.track_error( error_code="UNKNOWN", - error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + error_message=str(exc), ) raise diff --git a/wildedge/integrations/mlx.py b/wildedge/integrations/mlx.py index 2284329..b78a5e3 100644 --- a/wildedge/integrations/mlx.py +++ b/wildedge/integrations/mlx.py @@ -8,7 +8,7 @@ import time from typing import TYPE_CHECKING -from wildedge import constants, quantizations +from wildedge import quantizations from wildedge.events.inference import GenerationOutputMeta, TextInputMeta from wildedge.integrations.base import BaseExtractor, patch_instance_call_once from wildedge.integrations.common import ( @@ -116,7 +116,7 @@ def patched_call(self_inner, *args, **kwargs): # type: ignore[no-untyped-def] except Exception as exc: handle.track_error( error_code="UNKNOWN", - error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + error_message=str(exc), ) raise @@ -273,7 +273,7 @@ def patched_generate(model, tokenizer, prompt, *args, **kwargs): # type: ignore if handle is not None: handle.track_error( error_code="UNKNOWN", - error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + error_message=str(exc), ) raise finally: diff --git a/wildedge/integrations/onnx.py b/wildedge/integrations/onnx.py index 1777709..894aaee 100644 --- a/wildedge/integrations/onnx.py +++ b/wildedge/integrations/onnx.py @@ -8,7 +8,7 @@ import time from typing import TYPE_CHECKING, Any -from wildedge import accelerators, constants +from wildedge import accelerators from wildedge.events.inference import ( ClassificationOutputMeta, HistogramSummary, @@ -330,7 +330,7 @@ def patched_run(output_names, input_feed, run_options=None): except Exception as exc: handle.track_error( error_code="UNKNOWN", - error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + error_message=str(exc), ) raise diff --git a/wildedge/integrations/tensorflow.py b/wildedge/integrations/tensorflow.py index c5ed639..114ba4a 100644 --- a/wildedge/integrations/tensorflow.py +++ b/wildedge/integrations/tensorflow.py @@ -13,7 +13,7 @@ except ImportError: _np = None # type: ignore[assignment] -from wildedge import accelerators, constants +from wildedge import accelerators from wildedge.events.inference import ClassificationOutputMeta, TopKPrediction from wildedge.integrations.base import BaseExtractor, patch_instance_call_once from wildedge.integrations.common import ( @@ -117,7 +117,7 @@ def patched_call(self_inner, *args, **kwargs): except Exception as exc: handle.track_error( error_code="UNKNOWN", - error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + error_message=str(exc), ) raise @@ -161,7 +161,7 @@ def patched_predict(*args, **kwargs): except Exception as exc: handle.track_error( error_code="UNKNOWN", - error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + error_message=str(exc), ) raise diff --git a/wildedge/integrations/transformers.py b/wildedge/integrations/transformers.py index b448978..bdf4a27 100644 --- a/wildedge/integrations/transformers.py +++ b/wildedge/integrations/transformers.py @@ -9,7 +9,7 @@ from collections import Counter from typing import TYPE_CHECKING -from wildedge import accelerators, constants, quantizations +from wildedge import accelerators, quantizations from wildedge.events.inference import ( ClassificationOutputMeta, EmbeddingOutputMeta, @@ -463,7 +463,7 @@ def patched_call(self, inputs, *args, **kwargs): # type: ignore[no-untyped-def] except Exception as exc: handle.track_error( error_code="UNKNOWN", - error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + error_message=str(exc), ) raise finally: diff --git a/wildedge/integrations/ultralytics.py b/wildedge/integrations/ultralytics.py index 2f5ed8b..397a26e 100644 --- a/wildedge/integrations/ultralytics.py +++ b/wildedge/integrations/ultralytics.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from wildedge import accelerators, constants +from wildedge import accelerators from wildedge.events.inference import ( ClassificationOutputMeta, DetectionOutputMeta, @@ -323,7 +323,7 @@ def patched_call(self_inner, *args, **kwargs): # type: ignore[no-untyped-def] except Exception as exc: handle.track_error( error_code="UNKNOWN", - error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + error_message=str(exc), ) raise From db8d8593a8237f0278ca9ba4f3c95fd03060c59b Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Thu, 22 Oct 2026 17:07:18 +0200 Subject: [PATCH 4/6] Build release notes from the version's CHANGELOG.md section The notes listed every commit subject since the previous tag, including merge commits and release housekeeping such as changelog moves. They are now the curated CHANGELOG.md section for the version. A missing or empty section fails the build instead of publishing empty notes. --- CONTRIBUTING.md | 2 +- scripts/build_changelog_comment.py | 42 +++++++----- tests/test_build_changelog_comment.py | 99 +++++++++++++++++++-------- 3 files changed, 94 insertions(+), 49 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9bd3aac..f01f4fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,7 +49,7 @@ Releases follow a `release/` branching flow: 1. Cut a `release/` branch from `devel`. -2. Bump the version in `pyproject.toml`. +2. Bump the version in `pyproject.toml` and move the Unreleased entries in `CHANGELOG.md` into a `## - ` section. The release notes are that section, so the release fails without it. 3. Open a pull request targeting `main`. CI will automatically run the full compatibility matrix and post a changelog preview comment. 4. Once merged, tag the commit as `v`. The release workflow publishes to PyPI and creates a GitHub release. diff --git a/scripts/build_changelog_comment.py b/scripts/build_changelog_comment.py index f05bdc6..e88bec5 100644 --- a/scripts/build_changelog_comment.py +++ b/scripts/build_changelog_comment.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 """Generate changelog text for release PRs and for tag-build release notes. +The notes are the version's section of CHANGELOG.md, so they list what changed +for users rather than every commit and merge since the previous tag. + The two callers differ only in wording: release-pr.yml renders a preview of notes that do not exist yet, while release.yml (which sets TAG_NAME) renders the notes themselves. @@ -13,6 +16,8 @@ import subprocess from pathlib import Path +CHANGELOG = Path("CHANGELOG.md") + def get_version() -> str: # Regex instead of tomllib so the script also runs on Python 3.10. @@ -24,9 +29,6 @@ def get_version() -> str: return match.group(1) -RELEASE_COMMIT_RE = re.compile(r"^Release \d+\.\d+\.\d+$") - - def get_previous_tag() -> str | None: result = subprocess.run( ["git", "describe", "--tags", "--abbrev=0", "--match", "v*", "HEAD^"], @@ -37,26 +39,28 @@ def get_previous_tag() -> str | None: return tag if result.returncode == 0 and tag else None -def get_commits_since(prev_tag: str | None) -> list[str]: - revision = f"{prev_tag}..HEAD" if prev_tag else "HEAD" - result = subprocess.run( - ["git", "log", revision, "--pretty=format:%s"], - capture_output=True, - text=True, - check=True, - ) - lines = [line for line in result.stdout.splitlines() if line.strip()] - return [line for line in lines if not RELEASE_COMMIT_RE.match(line)] +def changelog_section(text: str, version: str) -> str: + """Body of the ``## `` section, up to the next ``## `` heading.""" + heading = re.compile(rf"^## \[?{re.escape(version)}\]?(?:\s|$)", re.MULTILINE) + match = heading.search(text) + if match is None: + raise SystemExit(f"CHANGELOG.md has no section for {version}") + line_end = text.find("\n", match.start()) + start = len(text) if line_end == -1 else line_end + 1 + end = re.compile(r"^## ", re.MULTILINE).search(text, start) + body = text[start : end.start() if end else len(text)].strip() + if not body: + raise SystemExit(f"CHANGELOG.md section for {version} is empty") + return body def build_comment( tag_name: str, - commits: list[str], + notes: str, prev_tag: str | None, *, preview: bool = True, ) -> str: - items = "\n".join(f"- {c}" for c in commits) if commits else "- No changes." range_str = f"{prev_tag}...{tag_name}" if prev_tag else tag_name heading = "Changelog preview for" if preview else "Changelog for" blurb = ( @@ -67,22 +71,22 @@ def build_comment( return ( f"## {heading} `{tag_name}`\n\n" f"{blurb}" - f"**What's Changed**\n\n{items}\n\n" + f"{notes}\n\n" f"**Full Changelog**: {range_str}\n" ) def main() -> None: output = os.environ.get("OUTPUT", "/tmp/changelog-preview.md") - # release.yml sets TAG_NAME on tag builds; release-pr.yml never does, so its + # release.yml sets TAG_NAME on tag builds and release-pr.yml never does. Its # absence is what distinguishes a PR preview from the real release notes. tag_name = os.environ.get("TAG_NAME") preview = not tag_name tag_name = tag_name or f"v{get_version()}" + notes = changelog_section(CHANGELOG.read_text(), tag_name.removeprefix("v")) prev_tag = get_previous_tag() - commits = get_commits_since(prev_tag) - Path(output).write_text(build_comment(tag_name, commits, prev_tag, preview=preview)) + Path(output).write_text(build_comment(tag_name, notes, prev_tag, preview=preview)) if __name__ == "__main__": diff --git a/tests/test_build_changelog_comment.py b/tests/test_build_changelog_comment.py index f3c860c..fa5af30 100644 --- a/tests/test_build_changelog_comment.py +++ b/tests/test_build_changelog_comment.py @@ -3,9 +3,34 @@ import importlib.util from pathlib import Path +import pytest + SCRIPT = Path(__file__).parent.parent / "scripts" / "build_changelog_comment.py" -COMMITS = ["Add a thing (#1)", "Fix another thing (#2)"] +CHANGELOG = """# Changelog + +Intro text. + +## Unreleased + +- Not released yet. + +## 1.2.3 - 2026-10-21 + +### Added + +- A thing. + +### Fixed + +- Another thing. + +## 1.2.2 - 2026-09-30 + +### Fixed + +- An older thing. +""" def load_script(): @@ -15,58 +40,74 @@ def load_script(): return module +def test_notes_are_the_version_section_of_the_changelog(): + module = load_script() + notes = module.changelog_section(CHANGELOG, "1.2.3") + + assert notes == "### Added\n\n- A thing.\n\n### Fixed\n\n- Another thing." + + +def test_last_section_runs_to_the_end_of_the_file(): + module = load_script() + assert ( + module.changelog_section(CHANGELOG, "1.2.2") == "### Fixed\n\n- An older thing." + ) + + +def test_bracketed_version_headings_are_found(): + module = load_script() + text = "## [2.0.0] - 2026-11-01\n\n- New.\n" + assert module.changelog_section(text, "2.0.0") == "- New." + + +def test_version_prefix_does_not_match_a_longer_version(): + module = load_script() + with pytest.raises(SystemExit, match=r"no section for 1\.2$"): + module.changelog_section(CHANGELOG, "1.2") + + +def test_missing_or_empty_section_fails_the_build(): + module = load_script() + with pytest.raises(SystemExit, match=r"no section for 9\.9\.9"): + module.changelog_section(CHANGELOG, "9.9.9") + with pytest.raises(SystemExit, match="is empty"): + module.changelog_section("## 1.0.0\n\n## 0.9.0\n\n- Old.\n", "1.0.0") + + def test_preview_wording_on_pull_requests(): module = load_script() - text = module.build_comment("v1.2.3", COMMITS, "v1.2.2", preview=True) + text = module.build_comment("v1.2.3", "- A thing.", "v1.2.2", preview=True) assert text.startswith("## Changelog preview for `v1.2.3`") assert "> Preview of the release notes" in text + assert "- A thing." in text assert "**Full Changelog**: v1.2.2...v1.2.3" in text - for commit in COMMITS: - assert f"- {commit}" in text def test_release_notes_drop_the_preview_wording(): """Tag builds render the notes themselves, not a preview of them.""" module = load_script() - text = module.build_comment("v1.2.3", COMMITS, "v1.2.2", preview=False) + text = module.build_comment("v1.2.3", "- A thing.", "v1.2.2", preview=False) assert text.startswith("## Changelog for `v1.2.3`") assert "preview" not in text.lower() assert "**Full Changelog**: v1.2.2...v1.2.3" in text - for commit in COMMITS: - assert f"- {commit}" in text def test_first_release_falls_back_to_the_tag_alone(): module = load_script() - text = module.build_comment("v0.1.0", COMMITS, None, preview=False) + text = module.build_comment("v0.1.0", "- First.", None, preview=False) assert "**Full Changelog**: v0.1.0" in text -def test_empty_commit_range_is_reported(): - module = load_script() - text = module.build_comment("v1.2.3", [], "v1.2.2", preview=False) - - assert "- No changes." in text - - -def test_release_commits_are_filtered_from_the_log(monkeypatch): +def test_get_version_reads_pyproject_without_tomllib(): module = load_script() - log = "Release 1.2.3\nAdd a thing (#1)\n\nBump version to 1.2.3\n" - monkeypatch.setattr( - module.subprocess, - "run", - lambda *a, **k: type("R", (), {"stdout": log, "returncode": 0})(), - ) - - assert module.get_commits_since("v1.2.2") == [ - "Add a thing (#1)", - "Bump version to 1.2.3", - ] + assert module.get_version().count(".") >= 1 -def test_get_version_reads_pyproject_without_tomllib(): +def test_current_version_has_a_changelog_section(): + """Catches a release whose CHANGELOG.md entries were left under Unreleased.""" module = load_script() - assert module.get_version().count(".") >= 1 + changelog = (SCRIPT.parent.parent / "CHANGELOG.md").read_text() + assert module.changelog_section(changelog, module.get_version()) From e8e70f94da50fd9b1bd2f5620d4acb7bafd7f021 Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Sat, 24 Oct 2026 21:32:36 +0200 Subject: [PATCH 5/6] Correct the 0.3.0 attachments changelog entry 0.2.x already uploaded only explicit attachments. Automatic GGUF capture was added and removed during 0.3.0 development and never shipped, so the entry described no change for users upgrading. --- CHANGELOG.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdbfe59..cc476a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,10 +39,6 @@ under Unreleased and move into a version section at release time. and environment configuration. `gguf` remains a compatibility alias; `all` selects the integration once under its new name. Model format metadata remains `gguf`. -- Automatic integrations no longer attach raw payloads, including GGUF prompts, - chat messages and replies. `attachments_enabled=True` enables uploads only for - attachments explicitly passed to `track_inference(attachments=[...])`. - Apps relying on automatic GGUF capture must supply attachments explicitly. - `wildedge.init(dsn=...)` reuses the existing default client when the DSN points to the same project, for example `dsn=os.environ["WILDEDGE_DSN"]` under `wildedge run`. It used to create a second client. @@ -82,7 +78,8 @@ under Unreleased and move into a version section at release time. calls, the generation config (`temperature`, `top_p`, `top_k`, `max_tokens`, `repeat_penalty`, penalties, `seed`, stop sequence count), and, for chat calls, `prompt_type="chat"` and `turn_index` measured on the last user - message. + message. Prompts, chat messages and replies are never attached. As before, + attachments upload only when passed to `track_inference(attachments=[...])`. - `import wildedge` no longer imports every installed ML framework and API client. Each library is imported when its integration is activated or a model from it is registered, so SDK import is faster and a broken unrelated From 61fb79983e62017430f068d8c3cf698327bee7af Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Sat, 24 Oct 2026 21:42:04 +0200 Subject: [PATCH 6/6] Date the 0.3.0 release 2026-10-24 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc476a1..4d7b234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ under Unreleased and move into a version section at release time. ## Unreleased -## 0.3.0 - 2026-10-21 +## 0.3.0 - 2026-10-24 ### Added