Repository: Flagsmith/flagsmith
Pull request #5366: feat: Add product version to API response headers
Author: login: emyller
Labels: name: feature
description: New feature or request
name: api
description: Issue related to the REST API
Created: 2025-04-22T00:10:08Z | Merged: 2025-04-28T21:53:03Z

- [ ] ~I have added information to `docs/` if required so people know about the feature!~
- [x] I have filled in the "Changes" section below?
- [x] I have filled in the "How did you test this code" section below?
- [x] I have used a [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) title for this Pull Request


## Changes

ref: #4253

- Read the release Git tag into an environment variable, then into Django settings.
- Introduce a middleware that adds `X-Flagsmith-Version` to **all** responses.


## How did you test this code?

Functional tests.


Diff:
diff --git a/api/app/settings/common.py b/api/app/settings/common.py
index 0ae04add3963..305571208339 100644
--- a/api/app/settings/common.py
+++ b/api/app/settings/common.py
@@ -285,6 +285,7 @@
     ],
 }
 MIDDLEWARE = [
+    "common.core.middleware.APIResponseVersionHeaderMiddleware",
     "common.gunicorn.middleware.RouteLoggerMiddleware",
     "django.middleware.security.SecurityMiddleware",
     "whitenoise.middleware.WhiteNoiseMiddleware",
diff --git a/api/conftest.py b/api/conftest.py
index 5cf06ddbe797..4924014de8f0 100644
--- a/api/conftest.py
+++ b/api/conftest.py
@@ -1,7 +1,6 @@
-import importlib
-import inspect
 import logging
 import os
+import site
 import typing
 from unittest.mock import MagicMock
 
@@ -22,7 +21,6 @@
 from moto import mock_dynamodb  # type: ignore[import-untyped]
 from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource, Table
 from pyfakefs.fake_filesystem import FakeFilesystem
-from pyfakefs.fake_filesystem_unittest import Patcher
 from pytest_django.fixtures import SettingsWrapper
 from pytest_django.plugin import blocking_manager_key
 from pytest_mock import MockerFixture
@@ -138,21 +136,19 @@ def pytest_configure(config: pytest.Config) -> None:
 
 
 @pytest.fixture
-def fs() -> typing.Generator[FakeFilesystem | None, None, None]:
-    app_path = os.path.dirname(os.path.abspath(__file__))
-    real_paths = [app_path]
-
-    for module_name in (
-        "django",
-        "tzdata",
-    ):
-        module_file = inspect.getfile(importlib.import_module(module_name))
-        real_paths.append(os.path.dirname(os.path.abspath(module_file)))
+def fs(fs: FakeFilesystem) -> FakeFilesystem:
+    """
+    Provide a fake filesystem for tests
 
-    with Patcher() as patcher:
-        if fs := patcher.fs:
-            fs.add_real_paths(real_paths)
-        yield fs
+    NOTE: Sometimes pyfakefs patching goes wonky, causing the fake file system
+    to be cached across tests. This can lead tests failing to access real files
+    even if they do not use this fixture. Because we can't fix this issue now,
+    it's safer to allow site-packages [read-only] access from tests.
+    """
+    app_path = os.path.dirname(os.path.abspath(__file__))
+    site_packages = site.getsitepackages()  # Allow files within dependencies
+    fs.add_real_paths([*site_packages, app_path])
+    return fs
 
 
 @pytest.fixture(scope="session")
diff --git a/api/tests/integration/test_integration_api_version_header.py b/api/tests/integration/test_integration_api_version_header.py
new file mode 100644
index 000000000000..1ab52c1e6865
--- /dev/null
+++ b/api/tests/integration/test_integration_api_version_header.py
@@ -0,0 +1,61 @@
+import pytest
+from common.core.utils import get_file_contents, get_versions_from_manifest
+from pyfakefs.fake_filesystem import FakeFilesystem
+from rest_framework import status
+from rest_framework.test import APIClient
+
+
+@pytest.fixture(autouse=True)
+def clear_lru_caches() -> None:
+    get_file_contents.cache_clear()
+    get_versions_from_manifest.cache_clear()
+
+
+@pytest.mark.parametrize(
+    "url",
+    [
+        "/robots.txt",
+        "/api/v1/auth/users/me/",
+    ],
+)
+@pytest.mark.parametrize(
+    "version_file_contents, expected_version",
+    [
+        ('{".": "v1.2.3"}', "v1.2.3"),
+        ('{"foo": "bar"}', "unknown"),
+        ("", "unknown"),
+    ],
+)
+def test_api_version_is_added_to_success_response_headers(
+    admin_client: APIClient,
+    expected_version: str,
+    fs: FakeFilesystem,
+    url: str,
+    version_file_contents: str,
+) -> None:
+    fs.create_file(".versions.json", contents=version_file_contents)
+
+    response = admin_client.get(url)
+    assert response.status_code == status.HTTP_200_OK
+    assert response.headers["Flagsmith-Version"] == expected_version
+
+
+@pytest.mark.parametrize(
+    "version_file_contents, expected_version",
+    [
+        ('{".": "v1.2.3"}', "v1.2.3"),
+        ('{"foo": "bar"}', "unknown"),
+        ("", "unknown"),
+    ],
+)
+def test_api_version_is_added_to_error_response_headers(
+    client: APIClient,
+    expected_version: str,
+    fs: FakeFilesystem,
+    version_file_contents: str,
+) -> None:
+    fs.create_file(".versions.json", contents=version_file_contents)
+
+    response = client.get("/wat")
+    assert response.status_code == status.HTTP_404_NOT_FOUND
+    assert response.headers["Flagsmith-Version"] == expected_version


Reviews:
user: login: matthewelwell
body: Approach and tests look fine to me. 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T08:29:24Z
user: login: khvn26
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T08:34:47Z
user: login: matthewelwell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T08:35:15Z
user: login: emyller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:42:19Z
user: login: matthewelwell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T17:03:12Z
user: login: khvn26
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T17:04:29Z
user: login: matthewelwell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T15:03:47Z
user: login: emyller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T17:01:12Z
user: login: emyller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T17:13:08Z
user: login: matthewelwell
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T17:32:50Z
user: login: matthewelwell
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T21:48:49Z

Review comments:
diff_hunk: @@ -20,3 +21,40 @@
 def test_swagger_docs_generation(url: str, client: APIClient) -> None:
     response = client.get(url)
     assert response.status_code == status.HTTP_200_OK
+
+
+@pytest.mark.parametrize(
+    "url",
+    [
+        "/robots.txt",
+        # TODO: Add other GET-only URLs
+    ],
+)
+def test_api_version_is_added_to_success_response_headers(
+    url: str,
+    client: APIClient,
+    settings: SettingsWrapper,
+) -> None:
+    settings.VERSION = "v1.2.3"  # NOTE: Maybe read from Git tag into an env var?
path: api/tests/unit/api/test_unit_api.py
user: login: matthewelwell
body: I think that this is a fine approach. I feel like reading the git tag could result in some flakiness. 

We can then just unit test the logic that reads the git tag into settings using mocking perhaps. 
created_at: 2025-04-22T08:29:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,3 +21,40 @@
 def test_swagger_docs_generation(url: str, client: APIClient) -> None:
     response = client.get(url)
     assert response.status_code == status.HTTP_200_OK
+
+
+@pytest.mark.parametrize(
+    "url",
+    [
+        "/robots.txt",
+        # TODO: Add other GET-only URLs
+    ],
+)
+def test_api_version_is_added_to_success_response_headers(
+    url: str,
+    client: APIClient,
+    settings: SettingsWrapper,
+) -> None:
+    settings.VERSION = "v1.2.3"  # NOTE: Maybe read from Git tag into an env var?
path: api/tests/unit/api/test_unit_api.py
user: login: khvn26
body: @matthewelwell Will we have to modify the `/version` endpoint to read from the setting as well?
created_at: 2025-04-22T08:34:47Z
author_association: MEMBER
diff_hunk: @@ -20,3 +21,40 @@
 def test_swagger_docs_generation(url: str, client: APIClient) -> None:
     response = client.get(url)
     assert response.status_code == status.HTTP_200_OK
+
+
+@pytest.mark.parametrize(
+    "url",
+    [
+        "/robots.txt",
+        # TODO: Add other GET-only URLs
+    ],
+)
+def test_api_version_is_added_to_success_response_headers(
+    url: str,
+    client: APIClient,
+    settings: SettingsWrapper,
+) -> None:
+    settings.VERSION = "v1.2.3"  # NOTE: Maybe read from Git tag into an env var?
path: api/tests/unit/api/test_unit_api.py
user: login: matthewelwell
body: Yes, I believe so. 
created_at: 2025-04-22T08:35:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,3 +21,40 @@
 def test_swagger_docs_generation(url: str, client: APIClient) -> None:
     response = client.get(url)
     assert response.status_code == status.HTTP_200_OK
+
+
+@pytest.mark.parametrize(
+    "url",
+    [
+        "/robots.txt",
+        # TODO: Add other GET-only URLs
+    ],
+)
+def test_api_version_is_added_to_success_response_headers(
+    url: str,
+    client: APIClient,
+    settings: SettingsWrapper,
+) -> None:
+    settings.VERSION = "v1.2.3"  # NOTE: Maybe read from Git tag into an env var?
path: api/tests/unit/api/test_unit_api.py
user: login: emyller
body: @matthewelwell My first instinct was to read the Git tag into a Docker arg when building the image, then passing it through an env var to the code at build time, then finally into a new Django setting `VERSION`, as my code suggests so far.

However, I just learned that `flagsmith` will already inherit version information from release-please:

https://github.com/Flagsmith/flagsmith/blob/647712caf6e4e61eac18fca0a38ca7ee2beaadef/Dockerfile#L128

Then as the next step, `flagsmith-common` reads from that file into the `/version` endpoint ([ref](https://github.com/Flagsmith/flagsmith-common/blob/13c2016f5b384818e9e796dea226d045d8769094/src/common/core/utils.py#L69-L74)).

I'd lean to agreeing with @khvn26's [comment](https://github.com/Flagsmith/flagsmith/pull/5366#issuecomment-2820585967) below but I'm not a fan of the two context being coupled: whatever code implemented to `flagsmith-common` in order to display version info can only work if the effort starts in the `flagsmith` context — there's an implicit dependency there.

So according to the team's opinion, what should be the best path?

1. Implement a new middleware to `flagsmith-common` reusing the version info machinery that already exists in it. Add this middleware to `flagsmith` settings. Call it a day.
2. Move `/version` to `flagsmith` so all the implementation lives in it, removing the coupled code and implicit dependency. I don't know whether that endpoint is reused across other applications so that might not be the optimal solution.
3. Something else I can't see?
created_at: 2025-04-22T16:42:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,3 +21,40 @@
 def test_swagger_docs_generation(url: str, client: APIClient) -> None:
     response = client.get(url)
     assert response.status_code == status.HTTP_200_OK
+
+
+@pytest.mark.parametrize(
+    "url",
+    [
+        "/robots.txt",
+        # TODO: Add other GET-only URLs
+    ],
+)
+def test_api_version_is_added_to_success_response_headers(
+    url: str,
+    client: APIClient,
+    settings: SettingsWrapper,
+) -> None:
+    settings.VERSION = "v1.2.3"  # NOTE: Maybe read from Git tag into an env var?
path: api/tests/unit/api/test_unit_api.py
user: login: matthewelwell
body: Thanks for the write up. I think, based on these options, that (1) is the best option. 
created_at: 2025-04-22T17:03:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,3 +21,40 @@
 def test_swagger_docs_generation(url: str, client: APIClient) -> None:
     response = client.get(url)
     assert response.status_code == status.HTTP_200_OK
+
+
+@pytest.mark.parametrize(
+    "url",
+    [
+        "/robots.txt",
+        # TODO: Add other GET-only URLs
+    ],
+)
+def test_api_version_is_added_to_success_response_headers(
+    url: str,
+    client: APIClient,
+    settings: SettingsWrapper,
+) -> None:
+    settings.VERSION = "v1.2.3"  # NOTE: Maybe read from Git tag into an env var?
path: api/tests/unit/api/test_unit_api.py
user: login: khvn26
body: `flagsmith-common` serves multiple roles at the moment:
- Code that needs to be shared between public (flagsmith/flagsmith) and private (saml, rbac, workflows-logic) codebases.
- Task processor library.
- "Platform library", i.e. common endpoints (healthchecks, `/metrics` `/version`), entrypoints (`flagsmith start`), common middleware, metrics tooling, and test helpers.

Taking the above into account, I believe option 1 makes sense. 
created_at: 2025-04-22T17:04:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,68 @@
+import pytest
+from common.core.utils import get_file_contents, get_versions_from_manifest
+from pyfakefs.fake_filesystem import FakeFilesystem
+from rest_framework import status
+from rest_framework.test import APIClient
+
+
+@pytest.fixture(autouse=True)
+def clear_lru_caches() -> None:
+    get_file_contents.cache_clear()
+    get_versions_from_manifest.cache_clear()
+
+
+@pytest.mark.parametrize(
+    "url",
+    [
+        "/robots.txt",
+        # TODO: Add other GET-only URLs
path: api/tests/unit/api/test_api_version_http_header.py
user: login: matthewelwell
body: The only other unauthenticated endpoint that I can think of is `/version` but maybe that's a little too meta? Perhaps we should remove this parameterisation and add another test for an authenticated endpoint (e.g. `/users/me/`)? Or just use an authenticated client regardless and keep the parameterisation? 
created_at: 2025-04-24T08:09:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -154,7 +154,7 @@ pygithub = "2.1.1"
 hubspot-api-client = "^8.2.1"
 djangorestframework-dataclasses = "^1.3.1"
 pyotp = "^2.9.0"
-flagsmith-common = "^1.10.0"
+flagsmith-common = { git = "https://github.com/flagsmith/flagsmith-common", branch = "feat/add-version-to-response-headers" }
path: api/pyproject.toml
user: login: matthewelwell
body: Just adding a comment here to remind us to update this once flagsmith-common is released. 
created_at: 2025-04-24T08:10:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -154,7 +154,7 @@ pygithub = "2.1.1"
 hubspot-api-client = "^8.2.1"
 djangorestframework-dataclasses = "^1.3.1"
 pyotp = "^2.9.0"
-flagsmith-common = "^1.10.0"
+flagsmith-common = { git = "https://github.com/flagsmith/flagsmith-common", branch = "feat/add-version-to-response-headers" }
path: api/pyproject.toml
user: login: matthewelwell
body: I wonder if we could have also deployed an alpha version of flagsmith-common to test it too, rather than reverting to a git dependency. Not too sure if we'd really benefit from doing that though. 
created_at: 2025-04-24T08:10:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+import pytest
+from common.core.utils import get_file_contents, get_versions_from_manifest
+from pyfakefs.fake_filesystem import FakeFilesystem
+from rest_framework import status
+from rest_framework.test import APIClient
+
+
+@pytest.fixture(autouse=True)
+def clear_lru_caches() -> None:
+    get_file_contents.cache_clear()
+    get_versions_from_manifest.cache_clear()
+
+
+@pytest.mark.parametrize(
+    "url",
+    [
+        "/robots.txt",
+        # TODO: Add other GET-only URLs
path: api/tests/unit/api/test_api_version_http_header.py
user: login: emyller
body: Yeah also `/version` comes from `flagsmith-common` so it feels a bit unfair. I like using an authenticated client even for `/robots.txt` — after all this specific test isn't concerned with authentication capabilities or lack thereof.
created_at: 2025-04-25T17:01:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -154,7 +154,7 @@ pygithub = "2.1.1"
 hubspot-api-client = "^8.2.1"
 djangorestframework-dataclasses = "^1.3.1"
 pyotp = "^2.9.0"
-flagsmith-common = "^1.10.0"
+flagsmith-common = { git = "https://github.com/flagsmith/flagsmith-common", branch = "feat/add-version-to-response-headers" }
path: api/pyproject.toml
user: login: emyller
body: Pinned to 1.11.0.

Regarding deploying an alpha: it may be a good idea but it feels like it adds too much bureaucracy for just one PR. Definitely the way to go when dealing with a bigger project IMHO.
created_at: 2025-04-25T17:13:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-22T00:10:11Z
body: [vc]: #L/bZ302O9eDO9Kg1Y2SvJeUTIYhWkWDXvu9jFO7AmV4=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJmbGFnc21pdGgtZnJvbnRlbmQtcHJldmlldyIsInJvb3REaXJlY3RvcnkiOiJmcm9udGVuZCIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9mbGFnc21pdGgvZmxhZ3NtaXRoLWZyb250ZW5kLXByZXZpZXcvR1JrckZ4QnB6NjFTM3J5eHFOUlU5RzhYTFZWaCIsInByZXZpZXdVcmwiOiJmbGFnc21pdGgtZnJvbnRlbmQtcHJldmlldy1naXQtZmVhdC1hZGQtdmVyc2lvLTdjZDU4YS1mbGFnc21pdGgudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9fSx7Im5hbWUiOiJkb2NzIiwicm9vdERpcmVjdG9yeSI6ImRvY3MiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vZmxhZ3NtaXRoL2RvY3MvQlFwRXBWQWdrVTNtdTRGc1o2VE5NZmlBRjFkdiIsInByZXZpZXdVcmwiOiJkb2NzLWdpdC1mZWF0LWFkZC12ZXJzaW9uLXRvLXJlc3BvbnNlLWhlYWRlcnMtZmxhZ3NtaXRoLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifX0seyJuYW1lIjoiZmxhZ3NtaXRoLWZyb250ZW5kLXN0YWdpbmciLCJyb290RGlyZWN0b3J5IjoiZnJvbnRlbmQiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vZmxhZ3NtaXRoL2ZsYWdzbWl0aC1mcm9udGVuZC1zdGFnaW5nL0JwQkdZWFRKV3BVUFlNVFlSWGRhaVRMYmF5OHoiLCJwcmV2aWV3VXJsIjoiZmxhZ3NtaXRoLWZyb250ZW5kLXN0YWdpbmctZ2l0LWZlYXQtYWRkLXZlcnNpby04NmNhNjMtZmxhZ3NtaXRoLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>3 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **docs** | ⬜️ Ignored ([Inspect](https://vercel.com/flagsmith/docs/BQpEpVAgkU3mu4FsZ6TNMfiAF1dv)) | [Visit Preview](https://docs-git-feat-add-version-to-response-headers-flagsmith.vercel.app) |  | Apr 28, 2025 9:17pm |
| **flagsmith-frontend-preview** | ⬜️ Ignored ([Inspect](https://vercel.com/flagsmith/flagsmith-frontend-preview/GRkrFxBpz61S3ryxqNRU9G8XLVVh)) | [Visit Preview](https://flagsmith-frontend-preview-git-feat-add-versio-7cd58a-flagsmith.vercel.app) |  | Apr 28, 2025 9:17pm |
| **flagsmith-frontend-staging** | ⬜️ Ignored ([Inspect](https://vercel.com/flagsmith/flagsmith-frontend-staging/BpBGYXTJWpUPYMTYRXdaiTLbay8z)) | [Visit Preview](https://flagsmith-frontend-staging-git-feat-add-versio-86ca63-flagsmith.vercel.app) |  | Apr 28, 2025 9:17pm |
</details>




author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-22T00:12:12Z
body: Uffizzi Preview `deployment-63002` was deleted.
author_association: CONTRIBUTOR
user: login: emyller
created_at: 2025-04-22T00:16:28Z
body: Messed up GitHub references.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-22T00:23:52Z
body: Uffizzi Preview `deployment-63161` was deleted.
author_association: CONTRIBUTOR
user: login: khvn26
created_at: 2025-04-22T08:36:17Z
body: I have a hunch this belongs to https://github.com/flagsmith/flagsmith-common, as the implementation is likely going to be a global middleware
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-04-23T21:32:56Z
body: ## [Codecov](https://app.codecov.io/gh/Flagsmith/flagsmith/pull/5366?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Flagsmith) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 97.62%. Comparing base [(`d4b5d6a`)](https://app.codecov.io/gh/Flagsmith/flagsmith/commit/d4b5d6a68383067f27655ac848890a924d311aee?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Flagsmith) to head [(`7bd82de`)](https://app.codecov.io/gh/Flagsmith/flagsmith/commit/7bd82deba6c1210e7964e703a1c0f091fc6226bf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Flagsmith).
> Report is 2 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #5366   +/-   ##
=======================================
  Coverage   97.61%   97.62%           
=======================================
  Files        1237     1238    +1     
  Lines       42979    43000   +21     
=======================================
+ Hits        41956    41977   +21     
  Misses       1023     1023           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Flagsmith/flagsmith/pull/5366?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Flagsmith).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Flagsmith).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-23T21:36:11Z
body: #### Docker builds report

| Image | Build Status | Security report |
| ----- | ------------ | --------------- |
| `ghcr.io/flagsmith/flagsmith-api-test:pr-5366` | Finished :white_check_mark: | Skipped |
| `ghcr.io/flagsmith/flagsmith-e2e:pr-5366` | Finished :white_check_mark: | Skipped |
| `ghcr.io/flagsmith/flagsmith-api:pr-5366` | Finished :white_check_mark: | [Results](https://github.com/Flagsmith/flagsmith/security/code-scanning?query=is:open+tool:Trivy+pr:5366) :white_check_mark: |
| `ghcr.io/flagsmith/flagsmith-frontend:pr-5366` | Finished :white_check_mark: | [Results](https://github.com/Flagsmith/flagsmith/security/code-scanning?query=is:open+tool:Trivy+pr:5366) :white_check_mark: |
| `ghcr.io/flagsmith/flagsmith:pr-5366` | Finished :white_check_mark: | [Results](https://github.com/Flagsmith/flagsmith/security/code-scanning?query=is:open+tool:Trivy+pr:5366) :white_check_mark: |
| `ghcr.io/flagsmith/flagsmith-private-cloud:pr-5366` | Finished :white_check_mark: | [Results](https://github.com/Flagsmith/flagsmith/security/code-scanning?query=is:open+tool:Trivy+pr:5366) :white_check_mark: |
author_association: CONTRIBUTOR