From af797af5a56046d3d316789a56c4bcb09d44810a Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Tue, 3 Nov 2026 13:46:43 +0100 Subject: [PATCH 1/3] refactor(foundry): read COMMIT_SHA once Read COMMIT_SHA in from_package and pass it to both version helpers. Co-Authored-By: Claude Opus 5.5 --- src/aignostics_foundry_core/foundry.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/aignostics_foundry_core/foundry.py b/src/aignostics_foundry_core/foundry.py index fd7a0ce..e74f6d4 100644 --- a/src/aignostics_foundry_core/foundry.py +++ b/src/aignostics_foundry_core/foundry.py @@ -191,6 +191,7 @@ def from_package(cls, package_name: str) -> FoundryContext: environment = _detect_environment(name_upper) project_path = _find_project_path(package_name) vcs_ref = os.environ.get("VCS_REF") or (project_path and _get_vcs_ref_from_git(project_path)) or "unknown" + commit_sha = os.getenv("COMMIT_SHA", "unknown") env_prefix = f"{name_upper}_" env_files = _build_env_file_list(name, name_upper, environment) db_url_key = f"{env_prefix}DB_URL" @@ -202,8 +203,8 @@ def from_package(cls, package_name: str) -> FoundryContext: return cls( name=name, version=version, - version_full=_build_version_full(version, vcs_ref), - version_with_vcs_ref=_build_version_with_vcs_ref(version, vcs_ref), + version_full=_build_version_full(version, vcs_ref, commit_sha), + version_with_vcs_ref=_build_version_with_vcs_ref(version, vcs_ref, commit_sha), environment=environment, env_file=env_files, env_prefix=env_prefix, @@ -257,17 +258,17 @@ def _get_vcs_ref_from_git(project_path: Path) -> str: return "unknown" -def _build_version_full(version: str, vcs_ref: str) -> str: +def _build_version_full(version: str, vcs_ref: str, commit_sha: str) -> str: """Append build metadata to *version* from environment variables. Args: version: The base version string (e.g. ``"1.2.3"``). vcs_ref: The VCS ref string (branch name, short SHA, or ``"unknown"``). + commit_sha: The commit SHA, or ``"unknown"``. Returns: The version string with optional ``+`` suffix. """ - commit_sha = os.getenv("COMMIT_SHA", "unknown") builder = os.getenv("BUILDER", "uv") build_date = os.getenv("BUILD_DATE", "unknown") ci_run_id = os.getenv("CI_RUN_ID", "unknown") @@ -294,18 +295,18 @@ def _build_version_full(version: str, vcs_ref: str) -> str: return result -def _build_version_with_vcs_ref(version: str, vcs_ref: str) -> str: +def _build_version_with_vcs_ref(version: str, vcs_ref: str, commit_sha: str) -> str: """Append VCS ref and commit SHA to *version*, omitting CI build metadata. Args: version: The base version string (e.g. ``"1.2.3"``). vcs_ref: The VCS ref string (branch name, short SHA, or ``"unknown"``). + commit_sha: The commit SHA, or ``"unknown"``. Returns: The version string with optional ``+[-]`` suffix, or the bare *version* when both values are ``"unknown"``. """ - commit_sha = os.getenv("COMMIT_SHA", "unknown") parts = [p for p in [vcs_ref, commit_sha] if p != "unknown"] if not parts: return version From e226916bb4b15f56e12ecf20a2a160ee439cd163 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Tue, 3 Nov 2026 13:48:14 +0100 Subject: [PATCH 2/3] feat(foundry): add commit_sha to FoundryContext Read COMMIT_SHA into the new commit_sha field. The value is None when the variable is unset, empty or blank. Co-Authored-By: Claude Opus 5.5 --- README.md | 5 ++-- src/aignostics_foundry_core/AGENTS.md | 5 ++-- src/aignostics_foundry_core/foundry.py | 7 +++++ tests/aignostics_foundry_core/foundry_test.py | 30 +++++++++++++++++++ tests/conftest.py | 3 ++ 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index edd2735..14727c9 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ boot() populate every field: - `name`, `version`, `version_full` — from `importlib.metadata` +- `commit_sha` — from `COMMIT_SHA`; `None` when unset or blank - `environment` — resolved from env vars in priority order (see [Configuration reference](#configuration-reference) below) - `env_prefix` (`"MYPROJECT_"`) — used by every settings class; all env vars for this project share this prefix - `is_container`, `is_cli`, `is_test`, `is_library` — detected automatically @@ -117,8 +118,8 @@ Read directly by `FoundryContext.from_package()` — no settings class. #### Build metadata -Read by `FoundryContext.from_package()` to build `version_full` and `version_with_vcs_ref`. All -optional; most useful in CI. +Read by `FoundryContext.from_package()` to build `version_full` and `version_with_vcs_ref`. +`COMMIT_SHA` also sets `commit_sha`. All optional; most useful in CI. | Variable | Default | Description | |---|---|---| diff --git a/src/aignostics_foundry_core/AGENTS.md b/src/aignostics_foundry_core/AGENTS.md index 9f4cc52..b1148b2 100644 --- a/src/aignostics_foundry_core/AGENTS.md +++ b/src/aignostics_foundry_core/AGENTS.md @@ -24,7 +24,7 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei | **user_agent** | Parameterised HTTP user-agent string builder | `user_agent(project_name, version, repository_url)` — builds `{project_name}-python-sdk/{version} (…)` string including platform info, current test, and GitHub Actions run URL | | **gui** | NiceGUI page helpers, auth decorators, and nav builder | `GUINamespace` (configurable page decorator namespace), `gui` (default singleton), `page_public/authenticated/admin/internal/internal_admin` decorators, `get_gui_user`, `require_gui_user`, `BaseNavBuilder`, `NavItem`, `NavGroup`, `gui_get_nav_groups(*, context=None)`, `BasePageBuilder`, `gui_register_pages(*, context=None)`, `gui_run(*, context=None, …)`; constants `WINDOW_SIZE`, `BROWSER_RECONNECT_TIMEOUT`, `RESPONSE_TIMEOUT` | | **console** | Themed terminal output | Module-level `console` object (Rich `Console`) with colour theme and `_get_console()` factory | -| **foundry** | Project context injection | `PackageMetadata`, `FoundryContext`, `FoundryContext.from_package()`, `set_context()`, `get_context()` — centralised project-specific values (name, version, `version_full`, `version_with_vcs_ref`, environment, env files, URLs, `python_version`, `metadata: PackageMetadata` (description, author_name, author_email), runtime mode flags `is_container`, `is_cli`, `is_test`, `is_library`, `database: DatabaseSettings \| None`) derived from package metadata and environment variables; `from_package()` populates `metadata` from `importlib.metadata` and `database` from `{env_prefix}DB_*` env vars when `{env_prefix}DB_URL` is present | +| **foundry** | Project context injection | `PackageMetadata`, `FoundryContext`, `FoundryContext.from_package()`, `set_context()`, `get_context()` — centralised project-specific values (name, version, `version_full`, `version_with_vcs_ref`, `commit_sha`, environment, env files, URLs, `python_version`, `metadata: PackageMetadata` (description, author_name, author_email), runtime mode flags `is_container`, `is_cli`, `is_test`, `is_library`, `database: DatabaseSettings \| None`) derived from package metadata and environment variables; `from_package()` populates `metadata` from `importlib.metadata` and `database` from `{env_prefix}DB_*` env vars when `{env_prefix}DB_URL` is present | | **di** | Dependency injection | `locate_subclasses(cls, *, context=None)`, `locate_implementations(cls, *, context=None)`, `load_modules(*, context=None)`, `discover_plugin_packages`, `clear_caches`, `PLUGIN_ENTRY_POINT_GROUP` for plugin and subclass discovery | | **health** | Service health checks | `Health` model and `HealthStatus` enum for tree-structured health status | | **scheduler** | Chancy-backed job module base class | `BaseJoblet` ABC with `get_queues()`, `get_jobs()`, `get_crons()`, `get_triggers()` hooks and `register_queues/jobs/crons/triggers()` / `unregister_queues/jobs/crons/triggers()` lifecycle methods; `key()` returns second-to-last module component for identification | @@ -49,7 +49,8 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei Constructor: `PackageMetadata.from_name(package_name)` — reads all five fields from `importlib.metadata` (``Summary``, ``Author-email``, ``Project-URL``). Defaults to empty values for direct construction (e.g. in tests). - - `FoundryContext(BaseModel)` — frozen; fields: `name`, `version`, `version_full`, `version_with_vcs_ref`, `environment`, + - `FoundryContext(BaseModel)` — frozen; fields: `name`, `version`, `version_full`, `version_with_vcs_ref`, + `commit_sha: str | None` (from `COMMIT_SHA`, stripped; `None` when unset or blank), `environment`, `env_file: list[Path]`, `env_prefix`, `python_version` (Python runtime version string, e.g. `"3.11.9"`), `python_version_minor` (computed; major and minor only, e.g. `"3.11"`), `metadata: PackageMetadata` (all package-derived fields: description, author, URLs; populated by `from_package()` via `PackageMetadata.from_name()`; defaults to diff --git a/src/aignostics_foundry_core/foundry.py b/src/aignostics_foundry_core/foundry.py index e74f6d4..a08e1bb 100644 --- a/src/aignostics_foundry_core/foundry.py +++ b/src/aignostics_foundry_core/foundry.py @@ -119,6 +119,11 @@ class FoundryContext(BaseModel): (``CI_RUN_ID``, ``CI_RUN_NUMBER``, ``BUILDER``, ``BUILD_DATE``). Falls back to :attr:`version` when neither variable is set. """ + commit_sha: str | None = None + """Commit SHA from the ``COMMIT_SHA`` environment variable. + + Whitespace is stripped. ``None`` when the variable is unset, empty or blank. + """ environment: str env_file: list[Path] = Field(default_factory=_empty_path_list) env_prefix: str = "" @@ -172,6 +177,7 @@ def from_package(cls, package_name: str) -> FoundryContext: * ``VCS_REF``, ``COMMIT_SHA``, ``BUILDER``, ``BUILD_DATE``, ``CI_RUN_ID``, ``CI_RUN_NUMBER`` — build metadata for :attr:`version_full`. + ``COMMIT_SHA`` also sets :attr:`commit_sha`. * ``{NAME}_ENVIRONMENT``, ``ENV``, ``VERCEL_ENV``, ``RAILWAY_ENVIRONMENT`` — deployment environment. * ``{NAME}_ENV_FILE`` — optional extra env-file path inserted at index 2 of @@ -205,6 +211,7 @@ def from_package(cls, package_name: str) -> FoundryContext: version=version, version_full=_build_version_full(version, vcs_ref, commit_sha), version_with_vcs_ref=_build_version_with_vcs_ref(version, vcs_ref, commit_sha), + commit_sha=None if commit_sha == "unknown" else (commit_sha.strip() or None), environment=environment, env_file=env_files, env_prefix=env_prefix, diff --git a/tests/aignostics_foundry_core/foundry_test.py b/tests/aignostics_foundry_core/foundry_test.py index 4ffa42f..1889d31 100644 --- a/tests/aignostics_foundry_core/foundry_test.py +++ b/tests/aignostics_foundry_core/foundry_test.py @@ -27,6 +27,7 @@ VCS_REF_VALUE = "abc123" VCS_REF_OVERRIDE = "ci-override-ref" COMMIT_SHA_VALUE = "deadbeef" +COMMIT_SHA_FULL = "c" * 40 CI_RUN_ID_VALUE = "99" CI_RUN_NUMBER_VALUE = "42" BUILD_DATE_VALUE = "2024-01-15" @@ -271,6 +272,35 @@ def test_from_package_version_full_omits_builder_and_extra_when_all_unknown( assert "---" not in ctx.version_full +# --------------------------------------------------------------------------- +# from_package — commit_sha +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +def test_from_package_commit_sha_from_env(monkeypatch: pytest.MonkeyPatch) -> None: + """commit_sha equals COMMIT_SHA when it is set.""" + monkeypatch.setenv("COMMIT_SHA", COMMIT_SHA_FULL) + ctx = FoundryContext.from_package(PACKAGE_NAME) + assert ctx.commit_sha == COMMIT_SHA_FULL + + +@pytest.mark.unit +def test_from_package_commit_sha_none_when_unset(monkeypatch: pytest.MonkeyPatch) -> None: + """commit_sha is None when COMMIT_SHA is unset.""" + monkeypatch.delenv("COMMIT_SHA", raising=False) + ctx = FoundryContext.from_package(PACKAGE_NAME) + assert ctx.commit_sha is None + + +@pytest.mark.unit +def test_from_package_commit_sha_none_when_empty(monkeypatch: pytest.MonkeyPatch) -> None: + """commit_sha is None when COMMIT_SHA is empty.""" + monkeypatch.setenv("COMMIT_SHA", "") + ctx = FoundryContext.from_package(PACKAGE_NAME) + assert ctx.commit_sha is None + + # --------------------------------------------------------------------------- # from_package — project_path # --------------------------------------------------------------------------- diff --git a/tests/conftest.py b/tests/conftest.py index a79e9a5..4773468 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -96,6 +96,7 @@ def make_context( # ruff: ignore[too-many-arguments] env_file: list[Path] | None = None, metadata: PackageMetadata | None = None, python_version: str = "", + commit_sha: str | None = None, **kwargs: bool, ) -> FoundryContext: """Create a minimal FoundryContext for testing. @@ -116,6 +117,7 @@ def make_context( # ruff: ignore[too-many-arguments] ``repository_url`` set from the *repository_url* argument is used. python_version: Python runtime version string (e.g. ``"3.11.9"``). Defaults to ``""`` so contexts constructed in tests work without specifying a version. + commit_sha: Optional commit SHA. **kwargs: Optional boolean flags forwarded to :class:`FoundryContext` (``is_test``, ``is_cli``, ``is_container``, ``is_library``). """ @@ -132,5 +134,6 @@ def make_context( # ruff: ignore[too-many-arguments] env_file=env_file or [], metadata=resolved_metadata, python_version=python_version, + commit_sha=commit_sha, **kwargs, # type: ignore[arg-type] ) From cd97551d513b49cef7e17a4c2c62031075b9edc9 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Tue, 3 Nov 2026 13:49:34 +0100 Subject: [PATCH 3/3] feat(sentry): send a valid release per commit Send the release @+. The old release used version_full, which can contain '/'. Sentry drops such a release. Drop the SHA suffix and log a warning if the name is not valid. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 ++ src/aignostics_foundry_core/AGENTS.md | 2 +- src/aignostics_foundry_core/sentry.py | 29 ++++++++++++++- tests/aignostics_foundry_core/sentry_test.py | 38 +++++++++++++++++++- tests/conftest.py | 4 ++- 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 14727c9..94174d8 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,9 @@ To send them, pass `LoggingIntegration(capture_sentry_logs=True)` or `LoguruIntegration(capture_sentry_logs=True)` to `boot(sentry_integrations=...)`. foundry-core requires sentry-sdk 2.68.0 or later, and it has no `{PREFIX}SENTRY_ENABLE_LOGS` setting. +The Sentry release is `@+`, or `@` if `COMMIT_SHA` is +not set. To link commits to the release, CI must create the same release name with `sentry-cli`. + #### OpenTelemetry (`{PREFIX}OTEL_`) Settings class: `OTelSettings`. OpenTelemetry is only initialised when `ENABLED=true` **and** the diff --git a/src/aignostics_foundry_core/AGENTS.md b/src/aignostics_foundry_core/AGENTS.md index b1148b2..0b9a0f7 100644 --- a/src/aignostics_foundry_core/AGENTS.md +++ b/src/aignostics_foundry_core/AGENTS.md @@ -184,7 +184,7 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei - **Purpose**: Bootstraps Sentry SDK with all project-specific metadata supplied as explicit parameters, making the initialisation reusable across any project without hard-coded constants. - **Key Features**: - `SentrySettings(OpaqueSettings)` — uses the active FoundryContext.env_prefix to derive the env prefix (`{ctx.env_prefix}SENTRY_`). Fields: `enabled`, `dsn` (validated HTTPS Sentry URL), `debug`, `send_default_pii`, `include_local_variables` (default `False`; frame locals are not sent), `max_request_body_size` (`"never"` | `"small"` | `"medium"` | `"always"`, default `"never"`; request bodies are not sent), `trace_propagation_targets` (`list[str]` of URL regexes, default `[]`; no outbound request gets `sentry-trace` or `baggage` headers; the env var takes a JSON list), `max_breadcrumbs`, `sample_rate`, `traces_sample_rate`, `profiles_sample_rate`, `profile_session_sample_rate`, `profile_lifecycle`. There is no `enable_logs` field: log records go to Sentry Logs only when a service passes `LoggingIntegration(capture_sentry_logs=True)` or `LoguruIntegration(capture_sentry_logs=True)` (sentry-sdk>=2.68.0) - - `sentry_initialize(integrations, *, context=None)` — derives all project-specific values (name, version, environment, URLs, runtime flags) from *context* (or the global context); env prefix and env file are read from `ctx.env_prefix` and `ctx.env_file`; initialises Sentry SDK when enabled and DSN present; sets `aignx/base` context; removes the query string and the fragment (`http.query`, `http.fragment` and the query of `url`) from HTTP breadcrumbs and from the span data of transactions, through `before_breadcrumb` and `before_send_transaction` hooks; suppresses noisy loggers; returns `True` on success, `False` otherwise + - `sentry_initialize(integrations, *, context=None)` — derives all project-specific values (name, version, environment, URLs, runtime flags) from *context* (or the global context); env prefix and env file are read from `ctx.env_prefix` and `ctx.env_file`; initialises Sentry SDK when enabled and DSN present; sets the release `@+` (`@` without a commit SHA; drops the SHA suffix if the name has `/` or `\` or exceeds 200 characters); sets `aignx/base` context; removes the query string and the fragment (`http.query`, `http.fragment` and the query of `url`) from HTTP breadcrumbs and from the span data of transactions, through `before_breadcrumb` and `before_send_transaction` hooks; suppresses noisy loggers; returns `True` on success, `False` otherwise - `set_sentry_user(user, role_claim)` — maps `sub` → `id`, `org_id` and the optional role claim into Sentry scope; no other claims (no email, name or other personal data); pass `None` to clear context; no-op when `sentry_sdk` is absent - **Location**: `aignostics_foundry_core/sentry.py` - **Dependencies**: `sentry-sdk>=2.68.0,<3` (mandatory); `loguru>=0.7,<1` diff --git a/src/aignostics_foundry_core/sentry.py b/src/aignostics_foundry_core/sentry.py index dea2d6c..ac829e9 100644 --- a/src/aignostics_foundry_core/sentry.py +++ b/src/aignostics_foundry_core/sentry.py @@ -28,6 +28,8 @@ _HTTP_BREADCRUMB_CATEGORY = "httplib" _URL_DATA_KEY = "url" _URL_PART_DATA_KEYS = frozenset({"http.query", "http.fragment"}) +_MAX_RELEASE_LENGTH = 200 +_INVALID_RELEASE_CHARS = ("/", "\\") def _validate_url_scheme(parsed_url: urllib.parse.ParseResult) -> None: @@ -177,6 +179,27 @@ def _before_send_transaction(event: "Event", _hint: dict[str, Any]) -> "Event": return event +def _release_name(ctx: "FoundryContext") -> str: + """Return the Sentry release ``@[+]``. + + Drops the SHA suffix if Sentry rejects the full name. + + Args: + ctx: The context that gives the name, the version and the commit SHA. + + Returns: + str: The release name. + """ + base = f"{ctx.name}@{ctx.version}" + if not ctx.commit_sha: + return base + release = f"{base}+{ctx.commit_sha}" + if len(release) > _MAX_RELEASE_LENGTH or any(char in release for char in _INVALID_RELEASE_CHARS): + logger.warning("Sentry release '{}' is invalid, using '{}'.", release, base) + return base + return release + + class SentrySettings(OpaqueSettings): """Configuration settings for Sentry integration. @@ -329,6 +352,10 @@ def sentry_initialize( HTTP breadcrumbs and the spans of transactions carry no query string and no fragment. Both can hold credentials, for example the signature of a signed URL. + The release is ``@+``, or ``@`` if the context + has no commit SHA. It does not use ``version_full``: that value can contain ``/``, and + Sentry drops such a release. CI can build the same name to link commits. + Args: integrations: List of Sentry SDK integrations to register, or ``None``. context: :class:`~aignostics_foundry_core.foundry.FoundryContext` providing @@ -353,7 +380,7 @@ def sentry_initialize( from sentry_sdk.integrations.logging import ignore_logger # ruff: ignore[import-outside-top-level] sentry_sdk.init( - release=f"{ctx.name}@{ctx.version_full}", + release=_release_name(ctx), environment=ctx.environment, dsn=settings.dsn.get_secret_value().strip(), max_breadcrumbs=settings.max_breadcrumbs, diff --git a/tests/aignostics_foundry_core/sentry_test.py b/tests/aignostics_foundry_core/sentry_test.py index 7b0c52a..615851e 100644 --- a/tests/aignostics_foundry_core/sentry_test.py +++ b/tests/aignostics_foundry_core/sentry_test.py @@ -64,6 +64,13 @@ _BAGGAGE_HEADER = "baggage" _APP_LOGGER_NAME = "app" _LOG_ITEM_TYPE = "log" +_COMMIT_SHA = "c" * 40 +_MAX_RELEASE_LENGTH = 200 +_PROD_VERSION = "0.33.0" +_PROD_VERSION_FULL = ( + f"{_PROD_VERSION}+release/v{_PROD_VERSION}-{_COMMIT_SHA}" + "---run.1---build.2---builder.github/docker/buildx---built.2026-11-03T09:04:14Z" +) class _CapturingTransport(Transport): @@ -264,7 +271,7 @@ def test_sentry_initialize_returns_false_when_sdk_absent(self, monkeypatch: pyte assert result is False def test_sentry_initialize_returns_true_when_enabled(self, sentry_capture: SentryCapture) -> None: - """Returns True and events carry the release ``{name}@{version_full}`` when enabled with a valid DSN.""" + """Returns True and events carry the release ``{name}@{version}`` when enabled with a valid DSN.""" result = sentry_capture.start() sentry_sdk.capture_message(_PROBE_MESSAGE) @@ -272,6 +279,35 @@ def test_sentry_initialize_returns_true_when_enabled(self, sentry_capture: Sentr (event,) = sentry_capture.events assert event["release"] == f"{TEST_PROJECT_NAME}@0.0.0" + def test_sentry_initialize_release_has_commit_sha(self, sentry_capture: SentryCapture) -> None: + """The event release is ``{name}@{version}+{commit_sha}`` when the context has a commit SHA.""" + sentry_capture.start(context=make_context(version="1.2.3", commit_sha=_COMMIT_SHA)) + sentry_sdk.capture_message(_PROBE_MESSAGE) + + (event,) = sentry_capture.events + assert event["release"] == f"{TEST_PROJECT_NAME}@1.2.3+{_COMMIT_SHA}" + + def test_sentry_initialize_release_is_valid_with_full_build_metadata(self, sentry_capture: SentryCapture) -> None: + """The event release is a valid Sentry release when version_full holds prod build metadata.""" + ctx = make_context(version=_PROD_VERSION, version_full=_PROD_VERSION_FULL, commit_sha=_COMMIT_SHA) + sentry_capture.start(context=ctx) + sentry_sdk.capture_message(_PROBE_MESSAGE) + + (event,) = sentry_capture.events + release = event["release"] + assert release == f"{TEST_PROJECT_NAME}@{_PROD_VERSION}+{_COMMIT_SHA}" + assert "/" not in release + assert "\\" not in release + assert len(release) <= _MAX_RELEASE_LENGTH + + def test_sentry_initialize_release_drops_invalid_commit_sha(self, sentry_capture: SentryCapture) -> None: + """The event release has no SHA suffix when the commit SHA makes the release invalid.""" + sentry_capture.start(context=make_context(commit_sha="feature/x")) + sentry_sdk.capture_message(_PROBE_MESSAGE) + + (event,) = sentry_capture.events + assert event["release"] == f"{TEST_PROJECT_NAME}@0.0.0" + def test_sentry_initialize_returns_false_when_enabled_but_dsn_none(self, monkeypatch: pytest.MonkeyPatch) -> None: """Returns False when enabled but no DSN is configured.""" monkeypatch.setenv(f"{_SENTRY_PREFIX}ENABLED", "true") diff --git a/tests/conftest.py b/tests/conftest.py index 4773468..36daeb2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -97,6 +97,7 @@ def make_context( # ruff: ignore[too-many-arguments] metadata: PackageMetadata | None = None, python_version: str = "", commit_sha: str | None = None, + version_full: str | None = None, **kwargs: bool, ) -> FoundryContext: """Create a minimal FoundryContext for testing. @@ -118,6 +119,7 @@ def make_context( # ruff: ignore[too-many-arguments] python_version: Python runtime version string (e.g. ``"3.11.9"``). Defaults to ``""`` so contexts constructed in tests work without specifying a version. commit_sha: Optional commit SHA. + version_full: Optional full version. Defaults to *version*. **kwargs: Optional boolean flags forwarded to :class:`FoundryContext` (``is_test``, ``is_cli``, ``is_container``, ``is_library``). """ @@ -125,7 +127,7 @@ def make_context( # ruff: ignore[too-many-arguments] return FoundryContext( name=name, version=version, - version_full=version, + version_full=version_full if version_full is not None else version, version_with_vcs_ref=version, environment=environment, env_prefix=env_prefix,